diff --git a/config/bollmaker.yaml b/config/bollmaker.yaml index 0cc53894c..5ecc67315 100644 --- a/config/bollmaker.yaml +++ b/config/bollmaker.yaml @@ -17,7 +17,7 @@ backtest: # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp startTime: "2022-05-01" - endTime: "2022-08-14" + endTime: "2023-11-01" sessions: - binance symbols: @@ -200,6 +200,15 @@ exchangeStrategies: # buyBelowNeutralSMA: when this set, it will only place buy order when the current price is below the SMA line. buyBelowNeutralSMA: true + # emaCross is used for turning buy on/off + # when short term EMA cross fast term EMA, turn on buy, + # otherwise, turn off buy + emaCross: + enabled: false + interval: 1h + fastWindow: 3 + slowWindow: 12 + exits: # roiTakeProfit is used to force taking profit by percentage of the position ROI (currently the price change) diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index 05e7d3c2e..e372eea1f 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -6,12 +6,11 @@ import ( "math" "sync" - indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2" - "github.com/c9s/bbgo/pkg/util" - "github.com/pkg/errors" "github.com/sirupsen/logrus" + indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" @@ -46,6 +45,16 @@ type BollingerSetting struct { BandWidth float64 `json:"bandWidth"` } +type EMACrossSetting struct { + Enabled bool `json:"enabled"` + Interval types.Interval `json:"interval"` + FastWindow int `json:"fastWindow"` + SlowWindow int `json:"slowWindow"` + + fastEMA, slowEMA *indicatorv2.EWMAStream + cross *indicatorv2.CrossStream +} + type Strategy struct { Environment *bbgo.Environment StandardIndicatorSet *bbgo.StandardIndicatorSet @@ -121,6 +130,9 @@ type Strategy struct { // BuyBelowNeutralSMA if true, the market maker will only place buy order when the current price is below the neutral band SMA. BuyBelowNeutralSMA bool `json:"buyBelowNeutralSMA"` + // EMACrossSetting is used for defining ema cross signal to turn on/off buy + EMACrossSetting *EMACrossSetting `json:"emaCross"` + // NeutralBollinger is the smaller range of the bollinger band // If price is in this band, it usually means the price is oscillating. // If price goes out of this band, we tend to not place sell orders or buy orders @@ -150,25 +162,24 @@ type Strategy struct { ShadowProtection bool `json:"shadowProtection"` ShadowProtectionRatio fixedpoint.Value `json:"shadowProtectionRatio"` - session *bbgo.ExchangeSession - book *types.StreamOrderBook - ExitMethods bbgo.ExitMethodSet `json:"exits"` // persistence fields Position *types.Position `json:"position,omitempty" persistence:"position"` ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` + session *bbgo.ExchangeSession + book *types.StreamOrderBook orderExecutor *bbgo.GeneralOrderExecutor - groupID uint32 - // defaultBoll is the BOLLINGER indicator we used for predicting the price. defaultBoll *indicatorv2.BOLLStream // neutralBoll is the neutral price section neutralBoll *indicatorv2.BOLLStream + shouldBuy bool + // StrategyController bbgo.StrategyController } @@ -202,6 +213,10 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.TrendEMA.Interval}) } + if s.EMACrossSetting != nil && s.EMACrossSetting.Enabled { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.EMACrossSetting.Interval}) + } + s.ExitMethods.SetAndSubscribe(session, s) } @@ -274,8 +289,8 @@ func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, k Quantity: sellQuantity, Price: askPrice, Market: s.Market, - GroupID: s.groupID, } + buyOrder := types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, @@ -283,7 +298,6 @@ func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, k Quantity: buyQuantity, Price: bidPrice, Market: s.Market, - GroupID: s.groupID, } var submitOrders []types.SubmitOrder @@ -435,14 +449,20 @@ func (s *Strategy) placeOrders(ctx context.Context, midPrice fixedpoint.Value, k } } + if !s.shouldBuy { + log.Infof("shouldBuy is turned off, skip placing buy order") + canBuy = false + } + if canSell { submitOrders = append(submitOrders, sellOrder) } + if canBuy { submitOrders = append(submitOrders, buyOrder) } - // condition for lower the average cost + // condition for lowering the average cost /* if midPrice < s.Position.AverageCost.MulFloat64(1.0-s.MinProfitSpread.Float64()) && canBuy { submitOrders = append(submitOrders, buyOrder) @@ -478,9 +498,26 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // StrategyController s.Status = types.StrategyStatusRunning + s.shouldBuy = true s.neutralBoll = session.Indicators(s.Symbol).BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth) s.defaultBoll = session.Indicators(s.Symbol).BOLL(s.DefaultBollinger.IntervalWindow, s.DefaultBollinger.BandWidth) + if s.EMACrossSetting != nil && s.EMACrossSetting.Enabled { + s.EMACrossSetting.fastEMA = session.Indicators(s.Symbol).EWMA(types.IntervalWindow{Interval: s.Interval, Window: s.EMACrossSetting.FastWindow}) + s.EMACrossSetting.slowEMA = session.Indicators(s.Symbol).EWMA(types.IntervalWindow{Interval: s.Interval, Window: s.EMACrossSetting.SlowWindow}) + s.EMACrossSetting.cross = indicatorv2.Cross(s.EMACrossSetting.fastEMA, s.EMACrossSetting.slowEMA) + s.EMACrossSetting.cross.OnUpdate(func(v float64) { + switch indicatorv2.CrossType(v) { + case indicatorv2.CrossOver: + s.shouldBuy = true + case indicatorv2.CrossUnder: + s.shouldBuy = false + // TODO: can partially close position when necessary + // s.orderExecutor.ClosePosition(ctx) + } + }) + } + // Setup dynamic spread if s.DynamicSpread.IsEnabled() { if s.DynamicSpread.Interval == "" { @@ -511,7 +548,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // calculate group id for orders instanceID := s.InstanceID() - s.groupID = util.FNV32(instanceID) // If position is nil, we need to allocate a new position for calculation if s.Position == nil { @@ -563,7 +599,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) session.UserDataStream.OnStart(func() { - if s.UseTickerPrice { + if !bbgo.IsBackTesting && s.UseTickerPrice { ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) if err != nil { return