From 00352b2a0d77f8b13ed9b7c72b80bf0713c84246 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 6 Apr 2023 11:36:32 +0800 Subject: [PATCH 1/8] FIX: recover even though inital order id is 0 --- pkg/strategy/grid2/strategy.go | 47 ++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e28ab1d3b..42a0978b3 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -2,6 +2,7 @@ package grid2 import ( "context" + "encoding/json" "fmt" "math" "sort" @@ -825,6 +826,7 @@ func (s *Strategy) newOrderUpdateHandler(ctx context.Context, session *bbgo.Exch s.handleOrderFilled(o) // sync the profits to redis + s.debugGridProfitStats("OrderUpdate") bbgo.Sync(ctx, s) s.updateGridNumOfOrdersMetricsWithLock() @@ -944,6 +946,7 @@ func (s *Strategy) CloseGrid(ctx context.Context) error { defer s.EmitGridClosed() + s.debugGridProfitStats("CloseGrid") bbgo.Sync(ctx, s) // now we can cancel the open orders @@ -1082,6 +1085,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) if len(orderIds) > 0 { s.GridProfitStats.InitialOrderID = orderIds[0] + s.debugGridProfitStats("openGrid") bbgo.Sync(ctx, s) } @@ -1212,6 +1216,23 @@ func (s *Strategy) debugOrders(desc string, orders []types.Order) { s.logger.Infof(sb.String()) } +func (s *Strategy) debugGridProfitStats(where string) { + if !s.Debug { + return + } + + stats := *s.GridProfitStats + // ProfitEntries may have too many profits, make it nil to readable + stats.ProfitEntries = nil + b, err := json.Marshal(stats) + if err != nil { + s.logger.WithError(err).Errorf("[%s] failed to debug grid profit stats", where) + return + } + + s.logger.Infof("[%s] grid profit stats: %s", where, string(b)) +} + func (s *Strategy) debugLog(format string, args ...interface{}) { if !s.Debug { return @@ -1441,10 +1462,12 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context s.debugOrders("emit filled orders", filledOrders) // 5. emit the filled orders - activeOrderBook := s.orderExecutor.ActiveMakerOrders() - for _, filledOrder := range filledOrders { - activeOrderBook.EmitFilled(filledOrder) - } + /* + activeOrderBook := s.orderExecutor.ActiveMakerOrders() + for _, filledOrder := range filledOrders { + activeOrderBook.EmitFilled(filledOrder) + } + */ // 6. emit grid ready s.EmitGridReady() @@ -2032,6 +2055,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.GridProfitStats.AddTrade(trade) }) orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + s.debugGridProfitStats("OnPositionUpdate") bbgo.Sync(ctx, s) }) orderExecutor.ActiveMakerOrders().OnFilled(s.newOrderUpdateHandler(ctx, session)) @@ -2132,6 +2156,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. } func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSession) { + s.debugGridProfitStats("startProcess") 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...") @@ -2187,19 +2212,12 @@ func (s *Strategy) recoverGridByScanningOrders(ctx context.Context, session *bbg } 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.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.debugLog("recover grid with group id: %d", s.OrderGroupID) // filter out the order with the group id belongs to this grid var openOrdersOnGrid []types.Order @@ -2211,6 +2229,13 @@ func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbg s.logger.Infof("found %d open orders belong to this grid on the %s order book", len(openOrdersOnGrid), s.Symbol) + // no initial order id means we don't need to recover + // 3/31 updated : Find there may be 0 initial order id when the strategy is not strategy, so we need to add more checking on it. + if s.GridProfitStats.InitialOrderID == 0 && len(openOrdersOnGrid) == 0 { + s.debugLog("new strategy, no need to recover") + return nil + } + historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) if !implemented { s.logger.Warn("ExchangeTradeHistoryService is not implemented, can not recover grid") From d953a6d7b885d8e4d7de6b9d75964ee8892723bd Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 6 Apr 2023 14:59:03 +0800 Subject: [PATCH 2/8] check by trades + open orders --- pkg/strategy/grid2/recover.go | 305 +++++++++++++++++++++++++++++++++ pkg/strategy/grid2/strategy.go | 273 ----------------------------- 2 files changed, 305 insertions(+), 273 deletions(-) create mode 100644 pkg/strategy/grid2/recover.go diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go new file mode 100644 index 000000000..c50a9fc57 --- /dev/null +++ b/pkg/strategy/grid2/recover.go @@ -0,0 +1,305 @@ +package grid2 + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/pkg/errors" +) + +func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { + 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 + } + + s.logger.Infof("recover grid with group id: %d", s.OrderGroupID) + + openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) + if err != nil { + return errors.Wrapf(err, "check if strategy exists error when query open orders") + } + + s.logger.Infof("found %d open orders left on the %s order book", len(openOrders), s.Symbol) + + // filter out the order with the group id belongs to this grid + var openOrdersOnGrid []types.Order + for _, order := range openOrders { + 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) + + if s.GridProfitStats.InitialOrderID != 0 { + // existiting strategy, need recover + } else if len(openOrdersOnGrid) != 0 { + // open orders on grid is not 0. this strategy is existing, need recover + } else { + // 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 { + return errors.Wrapf(err, "check if strategy exists error when query trades") + } + + if len(trades) == 0 { + s.logger.Info("new strategy, no need to recover") + return nil + } + } + + s.logger.Infof("start to recover") + if err := s.recoverGridWithOpenOrdersByScanningTrades(ctx, historyService, openOrdersOnGrid); err != nil { + return errors.Wrap(err, "grid recover 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.debugLog("open orders nums: %d, expected nums: %d", openOrdersOnGridNums, expectedOrderNums) + if expectedOrderNums == openOrdersOnGridNums { + // no need to recover + return nil + } else if expectedOrderNums < openOrdersOnGridNums { + return fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") + } + + // 1. build pin-order map + pinOrdersOpen, err := s.buildPinOrderMap(grid.Pins, openOrdersOnGrid) + if err != nil { + return errors.Wrapf(err, "failed to build pin order map with open orders") + } + + // 2. build the filled pin-order map by querying trades + pinOrdersFilled, err := s.buildFilledPinOrderMapFromTrades(ctx, historyService, pinOrdersOpen) + if err != nil { + return errors.Wrapf(err, "failed to build filled pin order map") + } + + // 3. get the filled orders from pin-order map + filledOrders := pinOrdersFilled.AscendingOrders() + numFilledOrders := len(filledOrders) + if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums) { + // nums of filled order is the same as Size - 1 - num(open orders) + } else if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums+1) { + filledOrders = filledOrders[1:] + } else { + return fmt.Errorf("not reasonable num of filled orders") + } + + // 4. verify the grid + if err := s.verifyFilledGrid(s.grid.Pins, pinOrdersOpen, filledOrders); err != nil { + return errors.Wrapf(err, "verify grid with error") + } + + s.debugOrders("emit filled orders", filledOrders) + + // 5. emit the filled orders + activeOrderBook := s.orderExecutor.ActiveMakerOrders() + for _, filledOrder := range filledOrders { + activeOrderBook.EmitFilled(filledOrder) + } + + // 6. emit grid ready + s.EmitGridReady() + + // 7. debug and send metrics + // wait for the reverse order to be placed + time.Sleep(2 * time.Second) + debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) + s.updateGridNumOfOrdersMetricsWithLock() + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) + + return nil +} + +func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrders []types.Order) error { + s.debugLog("pins: %+v", pins) + s.debugLog("open pin orders:\n%s", pinOrders.String()) + s.debugOrders("filled orders", filledOrders) + + for _, filledOrder := range filledOrders { + price := filledOrder.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] = filledOrder + } + } + + s.debugLog("filled pin orders:\n%+v", pinOrders.String()) + + side := 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 side == types.SideTypeBuy { + side = types.SideTypeSell + continue + } + + return fmt.Errorf("not only one empty order in this grid") + } + + if order.Side != side { + return fmt.Errorf("the side is wrong") + } + } + + if side != 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 + + // this trade doesn't belong to this grid + if order.GroupID != s.OrderGroupID { + continue + } + + // 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 + + // wait 100 ms to avoid rate limit + time.Sleep(100 * time.Millisecond) + } + + // stop condition + if int64(len(trades)) < limit { + break + } + } + + return pinOrdersFilled, nil +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 42a0978b3..e6a8d4fc9 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1409,241 +1409,6 @@ 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.debugLog("open orders nums: %d, expected nums: %d", openOrdersOnGridNums, expectedOrderNums) - if expectedOrderNums == openOrdersOnGridNums { - // no need to recover - return nil - } else if expectedOrderNums < openOrdersOnGridNums { - return fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") - } - - // 1. build pin-order map - pinOrdersOpen, err := s.buildPinOrderMap(grid.Pins, openOrdersOnGrid) - if err != nil { - return errors.Wrapf(err, "failed to build pin order map with open orders") - } - - // 2. build the filled pin-order map by querying trades - pinOrdersFilled, err := s.buildFilledPinOrderMapFromTrades(ctx, historyService, pinOrdersOpen) - if err != nil { - return errors.Wrapf(err, "failed to build filled pin order map") - } - - // 3. get the filled orders from pin-order map - filledOrders := pinOrdersFilled.AscendingOrders() - numFilledOrders := len(filledOrders) - if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums) { - // nums of filled order is the same as Size - 1 - num(open orders) - } else if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums+1) { - filledOrders = filledOrders[1:] - } else { - return fmt.Errorf("not reasonable num of filled orders") - } - - // 4. verify the grid - if err := s.verifyFilledGrid(s.grid.Pins, pinOrdersOpen, filledOrders); err != nil { - return errors.Wrapf(err, "verify grid with error") - } - - s.debugOrders("emit filled orders", filledOrders) - - // 5. emit the filled orders - /* - activeOrderBook := s.orderExecutor.ActiveMakerOrders() - for _, filledOrder := range filledOrders { - activeOrderBook.EmitFilled(filledOrder) - } - */ - - // 6. emit grid ready - s.EmitGridReady() - - // 7. debug and send metrics - // wait for the reverse order to be placed - time.Sleep(2 * time.Second) - debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) - s.updateGridNumOfOrdersMetricsWithLock() - s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) - - return nil -} - -func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrders []types.Order) error { - s.debugLog("pins: %+v", pins) - s.debugLog("open pin orders:\n%s", pinOrders.String()) - s.debugOrders("filled orders", filledOrders) - - for _, filledOrder := range filledOrders { - price := filledOrder.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] = filledOrder - } - } - - s.debugLog("filled pin orders:\n%+v", pinOrders.String()) - - side := 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 side == types.SideTypeBuy { - side = types.SideTypeSell - continue - } - - return fmt.Errorf("not only one empty order in this grid") - } - - if order.Side != side { - return fmt.Errorf("the side is wrong") - } - } - - if side != 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 - - // this trade doesn't belong to this grid - if order.GroupID != s.OrderGroupID { - continue - } - - // 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 - - // wait 100 ms to avoid rate limit - time.Sleep(100 * time.Millisecond) - } - - // stop condition - if int64(len(trades)) < limit { - break - } - } - - return pinOrdersFilled, nil -} - func (s *Strategy) recoverGridWithOpenOrders(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrders []types.Order) error { grid := s.newGrid() @@ -2211,44 +1976,6 @@ func (s *Strategy) recoverGridByScanningOrders(ctx context.Context, session *bbg return nil } -func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { - 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.debugLog("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 { - 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) - - // no initial order id means we don't need to recover - // 3/31 updated : Find there may be 0 initial order id when the strategy is not strategy, so we need to add more checking on it. - if s.GridProfitStats.InitialOrderID == 0 && len(openOrdersOnGrid) == 0 { - s.debugLog("new strategy, no need to recover") - return nil - } - - 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) { From 9fa647ed65d9964ee608be11c6b2ae2d04464af8 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 6 Apr 2023 16:12:19 +0800 Subject: [PATCH 3/8] rename method --- pkg/strategy/grid2/recover.go | 6 +++--- pkg/strategy/grid2/strategy.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index c50a9fc57..85ed5a91f 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -12,7 +12,7 @@ import ( "github.com/pkg/errors" ) -func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { +func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { historyService, implemented := session.Exchange.(types.ExchangeTradeHistoryService) // if the exchange doesn't support ExchangeTradeHistoryService, do not run recover if !implemented { @@ -64,14 +64,14 @@ func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbg } s.logger.Infof("start to recover") - if err := s.recoverGridWithOpenOrdersByScanningTrades(ctx, historyService, openOrdersOnGrid); err != nil { + if err := s.recoverWithOpenOrdersByScanningTrades(ctx, historyService, openOrdersOnGrid); err != nil { return errors.Wrap(err, "grid recover error") } return nil } -func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, openOrdersOnGrid []types.Order) error { +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") } diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index e6a8d4fc9..89ab8aaa0 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1942,14 +1942,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 { s.debugLog("recover grid by scanning trades") - return s.recoverGridByScanningTrades(ctx, session) + return s.recoverByScanningTrades(ctx, session) } s.debugLog("recover grid by scanning orders") - return s.recoverGridByScanningOrders(ctx, session) + return s.recoverByScanningOrders(ctx, session) } -func (s *Strategy) recoverGridByScanningOrders(ctx context.Context, session *bbgo.ExchangeSession) error { +func (s *Strategy) recoverByScanningOrders(ctx context.Context, session *bbgo.ExchangeSession) error { openOrders, err := queryOpenOrdersUntilSuccessful(ctx, session.Exchange, s.Symbol) if err != nil { return err From c54507e07f3f9c250037c42679ce337faeff4a89 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 6 Apr 2023 17:53:01 +0800 Subject: [PATCH 4/8] modif log message --- pkg/strategy/grid2/recover.go | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 85ed5a91f..659144e81 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -24,26 +24,22 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) if err != nil { - return errors.Wrapf(err, "check if strategy exists error when query open orders") + return errors.Wrapf(err, "unable to query open orders when recovering") } s.logger.Infof("found %d open orders left on the %s order book", len(openOrders), s.Symbol) // filter out the order with the group id belongs to this grid - var openOrdersOnGrid []types.Order - for _, order := range openOrders { - if order.GroupID == s.OrderGroupID { - openOrdersOnGrid = append(openOrdersOnGrid, order) - } - } + openOrdersOnGrid := filterOrdersOnGrid(s.OrderGroupID, openOrders) s.logger.Infof("found %d open orders belong to this grid on the %s order book", len(openOrdersOnGrid), s.Symbol) if s.GridProfitStats.InitialOrderID != 0 { - // existiting strategy, need recover + s.logger.Info("InitialOrderID is already there, need to recover") } else if len(openOrdersOnGrid) != 0 { - // open orders on grid is not 0. this strategy is existing, need recover + s.logger.Info("even though InitialOrderID is 0, there are open orders on grid so need to recover") } else { + s.logger.Info("InitialOrderID is 0 and there is no open orders on grid, query trades to check it") // 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{ @@ -54,16 +50,16 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex }) if err != nil { - return errors.Wrapf(err, "check if strategy exists error when query trades") + return errors.Wrapf(err, "unable to query trades when recovering") } if len(trades) == 0 { - s.logger.Info("new strategy, no need to recover") + s.logger.Info("0 trades found, it's a new strategy so no need to recover") return nil } } - s.logger.Infof("start to recover") + s.logger.Infof("start recovering") if err := s.recoverWithOpenOrdersByScanningTrades(ctx, historyService, openOrdersOnGrid); err != nil { return errors.Wrap(err, "grid recover error") } @@ -303,3 +299,16 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history return pinOrdersFilled, nil } + +func filterOrdersOnGrid(groupID uint32, orders []types.Order) []types.Order { + var filteredOrders []types.Order + for _, order := range orders { + if order.GroupID != groupID { + continue + } + + filteredOrders = append(filteredOrders, order) + } + + return filteredOrders +} From 542467245e91e8115ee6a132270fdbed162e1887 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 6 Apr 2023 18:00:21 +0800 Subject: [PATCH 5/8] remove OrderGroupID checking --- pkg/strategy/grid2/recover.go | 33 ++++----------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 659144e81..b6c6efc6e 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -20,8 +20,6 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex return nil } - s.logger.Infof("recover grid with group id: %d", s.OrderGroupID) - openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol) if err != nil { return errors.Wrapf(err, "unable to query open orders when recovering") @@ -29,17 +27,12 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex s.logger.Infof("found %d open orders left on the %s order book", len(openOrders), s.Symbol) - // filter out the order with the group id belongs to this grid - openOrdersOnGrid := filterOrdersOnGrid(s.OrderGroupID, openOrders) - - s.logger.Infof("found %d open orders belong to this grid on the %s order book", len(openOrdersOnGrid), s.Symbol) - if s.GridProfitStats.InitialOrderID != 0 { s.logger.Info("InitialOrderID is already there, need to recover") - } else if len(openOrdersOnGrid) != 0 { - s.logger.Info("even though InitialOrderID is 0, there are open orders on grid so need to recover") + } else if len(openOrders) != 0 { + s.logger.Info("even though InitialOrderID is 0, there are open orders so need to recover") } else { - s.logger.Info("InitialOrderID is 0 and there is no open orders on grid, query trades to check it") + s.logger.Info("InitialOrderID is 0 and there is no open orders, query trades to check it") // 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{ @@ -60,7 +53,7 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex } s.logger.Infof("start recovering") - if err := s.recoverWithOpenOrdersByScanningTrades(ctx, historyService, openOrdersOnGrid); err != nil { + if err := s.recoverWithOpenOrdersByScanningTrades(ctx, historyService, openOrders); err != nil { return errors.Wrap(err, "grid recover error") } @@ -261,11 +254,6 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history // add 1 to avoid duplicate fromTradeID = trade.ID + 1 - // this trade doesn't belong to this grid - if order.GroupID != s.OrderGroupID { - continue - } - // checked the trade's order is filled order pin := order.Price v, exist := pinOrdersOpen[pin] @@ -299,16 +287,3 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history return pinOrdersFilled, nil } - -func filterOrdersOnGrid(groupID uint32, orders []types.Order) []types.Order { - var filteredOrders []types.Order - for _, order := range orders { - if order.GroupID != groupID { - continue - } - - filteredOrders = append(filteredOrders, order) - } - - return filteredOrders -} From fba73f11ea33bc722abbc6dc96d719b8d69a2103 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 6 Apr 2023 23:24:08 +0800 Subject: [PATCH 6/8] grid2: update metrics and trigger ready callback --- pkg/strategy/grid2/recover.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index b6c6efc6e..1f8f8a791 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -6,10 +6,11 @@ import ( "strconv" "time" + "github.com/pkg/errors" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" - "github.com/pkg/errors" ) func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.ExchangeSession) error { @@ -77,6 +78,9 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi s.debugLog("open orders nums: %d, expected nums: %d", openOrdersOnGridNums, expectedOrderNums) if expectedOrderNums == openOrdersOnGridNums { // no need to recover + s.EmitGridReady() + s.updateGridNumOfOrdersMetricsWithLock() + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil } else if expectedOrderNums < openOrdersOnGridNums { return fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") From cc5ebd5b2cd8d654b24fece4023fcced201b5034 Mon Sep 17 00:00:00 2001 From: chiahung Date: Thu, 6 Apr 2023 23:57:54 +0800 Subject: [PATCH 7/8] move emit ready and update metrics --- pkg/strategy/grid2/recover.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index 1f8f8a791..aaecea880 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -58,6 +58,16 @@ func (s *Strategy) recoverByScanningTrades(ctx context.Context, session *bbgo.Ex return errors.Wrap(err, "grid recover error") } + // emit ready after recover + s.EmitGridReady() + + // 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()) + s.updateGridNumOfOrdersMetricsWithLock() + s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) + return nil } @@ -122,16 +132,6 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi activeOrderBook.EmitFilled(filledOrder) } - // 6. emit grid ready - s.EmitGridReady() - - // 7. debug and send metrics - // wait for the reverse order to be placed - time.Sleep(2 * time.Second) - debugGrid(s.logger, grid, s.orderExecutor.ActiveMakerOrders()) - s.updateGridNumOfOrdersMetricsWithLock() - s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) - return nil } From 6029bd268d4b1630dce09d959ff7de8cd8d732e4 Mon Sep 17 00:00:00 2001 From: chiahung Date: Fri, 7 Apr 2023 00:40:32 +0800 Subject: [PATCH 8/8] update log message --- pkg/strategy/grid2/recover.go | 66 ++++++++++++++++++---------------- pkg/strategy/grid2/strategy.go | 10 +++--- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index aaecea880..33f9d9389 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -83,16 +83,16 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi // add open orders to active order book s.addOrdersToActiveOrderBook(openOrdersOnGrid) - expectedOrderNums := s.GridNum - 1 - openOrdersOnGridNums := int64(len(openOrdersOnGrid)) - s.debugLog("open orders nums: %d, expected nums: %d", openOrdersOnGridNums, expectedOrderNums) - if expectedOrderNums == openOrdersOnGridNums { + expectedNumOfOrders := s.GridNum - 1 + numGridOpenOrders := int64(len(openOrdersOnGrid)) + s.debugLog("open orders nums: %d, expected nums: %d", numGridOpenOrders, expectedNumOfOrders) + if expectedNumOfOrders == numGridOpenOrders { // no need to recover s.EmitGridReady() s.updateGridNumOfOrdersMetricsWithLock() s.updateOpenOrderPricesMetrics(s.orderExecutor.ActiveMakerOrders().Orders()) return nil - } else if expectedOrderNums < openOrdersOnGridNums { + } else if expectedNumOfOrders < numGridOpenOrders { return fmt.Errorf("amount of grid's open orders should not > amount of expected grid's orders") } @@ -111,9 +111,10 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi // 3. get the filled orders from pin-order map filledOrders := pinOrdersFilled.AscendingOrders() numFilledOrders := len(filledOrders) - if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums) { + if numFilledOrders == int(expectedNumOfOrders-numGridOpenOrders) { // nums of filled order is the same as Size - 1 - num(open orders) - } else if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums+1) { + s.logger.Infof("nums of filled order is the same as Size - 1 - len(open orders) : %d = %d - 1 - %d", numFilledOrders, s.grid.Size, numGridOpenOrders) + } else if numFilledOrders == int(expectedNumOfOrders-numGridOpenOrders+1) { filledOrders = filledOrders[1:] } else { return fmt.Errorf("not reasonable num of filled orders") @@ -136,24 +137,17 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi } func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrders []types.Order) error { - s.debugLog("pins: %+v", pins) - s.debugLog("open pin orders:\n%s", pinOrders.String()) - s.debugOrders("filled orders", filledOrders) + 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) - for _, filledOrder := range filledOrders { - price := filledOrder.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] = filledOrder - } + if err := addOrdersIntoPinOrderMap(pinOrders, filledOrders); err != nil { + return errors.Wrapf(err, "verifying filled grid error when add orders into pin order map") } - s.debugLog("filled pin orders:\n%+v", pinOrders.String()) + s.debugLog("verifying filled grid - filled pin orders:\n%+v", pinOrders.String()) - side := types.SideTypeBuy + expectedSide := types.SideTypeBuy for _, pin := range pins { order, exist := pinOrders[fixedpoint.Value(pin)] if !exist { @@ -164,20 +158,20 @@ func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrd // 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 side == types.SideTypeBuy { - side = types.SideTypeSell + if expectedSide == types.SideTypeBuy { + expectedSide = types.SideTypeSell continue } - return fmt.Errorf("not only one empty order in this grid") + return fmt.Errorf("found more than one empty pins") } - if order.Side != side { - return fmt.Errorf("the side is wrong") + if order.Side != expectedSide { + return fmt.Errorf("the side of order (%s) is wrong, expected: %s", order.Side, expectedSide) } } - if side != types.SideTypeSell { + if expectedSide != types.SideTypeSell { return fmt.Errorf("there is no empty pin in the grid") } @@ -278,9 +272,6 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history continue } pinOrdersFilled[pin] = *order - - // wait 100 ms to avoid rate limit - time.Sleep(100 * time.Millisecond) } // stop condition @@ -291,3 +282,18 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history 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 +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 89ab8aaa0..45fca871b 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -1216,7 +1216,7 @@ func (s *Strategy) debugOrders(desc string, orders []types.Order) { s.logger.Infof(sb.String()) } -func (s *Strategy) debugGridProfitStats(where string) { +func (s *Strategy) debugGridProfitStats(trigger string) { if !s.Debug { return } @@ -1226,11 +1226,11 @@ func (s *Strategy) debugGridProfitStats(where string) { stats.ProfitEntries = nil b, err := json.Marshal(stats) if err != nil { - s.logger.WithError(err).Errorf("[%s] failed to debug grid profit stats", where) + s.logger.WithError(err).Errorf("[%s] failed to debug grid profit stats", trigger) return } - s.logger.Infof("[%s] grid profit stats: %s", where, string(b)) + s.logger.Infof("trigger %s => grid profit stats : %s", trigger, string(b)) } func (s *Strategy) debugLog(format string, args ...interface{}) { @@ -1941,11 +1941,11 @@ func (s *Strategy) startProcess(ctx context.Context, session *bbgo.ExchangeSessi func (s *Strategy) recoverGrid(ctx context.Context, session *bbgo.ExchangeSession) error { if s.RecoverGridByScanningTrades { - s.debugLog("recover grid by scanning trades") + s.debugLog("recovering grid by scanning trades") return s.recoverByScanningTrades(ctx, session) } - s.debugLog("recover grid by scanning orders") + s.debugLog("recovering grid by scanning orders") return s.recoverByScanningOrders(ctx, session) }