Skip to content

Commit

Permalink
Merge pull request #1580 from c9s/kbearXD/dca2/profit
Browse files Browse the repository at this point in the history
FIX: [dca2] must calculate and emit profit at the end of the round
  • Loading branch information
kbearXD committed Mar 14, 2024
2 parents 3a98660 + 2b52211 commit 3981970
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 11 deletions.
4 changes: 4 additions & 0 deletions pkg/exchange/max/maxapi/order.go
Expand Up @@ -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.
Expand Down
19 changes: 13 additions & 6 deletions pkg/strategy/dca2/recover.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -202,13 +206,16 @@ 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")
}
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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/strategy/dca2/state.go
Expand Up @@ -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")
}

Expand Down
40 changes: 36 additions & 4 deletions pkg/strategy/dca2/strategy.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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())
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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{}
Expand All @@ -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 {
Expand Down

0 comments on commit 3981970

Please sign in to comment.