diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 0a69245cd..648d4bdbe 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -13,6 +13,14 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +type QueryTradesService interface { + QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) +} + +type QueryOrderService interface { + QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) +} + func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { defer func() { s.updateGridNumOfOrdersMetricsWithLock() @@ -59,9 +67,20 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex } s.logger.Infof("start recovering") - if err := s.recoverWithOpenOrdersByScanningTrades(ctx, historyService, openOrders); err != nil { + filledOrders, err := s.getFilledOrdersByScanningTrades(ctx, historyService, s.orderQueryService, openOrders) + if err != nil { return errors.Wrap(err, "grid recover error") } + 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) + } // emit ready after recover s.EmitGridReady() @@ -96,11 +115,7 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex return nil } -func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrdersOnGrid []types.Order) error { - if s.orderQueryService == nil { - return fmt.Errorf("orderQueryService is nil, it can't get orders by trade") - } - +func (s *Strategy) getFilledOrdersByScanningTrades(ctx context.Context, queryTradesService QueryTradesService, queryOrderService QueryOrderService, openOrdersOnGrid []types.Order) ([]types.Order, error) { // set grid grid := s.newGrid() s.setGrid(grid) @@ -110,22 +125,21 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi s.debugLog("open orders nums: %d, expected nums: %d", numGridOpenOrders, expectedNumOfOrders) if expectedNumOfOrders == numGridOpenOrders { // no need to recover, only need to add open orders back to active order book - s.addOrdersToActiveOrderBook(openOrdersOnGrid) - return nil + return nil, nil } else if expectedNumOfOrders < numGridOpenOrders { - return fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") + return nil, fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") } // 1. build twin-order map twinOrdersOpen, err := s.buildTwinOrderMap(grid.Pins, openOrdersOnGrid) if err != nil { - return errors.Wrapf(err, "failed to build pin order map with open orders") + return nil, errors.Wrapf(err, "failed to build pin order map with open orders") } // 2. build the filled twin-order map by querying trades - twinOrdersFilled, err := s.buildFilledTwinOrderMapFromTrades(ctx, historyService, twinOrdersOpen) + twinOrdersFilled, err := s.buildFilledTwinOrderMapFromTrades(ctx, queryTradesService, queryOrderService, twinOrdersOpen) if err != nil { - return errors.Wrapf(err, "failed to build filled pin order map") + return nil, errors.Wrapf(err, "failed to build filled pin order map") } // 3. get the filled orders from twin-order map @@ -133,182 +147,10 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi // 4. verify the grid if err := s.verifyFilledTwinGrid(s.grid.Pins, twinOrdersOpen, filledOrders); err != nil { - return errors.Wrapf(err, "verify grid with error") + return nil, errors.Wrapf(err, "verify grid with error") } - // 5. add open orders to active order book. - s.addOrdersToActiveOrderBook(openOrdersOnGrid) - - // 6. emit the filled orders - s.debugOrders("emit filled orders", filledOrders) - activeOrderBook := s.orderExecutor.ActiveMakerOrders() - for _, filledOrder := range filledOrders { - activeOrderBook.EmitFilled(filledOrder) - } - - return nil -} - -func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrders []types.Order) error { - s.debugLog("verifying filled grid - pins: %+v", pins) - s.debugLog("verifying filled grid - open pin orders:\n%s", pinOrders.String()) - s.debugOrders("verifying filled grid - filled orders", filledOrders) - - if err := addOrdersIntoPinOrderMap(pinOrders, filledOrders); err != nil { - return errors.Wrapf(err, "verifying filled grid error when add orders into pin order map") - } - - s.debugLog("verifying filled grid - filled pin orders:\n%+v", pinOrders.String()) - - expectedSide := types.SideTypeBuy - for _, pin := range pins { - order, exist := pinOrders[fixedpoint.Value(pin)] - if !exist { - return fmt.Errorf("there is no order at price (%+v)", pin) - } - - // if there is order with OrderID = 0, means we hit the empty pin - // there must be only one empty pin in the grid - // all orders below this pin need to be bid orders, above this pin need to be ask orders - if order.OrderID == 0 { - if expectedSide == types.SideTypeBuy { - expectedSide = types.SideTypeSell - continue - } - - return fmt.Errorf("found more than one empty pins") - } - - if order.Side != expectedSide { - return fmt.Errorf("the side of order (%s) is wrong, expected: %s", order.Side, expectedSide) - } - } - - if expectedSide != types.SideTypeSell { - return fmt.Errorf("there is no empty pin in the grid") - } - - return nil -} - -// buildPinOrderMap 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) buildPinOrderMap(pins []Pin, openOrders []types.Order) (PinOrderMap, error) { - pinOrderMap := make(PinOrderMap) - - for _, pin := range pins { - pinOrderMap[fixedpoint.Value(pin)] = types.Order{} - } - - for _, openOrder := range openOrders { - pin := openOrder.Price - v, exist := pinOrderMap[pin] - if !exist { - return nil, fmt.Errorf("the price of the order (id: %d) is not in pins", openOrder.OrderID) - } - - if v.OrderID != 0 { - return nil, fmt.Errorf("there are duplicated open orders at the same pin") - } - - pinOrderMap[pin] = openOrder - } - - return pinOrderMap, nil -} - -// buildFilledPinOrderMapFromTrades 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 -func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, pinOrdersOpen PinOrderMap) (PinOrderMap, error) { - pinOrdersFilled := make(PinOrderMap) - - // existedOrders is used to avoid re-query the same orders - existedOrders := pinOrdersOpen.SyncOrderMap() - - var limit int64 = 1000 - // get the filled orders when bbgo is down in order from trades - // [NOTE] only retrieve from last 24 hours !!! - var fromTradeID uint64 = 0 - for { - trades, err := historyService.QueryTrades(ctx, s.Symbol, &types.TradeQueryOptions{ - LastTradeID: fromTradeID, - Limit: limit, - }) - - if err != nil { - return nil, errors.Wrapf(err, "failed to query trades to recover the grid with open orders") - } - - s.debugLog("QueryTrades return %d trades", len(trades)) - - for _, trade := range trades { - s.debugLog(trade.String()) - if existedOrders.Exists(trade.OrderID) { - // already queries, skip - continue - } - - order, err := s.orderQueryService.QueryOrder(ctx, types.OrderQuery{ - OrderID: strconv.FormatUint(trade.OrderID, 10), - }) - - if err != nil { - return nil, errors.Wrapf(err, "failed to query order by trade") - } - - s.debugLog("%s (group_id: %d)", order.String(), order.GroupID) - - // avoid query this order again - existedOrders.Add(*order) - - // add 1 to avoid duplicate - fromTradeID = trade.ID + 1 - - // checked the trade's order is filled order - pin := order.Price - v, exist := pinOrdersOpen[pin] - if !exist { - return nil, fmt.Errorf("the price of the order with the same GroupID is not in pins") - } - - // skip open orders on grid - if v.OrderID != 0 { - continue - } - - // check the order's creation time - if pinOrder, exist := pinOrdersFilled[pin]; exist && pinOrder.CreationTime.Time().After(order.CreationTime.Time()) { - // do not replace the pin order if the order's creation time is not after pin order's creation time - // this situation should not happen actually, because the trades is already sorted. - s.logger.Infof("pinOrder's creation time (%s) should not be after order's creation time (%s)", pinOrder.CreationTime, order.CreationTime) - continue - } - pinOrdersFilled[pin] = *order - } - - // stop condition - if int64(len(trades)) < limit { - break - } - } - - return pinOrdersFilled, nil -} - -func addOrdersIntoPinOrderMap(pinOrders PinOrderMap, orders []types.Order) error { - for _, order := range orders { - price := order.Price - if o, exist := pinOrders[price]; !exist { - return fmt.Errorf("the price (%+v) is not in pins", price) - } else if o.OrderID != 0 { - return fmt.Errorf("there is already an order at this price (%+v)", price) - } else { - pinOrders[price] = order - } - } - - return nil + return filledOrders, nil } func (s *Strategy) verifyFilledTwinGrid(pins []Pin, twinOrders TwinOrderMap, filledOrders []types.Order) error { @@ -384,7 +226,7 @@ func (s *Strategy) buildTwinOrderMap(pins []Pin, openOrders []types.Order) (Twin // 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 -func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, twinOrdersOpen TwinOrderMap) (TwinOrderMap, error) { +func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, queryTradesService QueryTradesService, queryOrderService QueryOrderService, twinOrdersOpen TwinOrderMap) (TwinOrderMap, error) { twinOrdersFilled := make(TwinOrderMap) // existedOrders is used to avoid re-query the same orders @@ -395,7 +237,7 @@ func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, histor // [NOTE] only retrieve from last 24 hours !!! var fromTradeID uint64 = 0 for { - trades, err := historyService.QueryTrades(ctx, s.Symbol, &types.TradeQueryOptions{ + trades, err := queryTradesService.QueryTrades(ctx, s.Symbol, &types.TradeQueryOptions{ LastTradeID: fromTradeID, Limit: limit, }) @@ -413,7 +255,7 @@ func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, histor continue } - order, err := s.orderQueryService.QueryOrder(ctx, types.OrderQuery{ + order, err := queryOrderService.QueryOrder(ctx, types.OrderQuery{ OrderID: strconv.FormatUint(trade.OrderID, 10), }) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 3cdcb7953..483734f01 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -6,7 +6,6 @@ import ( "context" "errors" "testing" - "time" "github.com/golang/mock/gomock" "github.com/sirupsen/logrus" @@ -1230,6 +1229,7 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { }) } +/* func Test_buildPinOrderMap(t *testing.T) { assert := assert.New(t) s := newTestStrategy() @@ -1585,3 +1585,4 @@ func Test_verifyFilledGrid(t *testing.T) { }) } +*/