diff --git a/pkg/exchange/max/maxapi/order.go b/pkg/exchange/max/maxapi/order.go index 6f783c33a9..26aaa196ab 100644 --- a/pkg/exchange/max/maxapi/order.go +++ b/pkg/exchange/max/maxapi/order.go @@ -46,6 +46,10 @@ const ( OrderStateFailed = OrderState("failed") ) +func IsFilledOrderState(state OrderState) bool { + return state == OrderStateDone || state == OrderStateFinalizing +} + type OrderType string // Order types that the API can return. diff --git a/pkg/strategy/dca2/recover.go b/pkg/strategy/dca2/recover.go index c875eed46e..f8d811ac16 100644 --- a/pkg/strategy/dca2/recover.go +++ b/pkg/strategy/dca2/recover.go @@ -45,11 +45,14 @@ func (s *Strategy) recover(ctx context.Context) error { } debugRoundOrders(s.logger, "current", currentRound) + // TODO: use flag // recover profit stats - if err := recoverProfitStats(ctx, s); err != nil { - return err - } - s.logger.Info("recover profit stats DONE") + /* + if err := recoverProfitStats(ctx, s); err != nil { + return err + } + s.logger.Info("recover profit stats DONE") + */ // recover position if err := recoverPosition(ctx, s.Position, queryService, currentRound); err != nil { @@ -79,8 +82,9 @@ func recoverState(ctx context.Context, maxOrderCount int, currentRound Round, or // 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) + // the number of open-positions orders may not be equal to maxOrderCount, because the notional may not enough to open maxOrderCount orders + if len(currentRound.OpenPositionOrders) > maxOrderCount { + return None, fmt.Errorf("there is take-profit order but the number of open-position orders (%d) is greater than maxOrderCount(%d). Please check it", len(currentRound.OpenPositionOrders), maxOrderCount) } takeProfitOrder := currentRound.TakeProfitOrder @@ -202,6 +206,8 @@ func recoverPosition(ctx context.Context, position *types.Position, queryService return nil } +// TODO: use flag to decide which to recover +/* func recoverProfitStats(ctx context.Context, strategy *Strategy) error { if strategy.ProfitStats == nil { return fmt.Errorf("profit stats is nil, please check it") @@ -209,6 +215,7 @@ func recoverProfitStats(ctx context.Context, strategy *Strategy) error { return strategy.CalculateAndEmitProfit(ctx) } +*/ func recoverStartTimeOfNextRound(ctx context.Context, currentRound Round, coolDownInterval types.Duration) time.Time { if currentRound.TakeProfitOrder.OrderID != 0 && currentRound.TakeProfitOrder.Status == types.OrderStatusFilled { diff --git a/pkg/strategy/dca2/state.go b/pkg/strategy/dca2/state.go index 44c56fa7e6..722a527748 100644 --- a/pkg/strategy/dca2/state.go +++ b/pkg/strategy/dca2/state.go @@ -201,7 +201,7 @@ func (s *Strategy) runTakeProfitReady(ctx context.Context, next State) { // reset position // calculate profit stats - if err := s.CalculateAndEmitProfit(ctx); err != nil { + if err := s.CalculateAndEmitProfitUntilSuccessful(ctx); err != nil { s.logger.WithError(err).Warn("failed to calculate and emit profit") } diff --git a/pkg/strategy/dca2/strategy.go b/pkg/strategy/dca2/strategy.go index 512dd34868..ecb4b24368 100644 --- a/pkg/strategy/dca2/strategy.go +++ b/pkg/strategy/dca2/strategy.go @@ -8,11 +8,14 @@ import ( "sync" "time" + "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" "go.uber.org/multierr" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/exchange" + maxapi "github.com/c9s/bbgo/pkg/exchange/max/maxapi" "github.com/c9s/bbgo/pkg/exchange/retry" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/strategy/common" @@ -370,7 +373,9 @@ func (s *Strategy) CleanUp(ctx context.Context) error { return werr } -func (s *Strategy) CalculateAndEmitProfit(ctx context.Context) error { +func (s *Strategy) CalculateAndEmitProfitUntilSuccessful(ctx context.Context) error { + fromOrderID := s.ProfitStats.FromOrderID + historyService, ok := s.ExchangeSession.Exchange.(types.ExchangeTradeHistoryService) if !ok { return fmt.Errorf("exchange %s doesn't support ExchangeTradeHistoryService", s.ExchangeSession.Exchange.Name()) @@ -381,6 +386,22 @@ func (s *Strategy) CalculateAndEmitProfit(ctx context.Context) error { return fmt.Errorf("exchange %s doesn't support ExchangeOrderQueryService", s.ExchangeSession.Exchange.Name()) } + var op = func() error { + if err := s.CalculateAndEmitProfit(ctx, historyService, queryService); err != nil { + return errors.Wrapf(err, "failed to calculate and emit profit, please check it") + } + + if s.ProfitStats.FromOrderID == fromOrderID { + return fmt.Errorf("FromOrderID (%d) is not updated, retry it", s.ProfitStats.FromOrderID) + } + + return nil + } + + return retry.GeneralLiteBackoff(ctx, op) +} + +func (s *Strategy) CalculateAndEmitProfit(ctx context.Context, historyService types.ExchangeTradeHistoryService, queryService types.ExchangeOrderQueryService) error { // TODO: pagination for it // query the orders s.logger.Infof("query %s closed orders from order id #%d", s.Symbol, s.ProfitStats.FromOrderID) @@ -390,6 +411,8 @@ func (s *Strategy) CalculateAndEmitProfit(ctx context.Context) error { } s.logger.Infof("there are %d closed orders from order id #%d", len(orders), s.ProfitStats.FromOrderID) + isMax := exchange.IsMaxExchange(s.ExchangeSession.Exchange) + var rounds []Round var round Round for _, order := range orders { @@ -402,9 +425,18 @@ func (s *Strategy) CalculateAndEmitProfit(ctx context.Context) error { case types.SideTypeBuy: round.OpenPositionOrders = append(round.OpenPositionOrders, order) case types.SideTypeSell: - if order.Status != types.OrderStatusFilled { - continue + if !isMax { + if order.Status != types.OrderStatusFilled { + s.logger.Infof("take-profit order is %s not filled, so this round is not finished. Skip it", order.Status) + continue + } + } else { + if !maxapi.IsFilledOrderState(maxapi.OrderState(order.OriginalStatus)) { + s.logger.Infof("isMax and take-profit order is %s not done or finalizing, so this round is not finished. Skip it", order.OriginalStatus) + continue + } } + round.TakeProfitOrder = order rounds = append(rounds, round) round = Round{} @@ -415,7 +447,7 @@ func (s *Strategy) CalculateAndEmitProfit(ctx context.Context) error { s.logger.Infof("there are %d rounds from order id #%d", len(rounds), s.ProfitStats.FromOrderID) for _, round := range rounds { - debugRoundOrders(s.logger, "calculate", round) + debugRoundOrders(s.logger, strconv.FormatInt(s.ProfitStats.Round, 10), round) var roundOrders []types.Order = round.OpenPositionOrders roundOrders = append(roundOrders, round.TakeProfitOrder) for _, order := range roundOrders {