From 5c88abe72fd3e53784fcf9e729c34e35fe475d7f Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 9 Jul 2023 21:23:42 +0800 Subject: [PATCH] add rsicross strategy --- config/rsicross.yaml | 53 +++++++++++ pkg/bbgo/order_executor_general.go | 2 + pkg/bbgo/trader.go | 7 +- pkg/cmd/strategy/builtin.go | 1 + pkg/dynamic/inject.go | 102 +------------------- pkg/dynamic/inject_test.go | 105 ++++++++++++++++++++ pkg/strategy/common/strategy.go | 4 +- pkg/strategy/linregmaker/strategy.go | 11 ++- pkg/strategy/rsicross/strategy.go | 137 +++++++++++++++++++++++++++ pkg/strategy/scmaker/strategy.go | 2 +- 10 files changed, 311 insertions(+), 113 deletions(-) create mode 100644 config/rsicross.yaml create mode 100644 pkg/dynamic/inject_test.go create mode 100644 pkg/strategy/rsicross/strategy.go diff --git a/config/rsicross.yaml b/config/rsicross.yaml new file mode 100644 index 000000000..3f4761bf6 --- /dev/null +++ b/config/rsicross.yaml @@ -0,0 +1,53 @@ +persistence: + json: + directory: var/data + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + binance: + exchange: binance + envVarPrefix: binance + +exchangeStrategies: +- on: binance + rsicross: + symbol: BTCUSDT + interval: 5m + fastWindow: 7 + slowWindow: 12 + + quantity: 0.1 + + ### RISK CONTROLS + ## circuitBreakEMA is used for calculating the price for circuitBreak + # circuitBreakEMA: + # interval: 1m + # window: 14 + + ## circuitBreakLossThreshold is the maximum loss threshold for realized+unrealized PnL + # circuitBreakLossThreshold: -10.0 + + ## positionHardLimit is the maximum position limit + # positionHardLimit: 500.0 + + ## maxPositionQuantity is the maximum quantity per order that could be controlled in positionHardLimit, + ## this parameter is used with positionHardLimit togerther + # maxPositionQuantity: 10.0 + +backtest: + startTime: "2022-01-01" + endTime: "2022-02-01" + symbols: + - BTCUSDT + sessions: [binance] + # syncSecKLines: true + accounts: + binance: + makerFeeRate: 0.0% + takerFeeRate: 0.075% + balances: + BTC: 0.0 + USDT: 10000.0 diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index cee2aa573..b3d3bdf13 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -417,6 +417,8 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos return createdOrder, nil } + log.WithError(err).Errorf("unable to submit order: %v", err) + log.Infof("reduce quantity and retry order") return e.reduceQuantityAndSubmitOrder(ctx, price, *submitOrder) } diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index 0d0c0ca8b..edc58588a 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -222,7 +222,6 @@ func (trader *Trader) injectFieldsAndSubscribe(ctx context.Context) error { // load and run Session strategies for sessionName, strategies := range trader.exchangeStrategies { var session = trader.environment.sessions[sessionName] - var orderExecutor = trader.getSessionOrderExecutor(sessionName) for _, strategy := range strategies { rs := reflect.ValueOf(strategy) @@ -237,10 +236,6 @@ func (trader *Trader) injectFieldsAndSubscribe(ctx context.Context) error { return err } - if err := dynamic.InjectField(rs, "OrderExecutor", orderExecutor, false); err != nil { - return errors.Wrapf(err, "failed to inject OrderExecutor on %T", strategy) - } - if defaulter, ok := strategy.(StrategyDefaulter); ok { if err := defaulter.Defaults(); err != nil { panic(err) @@ -441,7 +436,7 @@ func (trader *Trader) injectCommonServices(ctx context.Context, s interface{}) e return fmt.Errorf("field Persistence is not a struct element, %s given", field) } - if err := dynamic.InjectField(elem, "Facade", ps, true); err != nil { + if err := dynamic.InjectField(elem.Interface(), "Facade", ps, true); err != nil { return err } diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index 995f9f5b8..3a7e33888 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -27,6 +27,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/pricealert" _ "github.com/c9s/bbgo/pkg/strategy/pricedrop" _ "github.com/c9s/bbgo/pkg/strategy/rebalance" + _ "github.com/c9s/bbgo/pkg/strategy/rsicross" _ "github.com/c9s/bbgo/pkg/strategy/rsmaker" _ "github.com/c9s/bbgo/pkg/strategy/schedule" _ "github.com/c9s/bbgo/pkg/strategy/scmaker" diff --git a/pkg/dynamic/inject.go b/pkg/dynamic/inject.go index 04a48599b..ed39ab6a5 100644 --- a/pkg/dynamic/inject.go +++ b/pkg/dynamic/inject.go @@ -3,22 +3,19 @@ package dynamic import ( "fmt" "reflect" - "testing" "time" "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/service" - "github.com/c9s/bbgo/pkg/types" ) type testEnvironment struct { startTime time.Time } -func InjectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnly bool) error { +func InjectField(target interface{}, fieldName string, obj interface{}, pointerOnly bool) error { + rs := reflect.ValueOf(target) field := rs.FieldByName(fieldName) + if !field.IsValid() { return nil } @@ -131,96 +128,3 @@ func ParseStructAndInject(f interface{}, objects ...interface{}) error { return nil } - -func Test_injectField(t *testing.T) { - type TT struct { - TradeService *service.TradeService - } - - // only pointer object can be set. - var tt = &TT{} - - // get the value of the pointer, or it can not be set. - var rv = reflect.ValueOf(tt).Elem() - - _, ret := HasField(rv, "TradeService") - assert.True(t, ret) - - ts := &service.TradeService{} - - err := InjectField(rv, "TradeService", ts, true) - assert.NoError(t, err) -} - -func Test_parseStructAndInject(t *testing.T) { - t.Run("skip nil", func(t *testing.T) { - ss := struct { - a int - Env *testEnvironment - }{ - a: 1, - Env: nil, - } - err := ParseStructAndInject(&ss, nil) - assert.NoError(t, err) - assert.Nil(t, ss.Env) - }) - t.Run("pointer", func(t *testing.T) { - ss := struct { - a int - Env *testEnvironment - }{ - a: 1, - Env: nil, - } - err := ParseStructAndInject(&ss, &testEnvironment{}) - assert.NoError(t, err) - assert.NotNil(t, ss.Env) - }) - - t.Run("composition", func(t *testing.T) { - type TT struct { - *service.TradeService - } - ss := TT{} - err := ParseStructAndInject(&ss, &service.TradeService{}) - assert.NoError(t, err) - assert.NotNil(t, ss.TradeService) - }) - - t.Run("struct", func(t *testing.T) { - ss := struct { - a int - Env testEnvironment - }{ - a: 1, - } - err := ParseStructAndInject(&ss, testEnvironment{ - startTime: time.Now(), - }) - assert.NoError(t, err) - assert.NotEqual(t, time.Time{}, ss.Env.startTime) - }) - t.Run("interface/any", func(t *testing.T) { - ss := struct { - Any interface{} // anything - }{ - Any: nil, - } - err := ParseStructAndInject(&ss, &testEnvironment{ - startTime: time.Now(), - }) - assert.NoError(t, err) - assert.NotNil(t, ss.Any) - }) - t.Run("interface/stringer", func(t *testing.T) { - ss := struct { - Stringer types.Stringer // stringer interface - }{ - Stringer: nil, - } - err := ParseStructAndInject(&ss, &types.Trade{}) - assert.NoError(t, err) - assert.NotNil(t, ss.Stringer) - }) -} diff --git a/pkg/dynamic/inject_test.go b/pkg/dynamic/inject_test.go new file mode 100644 index 000000000..e2f9464dd --- /dev/null +++ b/pkg/dynamic/inject_test.go @@ -0,0 +1,105 @@ +package dynamic + +import ( + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/service" + "github.com/c9s/bbgo/pkg/types" +) + +func Test_injectField(t *testing.T) { + type TT struct { + TradeService *service.TradeService + } + + // only pointer object can be set. + var tt = &TT{} + + // get the value of the pointer, or it can not be set. + var rv = reflect.ValueOf(tt).Elem() + + _, ret := HasField(rv, "TradeService") + assert.True(t, ret) + + ts := &service.TradeService{} + + err := InjectField(rv, "TradeService", ts, true) + assert.NoError(t, err) +} + +func Test_parseStructAndInject(t *testing.T) { + t.Run("skip nil", func(t *testing.T) { + ss := struct { + a int + Env *testEnvironment + }{ + a: 1, + Env: nil, + } + err := ParseStructAndInject(&ss, nil) + assert.NoError(t, err) + assert.Nil(t, ss.Env) + }) + t.Run("pointer", func(t *testing.T) { + ss := struct { + a int + Env *testEnvironment + }{ + a: 1, + Env: nil, + } + err := ParseStructAndInject(&ss, &testEnvironment{}) + assert.NoError(t, err) + assert.NotNil(t, ss.Env) + }) + + t.Run("composition", func(t *testing.T) { + type TT struct { + *service.TradeService + } + ss := TT{} + err := ParseStructAndInject(&ss, &service.TradeService{}) + assert.NoError(t, err) + assert.NotNil(t, ss.TradeService) + }) + + t.Run("struct", func(t *testing.T) { + ss := struct { + a int + Env testEnvironment + }{ + a: 1, + } + err := ParseStructAndInject(&ss, testEnvironment{ + startTime: time.Now(), + }) + assert.NoError(t, err) + assert.NotEqual(t, time.Time{}, ss.Env.startTime) + }) + t.Run("interface/any", func(t *testing.T) { + ss := struct { + Any interface{} // anything + }{ + Any: nil, + } + err := ParseStructAndInject(&ss, &testEnvironment{ + startTime: time.Now(), + }) + assert.NoError(t, err) + assert.NotNil(t, ss.Any) + }) + t.Run("interface/stringer", func(t *testing.T) { + ss := struct { + Stringer types.Stringer // stringer interface + }{ + Stringer: nil, + } + err := ParseStructAndInject(&ss, &types.Trade{}) + assert.NoError(t, err) + assert.NotNil(t, ss.Stringer) + }) +} diff --git a/pkg/strategy/common/strategy.go b/pkg/strategy/common/strategy.go index 3ad80b468..41fec7136 100644 --- a/pkg/strategy/common/strategy.go +++ b/pkg/strategy/common/strategy.go @@ -20,7 +20,7 @@ type Strategy struct { OrderExecutor *bbgo.GeneralOrderExecutor } -func (s *Strategy) Setup(ctx context.Context, environ *bbgo.Environment, session *bbgo.ExchangeSession, market types.Market, strategyID, instanceID string) { +func (s *Strategy) Initialize(ctx context.Context, environ *bbgo.Environment, session *bbgo.ExchangeSession, market types.Market, strategyID, instanceID string) { s.parent = ctx s.ctx, s.cancel = context.WithCancel(ctx) @@ -53,6 +53,6 @@ func (s *Strategy) Setup(ctx context.Context, environ *bbgo.Environment, session s.OrderExecutor.BindProfitStats(s.ProfitStats) s.OrderExecutor.Bind() s.OrderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { - bbgo.Sync(ctx, s) + // bbgo.Sync(ctx, s) }) } diff --git a/pkg/strategy/linregmaker/strategy.go b/pkg/strategy/linregmaker/strategy.go index b8e5f9be0..fcc4b4f71 100644 --- a/pkg/strategy/linregmaker/strategy.go +++ b/pkg/strategy/linregmaker/strategy.go @@ -3,9 +3,10 @@ package linregmaker import ( "context" "fmt" - "github.com/c9s/bbgo/pkg/risk/dynamicrisk" "sync" + "github.com/c9s/bbgo/pkg/risk/dynamicrisk" + "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/util" @@ -221,20 +222,20 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { }) } - // Setup Exits + // Initialize Exits s.ExitMethods.SetAndSubscribe(session, s) - // Setup dynamic spread + // Initialize dynamic spread if s.DynamicSpread.IsEnabled() { s.DynamicSpread.Initialize(s.Symbol, session) } - // Setup dynamic exposure + // Initialize dynamic exposure if s.DynamicExposure.IsEnabled() { s.DynamicExposure.Initialize(s.Symbol, session) } - // Setup dynamic quantities + // Initialize dynamic quantities if len(s.DynamicQuantityIncrease) > 0 { s.DynamicQuantityIncrease.Initialize(s.Symbol, session) } diff --git a/pkg/strategy/rsicross/strategy.go b/pkg/strategy/rsicross/strategy.go new file mode 100644 index 000000000..167ab7ee2 --- /dev/null +++ b/pkg/strategy/rsicross/strategy.go @@ -0,0 +1,137 @@ +package rsicross + +import ( + "context" + "fmt" + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/risk/riskcontrol" + "github.com/c9s/bbgo/pkg/strategy/common" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "rsicross" + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + *common.Strategy + + Environment *bbgo.Environment + Market types.Market + + Symbol string `json:"symbol"` + Interval types.Interval `json:"interval"` + SlowWindow int `json:"slowWindow"` + FastWindow int `json:"fastWindow"` + + bbgo.OpenPositionOptions + + // risk related parameters + PositionHardLimit fixedpoint.Value `json:"positionHardLimit"` + MaxPositionQuantity fixedpoint.Value `json:"maxPositionQuantity"` + CircuitBreakLossThreshold fixedpoint.Value `json:"circuitBreakLossThreshold"` + CircuitBreakEMA types.IntervalWindow `json:"circuitBreakEMA"` + + positionRiskControl *riskcontrol.PositionRiskControl + circuitBreakRiskControl *riskcontrol.CircuitBreakRiskControl +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s:%s:%d-%d", ID, s.Symbol, s.Interval, s.FastWindow, s.SlowWindow) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + s.Strategy = &common.Strategy{} + s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) + + if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() { + log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...") + s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.OrderExecutor, s.PositionHardLimit, s.MaxPositionQuantity) + } + + if !s.CircuitBreakLossThreshold.IsZero() { + log.Infof("circuitBreakLossThreshold is configured, setting up CircuitBreakRiskControl...") + s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl( + s.Position, + session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA), + s.CircuitBreakLossThreshold, + s.ProfitStats) + } + + fastRsi := session.Indicators(s.Symbol).RSI(types.IntervalWindow{Interval: s.Interval, Window: s.FastWindow}) + slowRsi := session.Indicators(s.Symbol).RSI(types.IntervalWindow{Interval: s.Interval, Window: s.SlowWindow}) + rsiCross := indicator.Cross(fastRsi, slowRsi) + rsiCross.OnUpdate(func(v float64) { + switch indicator.CrossType(v) { + case indicator.CrossOver: + opts := s.OpenPositionOptions + opts.Long = true + + if price, ok := session.LastPrice(s.Symbol); ok { + opts.Price = price + } + + // opts.Price = closePrice + opts.Tags = []string{"rsiCrossOver"} + if _, err := s.OrderExecutor.OpenPosition(ctx, opts); err != nil { + logErr(err, "unable to open position") + } + + case indicator.CrossUnder: + if err := s.OrderExecutor.ClosePosition(ctx, fixedpoint.One); err != nil { + logErr(err, "failed to close position") + } + + } + }) + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + }) + + return nil +} + +func (s *Strategy) preloadKLines(inc *indicator.KLineStream, session *bbgo.ExchangeSession, symbol string, interval types.Interval) { + if store, ok := session.MarketDataStore(symbol); ok { + if kLinesData, ok := store.KLinesOfInterval(interval); ok { + for _, k := range *kLinesData { + inc.EmitUpdate(k) + } + } + } +} + +func logErr(err error, msgAndArgs ...interface{}) bool { + if err == nil { + return false + } + + if len(msgAndArgs) == 0 { + log.WithError(err).Error(err.Error()) + } else if len(msgAndArgs) == 1 { + msg := msgAndArgs[0].(string) + log.WithError(err).Error(msg) + } else if len(msgAndArgs) > 1 { + msg := msgAndArgs[0].(string) + log.WithError(err).Errorf(msg, msgAndArgs[1:]...) + } + + return true +} diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 302dbc6b6..6dc1dc659 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -101,7 +101,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { s.Strategy = &common.Strategy{} - s.Strategy.Setup(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) + s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) s.book = types.NewStreamBook(s.Symbol) s.book.BindStream(session.UserDataStream)