Skip to content

Commit

Permalink
mm: Cancel orders if unable to get basis price
Browse files Browse the repository at this point in the history
  • Loading branch information
martonp committed Jul 24, 2024
1 parent c5b2e8b commit 30e02f6
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 80 deletions.
117 changes: 59 additions & 58 deletions client/mm/exchange_adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2195,108 +2195,109 @@ 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, 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
}
orderLatestState, err := u.clientCore.Order(o.ID)
if err != nil {
u.log.Errorf("Error fetching order %s: %v", o.ID, err)
continue
}
if orderLatestState.Status > order.OrderStatusBooked {
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)
}
}

// Use this in case the order book is not available.
orderAlwaysBooked := func(_ order.OrderID, _ bool) bool {
return true
}
return done
}

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, 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, true)
return
}

currentEpoch := book.CurrentEpoch()
if doCancels(&currentEpoch, book.OrderIsBooked) {
if u.tryCancelOrders(ctx, &currentEpoch, true) {
return
}

i := 0

timeout := time.Millisecond * time.Duration(3*mktCfg.EpochLen)
timer := time.NewTimer(timeout)
defer timer.Stop()

i := 0
for {
select {
case n := <-bookFeed.Next():
if n.Action == core.EpochMatchSummary {
payload := n.Payload.(*core.EpochMatchSummaryPayload)
currentEpoch := payload.Epoch + 1
if doCancels(&currentEpoch, book.OrderIsBooked) {
case ni := <-bookFeed.Next():
switch epoch := ni.Payload.(type) {
case *core.ResolvedEpoch:
if u.tryCancelOrders(ctx, &epoch.Current, true) {
return
}
timer.Reset(timeout)
i++
}
case <-timer.C:
doCancels(nil, orderAlwaysBooked)
u.tryCancelOrders(ctx, nil, true)
return
}

Expand Down
47 changes: 26 additions & 21 deletions client/mm/mm_basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,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 {
Expand All @@ -325,7 +346,7 @@ 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) {
Expand All @@ -335,30 +356,14 @@ func (m *basicMarketMaker) rebalance(newEpoch uint64) {
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, 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)
}
Expand Down
3 changes: 2 additions & 1 deletion client/mm/mm_basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"decred.org/dcrdex/client/core"
"decred.org/dcrdex/client/orderbook"
"decred.org/dcrdex/dex/calc"
)

Expand Down Expand Up @@ -359,7 +360,7 @@ func TestBasicMMRebalance(t *testing.T) {
BuyPlacements: tt.cfgBuyPlacements,
SellPlacements: tt.cfgSellPlacements,
})
mm.rebalance(100)
mm.rebalance(100, &orderbook.OrderBook{})

Check failure on line 363 in client/mm/mm_basic_test.go

View workflow job for this annotation

GitHub Actions / Go CI (1.20)

too many arguments in call to mm.rebalance

Check failure on line 363 in client/mm/mm_basic_test.go

View workflow job for this annotation

GitHub Actions / Go CI (1.21)

too many arguments in call to mm.rebalance

if len(tcore.multiTradesPlaced) != 2 {
t.Fatal("expected both buy and sell orders placed")
Expand Down

0 comments on commit 30e02f6

Please sign in to comment.