From b3d58a9e056b1bb7f2f72f8e38e503b55b8f1649 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 25 Sep 2024 14:13:23 +0800 Subject: [PATCH] bbgo: add types.ExchangeOrderQueryService support for checking canceled orders --- pkg/bbgo/activeorderbook.go | 99 +++++++++++++++++++++++++++---------- 1 file changed, 72 insertions(+), 27 deletions(-) diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index adfbf1666..f306d5234 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -3,17 +3,21 @@ package bbgo import ( "context" "encoding/json" + "fmt" + "strconv" "sync" "time" "github.com/pkg/errors" log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/exchange/retry" "github.com/c9s/bbgo/pkg/sigchan" "github.com/c9s/bbgo/pkg/types" ) const DefaultCancelOrderWaitTime = 20 * time.Millisecond +const DefaultOrderCancelTimeout = 5 * time.Second // ActiveOrderBook manages the local active order books. // @@ -35,6 +39,7 @@ type ActiveOrderBook struct { mu sync.Mutex cancelOrderWaitTime time.Duration + cancelOrderTimeout time.Duration } func NewActiveOrderBook(symbol string) *ActiveOrderBook { @@ -44,6 +49,7 @@ func NewActiveOrderBook(symbol string) *ActiveOrderBook { pendingOrderUpdates: types.NewSyncOrderMap(), C: sigchan.New(1), cancelOrderWaitTime: DefaultCancelOrderWaitTime, + cancelOrderTimeout: DefaultOrderCancelTimeout, } } @@ -174,7 +180,7 @@ func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange, hasSymbol := b.Symbol != "" for _, o := range orders { if hasSymbol && o.Symbol != b.Symbol { - return errors.New("[ActiveOrderBook] cancel " + b.Symbol + " orderbook with different symbol: " + o.Symbol) + return fmt.Errorf("[ActiveOrderBook] canceling %s orderbook with different symbol: %s", b.Symbol, o.Symbol) } } } @@ -186,7 +192,6 @@ func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange, log.Debugf("[ActiveOrderBook] gracefully cancelling %s orders...", b.Symbol) waitTime := b.cancelOrderWaitTime - orderCancelTimeout := 5 * time.Second startTime := time.Now() // ensure every order is canceled @@ -204,7 +209,7 @@ func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange, log.Debugf("[ActiveOrderBook] waiting %s for %d %s orders to be cancelled...", waitTime, len(orders), b.Symbol) if cancelAll { - clear, err := b.waitAllClear(ctx, waitTime, orderCancelTimeout) + clear, err := b.waitAllClear(ctx, waitTime, b.cancelOrderTimeout) if err != nil { if !errors.Is(err, context.Canceled) { log.WithError(err).Errorf("order cancel error") @@ -230,36 +235,64 @@ func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange, } // verify the current open orders via the RESTful API - log.Warnf("[ActiveOrderBook] using open orders API to verify the active orders...") + if orderQueryService, ok := ex.(types.ExchangeOrderQueryService); ok { + for idx, o := range orders { + retOrder, err := retry.QueryOrderUntilSuccessful(ctx, orderQueryService, types.OrderQuery{ + Symbol: o.Symbol, + OrderID: strconv.FormatUint(o.OrderID, 10), + }) - var symbolOrdersMap = categorizeOrderBySymbol(orders) - var errOccurred bool - var leftOrders types.OrderSlice - for symbol, symbolOrders := range symbolOrdersMap { - openOrders, err := ex.QueryOpenOrders(ctx, symbol) - if err != nil { - errOccurred = true - log.WithError(err).Errorf("can not query %s open orders", symbol) - break - } + if err != nil { + log.WithError(err).Errorf("unable to update order #%d", o.OrderID) + continue + } else if retOrder != nil { + b.Update(*retOrder) - openOrderMap := types.NewOrderMap(openOrders...) - for _, o := range symbolOrders { - // if it's not on the order book (open orders), - // we should remove it from our local side - if !openOrderMap.Exists(o.OrderID) { - b.Remove(o) - } else { - leftOrders.Add(o) + orders[idx] = *retOrder } } + + if cancelAll { + orders = b.Orders() + } else { + // partial cancel + orders = filterCanceledOrders(orders) + } + + } else { + log.Warnf("[ActiveOrderBook] using open orders API to verify the active orders...") + + var symbolOrdersMap = categorizeOrderBySymbol(orders) + var errOccurred bool + var leftOrders types.OrderSlice + for symbol, symbolOrders := range symbolOrdersMap { + openOrders, err := ex.QueryOpenOrders(ctx, symbol) + if err != nil { + errOccurred = true + log.WithError(err).Errorf("can not query %s open orders", symbol) + break + } + + openOrderMap := types.NewOrderMap(openOrders...) + for _, o := range symbolOrders { + // if it's not on the order book (open orders), + // we should remove it from our local side + if !openOrderMap.Exists(o.OrderID) { + b.Remove(o) + } else { + leftOrders.Add(o) + } + } + } + + // if an error occurs, we cannot update the orders because it will result in an empty order slice. + if !errOccurred { + // update order slice for the next try + orders = leftOrders + } + } - // if an error occurs, we cannot update the orders because it will result in an empty order slice. - if !errOccurred { - // update order slice for the next try - orders = leftOrders - } } log.Debugf("[ActiveOrderBook] all %s orders are cancelled successfully in %s", b.Symbol, time.Since(startTime)) @@ -489,3 +522,15 @@ func categorizeOrderBySymbol(orders types.OrderSlice) map[string]types.OrderSlic return orderMap } + +func filterCanceledOrders(orders types.OrderSlice) (ret types.OrderSlice) { + for _, o := range orders { + if o.Status == types.OrderStatusCanceled { + continue + } + + ret = append(ret, o) + } + + return ret +}