diff --git a/pkg/strategy/grid2/recover.go b/pkg/strategy/grid2/recover.go index fec69a2ef..67f7111d4 100644 --- a/pkg/strategy/grid2/recover.go +++ b/pkg/strategy/grid2/recover.go @@ -116,32 +116,23 @@ func (s *Strategy) recoverWithOpenOrdersByScanningTrades(ctx context.Context, hi 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) + // 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") } - // 2. build the filled pin-order map by querying trades - pinOrdersFilled, err := s.buildFilledPinOrderMapFromTrades(ctx, historyService, pinOrdersOpen) + // 2. build the filled twin-order map by querying trades + twinOrdersFilled, err := s.buildFilledTwinOrderMapFromTrades(ctx, historyService, twinOrdersOpen) 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(expectedNumOfOrders-numGridOpenOrders) { - // nums of filled order is the same as Size - 1 - num(open orders) - 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") - } + // 3. get the filled orders from twin-order map + filledOrders := twinOrdersFilled.AscendingOrders() // 4. verify the grid - if err := s.verifyFilledGrid(s.grid.Pins, pinOrdersOpen, filledOrders); err != nil { + if err := s.verifyFilledTwinGrid(s.grid.Pins, twinOrdersOpen, filledOrders); err != nil { return errors.Wrapf(err, "verify grid with error") } @@ -319,3 +310,173 @@ func addOrdersIntoPinOrderMap(pinOrders PinOrderMap, orders []types.Order) error return nil } + +func (s *Strategy) verifyFilledTwinGrid(pins []Pin, twinOrders TwinOrderMap, filledOrders []types.Order) error { + s.debugLog("verifying filled grid - pins: %+v", pins) + s.debugLog("verifying filled grid - open twin orders:\n%s", twinOrders.String()) + s.debugOrders("verifying filled grid - filled orders", filledOrders) + + 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 { + 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") + } + } + + 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 +func (s *Strategy) buildFilledTwinOrderMapFromTrades(ctx context.Context, historyService types.ExchangeTradeHistoryService, twinOrdersOpen TwinOrderMap) (TwinOrderMap, error) { + twinOrdersFilled := make(TwinOrderMap) + + // existedOrders is used to avoid re-query the same orders + existedOrders := twinOrdersOpen.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 + + twinOrderKey, err := findTwinOrderMapKey(s.grid, *order) + if err != nil { + return nil, errors.Wrapf(err, "failed to find grid order map's key when recover") + } + + twinOrderOpen, exist := twinOrdersOpen[twinOrderKey] + if !exist { + return nil, fmt.Errorf("the price of the order with the same GroupID is not in pins") + } + + 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 { + break + } + } + + return twinOrdersFilled, nil +} + +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 +} diff --git a/pkg/strategy/grid2/twin_order.go b/pkg/strategy/grid2/twin_order.go new file mode 100644 index 000000000..12ffbaf95 --- /dev/null +++ b/pkg/strategy/grid2/twin_order.go @@ -0,0 +1,113 @@ +package grid2 + +import ( + "fmt" + "sort" + "strings" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// For grid trading, there are twin orders between a grid +// e.g. 100, 200, 300, 400, 500 +// BUY 100 and SELL 200 are a twin. +// BUY 200 and SELL 300 are a twin. +// Because they can't be placed on orderbook at the same time + +type TwinOrder struct { + BuyOrder types.Order + SellOrder types.Order +} + +func (t *TwinOrder) IsValid() bool { + // XOR + return (t.BuyOrder.OrderID == 0) != (t.SellOrder.OrderID == 0) +} + +func (t *TwinOrder) Exist() bool { + return t.BuyOrder.OrderID != 0 || t.SellOrder.OrderID != 0 +} + +func (t *TwinOrder) GetOrder() types.Order { + if t.BuyOrder.OrderID != 0 { + return t.BuyOrder + } + + return t.SellOrder +} + +func (t *TwinOrder) SetOrder(order types.Order) { + if order.Side == types.SideTypeBuy { + t.BuyOrder = order + t.SellOrder = types.Order{} + } else { + t.SellOrder = order + t.BuyOrder = types.Order{} + } +} + +type TwinOrderMap map[fixedpoint.Value]TwinOrder + +func findTwinOrderMapKey(grid *Grid, order types.Order) (fixedpoint.Value, error) { + if order.Side == types.SideTypeSell { + return order.Price, nil + } + + if order.Side == types.SideTypeBuy { + pin, ok := grid.NextHigherPin(order.Price) + if !ok { + return fixedpoint.Zero, fmt.Errorf("there is no next higher price for this order (%d, price: %s)", order.OrderID, order.Price) + } + + return fixedpoint.Value(pin), nil + } + + return fixedpoint.Zero, fmt.Errorf("unsupported side: %s of this order (%d)", order.Side, order.OrderID) +} + +func (m TwinOrderMap) AscendingOrders() []types.Order { + var orders []types.Order + for _, order := range m { + // skip empty order + if !order.Exist() { + continue + } + + orders = append(orders, order.GetOrder()) + } + + types.SortOrdersUpdateTimeAscending(orders) + + return orders +} + +func (m TwinOrderMap) SyncOrderMap() *types.SyncOrderMap { + orderMap := types.NewSyncOrderMap() + for _, twin := range m { + orderMap.Add(twin.GetOrder()) + } + + return orderMap +} + +func (m TwinOrderMap) String() string { + var sb strings.Builder + var pins []fixedpoint.Value + for pin, _ := range m { + pins = append(pins, pin) + } + + sort.Slice(pins, func(i, j int) bool { + return pins[j] < pins[i] + }) + + sb.WriteString("================== TWIN ORDER MAP ==================\n") + for _, pin := range pins { + twin := m[pin] + twinOrder := twin.GetOrder() + sb.WriteString(fmt.Sprintf("-> %8s) %s\n", pin, twinOrder.String())) + } + sb.WriteString("================== END OF PIN ORDER MAP ==================\n") + return sb.String() +}