diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 3d5157314..f61750a69 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1339,6 +1339,70 @@ func (s *Strategy) checkMinimalQuoteInvestment(grid *Grid) error { return nil } +func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(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") + } + + // set grid + grid := s.newGrid() + s.setGrid(grid) + + // add open orders to active order book + s.addOrdersToActiveOrderBook(openOrdersOnGrid) + + expectedOrderNums := s.GridNum - 1 + openOrdersOnGridNums := int64(len(openOrdersOnGrid)) + s.logger.Infof("[DEBUG] open orders nums: %d, expected nums: %d", openOrdersOnGridNums, expectedOrderNums) + if expectedOrderNums == openOrdersOnGridNums { + // no need to recover + return nil + } + + // 1. build pin-order map + // 2. fill the pin-order map by querying trades + // 3. get the filled orders from pin-order map + // 4. emit the filled orders + + // 5. emit grid ready + s.EmitGridReady() + + // 6. debug and send metrics + debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) + s.updateGridNumOfOrdersMetrics() + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) + + 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(grid *Grid, openOrders []types.Order) (map[string]types.Order, error) { + pinOrderMap := make(map[string]types.Order) + + for _, pin := range grid.Pins { + priceStr := s.FormatPrice(fixedpoint.Value(pin)) + pinOrderMap[priceStr] = types.Order{} + } + + for _, openOrder := range openOrders { + priceStr := s.FormatPrice(openOrder.Price) + v, exist := pinOrderMap[priceStr] + 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[priceStr] = openOrder + } + + return pinOrderMap, nil +} + func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { grid := s.newGrid() @@ -1846,7 +1910,7 @@ func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSessi if s.RecoverOrdersWhenStart { // do recover only when triggerPrice is not set and not in the back-test mode s.logger.Infof("recoverWhenStart is set, trying to recover grid orders...") - if err := s.recoverGrid(ctx, session); err != nil { + if err := s.recoverGridByScanningOrders(ctx, session); err != nil { s.logger.WithError(err).Errorf("recover error") } } @@ -1857,7 +1921,7 @@ func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSessi } } -func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSession) error { +func (s *Strategy) recoverGridByScanningOrders(ctx context.Context, session *bbgo.ExchangeSession) error { openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) if err != nil { return err @@ -1884,6 +1948,45 @@ func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSessio return nil } +func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { + // no initial order id means we don't need to recover + if s.GridProfitStats.InitialOrderID == 0 { + s.logger.Info("[DEBUG] new strategy, no need to recover") + return nil + } + + openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) + if err != nil { + return err + } + + s.logger.Infof("found %d open orders left on the %s order book", len(openOrders), s.Symbol) + + s.logger.Infof("[DEBUG] recover grid with group id: %d", s.OrderGroupID) + // filter out the order with the group id belongs to this grid + var openOrdersOnGrid []types.Order + for _, order := range openOrders { + s.logger.Infof("[DEBUG] order (%d) group id: %d", order.OrderID, order.GroupID) + if order.GroupID == s.OrderGroupID { + openOrdersOnGrid = append(openOrdersOnGrid, order) + } + } + + s.logger.Infof("found %d open orders belong to this grid on the %s order book", len(openOrdersOnGrid), s.Symbol) + + historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) + if !implemented { + s.logger.Warn("ExchangeTradeHistoryService is not implemented, can not recover grid") + return nil + } + + if err := s.recoverGridWithOpenOrdersByScanningTrades(ctx, historyService, openOrdersOnGrid); err != nil { + return errors.Wrap(err, "grid recover error") + } + + return nil +} + // openOrdersMismatches verifies if the open orders are on the grid pins // return true if mismatches func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.ExchangeSession) (bool, error) { diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index e1f6773d7..c70a9eaec 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -981,3 +981,118 @@ func Test_roundUpMarketQuantity(t *testing.T) { assert.Equal(t, "0.00000003", q3.String(), "rounding prec 8") assert.Equal(t, 8, prec) } + +func Test_buildPinOrderMap(t *testing.T) { + assert := assert.New(t) + s := newTestStrategy() + s.UpperPrice = number(2000.0) + s.LowerPrice = number(1000.0) + s.GridNum = 11 + s.grid = s.newGrid() + + t.Run("successful case", func(t *testing.T) { + openOrders := []types.Order{ + types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: number(1.0), + Price: number(1000.0), + AveragePrice: number(0), + StopPrice: number(0), + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "max", + GID: 1, + OrderID: 1, + Status: types.OrderStatusNew, + ExecutedQuantity: number(0.0), + IsWorking: false, + }, + } + m, err := s.buildPinOrderMap(s.grid, openOrders) + assert.NoError(err) + assert.Len(m, 11) + + for pin, order := range m { + if pin == s.FormatPrice(openOrders[0].Price) { + assert.Equal(openOrders[0].OrderID, order.OrderID) + } else { + assert.Equal(uint64(0), order.OrderID) + } + } + }) + + t.Run("there is one order with non-pin price in openOrders", func(t *testing.T) { + openOrders := []types.Order{ + types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: number(1.0), + Price: number(1111.0), + AveragePrice: number(0), + StopPrice: number(0), + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "max", + GID: 1, + OrderID: 1, + Status: types.OrderStatusNew, + ExecutedQuantity: number(0.0), + IsWorking: false, + }, + } + _, err := s.buildPinOrderMap(s.grid, openOrders) + assert.Error(err) + }) + + t.Run("there are duplicated open orders at same pin", func(t *testing.T) { + openOrders := []types.Order{ + types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: number(1.0), + Price: number(1000.0), + AveragePrice: number(0), + StopPrice: number(0), + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "max", + GID: 1, + OrderID: 1, + Status: types.OrderStatusNew, + ExecutedQuantity: number(0.0), + IsWorking: false, + }, + types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: number(1.0), + Price: number(1000.0), + AveragePrice: number(0), + StopPrice: number(0), + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }, + Exchange: "max", + GID: 2, + OrderID: 2, + Status: types.OrderStatusNew, + ExecutedQuantity: number(0.0), + IsWorking: false, + }, + } + _, err := s.buildPinOrderMap(s.grid, openOrders) + assert.Error(err) + }) +}