-
Notifications
You must be signed in to change notification settings - Fork 106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
client/mm: Move shared logic to unifiedExchangeAdaptor #2678
Conversation
8542eeb
to
1ab48c0
Compare
1ab48c0
to
5eadaf5
Compare
dea188d
to
2a107f8
Compare
This diff moves as much shared logic from bot implementations to the unifiedExchangeAdaptor as possible. - All fiat rate and order fee tracking is moved to the unifiedExchangeAdaptor. - The MultiTrade function of the unifiedExchangeAdaptor now takes all of the placements that a bot would make, if they had not placed any trades yet, and if they had unlimited balance. All of the logic that used to be in the basic market maker and mm+arb strategies involving determining which trades need to be cancelled/replaced and how many trades the bot has balance for is now handled in the MultiTrade function. - The rebalancing functionalities in simple arb and arb+mm are now handled by the PrepareRebalance and FreeUpFunds functions. - An OrderFeesInUnits function is added that calculates the total order fees in either the base or quote units. This greatly simplifies the basic market maker’s logic that determines the break even half-spread. The simple arb and arb+mm bots also use this function to take into account order fees, which they did not do previously. - The base/quote wallet settings were repeated in each of the bot strategy settings, and the auto-rebalance config was repeated in both the simple arb and arb+mm. These are moved to the top level config / the bot CEX config, respectively.
2a107f8
to
5f9422e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type botCoreAdaptor interface { | ||
NotificationFeed() *core.NoteFeed | ||
SyncBook(host string, base, quote uint32) (*orderbook.OrderBook, core.BookFeed, error) | ||
SingleLotFees(form *core.SingleLotFeesForm) (uint64, uint64, uint64, error) | ||
Cancel(oidB dex.Bytes) error | ||
MaxBuy(host string, base, quote uint32, rate uint64) (*core.MaxOrderEstimate, error) | ||
MaxSell(host string, base, quote uint32) (*core.MaxOrderEstimate, error) | ||
DEXBalance(assetID uint32) (*botBalance, error) | ||
MultiTrade(pw []byte, form *core.MultiTradeForm) ([]*core.Order, error) | ||
MaxFundingFees(fromAsset uint32, host string, numTrades uint32, fromSettings map[string]string) (uint64, error) | ||
FiatConversionRates() map[uint32]float64 | ||
DEXTrade(rate, qty uint64, sell bool) (*core.Order, error) | ||
ExchangeMarket(host string, base, quote uint32) (*core.Market, error) | ||
MultiTrade(placements []*multiTradePlacement, sell bool, driftTolerance float64, currEpoch uint64, dexReserves, cexReserves map[uint32]uint64) []*order.OrderID | ||
CancelAllOrders() bool | ||
ExchangeRateFromFiatSources() uint64 | ||
OrderFeesInUnits(sell, base bool, rate uint64) (uint64, error) | ||
SubscribeOrderUpdates() (updates <-chan *core.Order) | ||
SufficientBalanceForDEXTrade(rate, qty uint64, sell bool) (bool, error) | ||
} | ||
|
||
// botCexAdaptor is an interface used by bots to access functionality | ||
// related to a CEX. Some of the functions are implemented by libxc.CEX, but | ||
// have some differences, since this interface is meant to be used by only | ||
// one caller. For example, Trade does not take a subscriptionID, and | ||
// SubscribeTradeUpdates does not return one. Deposit is not available | ||
// on libxc.CEX as it involves sending funds from the DEX wallet, but it is | ||
// exposed here. | ||
// botCexAdaptor is an interface used by bots to access CEX related | ||
// functions. Common functionality used by multiple market making | ||
// strategies is implemented here. The functions in this interface | ||
// take assetID parameters, unlike botCoreAdaptor, to support a | ||
// multi-hop strategy. | ||
type botCexAdaptor interface { | ||
CEXBalance(assetID uint32) (*botBalance, error) | ||
CancelTrade(ctx context.Context, baseID, quoteID uint32, tradeID string) error | ||
SubscribeMarket(ctx context.Context, baseID, quoteID uint32) error | ||
SubscribeTradeUpdates() (updates <-chan *libxc.Trade, unsubscribe func()) | ||
Trade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64) (*libxc.Trade, error) | ||
CEXTrade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64) (*libxc.Trade, error) | ||
SufficientBalanceForCEXTrade(baseID, quoteID uint32, sell bool, rate, qty uint64) (bool, error) | ||
VWAP(baseID, quoteID uint32, sell bool, qty uint64) (vwap, extrema uint64, filled bool, err error) | ||
Deposit(ctx context.Context, assetID uint32, amount uint64, onConfirm func()) error | ||
Withdraw(ctx context.Context, assetID uint32, amount uint64, onConfirm func()) error | ||
PrepareRebalance(ctx context.Context, assetID uint32) (rebalance int64, dexReserves, cexReserves uint64) | ||
FreeUpFunds(assetID uint32, cex bool, amt uint64, currEpoch uint64) | ||
Deposit(ctx context.Context, assetID uint32, amount uint64) error | ||
Withdraw(ctx context.Context, assetID uint32, amount uint64) error | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These intermediate interfaces seem superfluous to me. Examining the way that the test structures (tCore
, tBotCoreAdaptor
, tCEX
, and tBotCexAdaptor
) are designed and used seems to confirm my feeling here. So I took a swing at eliminating these intermediate interfaces and just using unifiedExchangeAdaptor
. We already have libxc.CEX
and clientCore
for testing stubs anyway. I think it's an improvement.
buck54321/dcrdex@5f9422e...buck54321:dcrdex:nomiddleinterfaces
I didn't rework all the tests there, but I did do the test structures in mm_test.go and one test (TestBasisPrice
) just as part of the proof-of-concept.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that what I'm asking here is challenging work to deeply reconfigure the tests. My view is that your refactor is so righteous, that we can literally eliminate an entire layer of abstraction. IDE navigation will be so much better. Now seems like the right time to adapt at this level and scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll look into it, but testing the bot logic is so much simpler with these interfaces. Take for example OrderFeesInUnits
, the rebalancing functions, and even MultiTrade
. These have pretty complicated logic. By having the interfaces, it's way easier to directly test the bots. TestBasisPrice
may have been easy to update, but the arb+mm and arb rebalancing tests will need to have so much more additional logic that does not involve directly testing the bots. Why is this better?
client/mm/exchange_adaptor.go
Outdated
market, err := u.ExchangeMarket(u.market.Host, u.market.BaseID, u.market.QuoteID) | ||
if err != nil { | ||
u.log.Errorf("Error getting market: %v", err) | ||
return 0 | ||
} | ||
|
||
return market.ConventionalRateToMsg(baseRate / quoteRate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling ExchangeMarket
just for the one method is a bit of overkill, but this refactor tightens things up a bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes look good, thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You haven't applied the changes here in ExchangeRateFromFiatSources
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
* lot of the base asset plus the total fees in base units. | ||
* | ||
* Solving for g, you get: | ||
* g = q / (f + 2l) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not getting this same result. What am I doing wrong?
(b + g) * l / (b - g) = l + f
(b + g) * l = (l + f) * (b - g)
lb + lg = lb - lg + fb + fg
lg + lg - fg = lb + fb - lb
g * (2l - f) = fb = q
g = q / (2l - f)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevermind. I see where I went wrong. +fg -> -fg
client/mm/mm_arb_market_maker.go
Outdated
func (a *arbMarketMaker) dexPlacementRate(cexRate uint64, sell bool) (uint64, error) { | ||
sellFeesInQuoteUnits, err := a.core.OrderFeesInUnits(true, false, cexRate) | ||
if err != nil { | ||
log.Errorf("error getting quote CEX balance: %v", err) | ||
return | ||
return 0, fmt.Errorf("error getting sell fees in quote units: %w", err) | ||
} | ||
|
||
processSide := func(sell bool) []*rateLots { | ||
var cfgPlacements []*ArbMarketMakingPlacement | ||
var existingOrders map[int][]*groupedOrder | ||
var remainingDEXBalance, remainingCEXBalance, fundingFees uint64 | ||
if sell { | ||
cfgPlacements = cfg.SellPlacements | ||
existingOrders = existingSells | ||
remainingDEXBalance = baseDEXBalance.Available | ||
remainingCEXBalance = quoteCEXBalance.Available | ||
fundingFees = sellFees.funding | ||
} else { | ||
cfgPlacements = cfg.BuyPlacements | ||
existingOrders = existingBuys | ||
remainingDEXBalance = quoteDEXBalance.Available | ||
remainingCEXBalance = baseCEXBalance.Available | ||
fundingFees = buyFees.funding | ||
} | ||
|
||
cexReserves := reserves.get(!sell, true) | ||
if cexReserves > remainingCEXBalance { | ||
log.Debugf("rebalance: not enough CEX balance to cover reserves") | ||
return nil | ||
} | ||
remainingCEXBalance -= cexReserves | ||
buyFeesInQuoteUnits, err := a.core.OrderFeesInUnits(false, false, cexRate) | ||
if err != nil { | ||
return 0, fmt.Errorf("error getting buy fees in quote units: %w", err) | ||
} | ||
|
||
dexReserves := reserves.get(sell, false) | ||
if dexReserves > remainingDEXBalance { | ||
log.Debugf("rebalance: not enough DEX balance to cover reserves") | ||
return nil | ||
} | ||
remainingDEXBalance -= dexReserves | ||
|
||
// Enough balance on the CEX needs to be maintained for counter-trades | ||
// for each existing trade on the DEX. Here, we reduce the available | ||
// balance on the CEX by the amount required for each order on the | ||
// DEX books. | ||
for _, ordersForPlacement := range existingOrders { | ||
for _, o := range ordersForPlacement { | ||
var requiredOnCEX uint64 | ||
if sell { | ||
rate := uint64(float64(o.rate) / (1 + cfg.Profit)) | ||
requiredOnCEX = calc.BaseToQuote(rate, o.lots*mkt.LotSize) | ||
} else { | ||
requiredOnCEX = o.lots * mkt.LotSize | ||
} | ||
if requiredOnCEX <= remainingCEXBalance { | ||
remainingCEXBalance -= requiredOnCEX | ||
} else { | ||
log.Warnf("rebalance: not enough CEX balance to cover existing order. cancelling.") | ||
addCancel(o) | ||
remainingCEXBalance = 0 | ||
} | ||
} | ||
} | ||
if remainingCEXBalance == 0 { | ||
log.Debug("rebalance: not enough CEX balance to place new orders") | ||
return nil | ||
} | ||
return dexPlacementRate(cexRate, sell, a.cfg.Profit, a.mkt, sellFeesInQuoteUnits+buyFeesInQuoteUnits) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we just need to add or subtract the halfSpread
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I had is wrong. It should only be taking into account the fees for either selling or buying, not both. Using the half-spread would be the same as this if initiating and redeeming cost the same amount. It's probably a negligible difference though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only value that really matters is the spread between our best buy and best sell. Using halfSpread
would keep the mid-gap the same as the cex. Treating the sides separately effectively shifts the gap if fees are assymetric.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see your point. My logic was that whenever an arb is done, it should cover the fees that are being incurred at that point. I'll just reuse the halfSpread
code then.
Edit: Acutally, what's wrong with shifting the gap? Shouldn't each arb just cover the fees that are actually being incurred at that point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My logic was that whenever an arb is done, it should cover the fees that are being incurred at that point.
Actually, that kinda sounds right too. Leave it your way.
client/mm/exchange_adaptor_test.go
Outdated
redemption: 4e4, | ||
}, | ||
sellFees: &orderFees{ | ||
swap: 5e4, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noting that when converting from MATIC to WBTC, the converted contribution is 0.
5e4 matic / 1e9 * 0.8 / 42500 * 1e8 = 0.09 Sats < 1 Sat
01a40f3
to
e82cca1
Compare
This diff moves as much shared logic from bot implementations to the
unifiedExchangeAdaptor
as possible.unifiedExchangeAdaptor
.MultiTrade
function of theunifiedExchangeAdaptor
now takes as a parameter all of the placements that a bot would make, if they had not placed any trades yet, and if they had unlimited balance. All of the logic that used to be in the basic market maker and mm+arb strategies involving determining which trades need to be cancelled/replaced an how many trades the bot has balance for is now handled in theMultiTrade
function.PrepareRebalance
andFreeUpFunds
functions.OrderFeesInUnits
function is added that calculates the total order fees in either the base or quote units. This greatly simplifies the basic market maker’s logic that determines the break even half-spread. The simple arb and arb+mm bots also use this function to take into account order fees, which they did not do previously.