-
Notifications
You must be signed in to change notification settings - Fork 100
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,core}: Market maker balance segregation #2332
Conversation
893ec48
to
2130d83
Compare
I've moved the balance tracking to be part of |
87b2e50
to
0eb37ac
Compare
New configurations are added to the market maker config specifying what part of the wallet's balance each market maket will have control over. Using this config, a `balanceHandler` object is created, which tracks each of the orders made by the market makers, decreasing and increasing their balances as needed over the lifecycle of the order. The `balanceHandler` is not called directly by the market makers, instead a `coreWithSegregatedBalance` is created which wraps the core methods so that from the point of view of the bot, they behave as if the entire balance of the wallet is only what has been allocated to that bot by the `balanceHandler`.
This does not change any behavior. It will be used to display on the UI.
client/mm/wrapped_core.go
Outdated
fundingFees, err := c.core.MaxFundingFees(quote, 1, options) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
swapFees, redeemFees, err := c.SingleLotFees(&core.SingleLotFeesForm{ | ||
Host: host, | ||
Base: base, | ||
Quote: quote, | ||
UseMaxFeeRate: true, | ||
}) |
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 market maker's rebalancing uses MaxBuy
, which in this implementation is always based on estimates using a constant number of inputs. Why not get accurate values when the wallets are connected? Do we really need offline estimates to begin with? Where are we presenting order estimates before wallets are connected?
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 actually stopped using MaxBuy
in the updates to the market maker. If we're getting accurate values, then there would need to be some locking to make sure that MaxBuy
-> MultiTrade
calls are atomic, if multiple bots are trading with the same asset. If they are not atomic, then the UTXOs when estimating the order would be different than the UTXOs when making the trade. I think a conservative estimate would be quicker and still safe.
client/core/core.go
Outdated
dc.assetsMtx.Lock() | ||
swapAsset := dc.assets[wallets.fromWallet.AssetID] |
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 assetsMtx
only protects the dc.assets
field, and the stored map[uint32]*dex.Asset
is read-only, so you can just
dc.assetsMtx.RLock()
swapAsset, redeemAsset := dc.assets[wallets.fromWallet.AssetID], dc.assets[wallets.toWallet.AssetID]
dc.assetsMtx.RUnlock()
...
and avoid all the unlocking below.
client/mm/mm.go
Outdated
var balanceMods []*balanceMod | ||
if orderInfo.order.Sell { | ||
balanceMods = []*balanceMod{ | ||
{false, orderInfo.order.BaseID, balTypeFundingOrder, match.Qty}, |
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.
Would be helpful to assign these bools to consts.
const (
balanceModIncrease = true
balanceModDecrease = false
)
client/mm/wrapped_core.go
Outdated
|
||
mm *MarketMaker | ||
botID string | ||
core clientCore |
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.
Looks like you're embedding now. Can drop this field?
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.
No, because sometimes the wrapped_core
needs to call a function with the same name in the actual core. In this case the field needs to be used, otherwise there will be an infinite loop.
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 can call methods on an embedded field by using the type name.
mkt, err := c.clientCore.ExchangeMarket(form.Host, form.Base, form.Quote)
client/mm/mm.go
Outdated
switch mod.typ { | ||
case balTypeAvailable: | ||
if mod.increase { | ||
assetBalance.Available += mod.amount | ||
} else { | ||
if assetBalance.Available < mod.amount { | ||
m.log.Errorf("modifyBotBalance: bot %s has insufficient balance for asset %d. "+ | ||
"balance: %d, amount: %d", botID, mod.assetID, assetBalance.Available, mod.amount) | ||
assetBalance.Available = 0 | ||
return | ||
} | ||
assetBalance.Available -= mod.amount | ||
} | ||
case balTypeFundingOrder: | ||
if mod.increase { | ||
assetBalance.FundingOrder += mod.amount | ||
} else { | ||
if assetBalance.FundingOrder < mod.amount { | ||
m.log.Errorf("modifyBotBalance: bot %s has insufficient funding order for asset %d. "+ | ||
"balance: %d, amount: %d", botID, mod.assetID, assetBalance.FundingOrder, mod.amount) | ||
assetBalance.FundingOrder = 0 | ||
return | ||
} | ||
assetBalance.FundingOrder -= mod.amount | ||
} | ||
case balTypePendingRedeem: | ||
if mod.increase { | ||
assetBalance.PendingRedeem += mod.amount | ||
} else { | ||
if assetBalance.PendingRedeem < mod.amount { | ||
m.log.Errorf("modifyBotBalance: bot %s has insufficient pending redeem for asset %d. "+ | ||
"balance: %d, amount: %d", botID, mod.assetID, assetBalance.PendingRedeem, mod.amount) | ||
assetBalance.PendingRedeem = 0 | ||
return | ||
} | ||
assetBalance.PendingRedeem -= mod.amount | ||
} | ||
case balTypePendingRefund: | ||
if mod.increase { | ||
assetBalance.PendingRefund += mod.amount | ||
} else { | ||
if assetBalance.PendingRefund < mod.amount { | ||
m.log.Errorf("modifyBotBalance: bot %s has insufficient pending refund for asset %d. "+ | ||
"balance: %d, amount: %d", botID, mod.assetID, assetBalance.PendingRefund, mod.amount) | ||
assetBalance.PendingRefund = 0 | ||
return | ||
} | ||
assetBalance.PendingRefund -= mod.amount | ||
} | ||
} |
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.
newFieldValue := func(balanceType string, initialValue uint64) uint64 {
if mod.increase {
return initialValue + mod.amount
} else {
if assetBalance.Available < mod.amount {
m.log.Errorf("modifyBotBalance: bot %s has insufficient %s for asset %d. "+
"balance: %d, amount: %d", botID, balanceType, mod.assetID, initialValue, mod.amount)
return 0
}
return initialValue - mod.amount
}
}
switch mod.typ {
case balTypeAvailable:
assetBalance.Available = newFieldValue("available balance", assetBalance.Available)
case balTypeFundingOrder:
assetBalance.FundingOrder = newFieldValue("funding order", assetBalance.FundingOrder)
case balTypePendingRedeem:
assetBalance.PendingRedeem = newFieldValue("pending redeem", assetBalance.PendingRedeem)
case balTypePendingRefund:
assetBalance.PendingRefund = newFieldValue("pending refund", assetBalance.PendingRefund)
}
client/mm/wrapped_core.go
Outdated
} | ||
|
||
var totalLots uint64 | ||
remainingBalance := quoteBalance - fundingFees |
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.
Need to switch on sell
here too.
client/mm/mm.go
Outdated
unusedLockedFundsReturned bool | ||
excessFeesReturned bool | ||
matchesSeen map[order.MatchID]struct{} | ||
matchesRefunded map[order.MatchID]struct{} |
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.
This is also used to indicate MatchComplete
status without a refund. How about naming it matchesSettled
or something.
client/mm/mm.go
Outdated
@@ -139,11 +794,11 @@ func (m *MarketMaker) Stop() { | |||
} | |||
} | |||
|
|||
// NewMarketMaker creates a new MarketMaker. | |||
func NewMarketMaker(core *core.Core, log dex.Logger) (*MarketMaker, error) { | |||
func NewMarketMaker(c clientCore, log dex.Logger) (*MarketMaker, 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.
Please move this up right under the MarketMaker
type declaration.
if cfg.BaseBalanceType == Percentage { | ||
baseRequired = baseBalance.balanceAvailable * cfg.BaseBalance / 100 | ||
} else { | ||
baseRequired = cfg.BaseBalance | ||
} |
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.
Do we need to validate the balance types across []*BotConfig
somehow? What if the user is trading on a couple markets with the same base asset, and they give Percentage
values that sum to > 100? What happens if they provide mixed Percentage
and Amount
balance types for the same asset?
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.
This is being validated in setupBalances
.
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 guess you mean because it would trigger an error below at
if baseRequired > baseBalance.balanceAvailable-baseBalance.balanceReserved { ...
I still wonder if it wouldn't be easy to screw up. As an example, say the balance of xyz is 2. We run one bot that reserves 1 xyz, and one bot that reserves 50% of xyz. If the balance of xyz changes to 1.5 for whatever reason, then those allocations cannot both be satisfied.
OK for now, but just wanted to point it out.
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.
Yeah, you'd have to change your settings in that case before the run starts.
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 still have some questions about how some things are going to work practically, but I'm comfortable with these changes and I want to get this in so we can get #2416 rebased and out of draft.
client/asset/btc/btc_test.go
Outdated
maxFundingFees := wallet.MaxFundingFees(3, useSplitOptions) | ||
expectedFees := feeRateLimit * (inputSize*12 + outputSize*4 + dexbtc.MinimumTxOverhead) |
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.
consts instead of magic numbers is better.
const maxSwaps = 3
const numInputs = 12
maxFundingFees := wallet.MaxFundingFees(maxSwaps, useSplitOptions)
expectedFees := feeRateLimit * (inputSize*numInputs + outputSize*(maxSwaps+1) + dexbtc.MinimumTxOverhead)
2e960d3
to
bdd260f
Compare
New configurations are added to the market maker config specifying what part of the wallet's balance each market maker will have control over. Using this config, the
MarketMaker
sets initial balances for each of the bots, and this is increased and decreased as the bot makes trades.A
wrappedCore
is also added. This wraps the core methods with additional logic that checks and updates each bot's balances of each asset. AwrappedCore
is passed to each bot, and thecore
methods behave as if the entire balances of the wallets are only what has been allocated to that bot.