FEATURE: use TwinOrder to recover

This commit is contained in:
chiahung 2023-04-27 21:06:29 +08:00
parent 5996b32ee1
commit 93d35cc423
2 changed files with 290 additions and 16 deletions

View File

@ -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
}

View File

@ -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()
}