From 1573a9acf3f3bd6136f31f109746489b324615fc Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 30 Jun 2022 16:35:00 +0800 Subject: [PATCH 01/15] strategy/supertrend: add linear regression as filter --- pkg/strategy/supertrend/strategy.go | 81 ++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index e91313be3d..561c33aa09 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -29,6 +29,51 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +// LinGre is Linear Regression baseline +type LinGre struct { + types.IntervalWindow + baseLineSlope float64 +} + +// Update Linear Regression baseline slope +func (lg *LinGre) Update(klines []types.KLine) { + if len(klines) < lg.Window { + lg.baseLineSlope = 0 + return + } + + var sumX, sumY, sumXSqr, sumXY float64 = 0, 0, 0, 0 + end := len(klines) - 1 // The last kline + for i := end; i >= end-lg.Window+1; i-- { + val := klines[i].GetClose().Float64() + per := float64(end - i + 1) + sumX += per + sumY += val + sumXSqr += per * per + sumXY += val * per + } + length := float64(lg.Window) + slope := (length*sumXY - sumX*sumY) / (length*sumXSqr - sumX*sumX) + average := sumY / length + endPrice := average - slope*sumX/length + slope + startPrice := endPrice + slope*(length-1) + lg.baseLineSlope = (length - 1) / (endPrice - startPrice) + + log.Debugf("linear regression baseline slope: %f", lg.baseLineSlope) +} + +func (lg *LinGre) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if lg.Interval != interval { + return + } + + lg.Update(window) +} + +func (lg *LinGre) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(lg.handleKLineWindowUpdate) +} + type Strategy struct { *bbgo.Graceful *bbgo.Persistence @@ -70,6 +115,9 @@ type Strategy struct { // SupertrendMultiplier ATR multiplier for calculation of supertrend SupertrendMultiplier float64 `json:"supertrendMultiplier"` + // Linear Regression + LinearRegression *LinGre `json:"linearRegression"` + // Leverage Leverage float64 `json:"leverage"` @@ -249,6 +297,22 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.ProfitStats = types.NewProfitStats(s.Market) } + // Linear Regression + if s.LinearRegression != nil { + if s.LinearRegression.Window == 0 { + s.LinearRegression = nil + } else { + // K-line store for Linear Regression + kLineStore, _ := session.MarketDataStore(s.Symbol) + s.LinearRegression.Bind(kLineStore) + + // Preload + if klines, ok := kLineStore.KLinesOfInterval(s.LinearRegression.Interval); ok { + s.LinearRegression.Update((*klines)[0:]) + } + } + } + // Setup order executor s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) s.orderExecutor.BindEnvironment(s.Environment) @@ -343,9 +407,22 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } + // Linear Regression + var lgSignal types.Direction + if s.LinearRegression != nil { + switch { + case s.LinearRegression.baseLineSlope > 0: + lgSignal = types.DirectionUp + case s.LinearRegression.baseLineSlope < 0: + lgSignal = types.DirectionDown + default: + lgSignal = types.DirectionNone + } + } + // Open position var side types.SideType - if stSignal == types.DirectionUp && demaSignal == types.DirectionUp { + if stSignal == types.DirectionUp && demaSignal == types.DirectionUp && (s.LinearRegression == nil || lgSignal == types.DirectionUp) { side = types.SideTypeBuy if s.StopLossByTriggeringK { s.currentStopLossPrice = kline.GetLow() @@ -353,7 +430,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if s.TakeProfitMultiplier > 0 { s.currentTakeProfitPrice = kline.GetClose().Add(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitMultiplier)) } - } else if stSignal == types.DirectionDown && demaSignal == types.DirectionDown { + } else if stSignal == types.DirectionDown && demaSignal == types.DirectionDown && (s.LinearRegression == nil || lgSignal == types.DirectionDown) { side = types.SideTypeSell if s.StopLossByTriggeringK { s.currentStopLossPrice = kline.GetHigh() From 0a0e5ac4d8312a1699860a42dede2b3980914567 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 5 Jul 2022 15:59:35 +0800 Subject: [PATCH 02/15] strategy/supertrend: config switch for stop by different signals --- config/supertrend.yaml | 25 +++++-- pkg/strategy/supertrend/strategy.go | 110 ++++++++++++++++------------ 2 files changed, 82 insertions(+), 53 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index db880bce23..5fa693f574 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -36,7 +36,7 @@ exchangeStrategies: symbol: BTCUSDT # interval is how long do you want to update your order price and quantity - interval: 1h + interval: 5m # leverage is the leverage of the orders leverage: 1.0 @@ -48,15 +48,26 @@ exchangeStrategies: # Supertrend indicator parameters superTrend: # ATR window used by Supertrend - averageTrueRangeWindow: 39 + averageTrueRangeWindow: 49 # ATR Multiplier for calculating super trend prices, the higher, the stronger the trends are - averageTrueRangeMultiplier: 3 + averageTrueRangeMultiplier: 4 + + # Use linear regression as trend confirmation + linearRegression: + interval: 5m + window: 80 # TP according to ATR multiple, 0 to disable this - takeProfitMultiplier: 3 + TakeProfitAtrMultiplier: 0 # Set SL price to the low of the triggering Kline - stopLossByTriggeringK: true + stopLossByTriggeringK: false + + # TP/SL by reversed supertrend signal + stopByReversedSupertrend: false + + # TP/SL by reversed DEMA signal + stopByReversedDema: false - # TP/SL by reversed signals - tpslBySignal: true + # TP/SL by reversed linear regression signal + stopByReversedLinGre: false diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 561c33aa09..51ca608152 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -22,6 +22,12 @@ const stateKey = "state-v1" var log = logrus.WithField("strategy", ID) +// TODO: SL by fixed percentage +// TODO: limit order if possible +// TODO: refine log +// TODO: lingre as indicator +// TODO: types.TradeStats + func init() { // Register the pointer of the strategy struct, // so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON) @@ -115,20 +121,26 @@ type Strategy struct { // SupertrendMultiplier ATR multiplier for calculation of supertrend SupertrendMultiplier float64 `json:"supertrendMultiplier"` - // Linear Regression - LinearRegression *LinGre `json:"linearRegression"` + // LinearRegression Use linear regression as trend confirmation + LinearRegression *LinGre `json:"linearRegression,omitempty"` // Leverage Leverage float64 `json:"leverage"` - // TakeProfitMultiplier TP according to ATR multiple, 0 to disable this - TakeProfitMultiplier float64 `json:"takeProfitMultiplier"` + // TakeProfitAtrMultiplier TP according to ATR multiple, 0 to disable this + TakeProfitAtrMultiplier float64 `json:"takeProfitAtrMultiplier"` - // StopLossByTriggeringK Set SL price to the low of the triggering Kline + // StopLossByTriggeringK Set SL price to the low/high of the triggering Kline StopLossByTriggeringK bool `json:"stopLossByTriggeringK"` - // TPSLBySignal TP/SL by reversed signals - TPSLBySignal bool `json:"tpslBySignal"` + // StopByReversedSupertrend TP/SL by reversed supertrend signal + StopByReversedSupertrend bool `json:"stopByReversedSupertrend"` + + // StopByReversedDema TP/SL by reversed DEMA signal + StopByReversedDema bool `json:"stopByReversedDema"` + + // StopByReversedLinGre TP/SL by reversed linear regression signal + StopByReversedLinGre bool `json:"stopByReversedLinGre"` currentTakeProfitPrice fixedpoint.Value currentStopLossPrice fixedpoint.Value @@ -360,10 +372,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Update indicators s.updateIndicators(kline) - // Get signals closePrice := kline.GetClose().Float64() openPrice := kline.GetOpen().Float64() + + // Supertrend signal stSignal := s.Supertrend.GetSignal() + + // DEMA signal var demaSignal types.Direction if closePrice > s.fastDEMA.Last() && closePrice > s.slowDEMA.Last() && !(openPrice > s.fastDEMA.Last() && openPrice > s.slowDEMA.Last()) { demaSignal = types.DirectionUp @@ -373,50 +388,53 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se demaSignal = types.DirectionNone } + // Linear Regression signal + var lgSignal types.Direction + if s.LinearRegression != nil { + switch { + case s.LinearRegression.baseLineSlope > 0: + lgSignal = types.DirectionUp + case s.LinearRegression.baseLineSlope < 0: + lgSignal = types.DirectionDown + default: + lgSignal = types.DirectionNone + } + } + base := s.Position.GetBase() baseSign := base.Sign() - // TP/SL if there's non-dust position + // TP/SL if there's non-dust position and meets the criteria if !s.Market.IsDustQuantity(base.Abs(), kline.GetClose()) { + stopNow := false + if s.StopLossByTriggeringK && !s.currentStopLossPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentStopLossPrice) > 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentStopLossPrice) < 0)) { - // SL by triggering Kline low - log.Infof("%s SL by triggering Kline low", s.Symbol) - bbgo.Notify("%s StopLoss by triggering the kline low", s.Symbol) - if err := s.ClosePosition(ctx, fixedpoint.One); err == nil { - s.currentStopLossPrice = fixedpoint.Zero - s.currentTakeProfitPrice = fixedpoint.Zero - } - } else if s.TakeProfitMultiplier > 0 && !s.currentTakeProfitPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) < 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) > 0)) { + // SL by triggering Kline low/high + bbgo.Notify("%s stop loss by triggering the kline low/high", s.Symbol) + stopNow = true + } else if s.TakeProfitAtrMultiplier > 0 && !s.currentTakeProfitPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) < 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) > 0)) { // TP by multiple of ATR - log.Infof("%s TP by multiple of ATR", s.Symbol) - bbgo.Notify("%s TakeProfit by multiple of ATR", s.Symbol) + bbgo.Notify("%s take profit by multiple of ATR", s.Symbol) + stopNow = true + } else if s.StopByReversedSupertrend && ((baseSign < 0 && stSignal == types.DirectionUp) || (baseSign > 0 && stSignal == types.DirectionDown)) { + // Use supertrend signal to TP/SL + bbgo.Notify("%s stop by the reversed signal of Supertrend", s.Symbol) + stopNow = true + } else if s.StopByReversedDema && ((baseSign < 0 && demaSignal == types.DirectionUp) || (baseSign > 0 && demaSignal == types.DirectionDown)) { + // Use DEMA signal to TP/SL + bbgo.Notify("%s stop by the reversed signal of DEMA", s.Symbol) + stopNow = true + } else if s.StopByReversedLinGre && ((baseSign < 0 && lgSignal == types.DirectionUp) || (baseSign > 0 && lgSignal == types.DirectionDown)) { + // Use linear regression signal to TP/SL + bbgo.Notify("%s stop by the reversed signal of linear regression", s.Symbol) + stopNow = true + } + + if stopNow { if err := s.ClosePosition(ctx, fixedpoint.One); err == nil { s.currentStopLossPrice = fixedpoint.Zero s.currentTakeProfitPrice = fixedpoint.Zero } - } else if s.TPSLBySignal { - // Use signals to TP/SL - log.Infof("%s TP/SL by reverse of DEMA or Supertrend", s.Symbol) - bbgo.Notify("%s TP/SL by reverse of DEMA or Supertrend", s.Symbol) - if (baseSign < 0 && (stSignal == types.DirectionUp || demaSignal == types.DirectionUp)) || (baseSign > 0 && (stSignal == types.DirectionDown || demaSignal == types.DirectionDown)) { - if err := s.ClosePosition(ctx, fixedpoint.One); err == nil { - s.currentStopLossPrice = fixedpoint.Zero - s.currentTakeProfitPrice = fixedpoint.Zero - } - } - } - } - - // Linear Regression - var lgSignal types.Direction - if s.LinearRegression != nil { - switch { - case s.LinearRegression.baseLineSlope > 0: - lgSignal = types.DirectionUp - case s.LinearRegression.baseLineSlope < 0: - lgSignal = types.DirectionDown - default: - lgSignal = types.DirectionNone } } @@ -427,16 +445,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if s.StopLossByTriggeringK { s.currentStopLossPrice = kline.GetLow() } - if s.TakeProfitMultiplier > 0 { - s.currentTakeProfitPrice = kline.GetClose().Add(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitMultiplier)) + if s.TakeProfitAtrMultiplier > 0 { + s.currentTakeProfitPrice = kline.GetClose().Add(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitAtrMultiplier)) } } else if stSignal == types.DirectionDown && demaSignal == types.DirectionDown && (s.LinearRegression == nil || lgSignal == types.DirectionDown) { side = types.SideTypeSell if s.StopLossByTriggeringK { s.currentStopLossPrice = kline.GetHigh() } - if s.TakeProfitMultiplier > 0 { - s.currentTakeProfitPrice = kline.GetClose().Sub(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitMultiplier)) + if s.TakeProfitAtrMultiplier > 0 { + s.currentTakeProfitPrice = kline.GetClose().Sub(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitAtrMultiplier)) } } From 5b3ba03042fde92cdd2370e7220bdabe005b51a9 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 5 Jul 2022 16:25:02 +0800 Subject: [PATCH 03/15] strategy/supertrend: preload indicators --- pkg/strategy/supertrend/strategy.go | 74 ++++++++++++++--------------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 51ca608152..22397fb07c 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -24,7 +24,6 @@ var log = logrus.WithField("strategy", ID) // TODO: SL by fixed percentage // TODO: limit order if possible -// TODO: refine log // TODO: lingre as indicator // TODO: types.TradeStats @@ -202,8 +201,7 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu orderForm := s.generateOrderForm(side, quantity, types.SideEffectTypeAutoRepay) - log.Infof("submit close position order %v", orderForm) - bbgo.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage) + bbgo.Notify("submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, orderForm) _, err := s.orderExecutor.SubmitOrders(ctx, orderForm) if err != nil { @@ -216,6 +214,10 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu // setupIndicators initializes indicators func (s *Strategy) setupIndicators() { + // K-line store for indicators + kLineStore, _ := s.session.MarketDataStore(s.Symbol) + + // DEMA if s.FastDEMAWindow == 0 { s.FastDEMAWindow = 144 } @@ -225,7 +227,19 @@ func (s *Strategy) setupIndicators() { s.SlowDEMAWindow = 169 } s.slowDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.SlowDEMAWindow}} + // Preload + if klines, ok := kLineStore.KLinesOfInterval(s.fastDEMA.Interval); ok { + for i := 0; i < len(*klines); i++ { + s.fastDEMA.Update((*klines)[i].GetClose().Float64()) + } + } + if klines, ok := kLineStore.KLinesOfInterval(s.slowDEMA.Interval); ok { + for i := 0; i < len(*klines); i++ { + s.slowDEMA.Update((*klines)[i].GetClose().Float64()) + } + } + // Supertrend if s.SupertrendWindow == 0 { s.SupertrendWindow = 39 } @@ -234,22 +248,26 @@ func (s *Strategy) setupIndicators() { } s.Supertrend = &indicator.Supertrend{IntervalWindow: types.IntervalWindow{Window: s.SupertrendWindow, Interval: s.Interval}, ATRMultiplier: s.SupertrendMultiplier} s.Supertrend.AverageTrueRange = &indicator.ATR{IntervalWindow: types.IntervalWindow{Window: s.SupertrendWindow, Interval: s.Interval}} + s.Supertrend.Bind(kLineStore) + // Preload + if klines, ok := kLineStore.KLinesOfInterval(s.Supertrend.Interval); ok { + for i := 0; i < len(*klines); i++ { + s.Supertrend.Update((*klines)[i].GetHigh().Float64(), (*klines)[i].GetLow().Float64(), (*klines)[i].GetClose().Float64()) + } + } -} - -// updateIndicators updates indicators -func (s *Strategy) updateIndicators(kline types.KLine) { - closePrice := kline.GetClose().Float64() + // Linear Regression + if s.LinearRegression != nil { + if s.LinearRegression.Window == 0 { + s.LinearRegression = nil + } else { + s.LinearRegression.Bind(kLineStore) - // Update indicators - if kline.Interval == s.fastDEMA.Interval { - s.fastDEMA.Update(closePrice) - } - if kline.Interval == s.slowDEMA.Interval { - s.slowDEMA.Update(closePrice) - } - if kline.Interval == s.Supertrend.Interval { - s.Supertrend.Update(kline.GetHigh().Float64(), kline.GetLow().Float64(), closePrice) + // Preload + if klines, ok := kLineStore.KLinesOfInterval(s.LinearRegression.Interval); ok { + s.LinearRegression.Update((*klines)[0:]) + } + } } } @@ -309,22 +327,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.ProfitStats = types.NewProfitStats(s.Market) } - // Linear Regression - if s.LinearRegression != nil { - if s.LinearRegression.Window == 0 { - s.LinearRegression = nil - } else { - // K-line store for Linear Regression - kLineStore, _ := session.MarketDataStore(s.Symbol) - s.LinearRegression.Bind(kLineStore) - - // Preload - if klines, ok := kLineStore.KLinesOfInterval(s.LinearRegression.Interval); ok { - s.LinearRegression.Update((*klines)[0:]) - } - } - } - // Setup order executor s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) s.orderExecutor.BindEnvironment(s.Environment) @@ -369,9 +371,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } - // Update indicators - s.updateIndicators(kline) - closePrice := kline.GetClose().Float64() openPrice := kline.GetOpen().Float64() @@ -460,16 +459,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // The default value of side is an empty string. Unless side is set by the checks above, the result of the following condition is false if side == types.SideTypeSell || side == types.SideTypeBuy { - log.Infof("open %s position for signal %v", s.Symbol, side) bbgo.Notify("open %s position for signal %v", s.Symbol, side) // Close opposite position if any if !s.Position.IsDust(kline.GetClose()) { if (side == types.SideTypeSell && s.Position.IsLong()) || (side == types.SideTypeBuy && s.Position.IsShort()) { - log.Infof("close existing %s position before open a new position", s.Symbol) bbgo.Notify("close existing %s position before open a new position", s.Symbol) _ = s.ClosePosition(ctx, fixedpoint.One) } else { - log.Infof("existing %s position has the same direction with the signal", s.Symbol) bbgo.Notify("existing %s position has the same direction with the signal", s.Symbol) return } From f0dc9d614725121337a87686dcdcfcfdea32acb0 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 5 Jul 2022 16:30:13 +0800 Subject: [PATCH 04/15] strategy/supertrend: add TradeStats --- pkg/strategy/supertrend/strategy.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 22397fb07c..9e783e8396 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -3,6 +3,7 @@ package supertrend import ( "context" "fmt" + "os" "sync" "github.com/c9s/bbgo/pkg/util" @@ -24,8 +25,6 @@ var log = logrus.WithField("strategy", ID) // TODO: SL by fixed percentage // TODO: limit order if possible -// TODO: lingre as indicator -// TODO: types.TradeStats func init() { // Register the pointer of the strategy struct, @@ -88,8 +87,9 @@ type Strategy struct { Market types.Market // persistence fields - Position *types.Position `json:"position,omitempty" persistence:"position"` - ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` // Order and trade orderExecutor *bbgo.GeneralOrderExecutor @@ -314,6 +314,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Position.Strategy = ID s.Position.StrategyInstanceID = s.InstanceID() + // Profit stats + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + // Trade stats + if s.TradeStats == nil { + s.TradeStats = &types.TradeStats{} + } + // Set fee rate if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 { s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{ @@ -322,15 +332,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) } - // Profit - if s.ProfitStats == nil { - s.ProfitStats = types.NewProfitStats(s.Market) - } - // Setup order executor s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) s.orderExecutor.BindEnvironment(s.Environment) s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.BindTradeStats(s.TradeStats) s.orderExecutor.Bind() // Sync position to redis on trade @@ -487,6 +493,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se close(s.stopC) _ = s.orderExecutor.GracefulCancel(ctx) + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) }) return nil From 91077ce61d579d3c2300b632d56479e8f009f6e5 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 5 Jul 2022 16:55:48 +0800 Subject: [PATCH 05/15] strategy/supertrend: add ExitMethod --- config/supertrend.yaml | 7 +++++++ pkg/strategy/supertrend/strategy.go | 11 +++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index 5fa693f574..5494ef691b 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -71,3 +71,10 @@ exchangeStrategies: # TP/SL by reversed linear regression signal stopByReversedLinGre: false + + exits: + # Protective stop loss + - protectiveStopLoss: + activationRatio: 0.6% + stopLossRatio: 1% + placeStopOrder: false diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 9e783e8396..44301a4d6b 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -23,8 +23,7 @@ const stateKey = "state-v1" var log = logrus.WithField("strategy", ID) -// TODO: SL by fixed percentage -// TODO: limit order if possible +// TODO: limit order for ATR TP func init() { // Register the pointer of the strategy struct, @@ -144,6 +143,9 @@ type Strategy struct { currentTakeProfitPrice fixedpoint.Value currentStopLossPrice fixedpoint.Value + // ExitMethods Exit methods + ExitMethods []bbgo.ExitMethod `json:"exits"` + // StrategyController bbgo.StrategyController } @@ -363,6 +365,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Setup indicators s.setupIndicators() + // Exit methods + for _, method := range s.ExitMethods { + method.Bind(session, s.orderExecutor) + } + s.currentStopLossPrice = fixedpoint.Zero s.currentTakeProfitPrice = fixedpoint.Zero From 2de16ac7d149a9d1c6e8c072fed270d6edb95d68 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Tue, 5 Jul 2022 17:11:58 +0800 Subject: [PATCH 06/15] strategy/supertrend: fix missing Bind() of DEMA --- pkg/strategy/supertrend/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 44301a4d6b..66d19ebd0d 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -224,11 +224,13 @@ func (s *Strategy) setupIndicators() { s.FastDEMAWindow = 144 } s.fastDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.FastDEMAWindow}} + s.fastDEMA.Bind(kLineStore) if s.SlowDEMAWindow == 0 { s.SlowDEMAWindow = 169 } s.slowDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.SlowDEMAWindow}} + s.slowDEMA.Bind(kLineStore) // Preload if klines, ok := kLineStore.KLinesOfInterval(s.fastDEMA.Interval); ok { for i := 0; i < len(*klines); i++ { From 1489759cf063878e711228c96ed1bda42a27856b Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 6 Jul 2022 10:49:50 +0800 Subject: [PATCH 07/15] strategy/supertrend: update doc --- config/supertrend.yaml | 2 +- doc/strategy/supertrend.md | 26 ++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index 5494ef691b..91f0a5b4f8 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -76,5 +76,5 @@ exchangeStrategies: # Protective stop loss - protectiveStopLoss: activationRatio: 0.6% - stopLossRatio: 1% + stopLossRatio: -2% placeStopOrder: false diff --git a/doc/strategy/supertrend.md b/doc/strategy/supertrend.md index 01421c94f1..f67ccae317 100644 --- a/doc/strategy/supertrend.md +++ b/doc/strategy/supertrend.md @@ -23,12 +23,30 @@ Supertrend strategy needs margin enabled in order to submit short orders, and yo - The MA window of the ATR indicator used by Supertrend. - `averageTrueRangeMultiplier` - Multiplier for calculating upper and lower bond prices, the higher, the stronger the trends are, but also makes it less sensitive. -- `takeProfitMultiplier` +- `linearRegression` + - Use linear regression as trend confirmation + - `interval` + - Time interval of linear regression + - `window` + - Window of linear regression +- `takeProfitAtrMultiplier` - TP according to ATR multiple, 0 to disable this. - `stopLossByTriggeringK` - - Set SL price to the low of the triggering Kline. -- `tpslBySignal` - - TP/SL by reversed signals. + - Set SL price to the low/high of the triggering Kline. +- `stopByReversedSupertrend` + - TP/SL by reversed supertrend signal. +- `stopByReversedDema` + - TP/SL by reversed DEMA signal. +- `stopByReversedLinGre` + - TP/SL by reversed linear regression signal. +- `exits` + - Exit methods to TP/SL + - `protectiveStopLoss` + - Protective stop loss + - `activationRatio` + - Ratio to activate the order + - `stopLossRatio` + - Ratio to stop loss (or take profit) #### Examples From 20955ad7dab32ab5fce6528d549f545eb6fdf202 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 6 Jul 2022 14:27:50 +0800 Subject: [PATCH 08/15] strategy/supertrend: update doc --- config/supertrend.yaml | 8 +++----- doc/strategy/supertrend.md | 9 +++------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index 91f0a5b4f8..d0cd94042c 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -73,8 +73,6 @@ exchangeStrategies: stopByReversedLinGre: false exits: - # Protective stop loss - - protectiveStopLoss: - activationRatio: 0.6% - stopLossRatio: -2% - placeStopOrder: false + # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) + - roiStopLoss: + percentage: 5% diff --git a/doc/strategy/supertrend.md b/doc/strategy/supertrend.md index f67ccae317..76fd08fd8b 100644 --- a/doc/strategy/supertrend.md +++ b/doc/strategy/supertrend.md @@ -41,12 +41,9 @@ Supertrend strategy needs margin enabled in order to submit short orders, and yo - TP/SL by reversed linear regression signal. - `exits` - Exit methods to TP/SL - - `protectiveStopLoss` - - Protective stop loss - - `activationRatio` - - Ratio to activate the order - - `stopLossRatio` - - Ratio to stop loss (or take profit) + - `roiStopLoss` + - The stop loss percentage of the position ROI (currently the price change) + - `percentage` #### Examples From 94cb1e672425892d5d07707aaec801b8c15c247b Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 6 Jul 2022 15:12:16 +0800 Subject: [PATCH 09/15] strategy/supertrend: update config --- config/supertrend.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index d0cd94042c..a4b5998271 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -75,4 +75,4 @@ exchangeStrategies: exits: # roiStopLoss is the stop loss percentage of the position ROI (currently the price change) - roiStopLoss: - percentage: 5% + percentage: 4% From c62e7bbb58d1fbe38a1f34ca93f166dfeb2ee82a Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 6 Jul 2022 16:26:30 +0800 Subject: [PATCH 10/15] strategy/supertrend: refactor to smaller functions --- pkg/strategy/supertrend/lingre.go | 65 +++++++++ pkg/strategy/supertrend/strategy.go | 198 ++++++++++++---------------- 2 files changed, 147 insertions(+), 116 deletions(-) create mode 100644 pkg/strategy/supertrend/lingre.go diff --git a/pkg/strategy/supertrend/lingre.go b/pkg/strategy/supertrend/lingre.go new file mode 100644 index 0000000000..1f0792d9bb --- /dev/null +++ b/pkg/strategy/supertrend/lingre.go @@ -0,0 +1,65 @@ +package supertrend + +import ( + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +// LinGre is Linear Regression baseline +type LinGre struct { + types.IntervalWindow + baseLineSlope float64 +} + +// Update Linear Regression baseline slope +func (lg *LinGre) Update(klines []types.KLine) { + if len(klines) < lg.Window { + lg.baseLineSlope = 0 + return + } + + var sumX, sumY, sumXSqr, sumXY float64 = 0, 0, 0, 0 + end := len(klines) - 1 // The last kline + for i := end; i >= end-lg.Window+1; i-- { + val := klines[i].GetClose().Float64() + per := float64(end - i + 1) + sumX += per + sumY += val + sumXSqr += per * per + sumXY += val * per + } + length := float64(lg.Window) + slope := (length*sumXY - sumX*sumY) / (length*sumXSqr - sumX*sumX) + average := sumY / length + endPrice := average - slope*sumX/length + slope + startPrice := endPrice + slope*(length-1) + lg.baseLineSlope = (length - 1) / (endPrice - startPrice) + + log.Debugf("linear regression baseline slope: %f", lg.baseLineSlope) +} + +func (lg *LinGre) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if lg.Interval != interval { + return + } + + lg.Update(window) +} + +func (lg *LinGre) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(lg.handleKLineWindowUpdate) +} + +// GetSignal get linear regression signal +func (lg *LinGre) GetSignal() types.Direction { + var lgSignal types.Direction = types.DirectionNone + + switch { + case lg.baseLineSlope > 0: + lgSignal = types.DirectionUp + case lg.baseLineSlope < 0: + lgSignal = types.DirectionDown + } + + return lgSignal +} diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 66d19ebd0d..c03a2c3236 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -32,51 +32,6 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } -// LinGre is Linear Regression baseline -type LinGre struct { - types.IntervalWindow - baseLineSlope float64 -} - -// Update Linear Regression baseline slope -func (lg *LinGre) Update(klines []types.KLine) { - if len(klines) < lg.Window { - lg.baseLineSlope = 0 - return - } - - var sumX, sumY, sumXSqr, sumXY float64 = 0, 0, 0, 0 - end := len(klines) - 1 // The last kline - for i := end; i >= end-lg.Window+1; i-- { - val := klines[i].GetClose().Float64() - per := float64(end - i + 1) - sumX += per - sumY += val - sumXSqr += per * per - sumXY += val * per - } - length := float64(lg.Window) - slope := (length*sumXY - sumX*sumY) / (length*sumXSqr - sumX*sumX) - average := sumY / length - endPrice := average - slope*sumX/length + slope - startPrice := endPrice + slope*(length-1) - lg.baseLineSlope = (length - 1) / (endPrice - startPrice) - - log.Debugf("linear regression baseline slope: %f", lg.baseLineSlope) -} - -func (lg *LinGre) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { - if lg.Interval != interval { - return - } - - lg.Update(window) -} - -func (lg *LinGre) Bind(updater indicator.KLineWindowUpdater) { - updater.OnKLineWindowUpdate(lg.handleKLineWindowUpdate) -} - type Strategy struct { *bbgo.Graceful *bbgo.Persistence @@ -214,6 +169,36 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu return err } +// preloadDema preloads DEMA indicators +func preloadDema(fastDEMA *indicator.DEMA, slowDEMA *indicator.DEMA, kLineStore *bbgo.MarketDataStore) { + if klines, ok := kLineStore.KLinesOfInterval(fastDEMA.Interval); ok { + for i := 0; i < len(*klines); i++ { + fastDEMA.Update((*klines)[i].GetClose().Float64()) + } + } + if klines, ok := kLineStore.KLinesOfInterval(slowDEMA.Interval); ok { + for i := 0; i < len(*klines); i++ { + slowDEMA.Update((*klines)[i].GetClose().Float64()) + } + } +} + +// preloadSupertrend preloads supertrend indicator +func preloadSupertrend(supertrend *indicator.Supertrend, kLineStore *bbgo.MarketDataStore) { + if klines, ok := kLineStore.KLinesOfInterval(supertrend.Interval); ok { + for i := 0; i < len(*klines); i++ { + supertrend.Update((*klines)[i].GetHigh().Float64(), (*klines)[i].GetLow().Float64(), (*klines)[i].GetClose().Float64()) + } + } +} + +// preloadLinGre preloads linear regression indicator +func preloadLinGre(linearRegression *LinGre, kLineStore *bbgo.MarketDataStore) { + if klines, ok := kLineStore.KLinesOfInterval(linearRegression.Interval); ok { + linearRegression.Update((*klines)[0:]) + } +} + // setupIndicators initializes indicators func (s *Strategy) setupIndicators() { // K-line store for indicators @@ -231,17 +216,7 @@ func (s *Strategy) setupIndicators() { } s.slowDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.SlowDEMAWindow}} s.slowDEMA.Bind(kLineStore) - // Preload - if klines, ok := kLineStore.KLinesOfInterval(s.fastDEMA.Interval); ok { - for i := 0; i < len(*klines); i++ { - s.fastDEMA.Update((*klines)[i].GetClose().Float64()) - } - } - if klines, ok := kLineStore.KLinesOfInterval(s.slowDEMA.Interval); ok { - for i := 0; i < len(*klines); i++ { - s.slowDEMA.Update((*klines)[i].GetClose().Float64()) - } - } + preloadDema(s.fastDEMA, s.slowDEMA, kLineStore) // Supertrend if s.SupertrendWindow == 0 { @@ -253,12 +228,7 @@ func (s *Strategy) setupIndicators() { s.Supertrend = &indicator.Supertrend{IntervalWindow: types.IntervalWindow{Window: s.SupertrendWindow, Interval: s.Interval}, ATRMultiplier: s.SupertrendMultiplier} s.Supertrend.AverageTrueRange = &indicator.ATR{IntervalWindow: types.IntervalWindow{Window: s.SupertrendWindow, Interval: s.Interval}} s.Supertrend.Bind(kLineStore) - // Preload - if klines, ok := kLineStore.KLinesOfInterval(s.Supertrend.Interval); ok { - for i := 0; i < len(*klines); i++ { - s.Supertrend.Update((*klines)[i].GetHigh().Float64(), (*klines)[i].GetLow().Float64(), (*klines)[i].GetClose().Float64()) - } - } + preloadSupertrend(s.Supertrend, kLineStore) // Linear Regression if s.LinearRegression != nil { @@ -266,15 +236,54 @@ func (s *Strategy) setupIndicators() { s.LinearRegression = nil } else { s.LinearRegression.Bind(kLineStore) - - // Preload - if klines, ok := kLineStore.KLinesOfInterval(s.LinearRegression.Interval); ok { - s.LinearRegression.Update((*klines)[0:]) - } + preloadLinGre(s.LinearRegression, kLineStore) } } } +// getDemaSignal get current DEMA signal +func (s *Strategy) getDemaSignal(openPrice float64, closePrice float64) types.Direction { + var demaSignal types.Direction = types.DirectionNone + + if closePrice > s.fastDEMA.Last() && closePrice > s.slowDEMA.Last() && !(openPrice > s.fastDEMA.Last() && openPrice > s.slowDEMA.Last()) { + demaSignal = types.DirectionUp + } else if closePrice < s.fastDEMA.Last() && closePrice < s.slowDEMA.Last() && !(openPrice < s.fastDEMA.Last() && openPrice < s.slowDEMA.Last()) { + demaSignal = types.DirectionDown + } + + return demaSignal +} + +func (s *Strategy) shouldStop(kline types.KLine, stSignal types.Direction, demaSignal types.Direction, lgSignal types.Direction) bool { + stopNow := false + base := s.Position.GetBase() + baseSign := base.Sign() + + if s.StopLossByTriggeringK && !s.currentStopLossPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentStopLossPrice) > 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentStopLossPrice) < 0)) { + // SL by triggering Kline low/high + bbgo.Notify("%s stop loss by triggering the kline low/high", s.Symbol) + stopNow = true + } else if s.TakeProfitAtrMultiplier > 0 && !s.currentTakeProfitPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) < 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) > 0)) { + // TP by multiple of ATR + bbgo.Notify("%s take profit by multiple of ATR", s.Symbol) + stopNow = true + } else if s.StopByReversedSupertrend && ((baseSign < 0 && stSignal == types.DirectionUp) || (baseSign > 0 && stSignal == types.DirectionDown)) { + // Use supertrend signal to TP/SL + bbgo.Notify("%s stop by the reversed signal of Supertrend", s.Symbol) + stopNow = true + } else if s.StopByReversedDema && ((baseSign < 0 && demaSignal == types.DirectionUp) || (baseSign > 0 && demaSignal == types.DirectionDown)) { + // Use DEMA signal to TP/SL + bbgo.Notify("%s stop by the reversed signal of DEMA", s.Symbol) + stopNow = true + } else if s.StopByReversedLinGre && ((baseSign < 0 && lgSignal == types.DirectionUp) || (baseSign > 0 && lgSignal == types.DirectionDown)) { + // Use linear regression signal to TP/SL + bbgo.Notify("%s stop by the reversed signal of linear regression", s.Symbol) + stopNow = true + } + + return stopNow +} + func (s *Strategy) generateOrderForm(side types.SideType, quantity fixedpoint.Value, marginOrderSideEffect types.MarginOrderSideEffectType) types.SubmitOrder { orderForm := types.SubmitOrder{ Symbol: s.Symbol, @@ -393,62 +402,19 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se stSignal := s.Supertrend.GetSignal() // DEMA signal - var demaSignal types.Direction - if closePrice > s.fastDEMA.Last() && closePrice > s.slowDEMA.Last() && !(openPrice > s.fastDEMA.Last() && openPrice > s.slowDEMA.Last()) { - demaSignal = types.DirectionUp - } else if closePrice < s.fastDEMA.Last() && closePrice < s.slowDEMA.Last() && !(openPrice < s.fastDEMA.Last() && openPrice < s.slowDEMA.Last()) { - demaSignal = types.DirectionDown - } else { - demaSignal = types.DirectionNone - } + demaSignal := s.getDemaSignal(openPrice, closePrice) // Linear Regression signal var lgSignal types.Direction if s.LinearRegression != nil { - switch { - case s.LinearRegression.baseLineSlope > 0: - lgSignal = types.DirectionUp - case s.LinearRegression.baseLineSlope < 0: - lgSignal = types.DirectionDown - default: - lgSignal = types.DirectionNone - } + lgSignal = s.LinearRegression.GetSignal() } - base := s.Position.GetBase() - baseSign := base.Sign() - // TP/SL if there's non-dust position and meets the criteria - if !s.Market.IsDustQuantity(base.Abs(), kline.GetClose()) { - stopNow := false - - if s.StopLossByTriggeringK && !s.currentStopLossPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentStopLossPrice) > 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentStopLossPrice) < 0)) { - // SL by triggering Kline low/high - bbgo.Notify("%s stop loss by triggering the kline low/high", s.Symbol) - stopNow = true - } else if s.TakeProfitAtrMultiplier > 0 && !s.currentTakeProfitPrice.IsZero() && ((baseSign < 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) < 0) || (baseSign > 0 && kline.GetClose().Compare(s.currentTakeProfitPrice) > 0)) { - // TP by multiple of ATR - bbgo.Notify("%s take profit by multiple of ATR", s.Symbol) - stopNow = true - } else if s.StopByReversedSupertrend && ((baseSign < 0 && stSignal == types.DirectionUp) || (baseSign > 0 && stSignal == types.DirectionDown)) { - // Use supertrend signal to TP/SL - bbgo.Notify("%s stop by the reversed signal of Supertrend", s.Symbol) - stopNow = true - } else if s.StopByReversedDema && ((baseSign < 0 && demaSignal == types.DirectionUp) || (baseSign > 0 && demaSignal == types.DirectionDown)) { - // Use DEMA signal to TP/SL - bbgo.Notify("%s stop by the reversed signal of DEMA", s.Symbol) - stopNow = true - } else if s.StopByReversedLinGre && ((baseSign < 0 && lgSignal == types.DirectionUp) || (baseSign > 0 && lgSignal == types.DirectionDown)) { - // Use linear regression signal to TP/SL - bbgo.Notify("%s stop by the reversed signal of linear regression", s.Symbol) - stopNow = true - } - - if stopNow { - if err := s.ClosePosition(ctx, fixedpoint.One); err == nil { - s.currentStopLossPrice = fixedpoint.Zero - s.currentTakeProfitPrice = fixedpoint.Zero - } + if !s.Market.IsDustQuantity(s.Position.GetBase().Abs(), kline.GetClose()) && s.shouldStop(kline, stSignal, demaSignal, lgSignal) { + if err := s.ClosePosition(ctx, fixedpoint.One); err == nil { + s.currentStopLossPrice = fixedpoint.Zero + s.currentTakeProfitPrice = fixedpoint.Zero } } From 6c93c42ef65a60a4ff806cc5a6d289ae6cd7728a Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 6 Jul 2022 16:45:19 +0800 Subject: [PATCH 11/15] strategy/supertrend: pull double dema into a single file --- pkg/strategy/supertrend/double_dema.go | 62 +++++++++++++++++++++++++ pkg/strategy/supertrend/lingre.go | 8 ++++ pkg/strategy/supertrend/strategy.go | 63 ++++---------------------- 3 files changed, 78 insertions(+), 55 deletions(-) create mode 100644 pkg/strategy/supertrend/double_dema.go diff --git a/pkg/strategy/supertrend/double_dema.go b/pkg/strategy/supertrend/double_dema.go new file mode 100644 index 0000000000..f7dae43a91 --- /dev/null +++ b/pkg/strategy/supertrend/double_dema.go @@ -0,0 +1,62 @@ +package supertrend + +import ( + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type DoubleDema struct { + Interval types.Interval `json:"interval"` + + // FastDEMAWindow DEMA window for checking breakout + FastDEMAWindow int `json:"fastDEMAWindow"` + // SlowDEMAWindow DEMA window for checking breakout + SlowDEMAWindow int `json:"slowDEMAWindow"` + fastDEMA *indicator.DEMA + slowDEMA *indicator.DEMA +} + +// getDemaSignal get current DEMA signal +func (dd *DoubleDema) getDemaSignal(openPrice float64, closePrice float64) types.Direction { + var demaSignal types.Direction = types.DirectionNone + + if closePrice > dd.fastDEMA.Last() && closePrice > dd.slowDEMA.Last() && !(openPrice > dd.fastDEMA.Last() && openPrice > dd.slowDEMA.Last()) { + demaSignal = types.DirectionUp + } else if closePrice < dd.fastDEMA.Last() && closePrice < dd.slowDEMA.Last() && !(openPrice < dd.fastDEMA.Last() && openPrice < dd.slowDEMA.Last()) { + demaSignal = types.DirectionDown + } + + return demaSignal +} + +// preloadDema preloads DEMA indicators +func (dd *DoubleDema) preloadDema(kLineStore *bbgo.MarketDataStore) { + if klines, ok := kLineStore.KLinesOfInterval(dd.fastDEMA.Interval); ok { + for i := 0; i < len(*klines); i++ { + dd.fastDEMA.Update((*klines)[i].GetClose().Float64()) + } + } + if klines, ok := kLineStore.KLinesOfInterval(dd.slowDEMA.Interval); ok { + for i := 0; i < len(*klines); i++ { + dd.slowDEMA.Update((*klines)[i].GetClose().Float64()) + } + } +} + +// setupDoubleDema initializes double DEMA indicators +func (dd *DoubleDema) setupDoubleDema(kLineStore *bbgo.MarketDataStore) { + // DEMA + if dd.FastDEMAWindow == 0 { + dd.FastDEMAWindow = 144 + } + dd.fastDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: dd.Interval, Window: dd.FastDEMAWindow}} + dd.fastDEMA.Bind(kLineStore) + + if dd.SlowDEMAWindow == 0 { + dd.SlowDEMAWindow = 169 + } + dd.slowDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: dd.Interval, Window: dd.SlowDEMAWindow}} + dd.slowDEMA.Bind(kLineStore) + dd.preloadDema(kLineStore) +} diff --git a/pkg/strategy/supertrend/lingre.go b/pkg/strategy/supertrend/lingre.go index 1f0792d9bb..6760e0b974 100644 --- a/pkg/strategy/supertrend/lingre.go +++ b/pkg/strategy/supertrend/lingre.go @@ -1,6 +1,7 @@ package supertrend import ( + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" ) @@ -63,3 +64,10 @@ func (lg *LinGre) GetSignal() types.Direction { return lgSignal } + +// preloadLinGre preloads linear regression indicator +func (lg *LinGre) preload(kLineStore *bbgo.MarketDataStore) { + if klines, ok := kLineStore.KLinesOfInterval(lg.Interval); ok { + lg.Update((*klines)[0:]) + } +} diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index c03a2c3236..522a71e252 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -59,13 +59,6 @@ type Strategy struct { // Interval is how long do you want to update your order price and quantity Interval types.Interval `json:"interval"` - // FastDEMAWindow DEMA window for checking breakout - FastDEMAWindow int `json:"fastDEMAWindow"` - // SlowDEMAWindow DEMA window for checking breakout - SlowDEMAWindow int `json:"slowDEMAWindow"` - fastDEMA *indicator.DEMA - slowDEMA *indicator.DEMA - // SuperTrend indicator // SuperTrend SuperTrend `json:"superTrend"` Supertrend *indicator.Supertrend @@ -74,6 +67,9 @@ type Strategy struct { // SupertrendMultiplier ATR multiplier for calculation of supertrend SupertrendMultiplier float64 `json:"supertrendMultiplier"` + // Double DEMA + DoubleDema + // LinearRegression Use linear regression as trend confirmation LinearRegression *LinGre `json:"linearRegression,omitempty"` @@ -169,20 +165,6 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu return err } -// preloadDema preloads DEMA indicators -func preloadDema(fastDEMA *indicator.DEMA, slowDEMA *indicator.DEMA, kLineStore *bbgo.MarketDataStore) { - if klines, ok := kLineStore.KLinesOfInterval(fastDEMA.Interval); ok { - for i := 0; i < len(*klines); i++ { - fastDEMA.Update((*klines)[i].GetClose().Float64()) - } - } - if klines, ok := kLineStore.KLinesOfInterval(slowDEMA.Interval); ok { - for i := 0; i < len(*klines); i++ { - slowDEMA.Update((*klines)[i].GetClose().Float64()) - } - } -} - // preloadSupertrend preloads supertrend indicator func preloadSupertrend(supertrend *indicator.Supertrend, kLineStore *bbgo.MarketDataStore) { if klines, ok := kLineStore.KLinesOfInterval(supertrend.Interval); ok { @@ -192,31 +174,13 @@ func preloadSupertrend(supertrend *indicator.Supertrend, kLineStore *bbgo.Market } } -// preloadLinGre preloads linear regression indicator -func preloadLinGre(linearRegression *LinGre, kLineStore *bbgo.MarketDataStore) { - if klines, ok := kLineStore.KLinesOfInterval(linearRegression.Interval); ok { - linearRegression.Update((*klines)[0:]) - } -} - // setupIndicators initializes indicators func (s *Strategy) setupIndicators() { // K-line store for indicators kLineStore, _ := s.session.MarketDataStore(s.Symbol) - // DEMA - if s.FastDEMAWindow == 0 { - s.FastDEMAWindow = 144 - } - s.fastDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.FastDEMAWindow}} - s.fastDEMA.Bind(kLineStore) - - if s.SlowDEMAWindow == 0 { - s.SlowDEMAWindow = 169 - } - s.slowDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.SlowDEMAWindow}} - s.slowDEMA.Bind(kLineStore) - preloadDema(s.fastDEMA, s.slowDEMA, kLineStore) + // Double DEMA + s.setupDoubleDema(kLineStore) // Supertrend if s.SupertrendWindow == 0 { @@ -234,26 +198,15 @@ func (s *Strategy) setupIndicators() { if s.LinearRegression != nil { if s.LinearRegression.Window == 0 { s.LinearRegression = nil + } else if s.LinearRegression.Interval == "" { + s.LinearRegression = nil } else { s.LinearRegression.Bind(kLineStore) - preloadLinGre(s.LinearRegression, kLineStore) + s.LinearRegression.preload(kLineStore) } } } -// getDemaSignal get current DEMA signal -func (s *Strategy) getDemaSignal(openPrice float64, closePrice float64) types.Direction { - var demaSignal types.Direction = types.DirectionNone - - if closePrice > s.fastDEMA.Last() && closePrice > s.slowDEMA.Last() && !(openPrice > s.fastDEMA.Last() && openPrice > s.slowDEMA.Last()) { - demaSignal = types.DirectionUp - } else if closePrice < s.fastDEMA.Last() && closePrice < s.slowDEMA.Last() && !(openPrice < s.fastDEMA.Last() && openPrice < s.slowDEMA.Last()) { - demaSignal = types.DirectionDown - } - - return demaSignal -} - func (s *Strategy) shouldStop(kline types.KLine, stSignal types.Direction, demaSignal types.Direction, lgSignal types.Direction) bool { stopNow := false base := s.Position.GetBase() From 8aa5b706b6554eca6968e4cd6bdf524e52daacaf Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 6 Jul 2022 17:05:38 +0800 Subject: [PATCH 12/15] strategy/supertrend: fix double dema missing interval --- pkg/strategy/supertrend/double_dema.go | 5 ++++- pkg/strategy/supertrend/strategy.go | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/supertrend/double_dema.go b/pkg/strategy/supertrend/double_dema.go index f7dae43a91..3f454dab7a 100644 --- a/pkg/strategy/supertrend/double_dema.go +++ b/pkg/strategy/supertrend/double_dema.go @@ -45,7 +45,9 @@ func (dd *DoubleDema) preloadDema(kLineStore *bbgo.MarketDataStore) { } // setupDoubleDema initializes double DEMA indicators -func (dd *DoubleDema) setupDoubleDema(kLineStore *bbgo.MarketDataStore) { +func (dd *DoubleDema) setupDoubleDema(kLineStore *bbgo.MarketDataStore, interval types.Interval) { + dd.Interval = interval + // DEMA if dd.FastDEMAWindow == 0 { dd.FastDEMAWindow = 144 @@ -58,5 +60,6 @@ func (dd *DoubleDema) setupDoubleDema(kLineStore *bbgo.MarketDataStore) { } dd.slowDEMA = &indicator.DEMA{IntervalWindow: types.IntervalWindow{Interval: dd.Interval, Window: dd.SlowDEMAWindow}} dd.slowDEMA.Bind(kLineStore) + dd.preloadDema(kLineStore) } diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 522a71e252..cbb5b0217b 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -56,6 +56,9 @@ type Strategy struct { // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` + // Double DEMA + DoubleDema + // Interval is how long do you want to update your order price and quantity Interval types.Interval `json:"interval"` @@ -67,9 +70,6 @@ type Strategy struct { // SupertrendMultiplier ATR multiplier for calculation of supertrend SupertrendMultiplier float64 `json:"supertrendMultiplier"` - // Double DEMA - DoubleDema - // LinearRegression Use linear regression as trend confirmation LinearRegression *LinGre `json:"linearRegression,omitempty"` @@ -180,7 +180,7 @@ func (s *Strategy) setupIndicators() { kLineStore, _ := s.session.MarketDataStore(s.Symbol) // Double DEMA - s.setupDoubleDema(kLineStore) + s.setupDoubleDema(kLineStore, s.Interval) // Supertrend if s.SupertrendWindow == 0 { From c43d4e0b24df681afd3c51d7c2a61d86f3544cbb Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 6 Jul 2022 18:11:09 +0800 Subject: [PATCH 13/15] strategy/supertrend: func to get order side --- config/supertrend.yaml | 6 ++--- pkg/strategy/supertrend/strategy.go | 42 +++++++++++++++++++---------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index a4b5998271..e51031ee21 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -18,8 +18,8 @@ backtest: # for testing max draw down (MDD) at 03-12 # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp - startTime: "2022-04-01" - endTime: "2022-04-30" + startTime: "2022-03-01" + endTime: "2022-06-30" symbols: - BTCUSDT accounts: @@ -27,7 +27,7 @@ backtest: makerCommission: 10 # 0.15% takerCommission: 15 # 0.15% balances: - BTC: 1.0 + BTC: 10.0 USDT: 10000.0 exchangeStrategies: diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index cbb5b0217b..5b5d07acc2 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -237,6 +237,18 @@ func (s *Strategy) shouldStop(kline types.KLine, stSignal types.Direction, demaS return stopNow } +func (s *Strategy) getSide(stSignal types.Direction, demaSignal types.Direction, lgSignal types.Direction) types.SideType { + var side types.SideType + + if stSignal == types.DirectionUp && demaSignal == types.DirectionUp && (s.LinearRegression == nil || lgSignal == types.DirectionUp) { + side = types.SideTypeBuy + } else if stSignal == types.DirectionDown && demaSignal == types.DirectionDown && (s.LinearRegression == nil || lgSignal == types.DirectionDown) { + side = types.SideTypeSell + } + + return side +} + func (s *Strategy) generateOrderForm(side types.SideType, quantity fixedpoint.Value, marginOrderSideEffect types.MarginOrderSideEffectType) types.SubmitOrder { orderForm := types.SubmitOrder{ Symbol: s.Symbol, @@ -348,14 +360,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } - closePrice := kline.GetClose().Float64() - openPrice := kline.GetOpen().Float64() + closePrice := kline.GetClose() + openPrice := kline.GetOpen() + closePrice64 := closePrice.Float64() + openPrice64 := openPrice.Float64() // Supertrend signal stSignal := s.Supertrend.GetSignal() // DEMA signal - demaSignal := s.getDemaSignal(openPrice, closePrice) + demaSignal := s.getDemaSignal(openPrice64, closePrice64) // Linear Regression signal var lgSignal types.Direction @@ -364,38 +378,38 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } // TP/SL if there's non-dust position and meets the criteria - if !s.Market.IsDustQuantity(s.Position.GetBase().Abs(), kline.GetClose()) && s.shouldStop(kline, stSignal, demaSignal, lgSignal) { + if !s.Market.IsDustQuantity(s.Position.GetBase().Abs(), closePrice) && s.shouldStop(kline, stSignal, demaSignal, lgSignal) { if err := s.ClosePosition(ctx, fixedpoint.One); err == nil { s.currentStopLossPrice = fixedpoint.Zero s.currentTakeProfitPrice = fixedpoint.Zero } } - // Open position - var side types.SideType - if stSignal == types.DirectionUp && demaSignal == types.DirectionUp && (s.LinearRegression == nil || lgSignal == types.DirectionUp) { - side = types.SideTypeBuy + // Get order side + side := s.getSide(stSignal, demaSignal, lgSignal) + // Set TP/SL price if needed + if side == types.SideTypeBuy { if s.StopLossByTriggeringK { s.currentStopLossPrice = kline.GetLow() } if s.TakeProfitAtrMultiplier > 0 { - s.currentTakeProfitPrice = kline.GetClose().Add(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitAtrMultiplier)) + s.currentTakeProfitPrice = closePrice.Add(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitAtrMultiplier)) } - } else if stSignal == types.DirectionDown && demaSignal == types.DirectionDown && (s.LinearRegression == nil || lgSignal == types.DirectionDown) { - side = types.SideTypeSell + } else if side == types.SideTypeSell { if s.StopLossByTriggeringK { s.currentStopLossPrice = kline.GetHigh() } if s.TakeProfitAtrMultiplier > 0 { - s.currentTakeProfitPrice = kline.GetClose().Sub(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitAtrMultiplier)) + s.currentTakeProfitPrice = closePrice.Sub(fixedpoint.NewFromFloat(s.Supertrend.AverageTrueRange.Last() * s.TakeProfitAtrMultiplier)) } } + // Open position // The default value of side is an empty string. Unless side is set by the checks above, the result of the following condition is false if side == types.SideTypeSell || side == types.SideTypeBuy { bbgo.Notify("open %s position for signal %v", s.Symbol, side) // Close opposite position if any - if !s.Position.IsDust(kline.GetClose()) { + if !s.Position.IsDust(closePrice) { if (side == types.SideTypeSell && s.Position.IsLong()) || (side == types.SideTypeBuy && s.Position.IsShort()) { bbgo.Notify("close existing %s position before open a new position", s.Symbol) _ = s.ClosePosition(ctx, fixedpoint.One) @@ -405,7 +419,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } - orderForm := s.generateOrderForm(side, s.calculateQuantity(kline.GetClose()), types.SideEffectTypeMarginBuy) + orderForm := s.generateOrderForm(side, s.calculateQuantity(closePrice), types.SideEffectTypeMarginBuy) log.Infof("submit open position order %v", orderForm) _, err := s.orderExecutor.SubmitOrders(ctx, orderForm) if err != nil { From 10355bf3594e8cb555685fee7b9dfd152e90072a Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Wed, 6 Jul 2022 19:05:02 +0800 Subject: [PATCH 14/15] strategy/supertrend: update config --- config/supertrend.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index e51031ee21..6442a41675 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -27,7 +27,7 @@ backtest: makerCommission: 10 # 0.15% takerCommission: 15 # 0.15% balances: - BTC: 10.0 + BTC: 1.0 USDT: 10000.0 exchangeStrategies: From 61174a7dfc51faaf6759643257890d2a1bc13a31 Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Thu, 7 Jul 2022 10:14:15 +0800 Subject: [PATCH 15/15] strategy/supertrend: update config --- config/supertrend.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/supertrend.yaml b/config/supertrend.yaml index 6442a41675..8f856b3d98 100644 --- a/config/supertrend.yaml +++ b/config/supertrend.yaml @@ -18,7 +18,7 @@ backtest: # for testing max draw down (MDD) at 03-12 # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp - startTime: "2022-03-01" + startTime: "2022-01-01" endTime: "2022-06-30" symbols: - BTCUSDT