From 91123edbd6f80a4fe3317fe695b4620c3f1c9bb7 Mon Sep 17 00:00:00 2001 From: kbearXD Date: Thu, 14 Mar 2024 11:40:03 +0800 Subject: [PATCH 1/3] dca2: must calculate and emit profit at the end of the round --- pkg/strategy/dca2/recover.go | 14 ++++++++--- pkg/strategy/dca2/state.go | 2 +- pkg/strategy/dca2/strategy.go | 46 ++++++++++++++++++++++++++++++++--- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/dca2/recover.go b/pkg/strategy/dca2/recover.go index c875eed46e..147dd3a840 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 { @@ -202,6 +205,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 +214,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..7416dbdb01 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.mustCalculateAndEmitProfit(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..cedf2a32f6 100644 --- a/pkg/strategy/dca2/strategy.go +++ b/pkg/strategy/dca2/strategy.go @@ -13,6 +13,8 @@ import ( "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 +372,9 @@ func (s *Strategy) CleanUp(ctx context.Context) error { return werr } -func (s *Strategy) CalculateAndEmitProfit(ctx context.Context) error { +func (s *Strategy) mustCalculateAndEmitProfit(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 +385,26 @@ func (s *Strategy) CalculateAndEmitProfit(ctx context.Context) error { return fmt.Errorf("exchange %s doesn't support ExchangeOrderQueryService", s.ExchangeSession.Exchange.Name()) } + maxTry := 10 + for try := 1; try < maxTry; try++ { + if err := s.CalculateAndEmitProfit(ctx, historyService, queryService); err != nil { + s.logger.WithError(err).Warnf("failed to calculate and emit profit at #%d try, please check it", try) + continue + } + + if s.ProfitStats.FromOrderID > fromOrderID { + break + } + } + + if s.ProfitStats.FromOrderID == fromOrderID { + return fmt.Errorf("after trying %d times, we still can't calculate and emit profit, please check it", maxTry) + } + + return nil +} + +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 +414,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 +428,23 @@ 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 { + switch maxapi.OrderState(order.OriginalStatus) { + case maxapi.OrderStateDone: + // the same as filled + case maxapi.OrderStateFinalizing: + // the same as filled + default: + 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{} From fb2a46e1c411f70f9ab4869a25d7f346f8906a52 Mon Sep 17 00:00:00 2001 From: kbearXD Date: Thu, 14 Mar 2024 14:10:14 +0800 Subject: [PATCH 2/3] use backoff retry --- pkg/strategy/dca2/state.go | 2 +- pkg/strategy/dca2/strategy.go | 19 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/pkg/strategy/dca2/state.go b/pkg/strategy/dca2/state.go index 7416dbdb01..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.mustCalculateAndEmitProfit(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 cedf2a32f6..cb3ffe6770 100644 --- a/pkg/strategy/dca2/strategy.go +++ b/pkg/strategy/dca2/strategy.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" "go.uber.org/multierr" @@ -372,7 +373,7 @@ func (s *Strategy) CleanUp(ctx context.Context) error { return werr } -func (s *Strategy) mustCalculateAndEmitProfit(ctx context.Context) error { +func (s *Strategy) CalculateAndEmitProfitUntilSuccessful(ctx context.Context) error { fromOrderID := s.ProfitStats.FromOrderID historyService, ok := s.ExchangeSession.Exchange.(types.ExchangeTradeHistoryService) @@ -385,23 +386,19 @@ func (s *Strategy) mustCalculateAndEmitProfit(ctx context.Context) error { return fmt.Errorf("exchange %s doesn't support ExchangeOrderQueryService", s.ExchangeSession.Exchange.Name()) } - maxTry := 10 - for try := 1; try < maxTry; try++ { + var op = func() error { if err := s.CalculateAndEmitProfit(ctx, historyService, queryService); err != nil { - s.logger.WithError(err).Warnf("failed to calculate and emit profit at #%d try, please check it", try) - continue + return errors.Wrapf(err, "failed to calculate and emit profit, please check it") } - if s.ProfitStats.FromOrderID > fromOrderID { - break + if s.ProfitStats.FromOrderID == fromOrderID { + return fmt.Errorf("FromOrderID (%d) is not updated, retry it", s.ProfitStats.FromOrderID) } - } - if s.ProfitStats.FromOrderID == fromOrderID { - return fmt.Errorf("after trying %d times, we still can't calculate and emit profit, please check it", maxTry) + return nil } - return nil + return retry.GeneralLiteBackoff(ctx, op) } func (s *Strategy) CalculateAndEmitProfit(ctx context.Context, historyService types.ExchangeTradeHistoryService, queryService types.ExchangeOrderQueryService) error { From 2b52211c1ce4f60673bb00a4d798c97045bc30a9 Mon Sep 17 00:00:00 2001 From: kbearXD Date: Thu, 14 Mar 2024 16:18:12 +0800 Subject: [PATCH 3/3] new function IsFilledOrderState for maxapi --- pkg/exchange/max/maxapi/order.go | 4 ++++ pkg/strategy/dca2/recover.go | 5 +++-- pkg/strategy/dca2/strategy.go | 9 ++------- 3 files changed, 9 insertions(+), 9 deletions(-) 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 147dd3a840..f8d811ac16 100644 --- a/pkg/strategy/dca2/recover.go +++ b/pkg/strategy/dca2/recover.go @@ -82,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 diff --git a/pkg/strategy/dca2/strategy.go b/pkg/strategy/dca2/strategy.go index cb3ffe6770..ecb4b24368 100644 --- a/pkg/strategy/dca2/strategy.go +++ b/pkg/strategy/dca2/strategy.go @@ -431,12 +431,7 @@ func (s *Strategy) CalculateAndEmitProfit(ctx context.Context, historyService ty continue } } else { - switch maxapi.OrderState(order.OriginalStatus) { - case maxapi.OrderStateDone: - // the same as filled - case maxapi.OrderStateFinalizing: - // the same as filled - default: + 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 } @@ -452,7 +447,7 @@ func (s *Strategy) CalculateAndEmitProfit(ctx context.Context, historyService ty 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 {