Skip to content
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

Merged
merged 6 commits into from
Aug 6, 2023

Conversation

martonp
Copy link
Contributor

@martonp martonp commented Apr 30, 2023

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. A wrappedCore is passed to each bot, and the core methods behave as if the entire balances of the wallets are only what has been allocated to that bot.

client/asset/interface.go Outdated Show resolved Hide resolved
client/asset/dcr/dcr.go Outdated Show resolved Hide resolved
client/mm/balance_handler.go Outdated Show resolved Hide resolved
client/mm/balance_handler.go Outdated Show resolved Hide resolved
client/mm/balance_handler.go Outdated Show resolved Hide resolved
client/mm/balance_handler.go Outdated Show resolved Hide resolved
client/mm/balance_handler.go Outdated Show resolved Hide resolved
client/asset/btc/btc.go Show resolved Hide resolved
client/mm/balance_handler.go Outdated Show resolved Hide resolved
client/mm/balance_handler.go Outdated Show resolved Hide resolved
client/mm/balance_handler.go Outdated Show resolved Hide resolved
client/mm/balance_handler.go Outdated Show resolved Hide resolved
client/mm/balance_handler.go Outdated Show resolved Hide resolved
client/mm/balance_handler.go Outdated Show resolved Hide resolved
client/mm/balance_handler.go Outdated Show resolved Hide resolved
client/asset/btc/btc.go Outdated Show resolved Hide resolved
@martonp
Copy link
Contributor Author

martonp commented Jul 17, 2023

I've moved the balance tracking to be part of MarketMaker instead of a separate balanceHandler struct. This is more convenient because the orderInfos are also useful for creating the history of all actions a bot took and tracking the P/L.

@martonp martonp force-pushed the balanceHandling branch 2 times, most recently from 87b2e50 to 0eb37ac Compare July 17, 2023 14:43
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.
Comment on lines 199 to 209
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,
})
Copy link
Member

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?

Copy link
Contributor Author

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.

Comment on lines 5516 to 5517
dc.assetsMtx.Lock()
swapAsset := dc.assets[wallets.fromWallet.AssetID]
Copy link
Member

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},
Copy link
Member

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
)


mm *MarketMaker
botID string
core clientCore
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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
Comment on lines 387 to 436
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
}
}
Copy link
Member

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)
}

}

var totalLots uint64
remainingBalance := quoteBalance - fundingFees
Copy link
Member

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{}
Copy link
Member

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) {
Copy link
Member

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.

client/mm/mm.go Show resolved Hide resolved
Comment on lines +297 to +301
if cfg.BaseBalanceType == Percentage {
baseRequired = baseBalance.balanceAvailable * cfg.BaseBalance / 100
} else {
baseRequired = cfg.BaseBalance
}
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

@buck54321 buck54321 left a 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.

Comment on lines 2051 to 2052
maxFundingFees := wallet.MaxFundingFees(3, useSplitOptions)
expectedFees := feeRateLimit * (inputSize*12 + outputSize*4 + dexbtc.MinimumTxOverhead)
Copy link
Member

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)

@buck54321 buck54321 merged commit de675a6 into decred:master Aug 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants