From 3b8a3bed5f62666fdec4ca828fefcfe8bbb8a8ad Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 23 Feb 2024 16:56:30 +0800 Subject: [PATCH 1/2] add universal cancel all orders api helper --- pkg/bbgo/graceful_shutdown.go | 3 + pkg/exchange/max/convert.go | 1 - pkg/strategy/dca2/strategy.go | 27 ++++---- pkg/util/tradingutil/cancel.go | 116 +++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 pkg/util/tradingutil/cancel.go diff --git a/pkg/bbgo/graceful_shutdown.go b/pkg/bbgo/graceful_shutdown.go index a3f61b49f..f82b390f6 100644 --- a/pkg/bbgo/graceful_shutdown.go +++ b/pkg/bbgo/graceful_shutdown.go @@ -23,6 +23,9 @@ func (g *GracefulShutdown) Shutdown(shutdownCtx context.Context) { wg.Wait() } +// OnShutdown helps you register your shutdown handler +// the first context object is where you want to register your shutdown handler, where the context has the isolated storage. +// in your handler, you will get another context for the timeout context. func OnShutdown(ctx context.Context, f ShutdownHandler) { isolatedContext := GetIsolationFromContext(ctx) isolatedContext.gracefulShutdown.OnShutdown(f) diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index 56df7959b..2bb61c970 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -172,7 +172,6 @@ func toGlobalOrder(maxOrder max.Order) (*types.Order, error) { executedVolume := maxOrder.ExecutedVolume remainingVolume := maxOrder.RemainingVolume isMargin := maxOrder.WalletType == max.WalletTypeMargin - return &types.Order{ SubmitOrder: types.SubmitOrder{ ClientOrderID: maxOrder.ClientOID, diff --git a/pkg/strategy/dca2/strategy.go b/pkg/strategy/dca2/strategy.go index b8e7fee05..cf673fa1f 100644 --- a/pkg/strategy/dca2/strategy.go +++ b/pkg/strategy/dca2/strategy.go @@ -8,15 +8,17 @@ import ( "sync" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/sirupsen/logrus" + "go.uber.org/multierr" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/exchange/retry" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/strategy/common" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" - "github.com/prometheus/client_golang/prometheus" - "github.com/sirupsen/logrus" - "go.uber.org/multierr" + "github.com/c9s/bbgo/pkg/util/tradingutil" ) const ID = "dca2" @@ -289,30 +291,29 @@ func (s *Strategy) CleanUp(ctx context.Context) error { return fmt.Errorf("Session is nil, please check it") } - service, support := session.Exchange.(advancedOrderCancelApi) - if !support { - return fmt.Errorf("advancedOrderCancelApi interface is not implemented, fallback to default graceful cancel, exchange %T", session) + // ignore the first cancel error, this skips one open-orders query request + if err := tradingutil.UniversalCancelAllOrders(ctx, session.Exchange, nil); err == nil { + return nil } + // if cancel all orders returns error, get the open orders and retry the cancel in each round var werr error for { s.logger.Infof("checking %s open orders...", s.Symbol) openOrders, err := retry.QueryOpenOrdersUntilSuccessful(ctx, session.Exchange, s.Symbol) if err != nil { - s.logger.WithError(err).Errorf("CancelOrdersByGroupID api call error") - werr = multierr.Append(werr, err) + s.logger.WithError(err).Errorf("unable to query open orders") + continue } + // all clean up if len(openOrders) == 0 { break } - s.logger.Infof("found %d open orders left, using cancel all orders api", len(openOrders)) - - s.logger.Infof("using cancal all orders api for canceling grid orders...") - if err := retry.CancelAllOrdersUntilSuccessful(ctx, service); err != nil { - s.logger.WithError(err).Errorf("CancelAllOrders api call error") + if err := tradingutil.UniversalCancelAllOrders(ctx, session.Exchange, openOrders); err != nil { + s.logger.WithError(err).Errorf("unable to cancel all orders") werr = multierr.Append(werr, err) } diff --git a/pkg/util/tradingutil/cancel.go b/pkg/util/tradingutil/cancel.go new file mode 100644 index 000000000..0e5e6c2b9 --- /dev/null +++ b/pkg/util/tradingutil/cancel.go @@ -0,0 +1,116 @@ +package tradingutil + +import ( + "context" + "errors" + "fmt" + + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/exchange/retry" + "github.com/c9s/bbgo/pkg/types" +) + +type CancelAllOrdersService interface { + CancelAllOrders(ctx context.Context) ([]types.Order, error) +} + +type CancelAllOrdersBySymbolService interface { + CancelOrdersBySymbol(ctx context.Context, symbol string) ([]types.Order, error) +} + +type CancelAllOrdersByGroupIDService interface { + CancelOrdersByGroupID(ctx context.Context, groupID uint32) ([]types.Order, error) +} + +// UniversalCancelAllOrders checks if the exchange instance supports the best order cancel strategy +// it tries the first interface CancelAllOrdersService that does not need any existing order information or symbol information. +// +// if CancelAllOrdersService is not supported, then it tries CancelAllOrdersBySymbolService which needs at least one symbol +// for the cancel api request. +func UniversalCancelAllOrders(ctx context.Context, exchange types.Exchange, openOrders []types.Order) error { + if service, ok := exchange.(CancelAllOrdersService); ok { + if _, err := service.CancelAllOrders(ctx); err == nil { + return nil + } else { + log.WithError(err).Errorf("unable to cancel all orders") + } + } + + if len(openOrders) == 0 { + return errors.New("to cancel all orders, openOrders can not be empty") + } + + if service, ok := exchange.(CancelAllOrdersBySymbolService); ok { + var symbols = CollectOrderSymbols(openOrders) + var anyErr error + for _, symbol := range symbols { + _, err := service.CancelOrdersBySymbol(ctx, symbol) + if err != nil { + anyErr = err + } + } + + if anyErr == nil { + return nil + } + } + + if service, ok := exchange.(CancelAllOrdersByGroupIDService); ok { + var groupIds = CollectOrderGroupIds(openOrders) + var anyErr error + for _, groupId := range groupIds { + if _, err := service.CancelOrdersByGroupID(ctx, groupId); err != nil { + anyErr = err + } + } + + if anyErr == nil { + return nil + } + } + + return fmt.Errorf("unable to cancel all orders: %+v", openOrders) +} + +func CollectOrderGroupIds(orders []types.Order) (groupIds []uint32) { + groupIdMap := map[uint32]struct{}{} + for _, o := range orders { + if o.GroupID > 0 { + groupIdMap[o.GroupID] = struct{}{} + } + } + + for id := range groupIdMap { + groupIds = append(groupIds, id) + } + + return groupIds +} + +func CollectOrderSymbols(orders []types.Order) (symbols []string) { + symbolMap := map[string]struct{}{} + for _, o := range orders { + symbolMap[o.Symbol] = struct{}{} + } + + for s := range symbolMap { + symbols = append(symbols, s) + } + + return symbols +} + +func CollectOpenOrders(ctx context.Context, ex types.Exchange, symbols ...string) ([]types.Order, error) { + var collectedOrders []types.Order + for _, symbol := range symbols { + openOrders, err := retry.QueryOpenOrdersUntilSuccessful(ctx, ex, symbol) + if err != nil { + return nil, err + } + + collectedOrders = append(collectedOrders, openOrders...) + } + + return collectedOrders, nil +} From 0b0bc7e1792d6f76026d24fff07f553d1d12934b Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 23 Feb 2024 18:33:30 +0800 Subject: [PATCH 2/2] tradingutil: return anyErr if anyErr is not nil --- pkg/util/tradingutil/cancel.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/util/tradingutil/cancel.go b/pkg/util/tradingutil/cancel.go index 0e5e6c2b9..437fefa9e 100644 --- a/pkg/util/tradingutil/cancel.go +++ b/pkg/util/tradingutil/cancel.go @@ -41,9 +41,9 @@ func UniversalCancelAllOrders(ctx context.Context, exchange types.Exchange, open return errors.New("to cancel all orders, openOrders can not be empty") } + var anyErr error if service, ok := exchange.(CancelAllOrdersBySymbolService); ok { var symbols = CollectOrderSymbols(openOrders) - var anyErr error for _, symbol := range symbols { _, err := service.CancelOrdersBySymbol(ctx, symbol) if err != nil { @@ -58,7 +58,6 @@ func UniversalCancelAllOrders(ctx context.Context, exchange types.Exchange, open if service, ok := exchange.(CancelAllOrdersByGroupIDService); ok { var groupIds = CollectOrderGroupIds(openOrders) - var anyErr error for _, groupId := range groupIds { if _, err := service.CancelOrdersByGroupID(ctx, groupId); err != nil { anyErr = err @@ -70,7 +69,11 @@ func UniversalCancelAllOrders(ctx context.Context, exchange types.Exchange, open } } - return fmt.Errorf("unable to cancel all orders: %+v", openOrders) + if anyErr != nil { + return anyErr + } + + return fmt.Errorf("unable to cancel all orders, openOrders:%+v", openOrders) } func CollectOrderGroupIds(orders []types.Order) (groupIds []uint32) {