瀏覽代碼

IBKR strategy

Alexey Kim 10 月之前
父節點
當前提交
19228aa988
共有 2 個文件被更改,包括 519 次插入0 次删除
  1. 127 0
      strategy/ibkr.go
  2. 392 0
      strategy/ibkr_test.go

+ 127 - 0
strategy/ibkr.go

@@ -0,0 +1,127 @@
+package main
+
+import (
+	"git.beejay.kim/Gshopper/sentio"
+	"github.com/samber/lo"
+	"time"
+)
+
+var Strategy = _ibkr{
+	clock: sentio.ClockUTC{},
+}
+
+type _ibkr struct {
+	clock sentio.Clock
+}
+
+func (s _ibkr) Name() string {
+	return "IBKR"
+}
+func (s _ibkr) Version() string {
+	return sentio.Version
+}
+
+func (s _ibkr) Model() string {
+	return "btc306"
+}
+
+func (s _ibkr) PositionSymbols() map[sentio.Side]string {
+	return map[sentio.Side]string{
+		sentio.LONG:  "521525019", // BITO
+		sentio.SHORT: "569311092", // BITI
+	}
+}
+
+func (s _ibkr) IsOpen() bool {
+	utc := s.clock.Now()
+	h := utc.Hour()
+	m := utc.Minute()
+
+	return (h == 13 && m >= 30 || h > 13) &&
+		h < 19 &&
+		utc.Weekday() != time.Saturday && utc.Weekday() != time.Sunday
+}
+
+func (s _ibkr) ShouldClosePositions(portfolio sentio.Portfolio, proba float64) []string {
+	if !s.IsOpen() {
+		p := lo.Map(portfolio.Positions(), func(item sentio.Position, _ int) string {
+			return item.GetSymbol()
+		})
+
+		symbols := lo.Values(s.PositionSymbols())
+		p = lo.Filter(p, func(item string, _ int) bool {
+			return lo.Contains(symbols, item)
+		})
+
+		return p
+	}
+
+	var (
+		utc     = s.clock.Now()
+		symbols []string
+	)
+
+	if portfolio == nil {
+		return nil
+	}
+
+	for side, symbol := range s.PositionSymbols() {
+		position, ok := portfolio.Get(symbol)
+		if !ok {
+			continue
+		}
+
+		if position.GetSize() <= 0 {
+			continue
+		}
+
+		if utc.Hour() >= 19 {
+			symbols = append(symbols, symbol)
+			continue
+		}
+
+		if sentio.LONG == side && proba < 1 {
+			symbols = append(symbols, symbol)
+			continue
+		}
+
+		if sentio.SHORT == side && proba > 1 {
+			symbols = append(symbols, symbol)
+			continue
+		}
+	}
+
+	return symbols
+}
+
+func (s _ibkr) ShouldOpenPosition(portfolio sentio.Portfolio, proba float64) (*string, float64) {
+	if !s.IsOpen() {
+		return nil, 0
+	}
+
+	if proba < 0 {
+		return nil, 0
+	}
+
+	for side, symbol := range s.PositionSymbols() {
+		position, ok := portfolio.Get(symbol)
+
+		if sentio.LONG == side && proba > 1.002 {
+			if !ok || position.GetSize() <= 0 {
+				return &symbol, .6
+			} else {
+				return &symbol, .3
+			}
+		}
+
+		if sentio.SHORT == side && proba < 0.998 {
+			if !ok || position.GetSize() <= 0 {
+				return &symbol, .8
+			} else {
+				return &symbol, .3
+			}
+		}
+	}
+
+	return nil, 0
+}

+ 392 - 0
strategy/ibkr_test.go

