From 4d7cf3f2d59419f882942db3e721bc8001235e1d Mon Sep 17 00:00:00 2001 From: martonp Date: Tue, 23 Jul 2024 20:28:14 +0200 Subject: [PATCH] mm: Cancel orders if unable to get basis price --- client/mm/exchange_adaptor.go | 104 ++++++++++++++++++---------------- client/mm/mm_basic.go | 54 ++++++++++-------- client/mm/mm_basic_test.go | 3 +- 3 files changed, 88 insertions(+), 73 deletions(-) diff --git a/client/mm/exchange_adaptor.go b/client/mm/exchange_adaptor.go index ad8bc00b75..065522fd20 100644 --- a/client/mm/exchange_adaptor.go +++ b/client/mm/exchange_adaptor.go @@ -2195,64 +2195,72 @@ func (u *unifiedExchangeAdaptor) OrderFeesInUnits(sell, base bool, rate uint64) return baseFeesInUnits + quoteFeesInUnits, nil } -func (u *unifiedExchangeAdaptor) cancelAllOrders(ctx context.Context) { - doCancels := func(epoch *uint64, dexOrderBooked func(oid order.OrderID, sell bool) bool) bool { - u.balancesMtx.RLock() - defer u.balancesMtx.RUnlock() +// tryCancelOrders cancels all booked DEX orders that are past the free cancel +// threshold. If cancelCEXOrders is true, it will also cancel CEX orders. True +// is returned if all orders have been cancelled. If cancelCEXOrders is false, +// false will always be returned. +func (u *unifiedExchangeAdaptor) tryCancelOrders(ctx context.Context, epoch *uint64, dexOrderBooked func(oid order.OrderID, sell bool) bool, cancelCEXOrders bool) bool { + u.balancesMtx.RLock() + defer u.balancesMtx.RUnlock() - done := true + done := true - freeCancel := func(orderEpoch uint64) bool { - if epoch == nil { - return true - } - return *epoch-orderEpoch >= 2 + freeCancel := func(orderEpoch uint64) bool { + if epoch == nil { + return true } + return *epoch-orderEpoch >= 2 + } - for _, pendingOrder := range u.pendingDEXOrders { - o := pendingOrder.currentState().order + for _, pendingOrder := range u.pendingDEXOrders { + o := pendingOrder.currentState().order - var oid order.OrderID - copy(oid[:], o.ID) - // We need to look in the order book to see if the cancel succeeded - // because the epoch summary note comes before the order statuses - // are updated. - if !dexOrderBooked(oid, o.Sell) { - continue - } + var oid order.OrderID + copy(oid[:], o.ID) + // We need to look in the order book to see if the cancel succeeded + // because the epoch summary note comes before the order statuses + // are updated. + if !dexOrderBooked(oid, o.Sell) { + continue + } - done = false - if freeCancel(o.Epoch) { - err := u.clientCore.Cancel(o.ID) - if err != nil { - u.log.Errorf("Error canceling order %s: %v", o.ID, err) - } + done = false + if freeCancel(o.Epoch) { + err := u.clientCore.Cancel(o.ID) + if err != nil { + u.log.Errorf("Error canceling order %s: %v", o.ID, err) } } + } - for _, pendingOrder := range u.pendingCEXOrders { - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() + if !cancelCEXOrders { + return false + } - tradeStatus, err := u.CEX.TradeStatus(ctx, pendingOrder.trade.ID, pendingOrder.trade.BaseID, pendingOrder.trade.QuoteID) - if err != nil { - u.log.Errorf("Error getting CEX trade status: %v", err) - continue - } - if tradeStatus.Complete { - continue - } + for _, pendingOrder := range u.pendingCEXOrders { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() - done = false - err = u.CEX.CancelTrade(ctx, u.baseID, u.quoteID, pendingOrder.trade.ID) - if err != nil { - u.log.Errorf("Error canceling CEX trade %s: %v", pendingOrder.trade.ID, err) - } + tradeStatus, err := u.CEX.TradeStatus(ctx, pendingOrder.trade.ID, pendingOrder.trade.BaseID, pendingOrder.trade.QuoteID) + if err != nil { + u.log.Errorf("Error getting CEX trade status: %v", err) + continue + } + if tradeStatus.Complete { + continue } - return done + done = false + err = u.CEX.CancelTrade(ctx, u.baseID, u.quoteID, pendingOrder.trade.ID) + if err != nil { + u.log.Errorf("Error canceling CEX trade %s: %v", pendingOrder.trade.ID, err) + } } + return done +} + +func (u *unifiedExchangeAdaptor) cancelAllOrders(ctx context.Context) { // Use this in case the order book is not available. orderAlwaysBooked := func(_ order.OrderID, _ bool) bool { return true @@ -2261,19 +2269,19 @@ func (u *unifiedExchangeAdaptor) cancelAllOrders(ctx context.Context) { book, bookFeed, err := u.clientCore.SyncBook(u.host, u.baseID, u.quoteID) if err != nil { u.log.Errorf("Error syncing book for cancellations: %v", err) - doCancels(nil, orderAlwaysBooked) + u.tryCancelOrders(ctx, nil, orderAlwaysBooked, true) return } mktCfg, err := u.clientCore.ExchangeMarket(u.host, u.baseID, u.quoteID) if err != nil { u.log.Errorf("Error getting market configuration: %v", err) - doCancels(nil, orderAlwaysBooked) + u.tryCancelOrders(ctx, nil, orderAlwaysBooked, true) return } currentEpoch := book.CurrentEpoch() - if doCancels(¤tEpoch, book.OrderIsBooked) { + if u.tryCancelOrders(ctx, ¤tEpoch, orderAlwaysBooked, true) { return } @@ -2289,14 +2297,14 @@ func (u *unifiedExchangeAdaptor) cancelAllOrders(ctx context.Context) { if n.Action == core.EpochMatchSummary { payload := n.Payload.(*core.EpochMatchSummaryPayload) currentEpoch := payload.Epoch + 1 - if doCancels(¤tEpoch, book.OrderIsBooked) { + if u.tryCancelOrders(ctx, ¤tEpoch, book.OrderIsBooked, true) { return } timer.Reset(timeout) i++ } case <-timer.C: - doCancels(nil, orderAlwaysBooked) + u.tryCancelOrders(ctx, nil, orderAlwaysBooked, true) return } diff --git a/client/mm/mm_basic.go b/client/mm/mm_basic.go index ddf3a04ada..dfd4d8db34 100644 --- a/client/mm/mm_basic.go +++ b/client/mm/mm_basic.go @@ -12,6 +12,7 @@ import ( "sync/atomic" "decred.org/dcrdex/client/core" + "decred.org/dcrdex/client/orderbook" "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/calc" ) @@ -300,7 +301,28 @@ func (m *basicMarketMaker) orderPrice(basisPrice, feeAdj uint64, sell bool, gapF return basisPrice - adj } -func (m *basicMarketMaker) ordersToPlace(basisPrice, feeAdj uint64) (buyOrders, sellOrders []*multiTradePlacement) { +func (m *basicMarketMaker) ordersToPlace() (buyOrders, sellOrders []*multiTradePlacement, err error) { + basisPrice := m.calculator.basisPrice() + if basisPrice == 0 { + return nil, nil, fmt.Errorf("no basis price available") + } + + feeGap, err := m.calculator.feeGapStats(basisPrice) + if err != nil { + return nil, nil, fmt.Errorf("error calculating fee gap stats: %w", err) + } + + m.registerFeeGap(feeGap) + var feeAdj uint64 + if needBreakEvenHalfSpread(m.cfg().GapStrategy) { + feeAdj = feeGap.FeeGap / 2 + } + + if m.log.Level() == dex.LevelTrace { + m.log.Tracef("ordersToPlace %s, basis price = %s, break-even fee adjustment = %s", + m.name, m.fmtRate(basisPrice), m.fmtRate(feeAdj)) + } + orders := func(orderPlacements []*OrderPlacement, sell bool) []*multiTradePlacement { placements := make([]*multiTradePlacement, 0, len(orderPlacements)) for i, p := range orderPlacements { @@ -325,46 +347,30 @@ func (m *basicMarketMaker) ordersToPlace(basisPrice, feeAdj uint64) (buyOrders, buyOrders = orders(m.cfg().BuyPlacements, false) sellOrders = orders(m.cfg().SellPlacements, true) - return buyOrders, sellOrders + return buyOrders, sellOrders, nil } -func (m *basicMarketMaker) rebalance(newEpoch uint64) { +func (m *basicMarketMaker) rebalance(newEpoch uint64, book *orderbook.OrderBook) { if !m.rebalanceRunning.CompareAndSwap(false, true) { return } defer m.rebalanceRunning.Store(false) m.log.Tracef("rebalance: epoch %d", newEpoch) - basisPrice := m.calculator.basisPrice() - if basisPrice == 0 { - m.log.Errorf("No basis price available") - return - } - feeGap, err := m.calculator.feeGapStats(basisPrice) + buyOrders, sellOrders, err := m.ordersToPlace() if err != nil { - m.log.Errorf("Could not calculate fee-gap stats: %v", err) + m.log.Errorf("error calculating orders to place: %v. cancelling all orders", err) + m.tryCancelOrders(m.ctx, &newEpoch, book.OrderIsBooked, false) return } - m.registerFeeGap(feeGap) - var feeAdj uint64 - if needBreakEvenHalfSpread(m.cfg().GapStrategy) { - feeAdj = feeGap.FeeGap / 2 - } - - if m.log.Level() == dex.LevelTrace { - m.log.Tracef("ordersToPlace %s, basis price = %s, break-even fee adjustment = %s", - m.name, m.fmtRate(basisPrice), m.fmtRate(feeAdj)) - } - - buyOrders, sellOrders := m.ordersToPlace(basisPrice, feeAdj) m.multiTrade(buyOrders, false, m.cfg().DriftTolerance, newEpoch) m.multiTrade(sellOrders, true, m.cfg().DriftTolerance, newEpoch) } func (m *basicMarketMaker) botLoop(ctx context.Context) (*sync.WaitGroup, error) { - _, bookFeed, err := m.core.SyncBook(m.host, m.baseID, m.quoteID) + book, bookFeed, err := m.core.SyncBook(m.host, m.baseID, m.quoteID) if err != nil { return nil, fmt.Errorf("failed to sync book: %v", err) } @@ -387,7 +393,7 @@ func (m *basicMarketMaker) botLoop(ctx context.Context) (*sync.WaitGroup, error) case ni := <-bookFeed.Next(): switch epoch := ni.Payload.(type) { case *core.ResolvedEpoch: - m.rebalance(epoch.Current) + m.rebalance(epoch.Current, book) } case <-ctx.Done(): return diff --git a/client/mm/mm_basic_test.go b/client/mm/mm_basic_test.go index 1e7f9ceba1..0dae626dcc 100644 --- a/client/mm/mm_basic_test.go +++ b/client/mm/mm_basic_test.go @@ -7,6 +7,7 @@ import ( "testing" "decred.org/dcrdex/client/core" + "decred.org/dcrdex/client/orderbook" "decred.org/dcrdex/dex/calc" ) @@ -359,7 +360,7 @@ func TestBasicMMRebalance(t *testing.T) { BuyPlacements: tt.cfgBuyPlacements, SellPlacements: tt.cfgSellPlacements, }) - mm.rebalance(100) + mm.rebalance(100, &orderbook.OrderBook{}) if len(tcore.multiTradesPlaced) != 2 { t.Fatal("expected both buy and sell orders placed")