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 23, 2024
1 parent c5b2e8b commit 4d7cf3f
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 73 deletions.
104 changes: 56 additions & 48 deletions client/mm/exchange_adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(&currentEpoch, book.OrderIsBooked) {
if u.tryCancelOrders(ctx, &currentEpoch, orderAlwaysBooked, true) {
return
}

Expand All @@ -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(&currentEpoch, book.OrderIsBooked) {
if u.tryCancelOrders(ctx, &currentEpoch, book.OrderIsBooked, true) {
return
}
timer.Reset(timeout)
i++
}
case <-timer.C:
doCancels(nil, orderAlwaysBooked)
u.tryCancelOrders(ctx, nil, orderAlwaysBooked, true)
return
}

Expand Down
54 changes: 30 additions & 24 deletions client/mm/mm_basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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
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{})

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

0 comments on commit 4d7cf3f

Please sign in to comment.