2023-04-06 06:59:03 +00:00
package grid2
import (
"context"
"fmt"
"strconv"
"time"
2023-04-06 15:24:08 +00:00
"github.com/pkg/errors"
2023-04-06 06:59:03 +00:00
"github.com/c9s/bbgo/pkg/bbgo"
2024-03-04 12:52:01 +00:00
"github.com/c9s/bbgo/pkg/exchange"
2023-09-19 03:12:14 +00:00
"github.com/c9s/bbgo/pkg/exchange/retry"
2023-04-06 06:59:03 +00:00
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
2023-04-06 08:12:19 +00:00
func ( s * Strategy ) recoverByScanningTrades ( ctx context . Context , session * bbgo . ExchangeSession ) error {
2023-04-18 07:12:50 +00:00
defer func ( ) {
s . updateGridNumOfOrdersMetricsWithLock ( )
} ( )
2024-03-04 12:52:01 +00:00
isMax := exchange . IsMaxExchange ( session . Exchange )
2023-11-24 06:17:19 +00:00
s . logger . Infof ( "isMax: %t" , isMax )
2023-04-18 07:12:50 +00:00
2023-04-06 06:59:03 +00:00
historyService , implemented := session . Exchange . ( types . ExchangeTradeHistoryService )
// if the exchange doesn't support ExchangeTradeHistoryService, do not run recover
if ! implemented {
s . logger . Warn ( "ExchangeTradeHistoryService is not implemented, can not recover grid" )
return nil
}
openOrders , err := session . Exchange . QueryOpenOrders ( ctx , s . Symbol )
if err != nil {
2023-04-06 09:53:01 +00:00
return errors . Wrapf ( err , "unable to query open orders when recovering" )
2023-04-06 06:59:03 +00:00
}
s . logger . Infof ( "found %d open orders left on the %s order book" , len ( openOrders ) , s . Symbol )
if s . GridProfitStats . InitialOrderID != 0 {
2023-04-06 09:53:01 +00:00
s . logger . Info ( "InitialOrderID is already there, need to recover" )
2023-04-06 10:00:21 +00:00
} else if len ( openOrders ) != 0 {
s . logger . Info ( "even though InitialOrderID is 0, there are open orders so need to recover" )
2023-04-06 06:59:03 +00:00
} else {
2023-04-06 10:00:21 +00:00
s . logger . Info ( "InitialOrderID is 0 and there is no open orders, query trades to check it" )
2023-04-06 06:59:03 +00:00
// initial order id may be new strategy or lost data in redis, so we need to check trades + open orders
// if there are open orders or trades, we need to recover
trades , err := historyService . QueryTrades ( ctx , s . Symbol , & types . TradeQueryOptions {
// from 1, because some API will ignore 0 last trade id
LastTradeID : 1 ,
// if there is any trades, we need to recover.
Limit : 1 ,
} )
if err != nil {
2023-04-06 09:53:01 +00:00
return errors . Wrapf ( err , "unable to query trades when recovering" )
2023-04-06 06:59:03 +00:00
}
if len ( trades ) == 0 {
2023-04-06 09:53:01 +00:00
s . logger . Info ( "0 trades found, it's a new strategy so no need to recover" )
2023-04-06 06:59:03 +00:00
return nil
}
}
2023-04-06 09:53:01 +00:00
s . logger . Infof ( "start recovering" )
2023-05-18 09:47:26 +00:00
filledOrders , err := s . getFilledOrdersByScanningTrades ( ctx , historyService , s . orderQueryService , openOrders )
if err != nil {
2023-04-06 06:59:03 +00:00
return errors . Wrap ( err , "grid recover error" )
}
2023-05-18 09:47:26 +00:00
s . debugOrders ( "emit filled orders" , filledOrders )
// add open orders into avtive maker orders
s . addOrdersToActiveOrderBook ( openOrders )
// emit the filled orders
activeOrderBook := s . orderExecutor . ActiveMakerOrders ( )
for _ , filledOrder := range filledOrders {
activeOrderBook . EmitFilled ( filledOrder )
}
2023-04-06 06:59:03 +00:00
2023-04-26 09:30:31 +00:00
// emit ready after recover
s . EmitGridReady ( )
2023-04-06 15:57:54 +00:00
// debug and send metrics
// wait for the reverse order to be placed
time . Sleep ( 2 * time . Second )
debugGrid ( s . logger , s . grid , s . orderExecutor . ActiveMakerOrders ( ) )
2023-04-26 08:14:53 +00:00
defer bbgo . Sync ( ctx , s )
if s . EnableProfitFixer {
until := time . Now ( )
2023-04-26 09:30:31 +00:00
since := until . Add ( - 7 * 24 * time . Hour )
2023-04-26 08:14:53 +00:00
if s . FixProfitSince != nil {
since = s . FixProfitSince . Time ( )
}
fixer := newProfitFixer ( s . grid , s . Symbol , historyService )
2023-04-26 15:48:02 +00:00
fixer . SetLogger ( s . logger )
2023-04-26 08:14:53 +00:00
// set initial order ID = 0 instead of s.GridProfitStats.InitialOrderID because the order ID could be incorrect
2023-04-26 15:48:02 +00:00
if err := fixer . Fix ( ctx , since , until , 0 , s . GridProfitStats ) ; err != nil {
2023-04-26 15:07:01 +00:00
return err
}
s . logger . Infof ( "fixed profitStats: %#v" , s . GridProfitStats )
2023-04-26 16:33:42 +00:00
s . EmitGridProfit ( s . GridProfitStats , nil )
2023-04-26 08:14:53 +00:00
}
2023-04-06 06:59:03 +00:00
return nil
}
2023-05-24 06:58:19 +00:00
func ( s * Strategy ) getFilledOrdersByScanningTrades ( ctx context . Context , queryTradesService types . ExchangeTradeHistoryService , queryOrderService types . ExchangeOrderQueryService , openOrdersOnGrid [ ] types . Order ) ( [ ] types . Order , error ) {
2023-04-06 06:59:03 +00:00
// set grid
grid := s . newGrid ( )
s . setGrid ( grid )
2023-04-06 16:40:32 +00:00
expectedNumOfOrders := s . GridNum - 1
numGridOpenOrders := int64 ( len ( openOrdersOnGrid ) )
s . debugLog ( "open orders nums: %d, expected nums: %d" , numGridOpenOrders , expectedNumOfOrders )
if expectedNumOfOrders == numGridOpenOrders {
2023-04-18 07:47:00 +00:00
// no need to recover, only need to add open orders back to active order book
2023-05-18 09:47:26 +00:00
return nil , nil
2023-04-06 16:40:32 +00:00
} else if expectedNumOfOrders < numGridOpenOrders {
2023-05-18 09:47:26 +00:00
return nil , fmt . Errorf ( "amount of grid's open orders should not > amount of expected grid's orders" )
2023-04-06 06:59:03 +00:00
}
2023-04-27 13:06:29 +00:00
// 1. build twin-order map
twinOrdersOpen , err := s . buildTwinOrderMap ( grid . Pins , openOrdersOnGrid )
2023-04-06 06:59:03 +00:00
if err != nil {
2023-05-18 09:47:26 +00:00
return nil , errors . Wrapf ( err , "failed to build pin order map with open orders" )
2023-04-06 06:59:03 +00:00
}
2023-04-27 13:06:29 +00:00
// 2. build the filled twin-order map by querying trades
2023-05-19 10:41:07 +00:00
expectedFilledNum := int ( expectedNumOfOrders - numGridOpenOrders )
twinOrdersFilled , err := s . buildFilledTwinOrderMapFromTrades ( ctx , queryTradesService , queryOrderService , twinOrdersOpen , expectedFilledNum )
2023-04-06 06:59:03 +00:00
if err != nil {
2023-05-18 09:47:26 +00:00
return nil , errors . Wrapf ( err , "failed to build filled pin order map" )
2023-04-06 06:59:03 +00:00
}
2023-04-27 13:06:29 +00:00
// 3. get the filled orders from twin-order map
filledOrders := twinOrdersFilled . AscendingOrders ( )
2023-04-06 06:59:03 +00:00
// 4. verify the grid
2023-04-27 13:06:29 +00:00
if err := s . verifyFilledTwinGrid ( s . grid . Pins , twinOrdersOpen , filledOrders ) ; err != nil {
2023-05-18 09:47:26 +00:00
return nil , errors . Wrapf ( err , "verify grid with error" )
2023-04-06 06:59:03 +00:00
}
2023-05-18 09:47:26 +00:00
return filledOrders , nil
2023-04-06 16:40:32 +00:00
}
2023-04-27 13:06:29 +00:00
func ( s * Strategy ) verifyFilledTwinGrid ( pins [ ] Pin , twinOrders TwinOrderMap , filledOrders [ ] types . Order ) error {
s . debugLog ( "verifying filled grid - pins: %+v" , pins )
s . debugOrders ( "verifying filled grid - filled orders" , filledOrders )
2023-04-28 08:02:22 +00:00
s . debugLog ( "verifying filled grid - open twin orders:\n%s" , twinOrders . String ( ) )
2023-04-27 13:06:29 +00:00
if err := s . addOrdersIntoTwinOrderMap ( twinOrders , filledOrders ) ; err != nil {
return errors . Wrapf ( err , "verifying filled grid error when add orders into twin order map" )
}
s . debugLog ( "verifying filled grid - filled twin orders:\n%+v" , twinOrders . String ( ) )
for i , pin := range pins {
2023-04-28 08:02:22 +00:00
// we use twinOrderMap to make sure there are no duplicated order at one grid, and we use the sell price as key so we skip the pins[0] which is only for buy price
2023-04-27 13:06:29 +00:00
if i == 0 {
continue
}
twin , exist := twinOrders [ fixedpoint . Value ( pin ) ]
if ! exist {
return fmt . Errorf ( "there is no order at price (%+v)" , pin )
}
if ! twin . Exist ( ) {
return fmt . Errorf ( "all the price need a twin" )
}
2023-04-28 08:02:22 +00:00
if ! twin . IsValid ( ) {
return fmt . Errorf ( "all the twins need to be valid" )
}
2023-04-27 13:06:29 +00:00
}
return nil
}
// buildTwinOrderMap build the pin-order map with grid and open orders.
// The keys of this map contains all required pins of this grid.
// If the Order of the pin is empty types.Order (OrderID == 0), it means there is no open orders at this pin.
func ( s * Strategy ) buildTwinOrderMap ( pins [ ] Pin , openOrders [ ] types . Order ) ( TwinOrderMap , error ) {
twinOrderMap := make ( TwinOrderMap )
for i , pin := range pins {
// twin order map only use sell price as key, so skip 0
if i == 0 {
continue
}
twinOrderMap [ fixedpoint . Value ( pin ) ] = TwinOrder { }
}
for _ , openOrder := range openOrders {
twinKey , err := findTwinOrderMapKey ( s . grid , openOrder )
if err != nil {
return nil , errors . Wrapf ( err , "failed to build twin order map" )
}
twinOrder , exist := twinOrderMap [ twinKey ]
if ! exist {
return nil , fmt . Errorf ( "the price of the openOrder (id: %d) is not in pins" , openOrder . OrderID )
}
if twinOrder . Exist ( ) {
return nil , fmt . Errorf ( "there are multiple order in a twin" )
}
twinOrder . SetOrder ( openOrder )
twinOrderMap [ twinKey ] = twinOrder
}
return twinOrderMap , nil
}
// buildFilledTwinOrderMapFromTrades will query the trades from last 24 hour and use them to build a pin order map
// It will skip the orders on pins at which open orders are already
2023-05-24 06:58:19 +00:00
func ( s * Strategy ) buildFilledTwinOrderMapFromTrades ( ctx context . Context , queryTradesService types . ExchangeTradeHistoryService , queryOrderService types . ExchangeOrderQueryService , twinOrdersOpen TwinOrderMap , expectedFillNum int ) ( TwinOrderMap , error ) {
2023-04-27 13:06:29 +00:00
twinOrdersFilled := make ( TwinOrderMap )
// existedOrders is used to avoid re-query the same orders
existedOrders := twinOrdersOpen . SyncOrderMap ( )
// get the filled orders when bbgo is down in order from trades
2023-05-19 10:41:07 +00:00
until := time . Now ( )
// the first query only query the last 1 hour, because mostly shutdown and recovery happens within 1 hour
since := until . Add ( - 1 * time . Hour )
// hard limit for recover
recoverSinceLimit := time . Date ( 2023 , time . March , 10 , 0 , 0 , 0 , 0 , time . UTC )
2023-05-24 06:58:19 +00:00
if s . RecoverGridWithin != 0 && until . Add ( - 1 * s . RecoverGridWithin ) . After ( recoverSinceLimit ) {
recoverSinceLimit = until . Add ( - 1 * s . RecoverGridWithin )
2023-05-19 10:41:07 +00:00
}
for {
if err := s . queryTradesToUpdateTwinOrdersMap ( ctx , queryTradesService , queryOrderService , twinOrdersOpen , twinOrdersFilled , existedOrders , since , until ) ; err != nil {
return nil , errors . Wrapf ( err , "failed to query trades to update twin orders map" )
}
until = since
since = until . Add ( - 6 * time . Hour )
if len ( twinOrdersFilled ) >= expectedFillNum {
s . logger . Infof ( "stop querying trades because twin orders filled (%d) >= expected filled nums (%d)" , len ( twinOrdersFilled ) , expectedFillNum )
break
}
if s . GridProfitStats != nil && s . GridProfitStats . Since != nil && until . Before ( * s . GridProfitStats . Since ) {
s . logger . Infof ( "stop querying trades because the time range is out of the strategy's since (%s)" , * s . GridProfitStats . Since )
break
}
if until . Before ( recoverSinceLimit ) {
s . logger . Infof ( "stop querying trades because the time range is out of the limit (%s)" , recoverSinceLimit )
break
}
}
return twinOrdersFilled , nil
}
2023-05-24 06:58:19 +00:00
func ( s * Strategy ) queryTradesToUpdateTwinOrdersMap ( ctx context . Context , queryTradesService types . ExchangeTradeHistoryService , queryOrderService types . ExchangeOrderQueryService , twinOrdersOpen , twinOrdersFilled TwinOrderMap , existedOrders * types . SyncOrderMap , since , until time . Time ) error {
2023-04-27 13:06:29 +00:00
var fromTradeID uint64 = 0
2023-05-19 10:41:07 +00:00
var limit int64 = 1000
2023-04-27 13:06:29 +00:00
for {
2024-04-29 09:16:04 +00:00
trades , err := retry . QueryTradesUntilSuccessful ( ctx , queryTradesService , s . Symbol , & types . TradeQueryOptions {
2023-05-19 10:41:07 +00:00
StartTime : & since ,
EndTime : & until ,
2023-04-27 13:06:29 +00:00
LastTradeID : fromTradeID ,
Limit : limit ,
} )
if err != nil {
2023-05-19 10:41:07 +00:00
return errors . Wrapf ( err , "failed to query trades to recover the grid with open orders" )
2023-04-27 13:06:29 +00:00
}
2023-05-19 10:41:07 +00:00
s . debugLog ( "QueryTrades from %s <-> %s (from: %d) return %d trades" , since , until , fromTradeID , len ( trades ) )
2023-04-27 13:06:29 +00:00
for _ , trade := range trades {
2023-05-19 10:41:07 +00:00
if trade . Time . After ( until ) {
return nil
}
2023-04-27 13:06:29 +00:00
s . debugLog ( trade . String ( ) )
2023-05-19 10:41:07 +00:00
2023-04-27 13:06:29 +00:00
if existedOrders . Exists ( trade . OrderID ) {
// already queries, skip
continue
}
2023-09-19 03:12:14 +00:00
order , err := retry . QueryOrderUntilSuccessful ( ctx , queryOrderService , types . OrderQuery {
Symbol : trade . Symbol ,
2023-04-27 13:06:29 +00:00
OrderID : strconv . FormatUint ( trade . OrderID , 10 ) ,
} )
if err != nil {
2023-09-19 03:12:14 +00:00
return errors . Wrapf ( err , "failed to query order by trade (trade id: %d, order id: %d)" , trade . ID , trade . OrderID )
2023-04-27 13:06:29 +00:00
}
2023-05-19 10:41:07 +00:00
s . debugLog ( order . String ( ) )
2023-04-27 13:06:29 +00:00
// avoid query this order again
existedOrders . Add ( * order )
// add 1 to avoid duplicate
fromTradeID = trade . ID + 1
twinOrderKey , err := findTwinOrderMapKey ( s . grid , * order )
if err != nil {
2023-05-19 10:41:07 +00:00
return errors . Wrapf ( err , "failed to find grid order map's key when recover" )
2023-04-27 13:06:29 +00:00
}
twinOrderOpen , exist := twinOrdersOpen [ twinOrderKey ]
if ! exist {
2023-05-19 10:41:07 +00:00
return fmt . Errorf ( "the price of the order with the same GroupID is not in pins" )
2023-04-27 13:06:29 +00:00
}
if twinOrderOpen . Exist ( ) {
continue
}
if twinOrder , exist := twinOrdersFilled [ twinOrderKey ] ; exist {
to := twinOrder . GetOrder ( )
if to . UpdateTime . Time ( ) . After ( order . UpdateTime . Time ( ) ) {
s . logger . Infof ( "twinOrder's update time (%s) should not be after order's update time (%s)" , to . UpdateTime , order . UpdateTime )
continue
}
}
twinOrder := TwinOrder { }
twinOrder . SetOrder ( * order )
twinOrdersFilled [ twinOrderKey ] = twinOrder
}
// stop condition
if int64 ( len ( trades ) ) < limit {
2023-05-19 10:41:07 +00:00
return nil
2023-04-27 13:06:29 +00:00
}
}
}
func ( s * Strategy ) addOrdersIntoTwinOrderMap ( twinOrders TwinOrderMap , orders [ ] types . Order ) error {
for _ , order := range orders {
k , err := findTwinOrderMapKey ( s . grid , order )
if err != nil {
return errors . Wrap ( err , "failed to add orders into twin order map" )
}
if v , exist := twinOrders [ k ] ; ! exist {
return fmt . Errorf ( "the price (%+v) is not in pins" , k )
} else if v . Exist ( ) {
return fmt . Errorf ( "there is already a twin order at this price (%+v)" , k )
} else {
twin := TwinOrder { }
twin . SetOrder ( order )
twinOrders [ k ] = twin
}
}
return nil
}