Merge pull request #1114 from c9s/feature/grids/recover-from-trades

FEATURE: verify the grids before emit filled orders
This commit is contained in:
Yo-An Lin 2023-03-15 22:10:04 +08:00 committed by GitHub
commit 50c626dad9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 288 additions and 18 deletions

View File

@ -1393,10 +1393,12 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context
expectedOrderNums := s.GridNum - 1
openOrdersOnGridNums := int64(len(openOrdersOnGrid))
s.logger.Infof("[DEBUG] open orders nums: %d, expected nums: %d", openOrdersOnGridNums, expectedOrderNums)
s.logger.Debugf("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
@ -1413,28 +1415,34 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context
// 3. get the filled orders from pin-order map
filledOrders := pinOrdersFilled.AscendingOrders()
numsFilledOrders := len(filledOrders)
i := 0
if numsFilledOrders == int(expectedOrderNums-openOrdersOnGridNums) {
numFilledOrders := len(filledOrders)
if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums) {
// nums of filled order is the same as Size - 1 - num(open orders)
} else if numsFilledOrders == int(expectedOrderNums-openOrdersOnGridNums+1) {
i++
} else if numFilledOrders == int(expectedOrderNums-openOrdersOnGridNums+1) {
filledOrders = filledOrders[1:]
} else {
return fmt.Errorf("not reasonable num of filled orders")
}
// 4. emit the 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 ; i < numsFilledOrders; i++ {
filledOrder := filledOrders[i]
s.logger.Infof("[DEBUG] emit filled order: %s (%s)", filledOrder.String(), filledOrder.UpdateTime)
for _, filledOrder := range filledOrders {
activeOrderBook.EmitFilled(filledOrder)
}
// 5. emit grid ready
// 6. emit grid ready
s.EmitGridReady()
// 6. debug and send metrics
// 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())
@ -1442,6 +1450,56 @@ func (s *Strategy) recoverGridWithOpenOrdersByScanningTrades(ctx context.Context
return nil
}
func (s *Strategy) verifyFilledGrid(pins []Pin, pinOrders PinOrderMap, filledOrders []types.Order) error {
s.logger.Debugf("pins: %+v", pins)
s.logger.Debugf("open pin orders: %+v", pinOrders)
s.debugOrders("filled orders", filledOrders)
for _, filledOrder := range filledOrders {
price := s.Market.FormatPrice(filledOrder.Price)
if o, exist := pinOrders[price]; !exist {
return fmt.Errorf("the price (%s) is not in pins", price)
} else if o.OrderID != 0 {
return fmt.Errorf("there is already an order at this price (%s)", price)
} else {
pinOrders[price] = filledOrder
}
}
s.logger.Debugf("filled pin orders: %+v", pinOrders)
side := types.SideTypeBuy
for _, pin := range pins {
price := s.Market.FormatPrice(fixedpoint.Value(pin))
order, exist := pinOrders[price]
if !exist {
return fmt.Errorf("there is no order at price (%s)", price)
}
// 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.
@ -1492,9 +1550,10 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history
return nil, errors.Wrapf(err, "failed to query trades to recover the grid with open orders")
}
s.logger.Infof("[DEBUG] len of trades: %d", len(trades))
s.logger.Debugf("QueryTrades return %d trades", len(trades))
for _, trade := range trades {
s.logger.Debugf(trade.String())
if existedOrders.Exists(trade.OrderID) {
// already queries, skip
continue
@ -1508,8 +1567,7 @@ func (s *Strategy) buildFilledPinOrderMapFromTrades(ctx context.Context, history
return nil, errors.Wrapf(err, "failed to query order by trade")
}
s.logger.Infof("[DEBUG] trade: %s", trade.String())
s.logger.Infof("[DEBUG] (group_id: %d) order: %s", order.GroupID, order.String())
s.logger.Debugf("%s (group_id: %d)", order.String(), order.GroupID)
// avoid query this order again
existedOrders.Add(*order)
@ -2083,9 +2141,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.logger.Debugf("recover grid by scanning trades")
return s.recoverGridByScanningTrades(ctx, session)
}
s.logger.Debugf("recover grid by scanning orders")
return s.recoverGridByScanningOrders(ctx, session)
}
@ -2119,7 +2179,7 @@ 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.Info("[DEBUG] new strategy, no need to recover")
s.logger.Debug("new strategy, no need to recover")
return nil
}
@ -2130,11 +2190,10 @@ func (s *Strategy) recoverGridByScanningTrades(ctx context.Context, session *bbg
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)
s.logger.Debugf("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)
}

View File

@ -1209,3 +1209,214 @@ func Test_getOrdersFromPinOrderMapInAscOrder(t *testing.T) {
assert.Equal(uint64(1), orders[1].OrderID)
assert.Equal(uint64(3), orders[2].OrderID)
}
func Test_verifyFilledGrid(t *testing.T) {
assert := assert.New(t)
s := newTestStrategy()
s.UpperPrice = number(400.0)
s.LowerPrice = number(100.0)
s.GridNum = 4
s.grid = s.newGrid()
t.Run("valid grid with buy/sell orders", func(t *testing.T) {
pinOrderMap := PinOrderMap{
"100.00": types.Order{
OrderID: 1,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeBuy,
},
},
"200.00": types.Order{},
"300.00": types.Order{
OrderID: 3,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeSell,
},
},
"400.00": types.Order{
OrderID: 4,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeSell,
},
},
}
assert.NoError(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil))
})
t.Run("valid grid with only buy orders", func(t *testing.T) {
pinOrderMap := PinOrderMap{
"100.00": types.Order{
OrderID: 1,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeBuy,
},
},
"200.00": types.Order{
OrderID: 2,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeBuy,
},
},
"300.00": types.Order{
OrderID: 3,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeBuy,
},
},
"400.00": types.Order{},
}
assert.NoError(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil))
})
t.Run("valid grid with only sell orders", func(t *testing.T) {
pinOrderMap := PinOrderMap{
"100.00": types.Order{},
"200.00": types.Order{
OrderID: 2,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeSell,
},
},
"300.00": types.Order{
OrderID: 3,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeSell,
},
},
"400.00": types.Order{
OrderID: 4,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeSell,
},
},
}
assert.NoError(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil))
})
t.Run("invalid grid with multiple empty pins", func(t *testing.T) {
pinOrderMap := PinOrderMap{
"100.00": types.Order{
OrderID: 1,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeBuy,
},
},
"200.00": types.Order{},
"300.00": types.Order{},
"400.00": types.Order{
OrderID: 4,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeSell,
},
},
}
assert.Error(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil))
})
t.Run("invalid grid without empty pin", func(t *testing.T) {
pinOrderMap := PinOrderMap{
"100.00": types.Order{
OrderID: 1,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeBuy,
},
},
"200.00": types.Order{
OrderID: 2,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeBuy,
},
},
"300.00": types.Order{
OrderID: 3,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeSell,
},
},
"400.00": types.Order{
OrderID: 4,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeSell,
},
},
}
assert.Error(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil))
})
t.Run("invalid grid with Buy-empty-Sell-Buy order", func(t *testing.T) {
pinOrderMap := PinOrderMap{
"100.00": types.Order{
OrderID: 1,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeBuy,
},
},
"200.00": types.Order{},
"300.00": types.Order{
OrderID: 3,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeSell,
},
},
"400.00": types.Order{
OrderID: 4,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeBuy,
},
},
}
assert.Error(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil))
})
t.Run("invalid grid with Sell-empty order", func(t *testing.T) {
pinOrderMap := PinOrderMap{
"100.00": types.Order{
OrderID: 1,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeSell,
},
},
"200.00": types.Order{
OrderID: 2,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeSell,
},
},
"300.00": types.Order{
OrderID: 3,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeSell,
},
},
"400.00": types.Order{},
}
assert.Error(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil))
})
t.Run("invalid grid with empty-Buy order", func(t *testing.T) {
pinOrderMap := PinOrderMap{
"100.00": types.Order{},
"200.00": types.Order{
OrderID: 2,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeBuy,
},
},
"300.00": types.Order{
OrderID: 3,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeBuy,
},
},
"400.00": types.Order{
OrderID: 4,
SubmitOrder: types.SubmitOrder{
Side: types.SideTypeBuy,
},
},
}
assert.Error(s.verifyFilledGrid(s.grid.Pins, pinOrderMap, nil))
})
}