From 58736b1b2d01496f43124a4b978be9b44ba29741 Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 29 Sep 2022 20:15:10 +0900 Subject: [PATCH 1/2] refactor: extract stoploss, fix highest/lowest in trailingExit --- config/drift.yaml | 28 ++++---- config/driftBTC.yaml | 38 +++++------ pkg/bbgo/order_executor_general.go | 2 +- pkg/strategy/drift/stoploss.go | 19 ++++++ pkg/strategy/drift/strategy.go | 103 +++++------------------------ 5 files changed, 67 insertions(+), 123 deletions(-) create mode 100644 pkg/strategy/drift/stoploss.go diff --git a/config/drift.yaml b/config/drift.yaml index fcbd29373..0ace1ba49 100644 --- a/config/drift.yaml +++ b/config/drift.yaml @@ -26,35 +26,31 @@ exchangeStrategies: canvasPath: "./output.png" symbol: ETHBUSD limitOrder: false + quantity: 0.01 # kline interval for indicators - interval: 2m - window: 6 + interval: 1m + window: 1 + useAtr: true + useStopLoss: true stoploss: 0.23% source: ohlc4 predictOffset: 2 noTrailingStopLoss: false trailingStopLossType: kline # stddev on high/low-source - hlVarianceMultiplier: 0.03 + hlVarianceMultiplier: 0.13 hlRangeWindow: 4 - smootherWindow: 3 - fisherTransformWindow: 117 - window1m: 42 - smootherWindow1m: 118 - fisherTransformWindow1m: 319 + smootherWindow: 19 + fisherTransformWindow: 73 atrWindow: 14 # orders not been traded will be canceled after `pendingMinutes` minutes - pendingMinutes: 3 + pendingMinutes: 5 noRebalance: true trendWindow: 12 rebalanceFilter: 2 trailingActivationRatio: [0.0015, 0.002, 0.004, 0.01] trailingCallbackRate: [0.0001, 0.00012, 0.001, 0.002] - #driftFilterPos: 0.4 - #driftFilterNeg: -0.42 - #ddriftFilterPos: 0 - #ddriftFilterNeg: 0 generateGraph: true graphPNLDeductFee: true @@ -92,7 +88,7 @@ sync: - ETHBUSD backtest: - startTime: "2022-09-01" + startTime: "2022-09-25" endTime: "2022-09-30" symbols: - ETHBUSD @@ -102,5 +98,5 @@ backtest: makerFeeRate: 0.0000 takerFeeRate: 0.0000 balances: - ETH: 0 - BUSD: 1000.0 + ETH: 0.03 + BUSD: 0 diff --git a/config/driftBTC.yaml b/config/driftBTC.yaml index 1d33df2d0..cce05a63f 100644 --- a/config/driftBTC.yaml +++ b/config/driftBTC.yaml @@ -27,46 +27,42 @@ exchangeStrategies: - on: binance drift: limitOrder: false - quantity: 0.001 + #quantity: 0.0012 canvasPath: "./output.png" symbol: BTCUSDT # kline interval for indicators interval: 1m - window: 4 - stoploss: 0.02% + window: 6 + useAtr: true + useStopLoss: false + stoploss: 0.04% source: hl2 predictOffset: 2 noTrailingStopLoss: false trailingStopLossType: realtime # stddev on high/low-source - hlVarianceMultiplier: 0.2 + hlVarianceMultiplier: 0.15 hlRangeWindow: 4 - smootherWindow: 31 - fisherTransformWindow: 80 - window1m: 8 - smootherWindow1m: 4 - fisherTransformWindow1m: 320 - atrWindow: 14 + smootherWindow: 3 + fisherTransformWindow: 181 + #fisherTransformWindow: 117 + atrWindow: 24 # orders not been traded will be canceled after `pendingMinutes` minutes - pendingMinutes: 3 + pendingMinutes: 5 noRebalance: true - trendWindow: 185 - rebalanceFilter: 1.2 - #driftFilterPos: 0.5 - #driftFilterNeg: -0.5 - #ddriftFilterPos: 0.0008 - #ddriftFilterNeg: -0.0008 + trendWindow: 15 + rebalanceFilter: -0.1 # ActivationRatio should be increasing order # when farest price from entry goes over that ratio, start using the callback ratio accordingly to do trailingstop #trailingActivationRatio: [0.01, 0.016, 0.05] #trailingActivationRatio: [0.001, 0.0081, 0.022] - trailingActivationRatio: [0.0007, 0.0016, 0.008, 0.01] + trailingActivationRatio: [0.0004, 0.008, 0.002, 0.01] #trailingActivationRatio: [] #trailingCallbackRate: [] #trailingCallbackRate: [0.002, 0.01, 0.1] #trailingCallbackRate: [0.0004, 0.0009, 0.018] - trailingCallbackRate: [0.0003, 0.0005, 0.0010, 0.0016] + trailingCallbackRate: [0.00005, 0.00012, 0.0008, 0.0016] generateGraph: true graphPNLDeductFee: false @@ -129,7 +125,7 @@ sync: - BTCUSDT backtest: - startTime: "2022-09-26" + startTime: "2022-09-25" endTime: "2022-09-30" symbols: - BTCUSDT @@ -140,4 +136,4 @@ backtest: takerFeeRate: 0.000 balances: BTC: 0 - USDT: 50 + USDT: 49 diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 0078d57f2..dae4670e4 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -207,7 +207,7 @@ type OpenPositionOptions struct { Leverage fixedpoint.Value `json:"leverage,omitempty" modifiable:"true"` // Quantity will be used first, it will override the leverage if it's given - Quantity fixedpoint.Value `json:"quantity,omitempty"` + Quantity fixedpoint.Value `json:"quantity,omitempty" modifiable:"true"` // LimitOrder set to true to open a position with a limit order // default is false, and will send MarketOrder diff --git a/pkg/strategy/drift/stoploss.go b/pkg/strategy/drift/stoploss.go new file mode 100644 index 000000000..1e507aa61 --- /dev/null +++ b/pkg/strategy/drift/stoploss.go @@ -0,0 +1,19 @@ +package drift + +func (s *Strategy) CheckStopLoss() bool { + stoploss := s.StopLoss.Float64() + atr := s.atr.Last() + if s.UseStopLoss { + if s.sellPrice > 0 && s.sellPrice*(1.+stoploss) <= s.highestPrice || + s.buyPrice > 0 && s.buyPrice*(1.-stoploss) >= s.lowestPrice { + return true + } + } + if s.UseAtr { + if s.sellPrice > 0 && s.sellPrice+atr <= s.highestPrice || + s.buyPrice > 0 && s.buyPrice-atr >= s.lowestPrice { + return true + } + } + return false +} diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 74b8edfc1..a1064686d 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -73,7 +73,6 @@ type Strategy struct { stdevHigh *indicator.StdDev stdevLow *indicator.StdDev drift *DriftMA - drift1m *DriftMA atr *indicator.ATR midPrice fixedpoint.Value lock sync.RWMutex `ignore:"true"` @@ -86,6 +85,8 @@ type Strategy struct { beta float64 + UseStopLoss bool `json:"useStopLoss" modifiable:"true"` + UseAtr bool `json:"useAtr" modifiable:"true"` StopLoss fixedpoint.Value `json:"stoploss" modifiable:"true"` CanvasPath string `json:"canvasPath"` PredictOffset int `json:"predictOffset"` @@ -93,9 +94,6 @@ type Strategy struct { NoTrailingStopLoss bool `json:"noTrailingStopLoss" modifiable:"true"` TrailingStopLossType string `json:"trailingStopLossType" modifiable:"true"` // trailing stop sources. Possible options are `kline` for 1m kline and `realtime` from order updates HLRangeWindow int `json:"hlRangeWindow"` - Window1m int `json:"window1m"` - FisherTransformWindow1m int `json:"fisherTransformWindow1m"` - SmootherWindow1m int `json:"smootherWindow1m"` SmootherWindow int `json:"smootherWindow"` FisherTransformWindow int `json:"fisherTransformWindow"` ATRWindow int `json:"atrWindow"` @@ -106,11 +104,6 @@ type Strategy struct { TrailingCallbackRate []float64 `json:"trailingCallbackRate" modifiable:"true"` TrailingActivationRatio []float64 `json:"trailingActivationRatio" modifiable:"true"` - DriftFilterNeg float64 //`json:"driftFilterNeg" modifiable:"true"` - DriftFilterPos float64 //`json:"driftFilterPos" modifiable:"true"` - DDriftFilterNeg float64 //`json:"ddriftFilterNeg" modifiable:"true"` - DDriftFilterPos float64 //`json:"ddriftFilterPos" modifiable:"true"` - buyPrice float64 `persistence:"buy_price"` sellPrice float64 `persistence:"sell_price"` highestPrice float64 `persistence:"highest_price"` @@ -143,10 +136,6 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // by default, bbgo only pre-subscribe 1000 klines. // this is not enough if we're subscribing 30m intervals using SerialMarketDataStore maxWindow := (s.Window + s.SmootherWindow + s.FisherTransformWindow) * s.Interval.Minutes() - maxWindow1m := s.Window1m + s.SmootherWindow1m + s.FisherTransformWindow1m - if maxWindow < maxWindow1m { - maxWindow = maxWindow1m - } bbgo.KLinePreloadLimit = int64((maxWindow/1000 + 1) * 1000) log.Errorf("set kLinePreloadLimit to %d, %d %d", bbgo.KLinePreloadLimit, s.Interval.Minutes(), maxWindow) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ @@ -212,20 +201,6 @@ func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error { }, } s.drift.SeriesBase.Series = s.drift - s.drift1m = &DriftMA{ - drift: &indicator.WeightedDrift{ - MA: &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: types.Interval1m, Window: s.Window1m}}, - IntervalWindow: types.IntervalWindow{Interval: types.Interval1m, Window: s.Window1m}, - }, - ma1: &indicator.EWMA{ - IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.SmootherWindow1m}, - }, - - ma2: &indicator.FisherTransform{ - IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.FisherTransformWindow1m}, - }, - } - s.drift1m.SeriesBase.Series = s.drift1m s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.ATRWindow}} s.trendLine = &indicator.EWMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.TrendWindow}} @@ -256,13 +231,6 @@ func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error { return errors.New("klines not exists") } log.Infof("loaded %d klines1m", klinesLength) - for _, kline := range *klines { - source := s.GetSource(&kline).Float64() - s.drift1m.Update(source, kline.Volume.Abs().Float64()) - if s.drift1m.Last() != s.drift1m.Last() { - panic(fmt.Sprintf("%f %v %f %f", source, s.drift1m.drift.Values.Index(1), s.drift1m.ma2.Last(), s.drift1m.drift.LastValue)) - } - } if s.kline1m != nil && klines != nil { s.kline1m.Set(&(*klines)[len(*klines)-1]) } @@ -278,18 +246,12 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, e } toCancel := false - drift := s.drift1m.Array(2) for _, order := range nonTraded { if order.Status != types.OrderStatusNew && order.Status != types.OrderStatusPartiallyFilled { continue } log.Warnf("%v | counter: %d, system: %d", order, s.orderPendingCounter[order.OrderID], s.minutesCounter) if s.minutesCounter-s.orderPendingCounter[order.OrderID] > s.PendingMinutes { - if order.Side == types.SideTypeBuy && drift[1] < drift[0] { - continue - } else if order.Side == types.SideTypeSell && drift[1] > drift[0] { - continue - } toCancel = true } else if order.Side == types.SideTypeBuy { // 75% of the probability @@ -328,6 +290,9 @@ func (s *Strategy) trailingCheck(price float64, direction string) bool { s.lowestPrice = price } isShort := direction == "short" + if isShort && s.sellPrice == 0 || !isShort && s.buyPrice == 0 { + return false + } for i := len(s.TrailingCallbackRate) - 1; i >= 0; i-- { trailingCallbackRate := s.TrailingCallbackRate[i] trailingActivationRatio := s.TrailingActivationRatio[i] @@ -389,15 +354,11 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) { return } - stoploss := s.StopLoss.Float64() - - exitShortCondition := s.sellPrice > 0 && (s.sellPrice*(1.+stoploss) <= pricef || - s.trailingCheck(pricef, "short")) - exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= pricef || - s.trailingCheck(pricef, "long")) + exitCondition := s.CheckStopLoss() || + s.trailingCheck(pricef, "short") || s.trailingCheck(pricef, "long") s.positionLock.Unlock() - if exitShortCondition || exitLongCondition { + if exitCondition { s.ClosePosition(ctx, fixedpoint.One) log.Infof("close position by orderbook changes") } @@ -432,21 +393,16 @@ func (s *Strategy) DrawIndicators(time types.Time) *types.Canvas { highestPrice := s.priceLines.Minus(mean).Abs().Highest(Length) highestDrift := s.drift.Abs().Highest(Length) hi := s.drift.drift.Abs().Highest(Length) - h1m := s.drift1m.Abs().Highest(Length * s.Interval.Minutes()) ratio := highestPrice / highestDrift //canvas.Plot("upband", s.ma.Add(s.stdevHigh), time, Length) canvas.Plot("ma", s.ma, time, Length) //canvas.Plot("downband", s.ma.Minus(s.stdevLow), time, Length) - canvas.Plot("pos", types.NumberSeries(s.DriftFilterPos*ratio+mean), time, Length) - canvas.Plot("neg", types.NumberSeries(s.DriftFilterNeg*ratio+mean), time, Length) fmt.Printf("%f %f\n", highestPrice, hi) - canvas.Plot("ppos", types.NumberSeries(s.DDriftFilterPos*(highestPrice/hi)+mean), time, Length) - canvas.Plot("nneg", types.NumberSeries(s.DDriftFilterNeg*(highestPrice/hi)+mean), time, Length) + canvas.Plot("trend", s.trendLine, time, Length) canvas.Plot("drift", s.drift.Mul(ratio).Add(mean), time, Length) canvas.Plot("driftOrig", s.drift.drift.Mul(highestPrice/hi).Add(mean), time, Length) - canvas.Plot("drift1m", s.drift1m.Mul(highestPrice/h1m).Add(mean), time, Length*s.Interval.Minutes(), types.Interval1m) canvas.Plot("zero", types.NumberSeries(mean), time, Length) canvas.Plot("price", s.priceLines, time, Length) return canvas @@ -586,7 +542,6 @@ func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value { func (s *Strategy) klineHandler1m(ctx context.Context, kline types.KLine) { s.kline1m.Set(&kline) - s.drift1m.Update(s.GetSource(&kline).Float64(), kline.Volume.Abs().Float64()) if s.Status != types.StrategyStatusRunning { return } @@ -594,7 +549,6 @@ func (s *Strategy) klineHandler1m(ctx context.Context, kline types.KLine) { atr := s.atr.Last() price := s.getLastPrice() pricef := price.Float64() - stoploss := s.StopLoss.Float64() lowf := math.Min(kline.Low.Float64(), pricef) highf := math.Max(kline.High.Float64(), pricef) @@ -605,11 +559,6 @@ func (s *Strategy) klineHandler1m(ctx context.Context, kline types.KLine) { if s.highestPrice > 0 && highf > s.highestPrice { s.highestPrice = highf } - drift := s.drift1m.Array(2) - if len(drift) < 2 { - s.positionLock.Unlock() - return - } numPending := 0 var err error @@ -628,13 +577,9 @@ func (s *Strategy) klineHandler1m(ctx context.Context, kline types.KLine) { return } - //log.Infof("d1m: %f, hf: %f, lf: %f", s.drift1m.Last(), highf, lowf) - exitShortCondition := s.sellPrice > 0 && (s.sellPrice*(1.+stoploss) <= highf || - s.trailingCheck(highf, "short") /* || s.drift1m.Last() > 0*/) - exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= lowf || - s.trailingCheck(lowf, "long") /* || s.drift1m.Last() < 0*/) + exitCondition := s.CheckStopLoss() || s.trailingCheck(highf, "short") || s.trailingCheck(lowf, "long") s.positionLock.Unlock() - if exitShortCondition || exitLongCondition { + if exitCondition { _ = s.ClosePosition(ctx, fixedpoint.One) } } @@ -677,7 +622,6 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { if s.Status != types.StrategyStatusRunning { return } - stoploss := s.StopLoss.Float64() s.positionLock.Lock() log.Infof("highdiff: %3.2f ma: %.2f, open: %8v, close: %8v, high: %8v, low: %8v, time: %v %v", s.stdevHigh.Last(), s.ma.Last(), kline.Open, kline.Close, kline.High, kline.Low, kline.StartTime, kline.EndTime) @@ -704,18 +648,6 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { s.Market.QuoteCurrency, balances[s.Market.QuoteCurrency].String(), ) - s.DriftFilterPos = s.drift.Filter(func(i int, v float64) bool { - return v >= 0 - }, 50).Mean(50) - s.DriftFilterNeg = s.drift.Filter(func(i int, v float64) bool { - return v <= 0 - }, 50).Mean(50) - s.DDriftFilterPos = s.drift.drift.Filter(func(i int, v float64) bool { - return v >= 0 - }, 50).Mean(50) - s.DDriftFilterNeg = s.drift.drift.Filter(func(i int, v float64) bool { - return v <= 0 - }, 50).Mean(50) shortCondition := drift[1] >= 0 && drift[0] <= 0 || (drift[1] >= drift[0] && drift[1] <= 0) || ddrift[1] >= 0 && ddrift[0] <= 0 || (ddrift[1] >= ddrift[0] && ddrift[1] <= 0) longCondition := drift[1] <= 0 && drift[0] >= 0 || (drift[1] <= drift[0] && drift[1] >= 0) || ddrift[1] <= 0 && ddrift[0] >= 0 || (ddrift[1] <= ddrift[0] && ddrift[1] >= 0) @@ -726,12 +658,9 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { shortCondition = false } } - exitShortCondition := s.sellPrice > 0 && (s.sellPrice*(1.+stoploss) <= highf || - s.trailingCheck(pricef, "short")) - exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= lowf || - s.trailingCheck(pricef, "long")) + exitCondition := s.CheckStopLoss() || s.trailingCheck(pricef, "short") || s.trailingCheck(pricef, "long") - if exitShortCondition || exitLongCondition { + if exitCondition { s.positionLock.Unlock() if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { log.WithError(err).Errorf("cannot cancel orders") @@ -918,7 +847,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.sellPrice = s.p.ApproximateAverageCost.Float64() //trade.Price.Float64() s.buyPrice = 0 s.highestPrice = 0 - s.lowestPrice = math.Min(s.lowestPrice, s.sellPrice) + if s.lowestPrice == 0 { + s.lowestPrice = s.sellPrice + } else { + s.lowestPrice = math.Min(s.lowestPrice, s.sellPrice) + } } bbgo.Notify("tag: %s, sp: %.4f bp: %.4f hp: %.4f lp: %.4f, trade: %s, pos: %s", tag, s.sellPrice, s.buyPrice, s.highestPrice, s.lowestPrice, trade.String(), s.p.String()) }) From 8e82e24c0528de8b15f91a1521096d2f7aff5cef Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 29 Sep 2022 20:31:10 +0900 Subject: [PATCH 2/2] fix: drift close position with retry limit --- pkg/strategy/drift/strategy.go | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index a1064686d..3e87049a1 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -152,6 +152,8 @@ func (s *Strategy) CurrentPosition() *types.Position { return s.Position } +const closeOrderRetryLimit = 5 + func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { order := s.p.NewMarketCloseOrder(percentage) if order == nil { @@ -159,19 +161,20 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu } order.Tag = "close" order.TimeInForce = "" - balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() - baseBalance := balances[s.Market.BaseCurrency].Available - price := s.getLastPrice() - if order.Side == types.SideTypeBuy { - quoteAmount := balances[s.Market.QuoteCurrency].Available.Div(price) - if order.Quantity.Compare(quoteAmount) > 0 { - order.Quantity = quoteAmount - } - } else if order.Side == types.SideTypeSell && order.Quantity.Compare(baseBalance) > 0 { - order.Quantity = baseBalance - } + order.MarginSideEffect = types.SideEffectTypeAutoRepay - for { + for i := 0; i < closeOrderRetryLimit; i++ { + price := s.getLastPrice() + balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() + baseBalance := balances[s.Market.BaseCurrency].Available + if order.Side == types.SideTypeBuy { + quoteAmount := balances[s.Market.QuoteCurrency].Available.Div(price) + if order.Quantity.Compare(quoteAmount) > 0 { + order.Quantity = quoteAmount + } + } else if order.Side == types.SideTypeSell && order.Quantity.Compare(baseBalance) > 0 { + order.Quantity = baseBalance + } if s.Market.IsDustQuantity(order.Quantity, price) { return nil } @@ -182,6 +185,7 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu } return nil } + return errors.New("exceed retry limit") } func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error { @@ -360,7 +364,6 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) { s.positionLock.Unlock() if exitCondition { s.ClosePosition(ctx, fixedpoint.One) - log.Infof("close position by orderbook changes") } }) s.getLastPrice = func() (lastPrice fixedpoint.Value) {