package bolladxema import ( "context" "errors" "fmt" "git.qtrade.icu/lychiyu/bbgo/pkg/bbgo" "git.qtrade.icu/lychiyu/bbgo/pkg/exchange/binance" "git.qtrade.icu/lychiyu/bbgo/pkg/fixedpoint" indicatorv2 "git.qtrade.icu/lychiyu/bbgo/pkg/indicator/v2" "git.qtrade.icu/lychiyu/bbgo/pkg/types" "github.com/sirupsen/logrus" "strconv" "strings" "sync" "time" ) /* 布林带+ADX+EMA策略 1. 布林带,判断是否在布林带内,在布林带上,做多,在布林带下,做空。 2. ADX,判断是否在ADX区间内,在区间内,做多,在区间外,做空。 3. EMA,判断是否在EMA区间内,在区间内,做多,在区间外,做空。 4. 默认开仓量,默认止盈止损。 */ const ID = "bolladxema" var log = logrus.WithField("strategy", ID) var ten = fixedpoint.NewFromInt(10) var Two = fixedpoint.NewFromInt(2) func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } type State struct { Counter int `json:"counter,omitempty"` } type BollingerSetting struct { types.IntervalWindow BandWidth float64 `json:"bandWidth"` } type Strategy struct { Environment *bbgo.Environment Market types.Market session *bbgo.ExchangeSession orderExecutor *bbgo.GeneralOrderExecutor exchange *binance.Exchange // persistence fields Position *types.Position `persistence:"position"` ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` DryRun bool `json:"dryRun"` Symbol string `json:"symbol"` Interval types.Interval `json:"interval"` Leverage fixedpoint.Value `json:"leverage,omitempty"` ProfitType int `json:"profitType"` PlaceOrderType int `json:"placeOrderType"` EnablePause bool `json:"enablePause"` TradeStartHour int `json:"tradeStartHour"` TradeEndHour int `json:"tradeEndHour"` PauseTradeLoss fixedpoint.Value `json:"pauseTradeLoss"` ProfitHRange fixedpoint.Value `json:"profitHRange"` LossHRange fixedpoint.Value `json:"lossHRange"` ProfitMRange fixedpoint.Value `json:"profitMRange"` LossMRange fixedpoint.Value `json:"lossMRange"` ProfitLRange fixedpoint.Value `json:"profitLRange"` LossLRange fixedpoint.Value `json:"lossLRange"` AtrProfitMultiple float64 `json:"atrProfitMultiple"` AtrLossMultiple float64 `json:"atrLossMultiple"` EnableADX bool `json:"enableADX"` ADXHSingle float64 `json:"adxHSingle"` ADXMSingle float64 `json:"adxMSingle"` ADXLSingle float64 `json:"adxLSingle"` LongCCI fixedpoint.Value `json:"longCCI"` ShortCCI fixedpoint.Value `json:"shortCCI"` State *State `persistence:"state"` Bollinger *BollingerSetting `json:"bollinger"` EMASetting types.IntervalWindow `json:"emaSetting"` ADXSetting types.IntervalWindow `json:"adxSetting"` ATRSetting types.IntervalWindow `json:"atrSetting"` CCISetting types.IntervalWindow `json:"cciSetting"` StageHalfAmount []fixedpoint.Value `json:"stageHalfAmount"` bbgo.QuantityOrAmount // 当前的盈利阶段 CurrentStage int Traded bool TradeSignal string TradeRetry int PauseTradeCount fixedpoint.Value // 最近一次暂停交易的时间 PauseTradeTime time.Time // 总盈利 TotalProfit fixedpoint.Value // 总手续费 TotalFree fixedpoint.Value // 总交易次数 TotalOrderCount int TotalProfitCount int TotalLossCount int LongOrder types.SubmitOrder LongProfitOrder types.SubmitOrder LongLossOrder types.SubmitOrder ShortOrder types.SubmitOrder ShortProfitOrder types.SubmitOrder ShortLossOrder types.SubmitOrder // 开仓 OpenTrade []types.Trade // 清仓 EndTrade []types.Trade OpenQuantity fixedpoint.Value EndQuantity fixedpoint.Value ADX *indicatorv2.ADXStream EMA *indicatorv2.EWMAStream BOLL *indicatorv2.BOLLStream ATR *indicatorv2.ATRStream CCI *indicatorv2.CCIStream bbgo.StrategyController } func (s *Strategy) Defaults() error { s.PauseTradeCount = fixedpoint.Zero s.TotalProfit = fixedpoint.Zero s.TotalFree = fixedpoint.Zero s.OpenQuantity = fixedpoint.Zero s.EndQuantity = fixedpoint.Zero s.PauseTradeTime = time.Now().Add(-24 * time.Hour) s.TradeRetry = 0 s.Traded = false s.TradeSignal = "" return nil } // ID should return the identity of this strategy func (s *Strategy) ID() string { return ID } func (s *Strategy) InstanceID() string { return ID + ":" + s.Symbol } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) if s.Bollinger != nil && s.Bollinger.Interval != "" && s.Bollinger.Interval != s.Interval { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Bollinger.Interval}) } if s.EMASetting.Interval != "" && s.EMASetting.Interval != s.Interval { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.EMASetting.Interval}) } if s.ADXSetting.Interval != "" && s.ADXSetting.Interval != s.Interval { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ADXSetting.Interval}) } if s.ATRSetting.Interval != "" && s.ATRSetting.Interval != s.Interval { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ATRSetting.Interval}) } if !bbgo.IsBackTesting { session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) } } func (s *Strategy) cancelOrders(ctx context.Context, symbol string) { if len(s.orderExecutor.ActiveMakerOrders().Orders()) <= 0 { return } log.Infof(fmt.Sprintf("[%s] the order is not filled, will cancel all orders", symbol)) if err := s.orderExecutor.GracefulCancel(ctx); err != nil { log.WithError(err).Errorf("failed to cancel orders") } s.Traded = false s.TradeSignal = "" s.TradeRetry = 0 } func (s *Strategy) isTradeTime(ctx context.Context) bool { // 如果时间一致则表示不限制交易时间 if s.TradeEndHour == s.TradeStartHour { return true } location, err := time.LoadLocation("Asia/Shanghai") if err != nil { return false } now := time.Now().In(location) hour := now.Hour() return !(hour >= s.TradeStartHour && hour < s.TradeEndHour) } func (s *Strategy) isPauseTrade(ctx context.Context) bool { if !s.EnablePause { return false } // 被暂停次数不为0,且最近一次的暂停时间和今天一致,则表示暂停 if s.PauseTradeCount != fixedpoint.Zero && s.PauseTradeTime.Day() == time.Now().Day() { return true } // 总收益大于(暂停次数+1)*暂停亏损,则表示暂停 if s.TotalProfit < s.PauseTradeLoss.Mul(s.PauseTradeCount.Add(fixedpoint.One)) { s.PauseTradeCount.Add(fixedpoint.One) s.PauseTradeTime = time.Now() return true } return false } func (s *Strategy) setInitialLeverage(ctx context.Context) error { log.Infof("setting futures leverage to %d", s.Leverage.Int()+1) var ok bool s.exchange, ok = s.session.Exchange.(*binance.Exchange) if !ok { return errors.New("not binance exchange, currently only support binance exchange") } futuresClient := s.exchange.GetFuturesClient() req := futuresClient.NewFuturesChangeInitialLeverageRequest() req.Symbol(s.Symbol) req.Leverage(s.Leverage.Int() + 1) resp, err := req.Do(ctx) if err != nil { return err } log.Infof("adjusted initial leverage: %+v", resp) return nil } func (s *Strategy) GetTradeSignal(k types.KLine, adx, bollUp, bollDown, ema, cciV float64) string { if k.High.Float64() >= bollUp && k.Low.Float64() <= bollDown { // k线跨越布林带,不入场 return "" } // 小于最小ADX信号 if s.EnableADX && adx < s.ADXLSingle { return "" } if k.Open >= k.Close && k.Low.Float64() <= bollDown && k.High.Float64() >= bollDown && k.Close.Float64() <= ema && cciV <= s.LongCCI.Float64() { // k线收跌,且触及下轨,但是最高价会在下轨上,并小于ema,开多 return "long" } if k.Open <= k.Close && k.High.Float64() >= bollUp && k.Low.Float64() <= bollUp && k.Close.Float64() >= ema && cciV >= s.ShortCCI.Float64() { // k线收涨,且触及上轨,但是最高价会在上轨下,并大于ema,开空 return "short" } return "" } func (s *Strategy) generateOrders(k types.KLine, bollDiff, adx float64) ([]types.SubmitOrder, error) { var orders []types.SubmitOrder symbol := k.Symbol // 止盈订单类型 profitOrderType := types.OrderTypeTakeProfitMarket // 止损订单类型 lossOrderType := types.OrderTypeStopMarket if s.PlaceOrderType == 1 { profitOrderType = types.OrderTypeStopMarket } if bbgo.IsBackTesting { profitOrderType = types.OrderTypeStopLimit lossOrderType = types.OrderTypeStopLimit } // 下单价格 placePrice := k.Close // 计算止损止盈价格,以ATR为基准或者固定百分比 lossPrice := fixedpoint.Zero profitPrice := fixedpoint.Zero lastATR, err := strconv.ParseFloat(strconv.FormatFloat(s.ATR.Last(0), 'f', 6, 64), 64) if err != nil { log.WithError(err).Error("failed parse atr last value float") lastATR = 0.0 } // 依据不同的adx来设置止盈止损 profitRange := s.ProfitLRange lossRange := s.LossLRange if adx >= s.ADXHSingle { profitRange = s.ProfitHRange lossRange = s.LossHRange } else if adx >= s.ADXMSingle { profitRange = s.ProfitMRange lossRange = s.LossMRange } //if bollDiff >= 0.03 { // profitRange = profitRange.Mul(fixedpoint.NewFromFloat(1.5)) //} if s.TradeSignal == "long" { // 做多 if s.ProfitType == 0 || s.ATR.Last(0) == 0.0 { lossPrice = placePrice.Mul(fixedpoint.One.Sub(lossRange)) profitPrice = placePrice.Mul(fixedpoint.One.Add(profitRange)) } else { lossPrice = placePrice.Sub(fixedpoint.Value(1e8 * lastATR * s.AtrLossMultiple)) profitPrice = placePrice.Add(fixedpoint.Value(1e8 * lastATR * s.AtrProfitMultiple)) } } else { //做空 if s.ProfitType == 0 || s.ATR.Last(0) == 0.0 { lossPrice = placePrice.Mul(fixedpoint.One.Add(lossRange)) profitPrice = placePrice.Mul(fixedpoint.One.Sub(profitRange)) } else { lossPrice = placePrice.Add(fixedpoint.Value(1e8 * lastATR * s.AtrLossMultiple)) profitPrice = placePrice.Sub(fixedpoint.Value(1e8 * lastATR * s.AtrProfitMultiple)) } } // 下单数量 placeQuantity := s.QuantityOrAmount.CalculateQuantity(placePrice).Mul(s.Leverage) msg := fmt.Sprintf("%v, will place order, amount %v, price %v, quantity %v, lossprice %v, profitprice: %v, atr: %v", s.Symbol, s.QuantityOrAmount.Amount.Float64(), placePrice.Float64(), placeQuantity.Float64(), lossPrice.Float64(), profitPrice.Float64(), lastATR) bbgo.Notify(msg) s.ShortOrder = types.SubmitOrder{ Symbol: symbol, Side: types.SideTypeSell, Type: types.OrderTypeLimit, Price: placePrice, PositionSide: types.PositionSideTypeShort, Quantity: placeQuantity, TimeInForce: types.TimeInForceGTC, Market: s.Market, } s.ShortProfitOrder = types.SubmitOrder{ Symbol: symbol, Side: types.SideTypeBuy, Type: profitOrderType, PositionSide: types.PositionSideTypeShort, StopPrice: profitPrice, TimeInForce: types.TimeInForceGTC, Market: s.Market, ClosePosition: true, } s.ShortLossOrder = types.SubmitOrder{ Symbol: symbol, Side: types.SideTypeBuy, Type: lossOrderType, PositionSide: types.PositionSideTypeShort, StopPrice: lossPrice, TimeInForce: types.TimeInForceGTC, Market: s.Market, ClosePosition: true, } s.LongOrder = types.SubmitOrder{ Symbol: symbol, Side: types.SideTypeBuy, Type: types.OrderTypeLimit, Price: placePrice, PositionSide: types.PositionSideTypeLong, Quantity: placeQuantity, TimeInForce: types.TimeInForceGTC, Market: s.Market, } s.LongProfitOrder = types.SubmitOrder{ Symbol: symbol, Side: types.SideTypeSell, Type: profitOrderType, PositionSide: types.PositionSideTypeLong, StopPrice: profitPrice, TimeInForce: types.TimeInForceGTC, Market: s.Market, ClosePosition: true, } s.LongLossOrder = types.SubmitOrder{ Symbol: symbol, Side: types.SideTypeSell, Type: lossOrderType, PositionSide: types.PositionSideTypeLong, StopPrice: lossPrice, TimeInForce: types.TimeInForceGTC, Market: s.Market, ClosePosition: true, } if s.TradeSignal == "short" { // 挂空单 orders = append(orders, s.ShortOrder) // 空单止盈 orders = append(orders, s.ShortProfitOrder) // 空单止损 orders = append(orders, s.ShortLossOrder) } if s.TradeSignal == "long" { // 挂多单 orders = append(orders, s.LongOrder) // 多单止盈 orders = append(orders, s.LongProfitOrder) // 多单止损 orders = append(orders, s.LongLossOrder) } return orders, nil } func (s *Strategy) placeOrders(ctx context.Context, k types.KLine, bollDiff, adx float64) { if s.TradeSignal == "" { return } symbol := k.Symbol orders, err := s.generateOrders(k, bollDiff, adx) if err != nil { log.WithError(err).Error(fmt.Sprintf("failed to generate orders (%s)", symbol)) return } log.Infof("orders: %+v", orders) if s.DryRun { log.Infof("dry run, not submitting orders (%s)", symbol) return } createdOrders, err := s.orderExecutor.SubmitOrders(ctx, orders...) if err != nil { log.WithError(err).Error(fmt.Sprintf("failed to submit orders (%s)", symbol)) return } log.Infof("created orders (%s): %+v", symbol, createdOrders) return } func (s *Strategy) notifyProfit(ctx context.Context, symbol string) { if s.EndQuantity != s.OpenQuantity { return } profit := fixedpoint.Zero openProfit := fixedpoint.Zero endProfit := fixedpoint.Zero free := fixedpoint.Zero var openMsgs []string var endMsgs []string // 开仓成本 for _, trade := range s.OpenTrade { openProfit = openProfit.Add(trade.Price.Mul(trade.Quantity)) free = free.Add(trade.Fee) openMsgs = append(openMsgs, fmt.Sprintf("price:%v, quantity:%v, fee:%v;", trade.Price.Float64(), trade.Quantity.Float64(), trade.Fee.Float64())) } // 清仓资产 for _, trade := range s.EndTrade { endProfit = endProfit.Add(trade.Price.Mul(trade.Quantity)) free = free.Add(trade.Fee) endMsgs = append(endMsgs, fmt.Sprintf("price:%v, quantity:%v, fee:%v;", trade.Price.Float64(), trade.Quantity.Float64(), trade.Fee.Float64())) } side := s.OpenTrade[0].Side // 做多 if side == types.SideTypeBuy { profit = endProfit.Sub(openProfit).Sub(free) } // 做空 if side == types.SideTypeSell { profit = openProfit.Sub(endProfit).Sub(free) } msg := fmt.Sprintf("Trade finish:\n symbol: %s, signal:%v, profit:%v, fee:%v \n Trade details:\n OpenTrade:\n %s\n CloseTrade:\n %s", symbol, s.TradeSignal, profit.Float64(), free.Float64(), strings.Join(openMsgs, "\n"), strings.Join(endMsgs, "\n")) s.updateAmount(ctx, profit) s.TotalProfit = s.TotalProfit.Add(profit) s.TotalFree = s.TotalFree.Add(free) s.TotalOrderCount += 1 if profit > fixedpoint.Zero { s.TotalProfitCount += 1 } else { s.TotalLossCount += 1 } log.Infof(msg) bbgo.Notify(msg) // 重置 s.OpenTrade = []types.Trade{} s.EndTrade = []types.Trade{} s.OpenQuantity = fixedpoint.Zero s.EndQuantity = fixedpoint.Zero // 记得取消订单 s.cancelOrders(ctx, symbol) bbgo.Notify(fmt.Sprintf("%v, Total Count:%v, Profit:%v, Fee:%v, Profit Count:%v, Loss Count:%v", s.Symbol, s.TotalOrderCount, s.TotalProfit.Float64(), s.TotalFree.Float64(), s.TotalProfitCount, s.TotalLossCount)) } func (s *Strategy) updateAmount(ctx context.Context, profit fixedpoint.Value) { // 更新amount newAmount := s.QuantityOrAmount.Amount.Add(profit) // 如果当前的总金额大于阶梯上的某一个值,则更新为减半 if newAmount >= s.StageHalfAmount[s.CurrentStage] { s.QuantityOrAmount.Amount = newAmount.Div(Two) bbgo.Notify(fmt.Sprintf("%v 结余资金:%v", s.Symbol, s.QuantityOrAmount.Amount.Float64())) s.CurrentStage += 1 bbgo.Sync(ctx, s) return } s.QuantityOrAmount.Amount = newAmount } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { instanceID := s.InstanceID() // Initialize the default value for state if s.State == nil { s.State = &State{Counter: 1} } if s.Position == nil { s.Position = types.NewPositionFromMarket(s.Market) } if s.ProfitStats == nil { s.ProfitStats = types.NewProfitStats(s.Market) } if s.TradeStats == nil { s.TradeStats = types.NewTradeStats(s.Symbol) } 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.TradeCollector().OnPositionUpdate(func(position *types.Position) { bbgo.Sync(ctx, s) }) s.orderExecutor.Bind() bbgo.Notify("BTC滚仓布林带策略开始运行") s.BOLL = session.Indicators(s.Symbol).BOLL(s.Bollinger.IntervalWindow, s.Bollinger.BandWidth) s.EMA = session.Indicators(s.Symbol).EMA(s.EMASetting) s.ADX = session.Indicators(s.Symbol).ADX(s.ADXSetting.Interval, s.ADXSetting.Window) s.ATR = session.Indicators(s.Symbol).ATR(s.ATRSetting.Interval, s.ATRSetting.Window) s.CCI = session.Indicators(s.Symbol).CCI(s.CCISetting.Interval, s.CCISetting.Window) session.MarketDataStream.OnKLineClosed(func(k types.KLine) { if k.Symbol != s.Symbol { return } adx := s.ADX.Last(0) // 小于最小ADX信号 if s.EnableADX && adx < s.ADXLSingle { return } if !s.isTradeTime(ctx) || s.isPauseTrade(ctx) { //pauseMsg := fmt.Sprintf("暂停交易:总收益:%v, 暂停次数:%v, 暂停时间:%v; 暂停时间段:[%v, %v)", // s.TotalProfit.Float64(), s.PauseTradeCount.Float64(), s.PauseTradeTime, s.TradeStartHour, // s.TradeEndHour) //bbgo.Notify(pauseMsg) return } if !s.Traded { // 如若在下一根k线未成交 则取消订单 if s.TradeSignal != "" && s.TradeRetry > 1 { bbgo.Notify(fmt.Sprintf("Trade signal not traded, cancel orders: %s", s.Symbol)) s.cancelOrders(ctx, s.Symbol) } if s.TradeSignal != "" && s.TradeRetry <= 1 { s.TradeRetry = s.TradeRetry + 1 } } if s.TradeSignal != "" { return } bollUp := s.BOLL.UpBand.Last(0) bolldown := s.BOLL.DownBand.Last(0) ema := s.EMA.Last(0) cciV := s.CCI.Last(0) signal := s.GetTradeSignal(k, adx, bollUp, bolldown, ema, cciV) if signal == "" { return } s.TradeSignal = signal msg := fmt.Sprintf("trade singal info, symbol:%s, single %s, time: %s,open:%f,close:%f, high:%f,low:%f, ema: %v, adx: %v, bollUp %f, bollDown %f, cci %f", s.Symbol, signal, k.EndTime, k.Open.Float64(), k.Close.Float64(), k.High.Float64(), k.Low.Float64(), ema, adx, bollUp, bolldown, cciV) bollDiff := (bollUp - bolldown) / bolldown s.placeOrders(ctx, k, bollDiff, adx) bbgo.Notify(msg) }) session.UserDataStream.OnOrderUpdate(func(order types.Order) { orderSymbol := order.Symbol if orderSymbol != s.Symbol { return } if order.Status == types.OrderStatusFilled { if order.Type == types.OrderTypeLimit && order.Side == types.SideTypeBuy { log.Infof("the long order is filled: %+v,id is %d, symbol is %s, type is %s, status is %s", order, order.OrderID, orderSymbol, order.Type, order.Status) s.Traded = true s.TradeRetry = 0 bbgo.Notify("Order traded notify:\n symbol:%s, signal:%s, price:%s, quantity:%s", order.Symbol, s.TradeSignal, order.Price, order.Quantity) } if order.Type == types.OrderTypeLimit && order.Side == types.SideTypeSell { log.Infof("the short order is filled: %+v,id is %d, symbol is %s, type is %s, status is %s", order, order.OrderID, orderSymbol, order.Type, order.Status) s.Traded = true s.TradeRetry = 0 bbgo.Notify("Order traded notify:\n symbol:%s, signal:%s, price:%s, quantity:%s", order.Symbol, s.TradeSignal, order.Price, order.Quantity) } if order.Type == types.OrderTypeMarket { log.Infof("the loss or profit order is filled: %+v,id is %d, symbol is %s, type is %s, "+ "status is %s", order, order.OrderID, orderSymbol, order.Type, order.Status) bbgo.Notify("Order stop profit or loss notify:\n %s", order.Symbol) s.Traded = false s.TradeRetry = 0 s.TradeSignal = "" } else { log.Infof("the order is: %+v,id is %d, symbol is %s, type is %s, status is %s", order, order.OrderID, orderSymbol, order.Type, order.Status) } } else if order.Status == types.OrderStatusCanceled { log.Infof("canceled order %+v", order) } }) session.UserDataStream.OnTradeUpdate(func(trade types.Trade) { symbol := trade.Symbol if symbol != s.Symbol { return } if (trade.Side == types.SideTypeBuy && s.TradeSignal == "long") || (trade.Side == types.SideTypeSell && s.TradeSignal == "short") { s.OpenTrade = append(s.OpenTrade, trade) s.OpenQuantity = s.OpenQuantity.Add(trade.Quantity) } if (trade.Side == types.SideTypeSell && s.TradeSignal == "long") || (trade.Side == types.SideTypeBuy && s.TradeSignal == "short") { s.EndTrade = append(s.EndTrade, trade) s.EndQuantity = s.EndQuantity.Add(trade.Quantity) s.notifyProfit(ctx, symbol) } log.Infof("trade: symbol %s, side %s, price %f, fee %f, quantity %f, buyer %v, maker %v", symbol, trade.Side, trade.Price.Float64(), trade.Fee.Float64(), trade.Quantity.Float64(), trade.IsBuyer, trade.IsMaker) }) s.OnSuspend(func() { // Cancel active orders _ = s.orderExecutor.GracefulCancel(ctx) }) s.OnEmergencyStop(func() { // Cancel active orders _ = s.orderExecutor.GracefulCancel(ctx) // Close 100% position //_ = s.ClosePosition(ctx, fixedpoint.One) }) bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() if err := s.orderExecutor.GracefulCancel(ctx); err != nil { log.WithError(err).Error("unable to cancel open orders...") } bbgo.Sync(ctx, s) }) return nil }