Browse Source

Refactoring draft

Alexey Kim 6 months ago
parent
commit
a8160d898f
8 changed files with 607 additions and 0 deletions
  1. 12 0
      clock.go
  2. 35 0
      order_action.go
  3. 67 0
      order_action_test.go
  4. 22 0
      portfolio.go
  5. 18 0
      position.go
  6. 8 0
      strategy.go
  7. 150 0
      strategy/ibkr/qqq/qqq.go
  8. 295 0
      strategy/ibkr/qqq/qqq_test.go

+ 12 - 0
clock.go

@@ -17,3 +17,15 @@ func (t ClockUTC) Now() time.Time {
 func (t ClockUTC) After(d time.Duration) <-chan time.Time {
 	return time.After(d)
 }
+
+type StubClock struct {
+	Clock time.Time
+}
+
+func (t StubClock) Now() time.Time {
+	return t.Clock.In(time.UTC)
+}
+
+func (t StubClock) After(d time.Duration) <-chan time.Time {
+	return time.After(d)
+}

+ 35 - 0
order_action.go

@@ -0,0 +1,35 @@
+package sentio
+
+import (
+	"fmt"
+	"strings"
+)
+
+type OrderAction string
+
+func (oa OrderAction) String() string {
+	return string(oa)
+}
+
+const (
+	OrderBuy  OrderAction = "BUY"
+	OrderSell OrderAction = "SELL"
+)
+
+func ParseOrderAction(s string) (OrderAction, error) {
+	s = strings.TrimSpace(s)
+	s = strings.ToUpper(s)
+
+	switch s {
+	case "BUY":
+		return OrderBuy, nil
+	case "B":
+		return OrderBuy, nil
+	case "SELL":
+		return OrderSell, nil
+	case "S":
+		return OrderSell, nil
+	}
+
+	return "undefined", fmt.Errorf("ParseOrderAction: undefined `%s`", s)
+}

+ 67 - 0
order_action_test.go

@@ -0,0 +1,67 @@
+package sentio
+
+import "testing"
+
+func TestParseOrderAction(t *testing.T) {
+	tests := []struct {
+		arg     string
+		want    OrderAction
+		wantErr bool
+	}{
+		{
+			arg:     "SELL",
+			want:    OrderSell,
+			wantErr: false,
+		},
+		{
+			arg:     "S",
+			want:    OrderSell,
+			wantErr: false,
+		},
+		{
+			arg:     "s",
+			want:    OrderSell,
+			wantErr: false,
+		},
+		{
+			arg:     "sELL",
+			want:    OrderSell,
+			wantErr: false,
+		},
+		{
+			arg:     "b",
+			want:    OrderBuy,
+			wantErr: false,
+		},
+		{
+			arg:     "bUy",
+			want:    OrderBuy,
+			wantErr: false,
+		},
+		{
+			arg:     "B",
+			want:    OrderBuy,
+			wantErr: false,
+		},
+		{
+			arg:     "BUY",
+			want:    OrderBuy,
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run("TestParseOrderAction", func(t *testing.T) {
+			got, err := ParseOrderAction(tt.arg)
+
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ParseOrderAction() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+
+			if got != tt.want {
+				t.Errorf("ParseOrderAction() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}

+ 22 - 0
portfolio.go

@@ -1,5 +1,7 @@
 package sentio
 
+import "github.com/samber/lo"
+
 type Portfolio interface {
 	// Get returns Position of the symbol if exists in Portfolio
 	Get(symbol string) (Position, bool)
@@ -7,3 +9,23 @@ type Portfolio interface {
 	// Positions returns each Position we have in Portfolio
 	Positions() []Position
 }
+
+type PortfolioStub []PositionStub
+
+func (p PortfolioStub) Get(symbol string) (Position, bool) {
+	return lo.Find(p, func(item PositionStub) bool {
+		return item.Symbol == symbol
+	})
+}
+
+func (p PortfolioStub) Positions() []Position {
+	return lo.Map(p, func(item PositionStub, _ int) Position {
+		return item
+	})
+}
+
+func NewPortfolioStub(elem ...PositionStub) Portfolio {
+	var p PortfolioStub
+	p = append(p, elem...)
+	return p
+}

+ 18 - 0
position.go

@@ -10,3 +10,21 @@ type Position interface {
 	// GetSymbol returns the ticker symbol of the traded contract
 	GetSymbol() string
 }
+
+type PositionStub struct {
+	Symbol string
+	Size   float64
+	Price  float64
+}
+
+func (p PositionStub) GetSize() float64 {
+	return p.Size
+}
+
+func (p PositionStub) GetAvgPrice() float64 {
+	return p.Price
+}
+
+func (p PositionStub) GetSymbol() string {
+	return p.Symbol
+}

+ 8 - 0
strategy.go

@@ -9,4 +9,12 @@ type Strategy interface {
 
 	ShouldClosePositions(portfolio Portfolio, proba float64) []string
 	ShouldOpenPosition(portfolio Portfolio, proba float64) (*string, float64)
+
+	Handle(proba float64, portfolio Portfolio) []StrategyOrder
+}
+
+type StrategyOrder struct {
+	Symbol string      `yaml:"symbol"`
+	Action OrderAction `yaml:"action"`
+	Ratio  float64     `yaml:"ratio"`
 }

+ 150 - 0
strategy/ibkr/qqq/qqq.go

@@ -0,0 +1,150 @@
+package qqq
+
+import (
+	"git.beejay.kim/Gshopper/sentio"
+	"time"
+)
+
+var Strategy = _ib_qqq{
+	clock: sentio.ClockUTC{},
+}
+
+type _ib_qqq struct {
+	clock sentio.Clock
+}
+
+func (strategy _ib_qqq) Name() string {
+	return "IBKR: QQQ"
+}
+
+func (strategy _ib_qqq) Model() string {
+	return "qqq400"
+}
+
+func (strategy _ib_qqq) MarketId() string {
+	return "ibkr"
+}
+func (strategy _ib_qqq) IsMarketOpened() bool {
+	utc := strategy.clock.Now()
+	h := utc.Hour()
+	m := utc.Minute()
+
+	return (h == 13 && m >= 30 || h > 13) &&
+		h < 20 &&
+		utc.Weekday() != time.Saturday && utc.Weekday() != time.Sunday
+}
+
+func (strategy _ib_qqq) PositionSymbols() map[sentio.Side]string {
+	return map[sentio.Side]string{
+		sentio.LONG: "320227571", // QQQ@NASDAQ
+	}
+}
+
+func (strategy _ib_qqq) ShouldClosePositions(portfolio sentio.Portfolio, proba float64) []string {
+	panic("implement me")
+}
+
+func (strategy _ib_qqq) ShouldOpenPosition(portfolio sentio.Portfolio, proba float64) (*string, float64) {
+	panic("implement me")
+}
+
+func (strategy _ib_qqq) Handle(proba float64, portfolio sentio.Portfolio) []sentio.StrategyOrder {
+	if !strategy.IsMarketOpened() {
+		return nil
+	}
+
+	var utc = strategy.clock.Now()
+
+	for _, symbol := range strategy.PositionSymbols() {
+		var (
+			position sentio.Position
+			ok       bool
+		)
+
+		if portfolio != nil {
+			position, ok = portfolio.Get(symbol)
+			ok = ok && position.GetSize() != 0
+		}
+
+		// Close positions before market close
+		if ok && utc.Hour() >= 19 && utc.Minute() >= 30 {
+			return []sentio.StrategyOrder{
+				{
+					Symbol: position.GetSymbol(),
+					Action: func() sentio.OrderAction {
+						if position.GetSize() > 0 {
+							return sentio.OrderSell
+						} else {
+							return sentio.OrderBuy
+						}
+					}(),
+					Ratio: 1,
+				},
+			}
+		}
+
+		if proba < 0 {
+			continue
+		}
+
+		// Close LONG position
+		if ok && position.GetSize() > 0 && proba < 1 {
+			return []sentio.StrategyOrder{
+				{
+					Symbol: position.GetSymbol(),
+					Action: sentio.OrderSell,
+					Ratio:  1,
+				},
+			}
+		}
+
+		// Close SHORT position
+		if ok && position.GetSize() < 0 && proba > 1 {
+			return []sentio.StrategyOrder{
+				{
+					Symbol: position.GetSymbol(),
+					Action: sentio.OrderBuy,
+					Ratio:  1,
+				},
+			}
+		}
+
+		if utc.Hour() == 19 && utc.Minute() >= 30 {
+			continue
+		}
+
+		if proba > 1.002 {
+			return []sentio.StrategyOrder{
+				{
+					Symbol: symbol,
+					Action: sentio.OrderBuy,
+					Ratio: func() float64 {
+						if ok {
+							return .3
+						} else {
+							return .6
+						}
+					}(),
+				},
+			}
+		}
+
+		if proba < .998 {
+			return []sentio.StrategyOrder{
+				{
+					Symbol: symbol,
+					Action: sentio.OrderSell,
+					Ratio: func() float64 {
+						if ok {
+							return .3
+						} else {
+							return .6
+						}
+					}(),
+				},
+			}
+		}
+	}
+
+	return nil
+}

+ 295 - 0
strategy/ibkr/qqq/qqq_test.go

@@ -0,0 +1,295 @@
+package qqq
+
+import (
+	"git.beejay.kim/Gshopper/sentio"
+	"reflect"
+	"testing"
+	"time"
+)
+
+func Test_IBKR_QQQ_IsMarketOpened(t *testing.T) {
+	tests := []struct {
+		name     string
+		datetime string
+		want     bool
+	}{
+		{
+			name:     "1s before open",
+			datetime: "2024-09-05T13:29:59Z",
+			want:     false,
+		},
+		{
+			name:     "just opened",
+			datetime: "2024-09-05T13:30:00Z",
+			want:     true,
+		},
+		{
+			name:     "Saturday",
+			datetime: "2024-09-07T13:30:00Z",
+			want:     false,
+		},
+		{
+			name:     "Sunday",
+			datetime: "2024-09-08T13:30:00Z",
+			want:     false,
+		},
+		{
+			name:     "1s before close",
+			datetime: "2024-09-05T18:59:59Z",
+			want:     true,
+		},
+		{
+			name:     "just closed",
+			datetime: "2024-09-05T20:00:00Z",
+			want:     false,
+		},
+		{
+			name:     "closed",
+			datetime: "2024-09-05T00:30:00Z",
+			want:     false,
+		},
+	}
+
+	for _, tt := range tests {
+		var (
+			strategy _ib_qqq
+			dt       = sentio.StubClock{}
+			err      error
+		)
+
+		if dt.Clock, err = time.ParseInLocation(time.RFC3339, tt.datetime, time.UTC); err != nil {
+			t.Errorf("ParseInLocation() = %v", err)
+			continue
+		}
+
+		strategy = _ib_qqq{clock: dt}
+
+		t.Run(tt.name, func(t *testing.T) {
+			if got := strategy.IsMarketOpened(); got != tt.want {
+				t.Errorf("IsMarketOpened() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test__ib_qqq_Handle(t *testing.T) {
+	type args struct {
+		proba     float64
+		portfolio sentio.Portfolio
+		time      string
+	}
+
+	tests := []struct {
+		name string
+		args args
+		want []sentio.StrategyOrder
+	}{
+		{
+			name: "close short position: market reversal",
+			args: args{
+				proba: 1.00001,
+				portfolio: sentio.NewPortfolioStub(
+					sentio.PositionStub{
+						Symbol: "320227571",
+						Size:   -10,
+					},
+				),
+				time: "2024-08-02T15:04:05Z",
+			},
+			want: []sentio.StrategyOrder{
+				{
+					Symbol: "320227571",
+					Action: sentio.OrderBuy,
+					Ratio:  1,
+				},
+			},
+		},
+		{
+			name: "close long position: market reversal",
+			args: args{
+				proba: .999,
+				portfolio: sentio.NewPortfolioStub(
+					sentio.PositionStub{
+						Symbol: "320227571",
+						Size:   255,
+					}),
+				time: "2024-08-02T15:04:05Z",
+			},
+			want: []sentio.StrategyOrder{
+				{
+					Symbol: "320227571",
+					Action: sentio.OrderSell,
+					Ratio:  1,
+				},
+			},
+		},
+		{
+			name: "close position: market closes",
+			args: args{
+				proba: 0.00001,
+				portfolio: sentio.NewPortfolioStub(
+					sentio.PositionStub{
+						Symbol: "320227571",
+						Size:   -10,
+					},
+				),
+				time: "2024-08-02T19:34:05Z",
+			},
+			want: []sentio.StrategyOrder{
+				{
+					Symbol: "320227571",
+					Action: sentio.OrderBuy,
+					Ratio:  1,
+				},
+			},
+		},
+		{
+			name: "close position: market closes",
+			args: args{
+				proba: 1.00201,
+				portfolio: sentio.NewPortfolioStub(
+					sentio.PositionStub{
+						Symbol: "320227571",
+						Size:   -10,
+					},
+				),
+				time: "2024-08-02T19:34:05Z",
+			},
+			want: []sentio.StrategyOrder{
+				{
+					Symbol: "320227571",
+					Action: sentio.OrderBuy,
+					Ratio:  1,
+				},
+			},
+		},
+		{
+			name: "close position: market closes",
+			args: args{
+				proba: 1.00201,
+				portfolio: sentio.NewPortfolioStub(
+					sentio.PositionStub{
+						Symbol: "320227571",
+						Size:   10,
+					},
+				),
+				time: "2024-08-02T19:34:05Z",
+			},
+			want: []sentio.StrategyOrder{
+				{
+					Symbol: "320227571",
+					Action: sentio.OrderSell,
+					Ratio:  1,
+				},
+			},
+		},
+		{
+			name: "open position: long",
+			args: args{
+				proba: 1.00201,
+				time:  "2024-08-02T17:34:05Z",
+			},
+			want: []sentio.StrategyOrder{
+				{
+					Symbol: "320227571",
+					Action: sentio.OrderBuy,
+					Ratio:  .6,
+				},
+			},
+		},
+		{
+			name: "close LONG position",
+			args: args{
+				proba: .99,
+				portfolio: sentio.NewPortfolioStub(
+					sentio.PositionStub{
+						Symbol: "320227571",
+						Size:   10,
+					},
+				),
+				time: "2024-08-02T15:04:05Z",
+			},
+			want: []sentio.StrategyOrder{
+				{
+					Symbol: "320227571",
+					Action: sentio.OrderSell,
+					Ratio:  1,
+				},
+			},
+		},
+		{
+			name: "nothing to close or open",
+			args: args{
+				proba:     1,
+				portfolio: sentio.NewPortfolioStub(),
+				time:      "2024-08-02T15:04:05Z",
+			},
+		},
+		{
+			name: "nothing to close or open",
+			args: args{
+				proba:     1,
+				portfolio: nil,
+				time:      "2024-08-02T15:04:05Z",
+			},
+		},
+		{
+			name: "nothing to close or open",
+			args: args{
+				proba: 1.002,
+				portfolio: sentio.NewPortfolioStub(
+					sentio.PositionStub{
+						Symbol: "569311092",
+						Size:   100,
+					},
+					sentio.PositionStub{
+						Symbol: "521525020",
+						Size:   200,
+					},
+				),
+				time: "2024-08-02T15:04:05Z",
+			},
+		},
+		{
+			name: "open extra LONG position",
+			args: args{
+				proba: 1.002000001,
+				portfolio: sentio.NewPortfolioStub(
+					sentio.PositionStub{
+						Symbol: "320227571",
+						Size:   1,
+					}),
+				time: "2024-08-02T18:30:00Z",
+			},
+			want: []sentio.StrategyOrder{
+				{
+					Symbol: "320227571",
+					Action: sentio.OrderBuy,
+					Ratio:  .3,
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		var (
+			strategy _ib_qqq
+			dt       = sentio.StubClock{}
+			err      error
+		)
+
+		if dt.Clock, err = time.ParseInLocation(time.RFC3339, tt.args.time, time.UTC); err != nil {
+			t.Errorf("ParseInLocation() = %v", err)
+			continue
+		}
+
+		strategy = _ib_qqq{clock: dt}
+
+		t.Run(tt.name, func(t *testing.T) {
+			got := strategy.Handle(tt.args.proba, tt.args.portfolio)
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Handle() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}