From 4288c82e25cb48470e7c016b6b221cce48a78c01 Mon Sep 17 00:00:00 2001 From: chiahung Date: Tue, 7 Mar 2023 20:58:05 +0800 Subject: [PATCH 1/2] FEATURE: recover grids with open orders by querying trades process and its buildPinOrderMap method --- pkg/strategy/grid2/strategy.go | 107 +++++++++++++++++++++++++- pkg/strategy/grid2/strategy_test.go | 115 ++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 2 deletions(-) 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) + }) +} From 67001fcbb79b49e36e44791c62cd2694b062ab74 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 9 Mar 2023 17:42:37 +0800 Subject: [PATCH 2/2] new config 'recoverGridByScanningTrades' --- pkg/strategy/grid2/strategy.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index f61750a69..91cc53316 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -157,7 +157,8 @@ type Strategy struct { // it makes sure that your grid configuration is profitable. FeeRate fixedpoint.Value `json:"feeRate"` - SkipSpreadCheck bool `json:"skipSpreadCheck"` + SkipSpreadCheck bool `json:"skipSpreadCheck"` + RecoverGridByScanningTrades bool `json:"recoverGridByScanningTrades"` GridProfitStats *GridProfitStats `persistence:"grid_profit_stats"` Position *types.Position `persistence:"position"` @@ -1369,7 +1370,7 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context // 6. debug and send metrics debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) - s.updateGridNumOfOrdersMetrics() + s.updateGridNumOfOrdersMetricsWithLock() s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil @@ -1910,7 +1911,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.recoverGridByScanningOrders(ctx, session); err != nil { + if err := s.recoverGrid(ctx, session); err != nil { s.logger.WithError(err).Errorf("recover error") } } @@ -1921,6 +1922,14 @@ func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSessi } } +func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSession) error { + if s.RecoverGridByScanningTrades { + return s.recoverGridByScanningTrades(ctx, session) + } + + return s.recoverGridByScanningOrders(ctx, session) +} + func (s *Strategy) recoverGridByScanningOrders(ctx context.Context, session *bbgo.ExchangeSession) error { openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) if err != nil { @@ -2018,4 +2027,4 @@ func roundUpMarketQuantity(market types.Market, v fixedpoint.Value, c string) (f } return v.Round(prec, fixedpoint.Up), prec -} \ No newline at end of file +}