package roll 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" ) const ID = "roll" var log = logrus.WithField("strategy", ID) var ten = fixedpoint.NewFromInt(10) var Two = fixedpoint.NewFromInt(2) var Delta = fixedpoint.NewFromFloat(0.00001) func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } type State struct { Counter int `json:"counter,omitempty"` } 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"` Symbol string `json:"symbol"` Interval types.Interval `json:"interval"` Leverage fixedpoint.Value `json:"leverage,omitempty"` NRInterval types.Interval `json:"nrInterval" modifiable:"true"` CCIInterval types.Interval `json:"cciInterval" modifiable:"true"` ATRInterval types.Interval `json:"atrInterval" modifiable:"true"` NrCount int `json:"nrCount" modifiable:"true"` CCIWindow int `json:"cciWindow"` ATRWindow int `json:"atrWindow"` StrictMode bool `json:"strictMode" modifiable:"true"` TradeStartHour int `json:"tradeStartHour"` TradeEndHour int `json:"tradeEndHour"` PauseTradeLoss fixedpoint.Value `json:"pauseTradeLoss"` LongCCI fixedpoint.Value `json:"longCCI"` ShortCCI fixedpoint.Value `json:"shortCCI"` DryRun bool `json:"dryRun"` EnablePause bool `json:"enablePause"` PlacePriceType int `json:"placePriceType"` ProfitOrderType int `json:"profitOrderType"` LossType int `json:"lossType"` ProfitRange fixedpoint.Value `json:"profitRange"` LossRange fixedpoint.Value `json:"lossRange"` MidPriceRange float64 `json:"midPriceRange"` AtrProfitRange float64 `json:"atrProfitRange"` AtrLossRange float64 `json:"atrLossRange"` StageHalfAmount []fixedpoint.Value `json:"stageHalfAmount"` bbgo.QuantityOrAmount nr *indicatorv2.NRStrean cci *indicatorv2.CCIStream atr *indicatorv2.ATRStream // 当前的盈利阶段 CurrentStage int Traded bool TradeType 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 // State is a state of your strategy // When BBGO shuts down, everything in the memory will be dropped // If you need to store something and restore this information back, // Simply define the "persistence" tag State *State `persistence:"state"` bbgo.StrategyController getLastPrice func() fixedpoint.Value } 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 return nil } // ID should return the identity of this strategy func (s *Strategy) ID() string { return ID } // InstanceID returns the identity of the current instance of this strategy. // You may have multiple instance of a strategy, with different symbols and settings. // This value will be used for persistence layer to separate the storage. // // Run: // // redis-cli KEYS "*" // // And you will see how this instance ID is used in redis. func (s *Strategy) InstanceID() string { return ID + ":" + s.Symbol } func (s *Strategy) Initialize() error { return nil } // Subscribe method subscribes specific market data from the given session. // Before BBGO is connected to the exchange, we need to collect what we want to subscribe. // Here the strategy needs kline data, so it adds the kline subscription. func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // We want 1m kline data of the symbol // It will be BTCUSDT 1m if our s.Symbol is BTCUSDT session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) if !bbgo.IsBackTesting { session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) } } // Position control func (s *Strategy) CurrentPosition() *types.Position { return s.Position } func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { order := s.Position.NewMarketCloseOrder(percentage) if order == nil { return nil } order.Tag = "close" order.TimeInForce = "" balances := s.orderExecutor.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 { if s.Market.IsDustQuantity(order.Quantity, price) { return nil } _, err := s.orderExecutor.SubmitOrders(ctx, *order) if err != nil { order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta)) continue } return nil } } 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.TradeType = "" s.TradeRetry = 0 } // isTradeTime 是否交易时间 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) getPlacePrice(ctx context.Context, kline types.KLine) fixedpoint.Value { placePrice := fixedpoint.Zero midPrice := (kline.High.Add(kline.Low)).Div(fixedpoint.One * 2) shouldMid := (((kline.High.Sub(kline.Low)).Div(kline.Low)).Abs()).Float64() <= s.MidPriceRange switch s.PlacePriceType { case 0: if s.TradeType == "long" { placePrice = kline.High } else if s.TradeType == "short" { placePrice = kline.Low } case 1: if s.TradeType == "long" { if !shouldMid { placePrice = kline.Low } else { placePrice = midPrice } } else if s.TradeType == "short" { if !shouldMid { placePrice = kline.High } else { placePrice = midPrice } } case 2: if s.TradeType == "long" { placePrice = midPrice } else if s.TradeType == "short" { placePrice = midPrice } } return placePrice } func (s *Strategy) generateOrders(ctx context.Context, kline types.KLine) ([]types.SubmitOrder, error) { var orders []types.SubmitOrder symbol := kline.Symbol log.Infof(fmt.Sprintf("place order keline info: symbol %s, high %v, low %v, open %v, close %v", symbol, kline.High.Float64(), kline.Low.Float64(), kline.Open.Float64(), kline.Close.Float64())) // 获取下单价格 if s.TradeType == "" { return orders, nil } // 获取下单价格 placePrice := s.getPlacePrice(ctx, kline) // 止盈订单类型 profitOrderType := types.OrderTypeTakeProfitMarket // 止损订单类型 lossOrderType := types.OrderTypeStopMarket if s.ProfitOrderType == 1 { profitOrderType = types.OrderTypeStopMarket } if bbgo.IsBackTesting { profitOrderType = types.OrderTypeStopLimit lossOrderType = types.OrderTypeStopLimit } // 计算止损止盈价格,以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 } if s.TradeType == "long" { if s.LossType == 0 || s.atr.Last(0) == 0.0 { lossPrice = placePrice.Sub(placePrice.Mul(s.LossRange)) profitPrice = placePrice.Add(placePrice.Mul(s.ProfitRange)) } else if s.LossType == 1 { lossPrice = placePrice.Sub(fixedpoint.Value(1e8 * lastATR * s.AtrLossRange)) profitPrice = placePrice.Add(fixedpoint.Value(1e8 * lastATR * s.AtrProfitRange)) } } else if s.TradeType == "short" { if s.LossType == 0 || s.atr.Last(0) == 0.0 { lossPrice = placePrice.Add(placePrice.Mul(s.LossRange)) profitPrice = placePrice.Sub(placePrice.Mul(s.ProfitRange)) } else if s.LossType == 1 { lossPrice = placePrice.Add(fixedpoint.Value(1e8 * lastATR * s.AtrLossRange)) profitPrice = placePrice.Sub(fixedpoint.Value(1e8 * lastATR * s.AtrProfitRange)) } } // 下单数量 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) log.Infof(msg) 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.TradeType == "short" { // 挂空单 orders = append(orders, s.ShortOrder) // 空单止盈 orders = append(orders, s.ShortProfitOrder) // 空单止损 orders = append(orders, s.ShortLossOrder) } if s.TradeType == "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, kline types.KLine) { symbol := kline.Symbol orders, err := s.generateOrders(ctx, kline) 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("价格:%v, 数量:%v, 手续费:%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("价格:%v, 数量:%v, 手续费:%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("交易完成:\n 币种: %s, 方向:%v, 收益:%v, 手续费:%v \n Trade详情:\n 开仓Trade:\n %s\n 清仓Trade:\n %s", symbol, s.TradeType, 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, 总交易次数:%v, 总收益:%v, 总手续费:%v, 盈利次数:%v, 亏损次数:%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 //for i, stage := range s.StageHalfAmount { // if newAmount <= stage { // s.CurrentStage = i // s.QuantityOrAmount.Amount = newAmount // bbgo.Sync(ctx, s) // return // } //} } // This strategy simply spent all available quote currency to buy the symbol whenever kline gets closed func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { instanceID := s.InstanceID() 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.Status = types.StrategyStatusRunning //s.OnSuspend(func() { // _ = s.orderExecutor.GracefulCancel(ctx) //}) //s.OnEmergencyStop(func() { // _ = s.orderExecutor.GracefulCancel(ctx) // _ = s.ClosePosition(ctx, fixedpoint.One) //}) 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滚仓CCINR策略开始运行") s.nr = session.Indicators(s.Symbol).NR(s.NRInterval, s.NrCount, s.StrictMode) s.cci = session.Indicators(s.Symbol).CCI(s.CCIInterval, s.CCIWindow) s.atr = session.Indicators(s.Symbol).ATR(s.ATRInterval, s.ATRWindow) session.MarketDataStream.OnKLineClosed(func(k types.KLine) { if k.Symbol != s.Symbol { return } if !s.Traded && k.Interval == s.NRInterval { // 如若在下一根k线未成交 则取消订单 if s.TradeType != "" && s.TradeRetry > 1 { bbgo.Notify(fmt.Sprintf("交易信号未成交,取消订单: %s", s.Symbol)) s.cancelOrders(ctx, s.Symbol) } if s.TradeType != "" && s.TradeRetry <= 1 { s.TradeRetry = s.TradeRetry + 1 } } }) s.nr.OnUpdate(func(v float64) { if s.Traded || s.nr.NrKLine.Symbol != s.Symbol { 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 } cciV := s.cci.Last(0) //if cciV > 150 || cciV < -150 { // testMsg := fmt.Sprintf("Test交易信号:币种:%s, 方向 %s, 时间: %s, 最高价:%f,最低价:%f, CCI: %v, ATR: %v", // s.Symbol, s.TradeType, s.nr.NrKLine.GetStartTime(), s.nr.NrKLine.High.Float64(), // s.nr.NrKLine.Low.Float64(), cciV, s.atr.Last(0)) // bbgo.Notify(testMsg) //} if cciV <= s.LongCCI.Float64() { s.TradeType = "long" } else if cciV >= s.ShortCCI.Float64() { s.TradeType = "short" } else { return } msg := fmt.Sprintf("交易信号:币种:%s, 方向 %s, 时间: %s, 最高价:%f,最低价:%f, CCI: %v, ATR: %v", s.Symbol, s.TradeType, s.nr.NrKLine.GetStartTime(), s.nr.NrKLine.High.Float64(), s.nr.NrKLine.Low.Float64(), cciV, s.atr.Last(0)) bbgo.Notify(msg) tk := s.nr.NrKLine s.placeOrders(ctx, tk) }) 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("订单成交通知:\n 币种:%s, 方向:%s, 价格:%s, 数量:%s", order.Symbol, s.TradeType, 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("订单成交通知:\n 币种:%s, 方向:%s, 价格:%s, 数量:%s", order.Symbol, s.TradeType, 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("订单止盈或止损通知:\n %s", order.Symbol) s.Traded = false s.TradeRetry = 0 s.TradeType = "" } 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.TradeType == "long") || (trade.Side == types.SideTypeSell && s.TradeType == "short") { s.OpenTrade = append(s.OpenTrade, trade) s.OpenQuantity = s.OpenQuantity.Add(trade.Quantity) } if (trade.Side == types.SideTypeSell && s.TradeType == "long") || (trade.Side == types.SideTypeBuy && s.TradeType == "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 }