2023-11-27 07:55:02 +00:00
package dca2
import (
"context"
"fmt"
"strconv"
"time"
"github.com/c9s/bbgo/pkg/bbgo"
2024-04-29 07:51:58 +00:00
"github.com/c9s/bbgo/pkg/exchange/retry"
2023-11-27 07:55:02 +00:00
"github.com/c9s/bbgo/pkg/types"
2024-04-29 07:51:58 +00:00
"github.com/pkg/errors"
2023-11-27 07:55:02 +00:00
)
2024-01-23 08:41:54 +00:00
var recoverSinceLimit = time . Date ( 2024 , time . January , 29 , 12 , 0 , 0 , 0 , time . Local )
2023-12-27 03:41:29 +00:00
type descendingClosedOrderQueryService interface {
2023-11-27 07:55:02 +00:00
QueryClosedOrdersDesc ( ctx context . Context , symbol string , since , until time . Time , lastOrderID uint64 ) ( [ ] types . Order , error )
2023-12-27 03:41:29 +00:00
}
2023-11-27 07:55:02 +00:00
func ( s * Strategy ) recover ( ctx context . Context ) error {
s . logger . Info ( "[DCA] recover" )
2024-04-16 05:37:53 +00:00
currentRound , err := s . collector . CollectCurrentRound ( ctx )
2023-11-27 07:55:02 +00:00
debugRoundOrders ( s . logger , "current" , currentRound )
2024-01-23 08:41:54 +00:00
// recover profit stats
2024-03-15 10:41:25 +00:00
if s . DisableProfitStatsRecover {
s . logger . Info ( "disableProfitStatsRecover is set, skip profit stats recovery" )
} else {
2024-03-14 03:40:03 +00:00
if err := recoverProfitStats ( ctx , s ) ; err != nil {
return err
}
s . logger . Info ( "recover profit stats DONE" )
2024-03-15 10:41:25 +00:00
}
2023-11-27 07:55:02 +00:00
// recover position
2024-03-15 10:41:25 +00:00
if s . DisablePositionRecover {
s . logger . Info ( "disablePositionRecover is set, skip position recovery" )
} else {
2024-04-16 05:37:53 +00:00
if err := recoverPosition ( ctx , s . Position , currentRound , s . collector . queryService ) ; err != nil {
2024-03-15 10:41:25 +00:00
return err
}
s . logger . Info ( "recover position DONE" )
2023-11-27 07:55:02 +00:00
}
// recover startTimeOfNextRound
startTimeOfNextRound := recoverStartTimeOfNextRound ( ctx , currentRound , s . CoolDownInterval )
2024-01-23 08:41:54 +00:00
s . startTimeOfNextRound = startTimeOfNextRound
2023-11-27 07:55:02 +00:00
2024-01-23 08:41:54 +00:00
// recover state
2024-03-04 12:52:01 +00:00
state , err := recoverState ( ctx , int ( s . MaxOrderCount ) , currentRound , s . OrderExecutor )
2024-01-23 08:41:54 +00:00
if err != nil {
return err
}
2024-02-22 08:15:54 +00:00
s . updateState ( state )
2024-01-23 08:41:54 +00:00
s . logger . Info ( "recover stats DONE" )
2023-11-27 07:55:02 +00:00
return nil
}
// recover state
2024-03-04 12:52:01 +00:00
func recoverState ( ctx context . Context , maxOrderCount int , currentRound Round , orderExecutor * bbgo . GeneralOrderExecutor ) ( State , error ) {
2024-01-23 08:41:54 +00:00
activeOrderBook := orderExecutor . ActiveMakerOrders ( )
orderStore := orderExecutor . OrderStore ( )
2023-12-27 03:41:29 +00:00
2024-01-23 08:41:54 +00:00
// dca stop at take-profit order stage
2023-11-27 07:55:02 +00:00
if currentRound . TakeProfitOrder . OrderID != 0 {
2024-03-14 08:18:12 +00:00
// 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 )
2023-12-27 03:41:29 +00:00
}
2024-01-23 08:41:54 +00:00
takeProfitOrder := currentRound . TakeProfitOrder
if takeProfitOrder . Status == types . OrderStatusFilled {
return WaitToOpenPosition , nil
} else if types . IsActiveOrder ( takeProfitOrder ) {
activeOrderBook . Add ( takeProfitOrder )
orderStore . Add ( takeProfitOrder )
return TakeProfitReady , nil
} else {
return None , fmt . Errorf ( "the status of take-profit order is %s. Please check it" , takeProfitOrder . Status )
2023-11-27 07:55:02 +00:00
}
}
2024-01-23 08:41:54 +00:00
// dca stop at no take-profit order stage
openPositionOrders := currentRound . OpenPositionOrders
numOpenPositionOrders := len ( openPositionOrders )
2023-11-27 07:55:02 +00:00
2024-01-23 08:41:54 +00:00
// new strategy
if len ( openPositionOrders ) == 0 {
return WaitToOpenPosition , nil
2023-11-27 07:55:02 +00:00
}
2024-01-23 08:41:54 +00:00
// should not happen
if numOpenPositionOrders > maxOrderCount {
return None , fmt . Errorf ( "the number of open-position orders (%d) is > max order number" , numOpenPositionOrders )
2023-11-27 07:55:02 +00:00
}
2024-01-23 08:41:54 +00:00
// collect open-position orders' status
2023-11-27 07:55:02 +00:00
var openedCnt , filledCnt , cancelledCnt int64
for _ , order := range currentRound . OpenPositionOrders {
switch order . Status {
case types . OrderStatusNew , types . OrderStatusPartiallyFilled :
2024-01-23 08:41:54 +00:00
activeOrderBook . Add ( order )
orderStore . Add ( order )
2023-11-27 07:55:02 +00:00
openedCnt ++
case types . OrderStatusFilled :
filledCnt ++
case types . OrderStatusCanceled :
cancelledCnt ++
default :
return None , fmt . Errorf ( "there is unexpected status %s of order %s" , order . Status , order )
}
}
2024-03-21 08:18:48 +00:00
// all open-position orders are still not filled -> OpenPositionReady
if filledCnt == 0 && cancelledCnt == 0 {
return OpenPositionReady , nil
}
2024-01-23 08:41:54 +00:00
2024-03-21 08:18:48 +00:00
// there are at least one open-position orders filled
if filledCnt > 0 && cancelledCnt == 0 {
if openedCnt > 0 {
2024-01-23 08:41:54 +00:00
return OpenPositionOrderFilled , nil
2024-03-21 08:18:48 +00:00
} else {
// all open-position orders filled, change to cancelling and place the take-profit order
2024-01-23 08:41:54 +00:00
return OpenPositionOrdersCancelling , nil
}
2023-11-27 07:55:02 +00:00
}
2024-03-21 08:18:48 +00:00
// there are at last one open-position orders cancelled ->
if cancelledCnt > 0 {
return OpenPositionOrdersCancelling , nil
2023-11-27 07:55:02 +00:00
}
2024-03-21 08:18:48 +00:00
return None , fmt . Errorf ( "unexpected order status combination (opened, filled, cancelled) = (%d, %d, %d)" , openedCnt , filledCnt , cancelledCnt )
2023-11-27 07:55:02 +00:00
}
2024-04-16 05:37:53 +00:00
func recoverPosition ( ctx context . Context , position * types . Position , currentRound Round , queryService types . ExchangeOrderQueryService ) error {
2023-11-27 07:55:02 +00:00
if position == nil {
2024-01-08 10:24:11 +00:00
return fmt . Errorf ( "position is nil, please check it" )
2023-11-27 07:55:02 +00:00
}
2024-04-29 07:51:58 +00:00
// reset position to recover
2023-11-27 07:55:02 +00:00
position . Reset ( )
2024-04-29 07:51:58 +00:00
var positionOrders [ ] types . Order
2023-11-27 07:55:02 +00:00
if currentRound . TakeProfitOrder . OrderID != 0 {
2024-04-29 07:51:58 +00:00
// if the take-profit order is already filled, the position is 0
2023-11-27 07:55:02 +00:00
if ! types . IsActiveOrder ( currentRound . TakeProfitOrder ) {
return nil
}
positionOrders = append ( positionOrders , currentRound . TakeProfitOrder )
}
for _ , order := range currentRound . OpenPositionOrders {
// no executed quantity order, no need to get trades
if order . ExecutedQuantity . IsZero ( ) {
continue
}
positionOrders = append ( positionOrders , order )
}
for _ , positionOrder := range positionOrders {
2024-04-29 07:51:58 +00:00
trades , err := retry . QueryOrderTradesUntilSuccessful ( ctx , queryService , types . OrderQuery {
2023-11-27 07:55:02 +00:00
Symbol : position . Symbol ,
OrderID : strconv . FormatUint ( positionOrder . OrderID , 10 ) ,
} )
if err != nil {
2024-04-29 07:51:58 +00:00
return errors . Wrapf ( err , "failed to get order (%d) trades" , positionOrder . OrderID )
2023-11-27 07:55:02 +00:00
}
position . AddTrades ( trades )
}
return nil
}
2024-01-09 08:01:10 +00:00
func recoverProfitStats ( ctx context . Context , strategy * Strategy ) error {
if strategy . ProfitStats == nil {
2024-01-08 10:24:11 +00:00
return fmt . Errorf ( "profit stats is nil, please check it" )
}
2024-03-15 10:41:25 +00:00
_ , err := strategy . UpdateProfitStats ( ctx )
return err
2023-11-27 07:55:02 +00:00
}
func recoverStartTimeOfNextRound ( ctx context . Context , currentRound Round , coolDownInterval types . Duration ) time . Time {
if currentRound . TakeProfitOrder . OrderID != 0 && currentRound . TakeProfitOrder . Status == types . OrderStatusFilled {
return currentRound . TakeProfitOrder . UpdateTime . Time ( ) . Add ( coolDownInterval . Duration ( ) )
}
return time . Time { }
}