Merge pull request #1616 from c9s/kbearXD/dca2/collect-round-before-take-profit

FEATURE: [dca2] recollect position before placing the take-profit order
This commit is contained in:
kbearXD 2024-04-15 18:11:49 +08:00 committed by GitHub
commit ade8585914
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 119 additions and 41 deletions

View File

@ -54,7 +54,7 @@ func (s *Strategy) recoverActiveOrders(ctx context.Context) error {
opts := common.SyncActiveOrdersOpts{ opts := common.SyncActiveOrdersOpts{
Logger: s.logger, Logger: s.logger,
Exchange: s.ExchangeSession.Exchange, Exchange: s.ExchangeSession.Exchange,
OrderQueryService: s.orderQueryService, OrderQueryService: s.roundCollector.queryService,
ActiveOrderBook: activeOrders, ActiveOrderBook: activeOrders,
OpenOrders: openOrders, OpenOrders: openOrders,
} }

View File

@ -24,25 +24,7 @@ type RecoverApiQueryService interface {
func (s *Strategy) recover(ctx context.Context) error { func (s *Strategy) recover(ctx context.Context) error {
s.logger.Info("[DCA] recover") s.logger.Info("[DCA] recover")
queryService, ok := s.ExchangeSession.Exchange.(RecoverApiQueryService) currentRound, err := s.roundCollector.CollectCurrentRound(ctx)
if !ok {
return fmt.Errorf("[DCA] exchange %s doesn't support queryAPI interface", s.ExchangeSession.ExchangeName)
}
openOrders, err := queryService.QueryOpenOrders(ctx, s.Symbol)
if err != nil {
return err
}
closedOrders, err := queryService.QueryClosedOrdersDesc(ctx, s.Symbol, recoverSinceLimit, time.Now(), 0)
if err != nil {
return err
}
currentRound, err := getCurrentRoundOrders(openOrders, closedOrders, s.OrderGroupID)
if err != nil {
return err
}
debugRoundOrders(s.logger, "current", currentRound) debugRoundOrders(s.logger, "current", currentRound)
// recover profit stats // recover profit stats
@ -59,6 +41,11 @@ func (s *Strategy) recover(ctx context.Context) error {
if s.DisablePositionRecover { if s.DisablePositionRecover {
s.logger.Info("disablePositionRecover is set, skip position recovery") s.logger.Info("disablePositionRecover is set, skip position recovery")
} else { } else {
queryService, ok := s.ExchangeSession.Exchange.(RecoverApiQueryService)
if !ok {
return fmt.Errorf("[DCA] exchange %s doesn't support queryAPI interface", s.ExchangeSession.ExchangeName)
}
if err := recoverPosition(ctx, s.Position, queryService, currentRound); err != nil { if err := recoverPosition(ctx, s.Position, queryService, currentRound); err != nil {
return err return err
} }

View File

@ -2,6 +2,7 @@ package dca2
import ( import (
"context" "context"
"fmt"
"strconv" "strconv"
"time" "time"
@ -19,8 +20,11 @@ type RoundCollector struct {
isMax bool isMax bool
// service // service
historyService types.ExchangeTradeHistoryService ex types.Exchange
queryService types.ExchangeOrderQueryService historyService types.ExchangeTradeHistoryService
queryService types.ExchangeOrderQueryService
tradeService types.ExchangeTradeService
queryClosedOrderDesc descendingClosedOrderQueryService
} }
func NewRoundCollector(logger *logrus.Entry, symbol string, groupID uint32, ex types.Exchange) *RoundCollector { func NewRoundCollector(logger *logrus.Entry, symbol string, groupID uint32, ex types.Exchange) *RoundCollector {
@ -37,14 +41,82 @@ func NewRoundCollector(logger *logrus.Entry, symbol string, groupID uint32, ex t
return nil return nil
} }
return &RoundCollector{ tradeService, ok := ex.(types.ExchangeTradeService)
logger: logger, if !ok {
symbol: symbol, logger.Errorf("exchange %s doesn't support ExchangeTradeService", ex.Name())
groupID: groupID, return nil
isMax: isMax,
historyService: historyService,
queryService: queryService,
} }
queryClosedOrderDesc, ok := ex.(descendingClosedOrderQueryService)
if !ok {
logger.Errorf("exchange %s doesn't support query closed orders desc", ex.Name())
return nil
}
return &RoundCollector{
logger: logger,
symbol: symbol,
groupID: groupID,
isMax: isMax,
ex: ex,
historyService: historyService,
queryService: queryService,
tradeService: tradeService,
queryClosedOrderDesc: queryClosedOrderDesc,
}
}
func (rc RoundCollector) CollectCurrentRound(ctx context.Context) (Round, error) {
openOrders, err := retry.QueryOpenOrdersUntilSuccessful(ctx, rc.ex, rc.symbol)
if err != nil {
return Round{}, err
}
var closedOrders []types.Order
var op = func() (err2 error) {
closedOrders, err2 = rc.queryClosedOrderDesc.QueryClosedOrdersDesc(ctx, rc.symbol, recoverSinceLimit, time.Now(), 0)
return err2
}
if err := retry.GeneralBackoff(ctx, op); err != nil {
return Round{}, err
}
openPositionSide := types.SideTypeBuy
takeProfitSide := types.SideTypeSell
var allOrders []types.Order
allOrders = append(allOrders, openOrders...)
allOrders = append(allOrders, closedOrders...)
types.SortOrdersDescending(allOrders)
var currentRound Round
lastSide := takeProfitSide
for _, order := range allOrders {
// group id filter is used for debug when local running
if order.GroupID != rc.groupID {
continue
}
if order.Side == takeProfitSide && lastSide == openPositionSide {
break
}
switch order.Side {
case openPositionSide:
currentRound.OpenPositionOrders = append(currentRound.OpenPositionOrders, order)
case takeProfitSide:
if currentRound.TakeProfitOrder.OrderID != 0 {
return currentRound, fmt.Errorf("there are two take-profit orders in one round, please check it")
}
currentRound.TakeProfitOrder = order
default:
}
lastSide = order.Side
}
return currentRound, nil
} }
func (rc *RoundCollector) CollectFinishRounds(ctx context.Context, fromOrderID uint64) ([]Round, error) { func (rc *RoundCollector) CollectFinishRounds(ctx context.Context, fromOrderID uint64) ([]Round, error) {
@ -96,13 +168,16 @@ func (rc *RoundCollector) CollectRoundTrades(ctx context.Context, round Round) (
debugRoundOrders(rc.logger, "collect round trades", round) debugRoundOrders(rc.logger, "collect round trades", round)
var roundTrades []types.Trade var roundTrades []types.Trade
var roundOrders []types.Order = round.OpenPositionOrders var roundOrders []types.Order = round.OpenPositionOrders
roundOrders = append(roundOrders, round.TakeProfitOrder)
// if the take-profit order's OrderID == 0 -> no take-profit order.
if round.TakeProfitOrder.OrderID != 0 {
roundOrders = append(roundOrders, round.TakeProfitOrder)
}
for _, order := range roundOrders { for _, order := range roundOrders {
rc.logger.Infof("collect trades from order: %s", order.String()) rc.logger.Infof("collect trades from order: %s", order.String())
if order.ExecutedQuantity.Sign() == 0 { if order.ExecutedQuantity.IsZero() {
rc.logger.Info("collect trads from order but no executed quantity ", order.String()) rc.logger.Info("collect trads from order but no executed quantity ", order.String())
continue continue
} else { } else {

View File

@ -94,7 +94,6 @@ type Strategy struct {
nextStateC chan State nextStateC chan State
state State state State
roundCollector *RoundCollector roundCollector *RoundCollector
orderQueryService types.ExchangeOrderQueryService
takeProfitPrice fixedpoint.Value takeProfitPrice fixedpoint.Value
startTimeOfNextRound time.Time startTimeOfNextRound time.Time
nextRoundPaused bool nextRoundPaused bool
@ -193,13 +192,6 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
s.OrderGroupID = util.FNV32(instanceID) % math.MaxInt32 s.OrderGroupID = util.FNV32(instanceID) % math.MaxInt32
} }
// orderQueryService
if service, ok := s.ExchangeSession.Exchange.(types.ExchangeOrderQueryService); ok {
s.orderQueryService = service
} else {
return fmt.Errorf("exchange %s doesn't support ExchangeOrderQueryService", s.ExchangeSession.ExchangeName)
}
// round collector // round collector
s.roundCollector = NewRoundCollector(s.logger, s.Symbol, s.OrderGroupID, s.ExchangeSession.Exchange) s.roundCollector = NewRoundCollector(s.logger, s.Symbol, s.OrderGroupID, s.ExchangeSession.Exchange)
if s.roundCollector == nil { if s.roundCollector == nil {

View File

@ -2,14 +2,38 @@ package dca2
import ( import (
"context" "context"
"fmt"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
"github.com/pkg/errors"
) )
func (s *Strategy) placeTakeProfitOrders(ctx context.Context) error { func (s *Strategy) placeTakeProfitOrders(ctx context.Context) error {
s.logger.Info("start placing take profit orders") s.logger.Info("start placing take profit orders")
order := generateTakeProfitOrder(s.Market, s.TakeProfitRatio, s.Position, s.OrderGroupID) currentRound, err := s.roundCollector.CollectCurrentRound(ctx)
if currentRound.TakeProfitOrder.OrderID != 0 {
return fmt.Errorf("there is a take-profit order before placing the take-profit order, please check it")
}
trades, err := s.roundCollector.CollectRoundTrades(ctx, currentRound)
if err != nil {
return errors.Wrap(err, "failed to place the take-profit order when collecting round trades")
}
roundPosition := types.NewPositionFromMarket(s.Market)
for _, trade := range trades {
if trade.FeeProcessing {
return fmt.Errorf("failed to place the take-profit order because there is a trade's fee not ready")
}
roundPosition.AddTrade(trade)
}
s.logger.Infof("position of this round before place the take-profit order: %s", roundPosition.String())
order := generateTakeProfitOrder(s.Market, s.TakeProfitRatio, roundPosition, s.OrderGroupID)
createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, order) createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, order)
if err != nil { if err != nil {
return err return err