@@ -0,0 +1,392 @@
+package main
+
+import (
+	"fmt"
+	"git.beejay.kim/Gshopper/sentio"
+	"github.com/samber/lo"
+	"reflect"
+	"slices"
+	"testing"
+	"time"
+)
+
+func Test__ibkr_IsOpen(t *testing.T) {
+	tests := []struct {
+		time string
+		want bool
+	}{
+		{
+			time: "2024-08-26T13:29:59Z", // 1s before open
+			want: false,
+		},
+		{
+			time: "2024-08-02T15:04:05Z", // Opened
+			want: true,
+		},
+		{
+			time: "2024-08-03T18:34:00Z", // Saturday
+			want: false,
+		},
+		{
+			time: "2024-08-25T15:00:00Z", // Sunday
+			want: false,
+		},
+		{
+			time: "2024-08-26T19:00:05Z", // Closed
+			want: false,
+		},
+	}
+
+	for _, tt := range tests {
+		var (
+			c   ClockFixed
+			s   _ibkr
+			err error
+		)
+
+		if c.fix, err = time.ParseInLocation(time.RFC3339, tt.time, time.UTC); err != nil {
+			t.Errorf("ParseInLocation() = %v", err)
+			return
+		}
+
+		s = _ibkr{
+			clock: c,
+		}
+
+		t.Run(fmt.Sprintf("IBKR:IsOpen:%s", tt.time), func(t *testing.T) {
+			got := s.IsOpen()
+			if got != tt.want {
+				t.Errorf("IsOpen() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test__ibkr_ShouldClosePositions(t *testing.T) {
+	type args struct {
+		portfolio sentio.Portfolio
+		proba     float64
+		time      string
+	}
+
+	tests := []struct {
+		args args
+		want []string
+	}{
+		{ // should close SHORT position
+			args: args{
+				portfolio: newStubPortfolio(
+					position_stub{
+						symbol: "521525019",
+						size:   10,
+						price:  20,
+					},
+					position_stub{
+						symbol: "569311092",
+						size:   10,
+						price:  8,
+					},
+				),
+				proba: 1.00001,
+				time:  "2024-08-02T15:04:05Z",
+			},
+			want: []string{"569311092"},
+		},
+		{ // should close LONG position
+			args: args{
+				portfolio: newStubPortfolio(
+					position_stub{
+						symbol: "521525019",
+						size:   10,
+						price:  20,
+					},
+					position_stub{
+						symbol: "569311092",
+						size:   10,
+						price:  8,
+					},
+				),
+				proba: .9,
+				time:  "2024-08-02T15:04:05Z",
+			},
+			want: []string{"521525019"},
+		},
+		{ // nothing to close
+			args: args{
+				portfolio: newStubPortfolio(),
+				proba:     .9,
+				time:      "2024-08-02T15:04:05Z",
+			},
+			want: nil,
+		},
+		{ // should close both positions
+			args: args{
+				portfolio: newStubPortfolio(
+					position_stub{
+						symbol: "521525019",
+						size:   10,
+						price:  20,
+					},
+					position_stub{
+						symbol: "521525020",
+						size:   10,
+						price:  20,
+					},
+					position_stub{
+						symbol: "569311092",
+						size:   10,
+						price:  8,
+					},
+				),
+				proba: .9,
+				time:  "2024-08-02T19:00:05Z",
+			},
+			want: []string{"521525019", "569311092"},
+		},
+		{ // should close nothing
+			args: args{
+				portfolio: newStubPortfolio(
+					position_stub{
+						symbol: "569311092",
+						size:   10,
+						price:  8,
+					},
+					position_stub{
+						symbol: "521525020",
+						size:   10,
+						price:  20,
+					},
+				),
+				proba: .9,
+				time:  "2024-08-02T18:00:05Z",
+			},
+			want: nil,
+		},
+		{ // should close nothing
+			args: args{
+				portfolio: newStubPortfolio(
+					position_stub{
+						symbol: "521525019",
+						size:   10,
+						price:  20,
+					},
+					position_stub{
+						symbol: "521525020",
+						size:   10,
+						price:  20,
+					},
+				),
+				proba: 1.2,
+				time:  "2024-08-02T18:00:05Z",
+			},
+			want: nil,
+		},
+		{ // should close all
+			args: args{
+				portfolio: newStubPortfolio(
+					position_stub{
+						symbol: "521525019",
+						size:   10,
+						price:  20,
+					},
+					position_stub{
+						symbol: "521525020",
+						size:   10,
+						price:  20,
+					},
+				),
+				proba: 1.2,
+				time:  "2024-08-02T12:00:05Z",
+			},
+			want: []string{"521525019"},
+		},
+		{ // should close nothing
+			args: args{
+				portfolio: newStubPortfolio(
+					position_stub{
+						symbol: "521525020",
+						size:   10,
+						price:  20,
+					},
+				),
+				proba: 1.1,
+				time:  "2024-08-02T12:00:05Z",
+			},
+			want: nil,
+		},
+	}
+
+	for _, tt := range tests {
+		var (
+			c   ClockFixed
+			s   _ibkr
+			err error
+		)
+
+		if c.fix, err = time.ParseInLocation(time.RFC3339, tt.args.time, time.UTC); err != nil {
+			t.Errorf("ParseInLocation() = %v", err)
+			return
+		}
+
+		s = _ibkr{
+			clock: c,
+		}
+
+		t.Run("IBKR:ShouldClosePositions", func(t *testing.T) {
+			if got := s.ShouldClosePositions(tt.args.portfolio, tt.args.proba); !slices.Equal(got, tt.want) {
+				t.Errorf("ShouldClosePositions() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test__ibkr_ShouldOpenPosition(t *testing.T) {
+	type args struct {
+		portfolio sentio.Portfolio
+		proba     float64
+		time      string
+	}
+
+	tests := []struct {
+		args   args
+		symbol *string
+		size   float64
+	}{
+		{
+			args: args{
+				portfolio: newStubPortfolio(),
+				proba:     1.00201,
+				time:      "2024-08-02T18:30:00Z",
+			},
+			symbol: lo.ToPtr("521525019"),
+			size:   .6,
+		},
+		{
+			args: args{
+				portfolio: newStubPortfolio(
+					position_stub{
+						symbol: "521525019",
+						size:   1,
+						price:  19,
+					},
+				),
+				proba: 1.00201,
+				time:  "2024-08-02T18:30:00Z",
+			},
+			symbol: lo.ToPtr("521525019"),
+			size:   .3,
+		},
+		{
+			args: args{
+				portfolio: newStubPortfolio(
+					position_stub{
+						symbol: "521525019",
+						size:   -.2,
+						price:  19,
+					},
+				),
+				proba: 1.00201,
+				time:  "2024-08-02T18:30:00Z",
+			},
+			symbol: lo.ToPtr("521525019"),
+			size:   .6,
+		},
+		{
+			args: args{
+				portfolio: newStubPortfolio(
+					position_stub{
+						symbol: "521525020",
+						size:   3,
+						price:  19,
+					},
+				),
+				proba: 1.00201,
+				time:  "2024-08-02T18:30:00Z",
+			},
+			symbol: lo.ToPtr("521525019"),
+			size:   .6,
+		},
+		{
+			args: args{
+				portfolio: newStubPortfolio(),
+				proba:     .9979,
+				time:      "2024-08-02T18:30:00Z",
+			},
+			symbol: lo.ToPtr("569311092"),
+			size:   .8,
+		},
+		{
+			args: args{
+				portfolio: newStubPortfolio(
+					position_stub{
+						symbol: "569311092",
+						size:   100,
+						price:  8,
+					},
+				),
+				proba: .9979,
+				time:  "2024-08-02T18:30:00Z",
+			},
+			symbol: lo.ToPtr("569311092"),
+			size:   .3,
+		},
+		{
+			args: args{
+				portfolio: newStubPortfolio(
+					position_stub{
+						symbol: "521525020",
+						size:   100,
+						price:  8,
+					},
+				),
+				proba: .9979,
+				time:  "2024-08-02T19:00:00Z",
+			},
+			symbol: nil,
+			size:   0,
+		},
+		{
+			args: args{
+				portfolio: newStubPortfolio(
+					position_stub{
+						symbol: "569311092",
+						size:   100,
+						price:  8,
+					},
+				),
+				proba: -1,
+				time:  "2024-08-02T18:30:00Z",
+			},
+			symbol: nil,
+			size:   0,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run("IBKR:ShouldOpenPosition", func(t *testing.T) {
+			var (
+				c   ClockFixed
+				s   _ibkr
+				err error
+			)
+
+			if c.fix, err = time.ParseInLocation(time.RFC3339, tt.args.time, time.UTC); err != nil {
+				t.Errorf("ParseInLocation() = %v", err)
+				return
+			}
+
+			s = _ibkr{
+				clock: c,
+			}
+
+			symbol, size := s.ShouldOpenPosition(tt.args.portfolio, tt.args.proba)
+			if !reflect.DeepEqual(symbol, tt.symbol) {
+				t.Errorf("ShouldOpenPosition() symbol = %v, want %v", symbol, tt.symbol)
+			}
+
+			if size != tt.size {
+				t.Errorf("ShouldOpenPosition() size = %v, want %v", size, tt.size)
+			}
+		})
+	}
+}