Merge pull request #1580 from c9s/kbearXD/dca2/profit

FIX: [dca2] must calculate and emit profit at the end of the round
This commit is contained in:
kbearXD 2024-03-14 16:42:07 +08:00 committed by GitHub
commit 3981970667
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 54 additions and 11 deletions

View File

@ -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.

View File

@ -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 {

View File

@ -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")
}

View File

@ -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 {