From b6d1b4309b638a45c60a1ef907e3c8e8e0f6b512 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 19 Jun 2022 15:57:59 +0800 Subject: [PATCH 1/6] refactor and update the support strategy --- pkg/bbgo/order_executor_general.go | 10 +- pkg/strategy/bollmaker/strategy.go | 4 +- pkg/strategy/pivotshort/strategy.go | 9 +- pkg/strategy/support/strategy.go | 147 ++++++++-------------------- 4 files changed, 53 insertions(+), 117 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 2a0b1c1df..751cddacb 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -85,10 +85,10 @@ func (e *GeneralOrderExecutor) Bind() { e.tradeCollector.BindStream(e.session.UserDataStream) } -func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) error { +func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) { formattedOrders, err := e.session.FormatOrders(submitOrders) if err != nil { - return err + return nil, err } createdOrders, err := e.session.Exchange.SubmitOrders(ctx, formattedOrders...) @@ -99,7 +99,7 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders .. e.orderStore.Add(createdOrders...) e.activeMakerOrders.Add(createdOrders...) e.tradeCollector.Process() - return err + return createdOrders, err } func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context) error { @@ -118,10 +118,10 @@ func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fix return nil } - return e.SubmitOrders(ctx, *submitOrder) + _, err := e.SubmitOrders(ctx, *submitOrder) + return err } - func (e *GeneralOrderExecutor) TradeCollector() *TradeCollector { return e.tradeCollector } diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index 9d15c34f3..49a8210c6 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -241,7 +241,7 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu bbgo.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, submitOrder) - err := s.orderExecutor.SubmitOrders(ctx, submitOrder) + _, err := s.orderExecutor.SubmitOrders(ctx, submitOrder) return err } @@ -470,7 +470,7 @@ func (s *Strategy) placeOrders(ctx context.Context, orderExecutor bbgo.OrderExec submitOrders[i] = adjustOrderQuantity(submitOrders[i], s.Market) } - _ = s.orderExecutor.SubmitOrders(ctx, submitOrders...) + _, _ = s.orderExecutor.SubmitOrders(ctx, submitOrders...) } func (s *Strategy) hasLongSet() bool { diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index fc7f53a37..a9431b043 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -143,7 +143,7 @@ func (s *Strategy) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoin } func (s *Strategy) placeLimitSell(ctx context.Context, price, quantity fixedpoint.Value) { - _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: s.Symbol, Price: price, Side: types.SideTypeSell, @@ -154,7 +154,7 @@ func (s *Strategy) placeLimitSell(ctx context.Context, price, quantity fixedpoin } func (s *Strategy) placeMarketSell(ctx context.Context, quantity fixedpoint.Value) { - _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeMarket, @@ -174,7 +174,8 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu } bbgo.Notify("Closing %s position by %f", s.Symbol, percentage.Float64()) - return s.orderExecutor.SubmitOrders(ctx, *submitOrder) + _, err := s.orderExecutor.SubmitOrders(ctx, *submitOrder) + return err } func (s *Strategy) InstanceID() string { @@ -470,7 +471,7 @@ func (s *Strategy) placeBounceSellOrders(ctx context.Context, resistancePrice fi } func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, quantity fixedpoint.Value) { - _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeLimit, diff --git a/pkg/strategy/support/strategy.go b/pkg/strategy/support/strategy.go index 676fa1dd7..3e9d36d34 100644 --- a/pkg/strategy/support/strategy.go +++ b/pkg/strategy/support/strategy.go @@ -131,6 +131,7 @@ func (control *TrailingStopControl) GenerateStopOrder(quantity fixedpoint.Value) type Strategy struct { *bbgo.Persistence + *bbgo.Environment *bbgo.Graceful `json:"-"` Symbol string `json:"symbol"` @@ -165,13 +166,13 @@ type Strategy struct { ScaleQuantity *bbgo.PriceVolumeScale `json:"scaleQuantity"` - orderExecutor bbgo.OrderExecutor + orderExecutor *bbgo.GeneralOrderExecutor - tradeCollector *bbgo.TradeCollector + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` - orderStore *bbgo.OrderStore - activeOrders *bbgo.ActiveOrderBook - state *State + state *State triggerEMA *indicator.EWMA longTermEMA *indicator.EWMA @@ -246,14 +247,7 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu } bbgo.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, submitOrder) - - createdOrders, err := s.submitOrders(ctx, s.orderExecutor, submitOrder) - if err != nil { - log.WithError(err).Errorf("can not place position close order") - } - - s.orderStore.Add(createdOrders...) - s.activeOrders.Add(createdOrders...) + _, err := s.orderExecutor.SubmitOrders(ctx, submitOrder) return err } @@ -298,33 +292,7 @@ func (s *Strategy) LoadState() error { } func (s *Strategy) submitOrders(ctx context.Context, orderExecutor bbgo.OrderExecutor, orderForms ...types.SubmitOrder) (types.OrderSlice, error) { - createdOrders, err := orderExecutor.SubmitOrders(ctx, orderForms...) - if err != nil { - return nil, err - } - - s.orderStore.Add(createdOrders...) - s.activeOrders.Add(createdOrders...) - s.tradeCollector.Emit() - return createdOrders, nil -} - -// Cancel order -func (s *Strategy) cancelOrder(orderID uint64, ctx context.Context, orderExecutor bbgo.OrderExecutor) error { - // Cancel the original order - order, ok := s.orderStore.Get(orderID) - if ok { - switch order.Status { - case types.OrderStatusCanceled, types.OrderStatusRejected, types.OrderStatusFilled: - // Do nothing - default: - if err := orderExecutor.CancelOrders(ctx, order); err != nil { - return err - } - } - } - - return nil + return s.orderExecutor.SubmitOrders(ctx, orderForms...) } var slippageModifier = fixedpoint.NewFromFloat(1.003) @@ -389,27 +357,34 @@ func (s *Strategy) calculateQuantity(session *bbgo.ExchangeSession, side types.S } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { - s.orderExecutor = orderExecutor + instanceID := s.InstanceID() + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + // trade stats + if s.TradeStats == nil { + s.TradeStats = &types.TradeStats{} + } + + 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() // StrategyController s.Status = types.StrategyStatusRunning s.OnSuspend(func() { // Cancel all order - if err := s.activeOrders.GracefulCancel(ctx, session.Exchange); err != nil { - errMsg := fmt.Sprintf("Not all %s orders are cancelled! Please check again.", s.Symbol) - log.WithError(err).Errorf(errMsg) - bbgo.Notify(errMsg) - } else { - bbgo.Notify("All %s orders are cancelled.", s.Symbol) - } - - // Save state - if err := s.SaveState(); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) - } else { - log.Infof("%s state is saved.", s.Symbol) - } + _ = s.orderExecutor.GracefulCancel(ctx) + _ = s.Persistence.Sync(s) }) s.OnEmergencyStop(func() { @@ -447,12 +422,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.Infof("adjusted minimal support volume to %s according to sensitivity %s", s.MinVolume.String(), s.Sensitivity.String()) } - market, ok := session.Market(s.Symbol) - if !ok { - return fmt.Errorf("market %s is not defined", s.Symbol) - } - s.Market = market - standardIndicatorSet, ok := session.StandardIndicatorSet(s.Symbol) if !ok { return fmt.Errorf("standardIndicatorSet is nil, symbol %s", s.Symbol) @@ -471,12 +440,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.longTermEMA = standardIndicatorSet.EWMA(s.LongTermMovingAverage) } - s.orderStore = bbgo.NewOrderStore(s.Symbol) - s.orderStore.BindStream(session.UserDataStream) - - s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) - s.activeOrders.BindStream(session.UserDataStream) - if !s.TrailingStopTarget.TrailingStopCallbackRatio.IsZero() { s.trailingStopControl = &TrailingStopControl{ symbol: s.Symbol, @@ -494,11 +457,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.Notify("%s state is restored => %+v", s.Symbol, s.state) } - s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.state.Position, s.orderStore) - if !s.TrailingStopTarget.TrailingStopCallbackRatio.IsZero() { // Update trailing stop when the position changes - s.tradeCollector.OnPositionUpdate(func(position *types.Position) { + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { // StrategyController if s.Status != types.StrategyStatusRunning { return @@ -506,9 +467,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if position.Base.Compare(s.Market.MinQuantity) > 0 { // Update order if we have a position // Cancel the original order - if err := s.cancelOrder(s.trailingStopControl.OrderID, ctx, orderExecutor); err != nil { - log.WithError(err).Errorf("Can not cancel the original trailing stop order!") - } + _ = s.orderExecutor.GracefulCancel(ctx) s.trailingStopControl.OrderID = 0 // Calculate minimum target price @@ -536,20 +495,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } } - // Save state - if err := s.SaveState(); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) - } else { - bbgo.Notify("%s position is saved", s.Symbol, s.state.Position) - } }) } - s.tradeCollector.BindStream(session.UserDataStream) - - // s.tradeCollector.BindStreamForBackground(session.UserDataStream) - // go s.tradeCollector.Run(ctx) - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { // StrategyController if s.Status != types.StrategyStatusRunning { @@ -576,9 +524,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.trailingStopControl.CurrentHighestPrice = highPrice // Cancel the original order - if err := s.cancelOrder(s.trailingStopControl.OrderID, ctx, orderExecutor); err != nil { - log.WithError(err).Errorf("Can not cancel the original trailing stop order!") - } + s.orderExecutor.GracefulCancel(ctx) s.trailingStopControl.OrderID = 0 // Calculate minimum target price @@ -679,7 +625,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se orderForm := types.SubmitOrder{ Symbol: s.Symbol, - Market: market, + Market: s.Market, Side: types.SideTypeBuy, Type: types.OrderTypeMarket, Quantity: quantity, @@ -711,17 +657,17 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se targetQuantity := quantity.Mul(target.QuantityPercentage) targetQuoteQuantity := targetPrice.Mul(targetQuantity) - if targetQuoteQuantity.Compare(market.MinNotional) <= 0 { + if targetQuoteQuantity.Compare(s.Market.MinNotional) <= 0 { continue } - if targetQuantity.Compare(market.MinQuantity) <= 0 { + if targetQuantity.Compare(s.Market.MinQuantity) <= 0 { continue } targetOrders = append(targetOrders, types.SubmitOrder{ Symbol: kline.Symbol, - Market: market, + Market: s.Market, Type: types.OrderTypeLimit, Side: types.SideTypeSell, Price: targetPrice, @@ -732,14 +678,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) } - if _, err := s.submitOrders(ctx, orderExecutor, targetOrders...); err != nil { - log.WithError(err).Error("submit profit target order error") - bbgo.Notify("submit %s profit trailing stop order error", s.Symbol) - return + _, err = s.orderExecutor.SubmitOrders(ctx, targetOrders...) + if err != nil { + bbgo.Notify("submit %s profit trailing stop order error: %s", s.Symbol, err.Error()) } } - - s.tradeCollector.Process() }) s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { @@ -747,15 +690,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Cancel trailing stop order if s.TrailingStopTarget.TrailingStopCallbackRatio.Sign() > 0 { - // Cancel all orders - if err := s.activeOrders.GracefulCancel(ctx, session.Exchange); err != nil { - errMsg := "Not all {s.Symbol} orders are cancelled! Please check again." - log.WithError(err).Errorf(errMsg) - bbgo.Notify(errMsg) - } else { - bbgo.Notify("All {s.Symbol} orders are cancelled.") - } - + _ = s.orderExecutor.GracefulCancel(ctx) s.trailingStopControl.OrderID = 0 } From f035667f3781ee47ad4f1e4ad9b9beb16ad00cbe Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 19 Jun 2022 17:23:10 +0800 Subject: [PATCH 2/6] support: refactor trailing stop order management --- pkg/strategy/support/strategy.go | 185 +++++++++---------------------- 1 file changed, 52 insertions(+), 133 deletions(-) diff --git a/pkg/strategy/support/strategy.go b/pkg/strategy/support/strategy.go index 3e9d36d34..04b4863e7 100644 --- a/pkg/strategy/support/strategy.go +++ b/pkg/strategy/support/strategy.go @@ -7,11 +7,9 @@ import ( "github.com/sirupsen/logrus" - "github.com/c9s/bbgo/pkg/indicator" - "github.com/c9s/bbgo/pkg/service" - "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" ) @@ -95,6 +93,12 @@ type TrailingStopControl struct { OrderID uint64 } +func (control *TrailingStopControl) UpdateCurrentHighestPrice(p fixedpoint.Value) bool { + orig := control.CurrentHighestPrice + control.CurrentHighestPrice = fixedpoint.Max(control.CurrentHighestPrice, p) + return orig.Compare(control.CurrentHighestPrice) == 0 +} + func (control *TrailingStopControl) IsHigherThanMin(minTargetPrice fixedpoint.Value) bool { targetPrice := control.CurrentHighestPrice.Mul(fixedpoint.One.Sub(control.trailingStopCallbackRatio)) @@ -168,9 +172,10 @@ type Strategy struct { orderExecutor *bbgo.GeneralOrderExecutor - Position *types.Position `persistence:"position"` - ProfitStats *types.ProfitStats `persistence:"profit_stats"` - TradeStats *types.TradeStats `persistence:"trade_stats"` + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + CurrentHighestPrice fixedpoint.Value `persistence:"current_highest_price"` state *State @@ -251,46 +256,6 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu return err } -func (s *Strategy) SaveState() error { - if err := s.Persistence.Save(s.state, ID, s.Symbol, stateKey); err != nil { - return err - } else { - log.Infof("state is saved => %+v", s.state) - } - return nil -} - -func (s *Strategy) LoadState() error { - var state State - - // load position - if err := s.Persistence.Load(&state, ID, s.Symbol, stateKey); err != nil { - if err != service.ErrPersistenceNotExists { - return err - } - - s.state = &State{} - } else { - s.state = &state - log.Infof("state is restored: %+v", s.state) - } - - if s.state.Position == nil { - s.state.Position = types.NewPositionFromMarket(s.Market) - } - - if s.trailingStopControl != nil { - if s.state.CurrentHighestPrice == nil { - s.trailingStopControl.CurrentHighestPrice = fixedpoint.Zero - } else { - s.trailingStopControl.CurrentHighestPrice = *s.state.CurrentHighestPrice - } - s.state.CurrentHighestPrice = &s.trailingStopControl.CurrentHighestPrice - } - - return nil -} - func (s *Strategy) submitOrders(ctx context.Context, orderExecutor bbgo.OrderExecutor, orderForms ...types.SubmitOrder) (types.OrderSlice, error) { return s.orderExecutor.SubmitOrders(ctx, orderForms...) } @@ -445,18 +410,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se symbol: s.Symbol, market: s.Market, marginSideEffect: s.MarginOrderSideEffect, - CurrentHighestPrice: fixedpoint.Zero, trailingStopCallbackRatio: s.TrailingStopTarget.TrailingStopCallbackRatio, minimumProfitPercentage: s.TrailingStopTarget.MinimumProfitPercentage, + CurrentHighestPrice: s.CurrentHighestPrice, } } - if err := s.LoadState(); err != nil { - return err - } else { - bbgo.Notify("%s state is restored => %+v", s.Symbol, s.state) - } - if !s.TrailingStopTarget.TrailingStopCallbackRatio.IsZero() { // Update trailing stop when the position changes s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { @@ -465,36 +424,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } - if position.Base.Compare(s.Market.MinQuantity) > 0 { // Update order if we have a position - // Cancel the original order - _ = s.orderExecutor.GracefulCancel(ctx) - s.trailingStopControl.OrderID = 0 - - // Calculate minimum target price - var minTargetPrice = fixedpoint.Zero - if s.trailingStopControl.minimumProfitPercentage.Sign() > 0 { - minTargetPrice = position.AverageCost.Mul(fixedpoint.One.Add(s.trailingStopControl.minimumProfitPercentage)) - } - - // Place new order if the target price is higher than the minimum target price - if s.trailingStopControl.IsHigherThanMin(minTargetPrice) { - orderForm := s.trailingStopControl.GenerateStopOrder(position.Base) - orders, err := s.submitOrders(ctx, orderExecutor, orderForm) - if err != nil { - log.WithError(err).Error("submit profit trailing stop order error") - bbgo.Notify("submit %s profit trailing stop order error", s.Symbol) - } else { - orderIds := orders.IDs() - if len(orderIds) > 0 { - s.trailingStopControl.OrderID = orderIds[0] - } else { - log.Error("submit profit trailing stop order error. unknown error") - bbgo.Notify("submit %s profit trailing stop order error", s.Symbol) - s.trailingStopControl.OrderID = 0 - } - } - } + if !position.IsLong() || position.IsDust(position.AverageCost) { + return } + + s.updateStopOrder(ctx) }) } @@ -515,46 +449,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se closePrice := kline.GetClose() highPrice := kline.GetHigh() + // check our trailing stop if s.TrailingStopTarget.TrailingStopCallbackRatio.Sign() > 0 { - if s.state.Position.Base.Compare(s.Market.MinQuantity) <= 0 { // Without a position - // Update trailing orders with current high price - s.trailingStopControl.CurrentHighestPrice = highPrice - } else if s.trailingStopControl.CurrentHighestPrice.Compare(highPrice) < 0 || s.trailingStopControl.OrderID == 0 { // With a position or no trailing stop order yet - // Update trailing orders with current high price if it's higher - s.trailingStopControl.CurrentHighestPrice = highPrice - - // Cancel the original order - s.orderExecutor.GracefulCancel(ctx) - s.trailingStopControl.OrderID = 0 - - // Calculate minimum target price - var minTargetPrice = fixedpoint.Zero - if s.trailingStopControl.minimumProfitPercentage.Sign() > 0 { - minTargetPrice = s.state.Position.AverageCost.Mul(fixedpoint.One.Add(s.trailingStopControl.minimumProfitPercentage)) + if s.Position.IsLong() && !s.Position.IsDust(closePrice) { + changed := s.trailingStopControl.UpdateCurrentHighestPrice(highPrice) + if changed { + // Cancel the original order + s.updateStopOrder(ctx) } - - // Place new order if the target price is higher than the minimum target price - if s.trailingStopControl.IsHigherThanMin(minTargetPrice) { - orderForm := s.trailingStopControl.GenerateStopOrder(s.state.Position.Base) - orders, err := s.submitOrders(ctx, orderExecutor, orderForm) - if err != nil || orders == nil { - log.WithError(err).Errorf("submit %s profit trailing stop order error", s.Symbol) - bbgo.Notify("submit %s profit trailing stop order error", s.Symbol) - } else { - orderIds := orders.IDs() - if len(orderIds) > 0 { - s.trailingStopControl.OrderID = orderIds[0] - } else { - log.Error("submit profit trailing stop order error. unknown error") - bbgo.Notify("submit %s profit trailing stop order error", s.Symbol) - s.trailingStopControl.OrderID = 0 - } - } - } - } - // Save state - if err := s.SaveState(); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) } } @@ -643,12 +545,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.WithError(err).Error("submit order error") return } - // Save state - if err := s.SaveState(); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) - } else { - bbgo.Notify("%s position is saved", s.Symbol, s.state.Position) - } if s.TrailingStopTarget.TrailingStopCallbackRatio.IsZero() { // submit fixed target orders var targetOrders []types.SubmitOrder @@ -691,15 +587,38 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Cancel trailing stop order if s.TrailingStopTarget.TrailingStopCallbackRatio.Sign() > 0 { _ = s.orderExecutor.GracefulCancel(ctx) - s.trailingStopControl.OrderID = 0 - } - - if err := s.SaveState(); err != nil { - log.WithError(err).Errorf("can not save state: %+v", s.state) - } else { - bbgo.Notify("%s position is saved", s.Symbol, s.state.Position) } }) return nil } + +func (s *Strategy) updateStopOrder(ctx context.Context) { + // cancel the original stop order + _ = s.orderExecutor.GracefulCancel(ctx) + s.trailingStopControl.OrderID = 0 + + // Calculate minimum target price + var minTargetPrice = fixedpoint.Zero + if s.trailingStopControl.minimumProfitPercentage.Sign() > 0 { + minTargetPrice = s.state.Position.AverageCost.Mul(fixedpoint.One.Add(s.trailingStopControl.minimumProfitPercentage)) + } + + // Place new order if the target price is higher than the minimum target price + if s.trailingStopControl.IsHigherThanMin(minTargetPrice) { + orderForm := s.trailingStopControl.GenerateStopOrder(s.state.Position.Base) + orders, err := s.orderExecutor.SubmitOrders(ctx, orderForm) + if err != nil { + bbgo.Notify("failed to submit the trailing stop order on %s", s.Symbol) + log.WithError(err).Error("submit profit trailing stop order error") + } + + if len(orders) == 0 { + log.Error("unexpected error: len(createdOrders) = 0") + return + } + + orderIds := orders.IDs() + s.trailingStopControl.OrderID = orderIds[0] + } +} From 6b26d5a956953be0a9352f50b807fa031cfdf4b4 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 19 Jun 2022 17:24:47 +0800 Subject: [PATCH 3/6] config: clean up the support config --- config/support.yaml | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/config/support.yaml b/config/support.yaml index 744c70ccf..94a2a7d12 100644 --- a/config/support.yaml +++ b/config/support.yaml @@ -3,19 +3,11 @@ notifications: slack: defaultChannel: "dev-bbgo" errorChannel: "bbgo-error" - - # if you want to route channel by symbol - symbolChannels: - "^BTC": "btc" - "^ETH": "eth" - "^BNB": "bnb" - # object routing rules routing: trade: "$symbol" order: "$symbol" submitOrder: "$session" # not supported yet - pnL: "bbgo-pnl" sessions: binance: @@ -35,9 +27,6 @@ riskControls: maxOrderAmount: 1000.0 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: "2020-09-04" endTime: "2020-09-14" symbols: @@ -46,7 +35,6 @@ backtest: account: binance: balances: - BTC: 0.0 USDT: 10000.0 exchangeStrategies: @@ -72,11 +60,10 @@ exchangeStrategies: maxBaseAssetBalance: 1000.0 minQuoteAssetBalance: 2000.0 - #trailingStopTarget: - # callbackRatio: 0.015 - # minimumProfitPercentage: 0.02 + trailingStopTarget: + callbackRatio: 1.5% + minimumProfitPercentage: 2% targets: - profitPercentage: 0.02 quantityPercentage: 0.5 - From bf0186cf55f9d48caf592cc1559626c948b3c784 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 19 Jun 2022 17:36:28 +0800 Subject: [PATCH 4/6] fix batch buffer size check Signed-off-by: c9s --- pkg/service/sync_task.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/service/sync_task.go b/pkg/service/sync_task.go index 796bccfe9..0727e0a76 100644 --- a/pkg/service/sync_task.go +++ b/pkg/service/sync_task.go @@ -130,7 +130,7 @@ func (sel SyncTask) execute(ctx context.Context, db *sqlx.DB, startTime time.Tim } if sel.BatchInsert != nil { - if batchBufferRefVal.Len() >= sel.BatchInsertBuffer-1 { + if batchBufferRefVal.Len() > sel.BatchInsertBuffer-1 { if sel.LogInsert { logrus.Infof("batch inserting %d %T", batchBufferRefVal.Len(), obj) } else { From 6e562e2ededf1d653da82ffaf148eae1e093031f Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 19 Jun 2022 17:41:52 +0800 Subject: [PATCH 5/6] increase batch insert size to 1000 for klines Signed-off-by: c9s --- config/support.yaml | 5 +++-- pkg/service/backtest.go | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config/support.yaml b/config/support.yaml index 94a2a7d12..c281be30b 100644 --- a/config/support.yaml +++ b/config/support.yaml @@ -29,9 +29,10 @@ riskControls: backtest: startTime: "2020-09-04" endTime: "2020-09-14" + sessions: + - binance symbols: - - BTCUSDT - - ETHUSDT + - LINKUSDT account: binance: balances: diff --git a/pkg/service/backtest.go b/pkg/service/backtest.go index 8c1edfac0..256514fcf 100644 --- a/pkg/service/backtest.go +++ b/pkg/service/backtest.go @@ -52,7 +52,7 @@ func (s *BacktestService) SyncKLineByInterval(ctx context.Context, exchange type q := &batch.KLineBatchQuery{Exchange: exchange} return q.Query(ctx, symbol, interval, startTime, endTime) }, - BatchInsertBuffer: 500, + BatchInsertBuffer: 1000, BatchInsert: func(obj interface{}) error { kLines := obj.([]types.KLine) return s.BatchInsert(kLines) From 2a1beddba4b3605ee21109015e989a9540f27afb Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 19 Jun 2022 17:49:38 +0800 Subject: [PATCH 6/6] support: fix support strategy stop order update --- config/support.yaml | 23 +++++----------------- pkg/strategy/support/strategy.go | 33 +++++++++++++++++++------------- pkg/types/order.go | 3 ++- pkg/types/ordermap.go | 7 ------- 4 files changed, 27 insertions(+), 39 deletions(-) diff --git a/config/support.yaml b/config/support.yaml index c281be30b..3f45bb2b2 100644 --- a/config/support.yaml +++ b/config/support.yaml @@ -13,22 +13,9 @@ sessions: binance: exchange: binance -riskControls: - # This is the session-based risk controller, which let you configure different risk controller by session. - sessionBased: - max: - orderExecutor: - bySymbol: - BTCUSDT: - basic: - minQuoteBalance: 100.0 - maxBaseAssetBalance: 3.0 - minBaseAssetBalance: 0.0 - maxOrderAmount: 1000.0 - backtest: - startTime: "2020-09-04" - endTime: "2020-09-14" + startTime: "2021-09-01" + endTime: "2021-09-30" sessions: - binance symbols: @@ -43,8 +30,8 @@ exchangeStrategies: - on: binance support: symbol: LINKUSDT - interval: 1m - minVolume: 1_000 + interval: 5m + minVolume: 80_000 triggerMovingAverage: interval: 5m window: 99 @@ -55,7 +42,7 @@ exchangeStrategies: scaleQuantity: byVolume: exp: - domain: [ 1_000, 200_000 ] + domain: [ 10_000, 200_000 ] range: [ 0.5, 1.0 ] maxBaseAssetBalance: 1000.0 diff --git a/pkg/strategy/support/strategy.go b/pkg/strategy/support/strategy.go index 04b4863e7..e79eca793 100644 --- a/pkg/strategy/support/strategy.go +++ b/pkg/strategy/support/strategy.go @@ -90,7 +90,7 @@ type TrailingStopControl struct { minimumProfitPercentage fixedpoint.Value CurrentHighestPrice fixedpoint.Value - OrderID uint64 + StopOrder *types.Order } func (control *TrailingStopControl) UpdateCurrentHighestPrice(p fixedpoint.Value) bool { @@ -134,9 +134,11 @@ func (control *TrailingStopControl) GenerateStopOrder(quantity fixedpoint.Value) // } type Strategy struct { - *bbgo.Persistence - *bbgo.Environment - *bbgo.Graceful `json:"-"` + *bbgo.Persistence `json:"-"` + *bbgo.Environment `json:"-"` + *bbgo.Graceful `json:"-"` + + session *bbgo.ExchangeSession Symbol string `json:"symbol"` Market types.Market `json:"-"` @@ -223,13 +225,13 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } func (s *Strategy) CurrentPosition() *types.Position { - return s.state.Position + return s.Position } func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { - base := s.state.Position.GetBase() + base := s.Position.GetBase() if base.IsZero() { - return fmt.Errorf("no opened %s position", s.state.Position.Symbol) + return fmt.Errorf("no opened %s position", s.Position.Symbol) } // make it negative @@ -322,6 +324,7 @@ func (s *Strategy) calculateQuantity(session *bbgo.ExchangeSession, side types.S } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + s.session = session instanceID := s.InstanceID() if s.Position == nil { @@ -595,18 +598,23 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se func (s *Strategy) updateStopOrder(ctx context.Context) { // cancel the original stop order - _ = s.orderExecutor.GracefulCancel(ctx) - s.trailingStopControl.OrderID = 0 + if s.trailingStopControl.StopOrder != nil { + if err := s.session.Exchange.CancelOrders(ctx, *s.trailingStopControl.StopOrder); err != nil { + log.WithError(err).Error("cancel order error") + } + s.trailingStopControl.StopOrder = nil + s.orderExecutor.TradeCollector().Process() + } // Calculate minimum target price var minTargetPrice = fixedpoint.Zero if s.trailingStopControl.minimumProfitPercentage.Sign() > 0 { - minTargetPrice = s.state.Position.AverageCost.Mul(fixedpoint.One.Add(s.trailingStopControl.minimumProfitPercentage)) + minTargetPrice = s.Position.AverageCost.Mul(fixedpoint.One.Add(s.trailingStopControl.minimumProfitPercentage)) } // Place new order if the target price is higher than the minimum target price if s.trailingStopControl.IsHigherThanMin(minTargetPrice) { - orderForm := s.trailingStopControl.GenerateStopOrder(s.state.Position.Base) + orderForm := s.trailingStopControl.GenerateStopOrder(s.Position.Base) orders, err := s.orderExecutor.SubmitOrders(ctx, orderForm) if err != nil { bbgo.Notify("failed to submit the trailing stop order on %s", s.Symbol) @@ -618,7 +626,6 @@ func (s *Strategy) updateStopOrder(ctx context.Context) { return } - orderIds := orders.IDs() - s.trailingStopControl.OrderID = orderIds[0] + s.trailingStopControl.StopOrder = &orders[0] } } diff --git a/pkg/types/order.go b/pkg/types/order.go index 60d744214..f560e165a 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -267,11 +267,12 @@ func (o Order) String() string { orderID = strconv.FormatUint(o.OrderID, 10) } - return fmt.Sprintf("ORDER %s | %s | %s | %s %-4s | %s/%s @ %s | %s", + return fmt.Sprintf("ORDER %s | %s | %s | %s | %s %-4s | %s/%s @ %s | %s", o.Exchange.String(), o.CreationTime.Time().Local().Format(time.RFC1123), orderID, o.Symbol, + o.Type, o.Side, o.ExecutedQuantity.String(), o.Quantity.String(), diff --git a/pkg/types/ordermap.go b/pkg/types/ordermap.go index e47677859..651f3d8c7 100644 --- a/pkg/types/ordermap.go +++ b/pkg/types/ordermap.go @@ -205,10 +205,3 @@ func (m *SyncOrderMap) Orders() (slice OrderSlice) { } type OrderSlice []Order - -func (s OrderSlice) IDs() (ids []uint64) { - for _, o := range s { - ids = append(ids, o.OrderID) - } - return ids -}