diff --git a/pkg/strategy/dca2/dev_mode.go b/pkg/strategy/dca2/dev_mode.go new file mode 100644 index 000000000..ceb58f90a --- /dev/null +++ b/pkg/strategy/dca2/dev_mode.go @@ -0,0 +1,7 @@ +package dca2 + +// DevMode, if Enabled is true. it means it will check the running field +type DevMode struct { + Enabled bool `json:"enabled"` + IsNewAccount bool `json:"isNewAccount"` +} diff --git a/pkg/strategy/dca2/open_position.go b/pkg/strategy/dca2/open_position.go index 3c7afc48e..42bc6a678 100644 --- a/pkg/strategy/dca2/open_position.go +++ b/pkg/strategy/dca2/open_position.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/exchange/retry" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" @@ -32,6 +33,21 @@ func (s *Strategy) placeOpenPositionOrders(ctx context.Context) error { s.debugOrders(createdOrders) + if s.DevMode != nil && s.DevMode.Enabled && s.DevMode.IsNewAccount { + if len(createdOrders) > 0 { + s.ProfitStats.FromOrderID = createdOrders[0].OrderID + } + + for _, createdOrder := range createdOrders { + if s.ProfitStats.FromOrderID > createdOrder.OrderID { + s.ProfitStats.FromOrderID = createdOrder.OrderID + } + } + + s.DevMode.IsNewAccount = false + bbgo.Sync(ctx, s) + } + return nil } diff --git a/pkg/strategy/dca2/profit_stats.go b/pkg/strategy/dca2/profit_stats.go index 2c02512b3..6a5624523 100644 --- a/pkg/strategy/dca2/profit_stats.go +++ b/pkg/strategy/dca2/profit_stats.go @@ -28,7 +28,7 @@ func newProfitStats(market types.Market, quoteInvestment fixedpoint.Value) *Prof return &ProfitStats{ Symbol: market.Symbol, Market: market, - Round: 0, + Round: 1, QuoteInvestment: quoteInvestment, CurrentRoundFee: make(map[string]fixedpoint.Value), TotalFee: make(map[string]fixedpoint.Value), diff --git a/pkg/strategy/dca2/recover.go b/pkg/strategy/dca2/recover.go index 373314f23..d114c40e0 100644 --- a/pkg/strategy/dca2/recover.go +++ b/pkg/strategy/dca2/recover.go @@ -7,11 +7,12 @@ import ( "time" "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) +var recoverSinceLimit = time.Date(2024, time.January, 29, 12, 0, 0, 0, time.Local) + type descendingClosedOrderQueryService interface { QueryClosedOrdersDesc(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) ([]types.Order, error) } @@ -34,7 +35,7 @@ func (s *Strategy) recover(ctx context.Context) error { return err } - closedOrders, err := queryService.QueryClosedOrdersDesc(ctx, s.Symbol, time.Date(2024, time.January, 12, 14, 0, 0, 0, time.Local), time.Now(), 0) + closedOrders, err := queryService.QueryClosedOrdersDesc(ctx, s.Symbol, recoverSinceLimit, time.Now(), 0) if err != nil { return err } @@ -45,12 +46,11 @@ func (s *Strategy) recover(ctx context.Context) error { } debugRoundOrders(s.logger, "current", currentRound) - // recover state - state, err := recoverState(ctx, s.Symbol, int(s.MaxOrderCount), openOrders, currentRound, s.OrderExecutor.ActiveMakerOrders(), s.OrderExecutor.OrderStore(), s.OrderGroupID) - if err != nil { + // recover profit stats + if err := recoverProfitStats(ctx, s); err != nil { return err } - s.logger.Info("recover stats DONE") + s.logger.Info("recover profit stats DONE") // recover position if err := recoverPosition(ctx, s.Position, queryService, currentRound); err != nil { @@ -58,73 +58,65 @@ func (s *Strategy) recover(ctx context.Context) error { } s.logger.Info("recover position DONE") - // recover profit stats - recoverProfitStats(ctx, s) - s.logger.Info("recover profit stats DONE") - // recover startTimeOfNextRound startTimeOfNextRound := recoverStartTimeOfNextRound(ctx, currentRound, s.CoolDownInterval) - - s.state = state s.startTimeOfNextRound = startTimeOfNextRound + // recover state + state, err := recoverState(ctx, s.ProfitStats.QuoteInvestment, int(s.MaxOrderCount), currentRound, s.OrderExecutor) + if err != nil { + return err + } + s.state = state + s.logger.Info("recover stats DONE") + return nil } // recover state -func recoverState(ctx context.Context, symbol string, maxOrderCount int, openOrders []types.Order, currentRound Round, activeOrderBook *bbgo.ActiveOrderBook, orderStore *core.OrderStore, groupID uint32) (State, error) { - if len(currentRound.OpenPositionOrders) == 0 { - // new strategy +func recoverState(ctx context.Context, quoteInvestment fixedpoint.Value, maxOrderCount int, currentRound Round, orderExecutor *bbgo.GeneralOrderExecutor) (State, error) { + activeOrderBook := orderExecutor.ActiveMakerOrders() + orderStore := orderExecutor.OrderStore() + + // dca stop at take-profit order stage + if currentRound.TakeProfitOrder.OrderID != 0 { + if len(currentRound.OpenPositionOrders) != maxOrderCount { + return None, fmt.Errorf("there is take-profit order but the number of open-position orders (%d) is not the same as maxOrderCount(%d). Please check it", len(currentRound.OpenPositionOrders), maxOrderCount) + } + + takeProfitOrder := currentRound.TakeProfitOrder + if takeProfitOrder.Status == types.OrderStatusFilled { + return WaitToOpenPosition, nil + } else if types.IsActiveOrder(takeProfitOrder) { + activeOrderBook.Add(takeProfitOrder) + orderStore.Add(takeProfitOrder) + return TakeProfitReady, nil + } else { + return None, fmt.Errorf("the status of take-profit order is %s. Please check it", takeProfitOrder.Status) + } + } + + // dca stop at no take-profit order stage + openPositionOrders := currentRound.OpenPositionOrders + numOpenPositionOrders := len(openPositionOrders) + + // new strategy + if len(openPositionOrders) == 0 { return WaitToOpenPosition, nil } - numOpenOrders := len(openOrders) - // dca stop at take profit order stage - if currentRound.TakeProfitOrder.OrderID != 0 { - if numOpenOrders == 0 { - // current round's take-profit order filled, wait to open next round - return WaitToOpenPosition, nil - } - - // check the open orders is take profit order or not - if numOpenOrders == 1 { - if openOrders[0].OrderID == currentRound.TakeProfitOrder.OrderID { - activeOrderBook.Add(openOrders[0]) - // current round's take-profit order still opened, wait to fill - return TakeProfitReady, nil - } else { - return None, fmt.Errorf("stop at taking profit stage, but the open order's OrderID is not the take-profit order's OrderID") - } - } - - return None, fmt.Errorf("stop at taking profit stage, but the number of open orders is > 1") - } - - numOpenPositionOrders := len(currentRound.OpenPositionOrders) + // should not happen if numOpenPositionOrders > maxOrderCount { - return None, fmt.Errorf("the number of open-position orders is > max order number") - } else if numOpenPositionOrders < maxOrderCount { - // The number of open-position orders should be the same as maxOrderCount - // If not, it may be the following possible cause - // 1. This strategy at position opening, so it may not place all orders we want successfully - // 2. There are some errors when placing open-position orders. e.g. cannot lock fund..... - return None, fmt.Errorf("the number of open-position orders is < max order number") - } - - if numOpenOrders > numOpenPositionOrders { - return None, fmt.Errorf("the number of open orders is > the number of open-position orders") - } - - if numOpenOrders == numOpenPositionOrders { - activeOrderBook.Add(openOrders...) - orderStore.Add(openOrders...) - return OpenPositionReady, nil + return None, fmt.Errorf("the number of open-position orders (%d) is > max order number", numOpenPositionOrders) } + // collect open-position orders' status var openedCnt, filledCnt, cancelledCnt int64 for _, order := range currentRound.OpenPositionOrders { switch order.Status { case types.OrderStatusNew, types.OrderStatusPartiallyFilled: + activeOrderBook.Add(order) + orderStore.Add(order) openedCnt++ case types.OrderStatusFilled: filledCnt++ @@ -135,21 +127,40 @@ func recoverState(ctx context.Context, symbol string, maxOrderCount int, openOrd } } + // the number of open-position orders is the same as maxOrderCount -> place open-position orders successfully + if numOpenPositionOrders == maxOrderCount { + // all open-position orders are still not filled -> OpenPositionReady + if filledCnt == 0 && cancelledCnt == 0 { + return OpenPositionReady, nil + } + + // there are at least one open-position orders filled -> OpenPositionOrderFilled + if filledCnt > 0 && cancelledCnt == 0 { + return OpenPositionOrderFilled, nil + } + + // there are at last one open-position orders cancelled -> + if cancelledCnt > 0 { + return OpenPositionOrdersCancelling, nil + } + + return None, fmt.Errorf("unexpected order status combination when numOpenPositionOrders(%d) == maxOrderCount(%d) (opened, filled, cancelled) = (%d, %d, %d)", numOpenPositionOrders, maxOrderCount, openedCnt, filledCnt, cancelledCnt) + } + + // the number of open-position orders is less than maxOrderCount -> failed to place open-position orders + // 1. This strategy is at position opening, so it may not place all orders we want successfully + // 2. There are some errors when placing open-position orders. e.g. cannot lock fund..... + if filledCnt == 0 && cancelledCnt == 0 { + // TODO: place the remaining open-position orders + return OpenPositionReady, nil + } + if filledCnt > 0 && cancelledCnt == 0 { - activeOrderBook.Add(openOrders...) - orderStore.Add(openOrders...) + // TODO: place the remaing open-position orders and change state to OpenPositionOrderFilled return OpenPositionOrderFilled, nil } - if openedCnt > 0 && filledCnt > 0 && cancelledCnt > 0 { - return OpenPositionOrdersCancelling, nil - } - - if openedCnt == 0 && filledCnt > 0 && cancelledCnt > 0 { - return OpenPositionOrdersCancelled, nil - } - - return None, fmt.Errorf("unexpected order status combination") + return None, fmt.Errorf("unexpected order status combination when numOpenPositionOrders(%d) < maxOrderCount(%d) (opened, filled, cancelled) = (%d, %d, %d)", numOpenPositionOrders, maxOrderCount, openedCnt, filledCnt, cancelledCnt) } func recoverPosition(ctx context.Context, position *types.Position, queryService RecoverApiQueryService, currentRound Round) error { @@ -197,29 +208,7 @@ func recoverProfitStats(ctx context.Context, strategy *Strategy) error { return fmt.Errorf("profit stats is nil, please check it") } - strategy.CalculateAndEmitProfit(ctx) - - return nil -} - -func recoverQuoteInvestment(currentRound Round) fixedpoint.Value { - if len(currentRound.OpenPositionOrders) == 0 { - return fixedpoint.Zero - } - - total := fixedpoint.Zero - for _, order := range currentRound.OpenPositionOrders { - total = total.Add(order.Quantity.Mul(order.Price)) - } - - if currentRound.TakeProfitOrder.OrderID != 0 && currentRound.TakeProfitOrder.Status == types.OrderStatusFilled { - total = total.Add(currentRound.TakeProfitOrder.Quantity.Mul(currentRound.TakeProfitOrder.Price)) - for _, order := range currentRound.OpenPositionOrders { - total = total.Sub(order.ExecutedQuantity.Mul(order.Price)) - } - } - - return total + return strategy.CalculateAndEmitProfit(ctx) } func recoverStartTimeOfNextRound(ctx context.Context, currentRound Round, coolDownInterval types.Duration) time.Time { diff --git a/pkg/strategy/dca2/recover_test.go b/pkg/strategy/dca2/recover_test.go index 55ebda510..62d793359 100644 --- a/pkg/strategy/dca2/recover_test.go +++ b/pkg/strategy/dca2/recover_test.go @@ -7,7 +7,7 @@ import ( "time" "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/core" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" "github.com/stretchr/testify/assert" ) @@ -24,7 +24,7 @@ func generateTestOrder(side types.SideType, status types.OrderStatus, createdAt } -func Test_GetCurrenctAndLastRoundOrders(t *testing.T) { +func Test_GetCurrenctRoundOrders(t *testing.T) { t.Run("case 1", func(t *testing.T) { now := time.Now() openOrders := []types.Order{ @@ -94,85 +94,74 @@ func (m *MockQueryOrders) QueryClosedOrdersDesc(ctx context.Context, symbol stri } func Test_RecoverState(t *testing.T) { - symbol := "BTCUSDT" + strategy := newTestStrategy() + quoteInvestment := fixedpoint.MustNewFromString("1000") t.Run("new strategy", func(t *testing.T) { - openOrders := []types.Order{} currentRound := Round{} - activeOrderBook := bbgo.NewActiveOrderBook(symbol) - orderStore := core.NewOrderStore(symbol) - state, err := recoverState(context.Background(), symbol, 5, openOrders, currentRound, activeOrderBook, orderStore, 0) + position := types.NewPositionFromMarket(strategy.Market) + orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position) + state, err := recoverState(context.Background(), quoteInvestment, 5, currentRound, orderExecutor) assert.NoError(t, err) assert.Equal(t, WaitToOpenPosition, state) }) t.Run("at open position stage and no filled order", func(t *testing.T) { now := time.Now() - openOrders := []types.Order{ - generateTestOrder(types.SideTypeBuy, types.OrderStatusPartiallyFilled, now.Add(-1*time.Second)), - generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-2*time.Second)), - generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-3*time.Second)), - generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-4*time.Second)), - generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-5*time.Second)), - } currentRound := Round{ - OpenPositionOrders: openOrders, + OpenPositionOrders: []types.Order{ + generateTestOrder(types.SideTypeBuy, types.OrderStatusPartiallyFilled, now.Add(-1*time.Second)), + generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-2*time.Second)), + generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-3*time.Second)), + generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-4*time.Second)), + generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-5*time.Second)), + }, } - orderStore := core.NewOrderStore(symbol) - activeOrderBook := bbgo.NewActiveOrderBook(symbol) - state, err := recoverState(context.Background(), symbol, 5, openOrders, currentRound, activeOrderBook, orderStore, 0) + position := types.NewPositionFromMarket(strategy.Market) + orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position) + state, err := recoverState(context.Background(), quoteInvestment, 5, currentRound, orderExecutor) assert.NoError(t, err) assert.Equal(t, OpenPositionReady, state) }) t.Run("at open position stage and there at least one filled order", func(t *testing.T) { now := time.Now() - openOrders := []types.Order{ - generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-2*time.Second)), - generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-3*time.Second)), - generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-4*time.Second)), - generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-5*time.Second)), - } currentRound := Round{ OpenPositionOrders: []types.Order{ generateTestOrder(types.SideTypeBuy, types.OrderStatusFilled, now.Add(-1*time.Second)), - openOrders[0], - openOrders[1], - openOrders[2], - openOrders[3], + generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-2*time.Second)), + generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-3*time.Second)), + generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-4*time.Second)), + generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-5*time.Second)), }, } - orderStore := core.NewOrderStore(symbol) - activeOrderBook := bbgo.NewActiveOrderBook(symbol) - state, err := recoverState(context.Background(), symbol, 5, openOrders, currentRound, activeOrderBook, orderStore, 0) + position := types.NewPositionFromMarket(strategy.Market) + orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position) + state, err := recoverState(context.Background(), quoteInvestment, 5, currentRound, orderExecutor) assert.NoError(t, err) assert.Equal(t, OpenPositionOrderFilled, state) }) t.Run("open position stage finish, but stop at cancelling", func(t *testing.T) { now := time.Now() - openOrders := []types.Order{ - generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-5*time.Second)), - } currentRound := Round{ OpenPositionOrders: []types.Order{ generateTestOrder(types.SideTypeBuy, types.OrderStatusFilled, now.Add(-1*time.Second)), generateTestOrder(types.SideTypeBuy, types.OrderStatusCanceled, now.Add(-2*time.Second)), generateTestOrder(types.SideTypeBuy, types.OrderStatusCanceled, now.Add(-3*time.Second)), generateTestOrder(types.SideTypeBuy, types.OrderStatusCanceled, now.Add(-4*time.Second)), - openOrders[0], + generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-5*time.Second)), }, } - orderStore := core.NewOrderStore(symbol) - activeOrderBook := bbgo.NewActiveOrderBook(symbol) - state, err := recoverState(context.Background(), symbol, 5, openOrders, currentRound, activeOrderBook, orderStore, 0) + position := types.NewPositionFromMarket(strategy.Market) + orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position) + state, err := recoverState(context.Background(), quoteInvestment, 5, currentRound, orderExecutor) assert.NoError(t, err) assert.Equal(t, OpenPositionOrdersCancelling, state) }) t.Run("open-position orders are cancelled", func(t *testing.T) { now := time.Now() - openOrders := []types.Order{} currentRound := Round{ OpenPositionOrders: []types.Order{ generateTestOrder(types.SideTypeBuy, types.OrderStatusFilled, now.Add(-1*time.Second)), @@ -182,20 +171,17 @@ func Test_RecoverState(t *testing.T) { generateTestOrder(types.SideTypeBuy, types.OrderStatusCanceled, now.Add(-5*time.Second)), }, } - orderStore := core.NewOrderStore(symbol) - activeOrderBook := bbgo.NewActiveOrderBook(symbol) - state, err := recoverState(context.Background(), symbol, 5, openOrders, currentRound, activeOrderBook, orderStore, 0) + position := types.NewPositionFromMarket(strategy.Market) + orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position) + state, err := recoverState(context.Background(), quoteInvestment, 5, currentRound, orderExecutor) assert.NoError(t, err) - assert.Equal(t, OpenPositionOrdersCancelled, state) + assert.Equal(t, OpenPositionOrdersCancelling, state) }) t.Run("at take profit stage, and not filled yet", func(t *testing.T) { now := time.Now() - openOrders := []types.Order{ - generateTestOrder(types.SideTypeSell, types.OrderStatusNew, now), - } currentRound := Round{ - TakeProfitOrder: openOrders[0], + TakeProfitOrder: generateTestOrder(types.SideTypeSell, types.OrderStatusNew, now), OpenPositionOrders: []types.Order{ generateTestOrder(types.SideTypeBuy, types.OrderStatusFilled, now.Add(-1*time.Second)), generateTestOrder(types.SideTypeBuy, types.OrderStatusCanceled, now.Add(-2*time.Second)), @@ -204,16 +190,15 @@ func Test_RecoverState(t *testing.T) { generateTestOrder(types.SideTypeBuy, types.OrderStatusCanceled, now.Add(-5*time.Second)), }, } - orderStore := core.NewOrderStore(symbol) - activeOrderBook := bbgo.NewActiveOrderBook(symbol) - state, err := recoverState(context.Background(), symbol, 5, openOrders, currentRound, activeOrderBook, orderStore, 0) + position := types.NewPositionFromMarket(strategy.Market) + orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position) + state, err := recoverState(context.Background(), quoteInvestment, 5, currentRound, orderExecutor) assert.NoError(t, err) assert.Equal(t, TakeProfitReady, state) }) t.Run("at take profit stage, take-profit order filled", func(t *testing.T) { now := time.Now() - openOrders := []types.Order{} currentRound := Round{ TakeProfitOrder: generateTestOrder(types.SideTypeSell, types.OrderStatusFilled, now), OpenPositionOrders: []types.Order{ @@ -224,9 +209,9 @@ func Test_RecoverState(t *testing.T) { generateTestOrder(types.SideTypeBuy, types.OrderStatusCanceled, now.Add(-5*time.Second)), }, } - orderStore := core.NewOrderStore(symbol) - activeOrderBook := bbgo.NewActiveOrderBook(symbol) - state, err := recoverState(context.Background(), symbol, 5, openOrders, currentRound, activeOrderBook, orderStore, 0) + position := types.NewPositionFromMarket(strategy.Market) + orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position) + state, err := recoverState(context.Background(), quoteInvestment, 5, currentRound, orderExecutor) assert.NoError(t, err) assert.Equal(t, WaitToOpenPosition, state) }) diff --git a/pkg/strategy/dca2/strategy.go b/pkg/strategy/dca2/strategy.go index be89baf22..b8e7fee05 100644 --- a/pkg/strategy/dca2/strategy.go +++ b/pkg/strategy/dca2/strategy.go @@ -66,6 +66,9 @@ type Strategy struct { // UseCancelAllOrdersApiWhenClose uses a different API to cancel all the orders on the market when closing a grid UseCancelAllOrdersApiWhenClose bool `json:"useCancelAllOrdersApiWhenClose"` + // dev mode + DevMode *DevMode `json:"devMode"` + // log logger *logrus.Entry LogFields logrus.Fields `json:"logFields"` @@ -141,6 +144,12 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.Position = types.NewPositionFromMarket(s.Market) } + // if dev mode is on and it's not a new strategy + if s.DevMode != nil && s.DevMode.Enabled && !s.DevMode.IsNewAccount { + s.ProfitStats = newProfitStats(s.Market, s.QuoteInvestment) + s.Position = types.NewPositionFromMarket(s.Market) + } + s.Position.Strategy = ID s.Position.StrategyInstanceID = instanceID @@ -202,14 +211,18 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.logger.Info("[DCA] user data stream authenticated") time.AfterFunc(3*time.Second, func() { if isInitialize := s.initializeNextStateC(); !isInitialize { - if s.RecoverWhenStart { + + // no need to recover when two situation + // 1. recoverWhenStart is false + // 2. dev mode is on and it's not new strategy + if !s.RecoverWhenStart || (s.DevMode != nil && s.DevMode.Enabled && !s.DevMode.IsNewAccount) { + s.state = WaitToOpenPosition + } else { // recover if err := s.recover(ctx); err != nil { s.logger.WithError(err).Error("[DCA] something wrong when state recovering") return } - } else { - s.state = WaitToOpenPosition } s.logger.Infof("[DCA] state: %d", s.state) @@ -382,6 +395,8 @@ func (s *Strategy) CalculateAndEmitProfit(ctx context.Context) error { // store into persistence bbgo.Sync(ctx, s) + s.logger.Infof("[DCA] profit stats:\n%s", s.ProfitStats.String()) + // emit profit s.EmitProfit(s.ProfitStats)