Skip to content

Commit

Permalink
Fee reserve top ups
Browse files Browse the repository at this point in the history
  • Loading branch information
martonp committed Oct 19, 2024
1 parent dfe0723 commit c3c29a3
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 16 deletions.
110 changes: 95 additions & 15 deletions client/mm/exchange_adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2680,6 +2680,7 @@ func (u *unifiedExchangeAdaptor) handleDEXNotification(n core.Notification) {
type lotCosts struct {
dexBase, dexQuote,
cexBase, cexQuote uint64
baseSwap, quoteSwap uint64
baseRedeem, quoteRedeem uint64
baseFunding, quoteFunding uint64 // per multi-order
}
Expand All @@ -2695,6 +2696,7 @@ func (u *unifiedExchangeAdaptor) lotCosts(sellVWAP, buyVWAP uint64) (*lotCosts,
perLot.dexBase += sellFees.bookingFeesPerLot
}
perLot.cexBase = u.lotSize
perLot.baseSwap = sellFees.Max.Swap
perLot.baseRedeem = buyFees.Max.Redeem
perLot.baseFunding = sellFees.funding

Expand All @@ -2705,27 +2707,32 @@ func (u *unifiedExchangeAdaptor) lotCosts(sellVWAP, buyVWAP uint64) (*lotCosts,
perLot.dexQuote += buyFees.bookingFeesPerLot
}
perLot.cexQuote = cexQuoteLot
perLot.quoteSwap = buyFees.Max.Swap
perLot.quoteRedeem = sellFees.Max.Redeem
perLot.quoteFunding = buyFees.funding
return perLot, nil
}

// distribution is a collection of asset distributions and per-lot estimates.
type distribution struct {
baseInv *assetInventory
quoteInv *assetInventory
perLot *lotCosts
baseInv *assetInventory
quoteInv *assetInventory
perLot *lotCosts
feeReserveTopUps map[uint32]uint64
}

func (u *unifiedExchangeAdaptor) newDistribution(perLot *lotCosts, additionalDEX, additionalCEX map[uint32]uint64) *distribution {
dist := &distribution{
baseInv: u.inventory(u.baseID, perLot.dexBase, perLot.cexBase),
quoteInv: u.inventory(u.quoteID, perLot.dexQuote, perLot.cexQuote),
perLot: perLot,
baseInv: u.inventory(u.baseID, u.baseFeeID, perLot.dexBase, perLot.cexBase),
quoteInv: u.inventory(u.quoteID, u.quoteFeeID, perLot.dexQuote, perLot.cexQuote),
perLot: perLot,
feeReserveTopUps: make(map[uint32]uint64),
}
dist.baseInv.dexAdditionalAvailable = additionalDEX[u.baseID]
dist.baseInv.dexFeeAdditionalAvailable = additionalDEX[u.baseFeeID]
dist.baseInv.cexAdditionalAvailable = additionalCEX[u.baseID]
dist.quoteInv.dexAdditionalAvailable = additionalDEX[u.quoteID]
dist.quoteInv.dexFeeAdditionalAvailable = additionalDEX[u.quoteFeeID]
dist.quoteInv.cexAdditionalAvailable = additionalCEX[u.quoteID]
return dist
}
Expand Down Expand Up @@ -2758,6 +2765,48 @@ func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, dexSellLo
quoteAvail = quoteInv.total - additionalQuoteFees
}

applyFeeReserveBuffer := func(amt uint64) uint64 {
return amt * 102 / 100 // 2% buffer
}

// Check if fee assets that are neither the base nor quote asset need to
// be topped up.
sameFeeAssets := u.baseFeeID == u.quoteFeeID
baseMayRequireTopUp := u.baseFeeID != u.baseID && u.baseFeeID != u.quoteID
quoteMayRequireTopUp := u.quoteFeeID != u.baseID && u.quoteFeeID != u.quoteID
if sameFeeAssets && baseMayRequireTopUp {
feeRequired := maxSellLots * (perLot.baseSwap + perLot.quoteRedeem)
feeRequired += maxBuyLots * (perLot.baseRedeem + perLot.quoteSwap)
feeRequired = applyFeeReserveBuffer(feeRequired)
if feeRequired > dist.baseInv.feeReserves {
topUp := utils.Min(feeRequired-dist.baseInv.feeReserves, dist.baseInv.dexFeeAdditionalAvailable)
if topUp > 0 {
dist.feeReserveTopUps[u.baseFeeID] += topUp
}
}
}

if !sameFeeAssets && baseMayRequireTopUp {
baseFeeRequired := perLot.baseSwap*maxSellLots + perLot.baseRedeem*maxBuyLots
baseFeeRequired = applyFeeReserveBuffer(baseFeeRequired)
if baseFeeRequired > dist.baseInv.feeReserves {
topUp := utils.Min(baseFeeRequired-dist.baseInv.feeReserves, dist.baseInv.dexFeeAdditionalAvailable)
if topUp > 0 {
dist.feeReserveTopUps[u.baseFeeID] += topUp
}
}
}
if !sameFeeAssets && quoteMayRequireTopUp {
quoteFeeRequired := perLot.quoteSwap*maxBuyLots + perLot.quoteRedeem*maxSellLots
quoteFeeRequired = applyFeeReserveBuffer(quoteFeeRequired)
if quoteFeeRequired > dist.quoteInv.feeReserves {
topUp := utils.Min(quoteFeeRequired-dist.quoteInv.feeReserves, dist.quoteInv.dexFeeAdditionalAvailable)
if topUp > 0 {
dist.feeReserveTopUps[u.quoteFeeID] += topUp
}
}
}

// matchability is the number of lots that can be matched with a specified
// asset distribution.
matchability := func(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots uint64) uint64 {
Expand Down Expand Up @@ -3020,20 +3069,39 @@ type distributionFunc func(dexAvail, cexAvail map[uint32]uint64) (*distribution,

func (u *unifiedExchangeAdaptor) doInternalTransfers(dist *distribution) {
u.balancesMtx.Lock()
dexDiffs, cexDiffs := make(map[uint32]int64), make(map[uint32]int64)

dexDiffs, cexDiffs := make(map[uint32]int64), make(map[uint32]int64)
dexDiffs[u.baseID] = int64(dist.baseInv.toInternalWithdraw - dist.baseInv.toInternalDeposit)
cexDiffs[u.baseID] = -int64(dist.baseInv.toInternalWithdraw - dist.baseInv.toInternalDeposit)
dexDiffs[u.quoteID] = int64(dist.quoteInv.toInternalWithdraw - dist.quoteInv.toInternalDeposit)
cexDiffs[u.quoteID] = -int64(dist.quoteInv.toInternalWithdraw - dist.quoteInv.toInternalDeposit)

u.baseDexBalances[u.baseID] += dexDiffs[u.baseID]
u.baseCexBalances[u.baseID] += cexDiffs[u.baseID]
u.baseDexBalances[u.quoteID] += dexDiffs[u.quoteID]
u.baseCexBalances[u.quoteID] += cexDiffs[u.quoteID]
toppedUpFeeReserves := false
for feeAsset, topUp := range dist.feeReserveTopUps {
toppedUpFeeReserves = toppedUpFeeReserves || topUp != 0
dexDiffs[feeAsset] += int64(topUp)
u.inventoryMods[feeAsset] += int64(topUp)
}

for assetID, diff := range dexDiffs {
u.baseDexBalances[assetID] += diff
}

for assetID, diff := range cexDiffs {
u.baseCexBalances[assetID] += diff
}

u.balancesMtx.Unlock()

if dexDiffs[u.baseID] != 0 || dexDiffs[u.quoteID] != 0 {
if toppedUpFeeReserves {
inventoryUpdates := make(map[uint32]int64)
for assetID, topUp := range dist.feeReserveTopUps {
inventoryUpdates[assetID] = int64(topUp)
}
u.updateInventoryEvent(inventoryUpdates)
}

if toppedUpFeeReserves || dist.baseInv.toInternalDeposit > 0 || dist.baseInv.toInternalWithdraw > 0 {
u.logBalanceAdjustments(dexDiffs, cexDiffs, "internal transfers")
}
}
Expand Down Expand Up @@ -3159,14 +3227,20 @@ type assetInventory struct {
dexPending uint64
dexLots uint64

// feeReserves is the amount of the fee asset that is available on the dex.
// This is only populated if the fee asset is neither the base nor quote
// asset.
feeReserves uint64

cex uint64
cexAvail uint64
cexLots uint64

total uint64

dexAdditionalAvailable uint64
cexAdditionalAvailable uint64
dexAdditionalAvailable uint64
dexFeeAdditionalAvailable uint64
cexAdditionalAvailable uint64

toDeposit uint64
toWithdraw uint64
Expand All @@ -3176,7 +3250,7 @@ type assetInventory struct {

// inventory generates a current view of the the bot's asset distribution.
// Use optimizeTransfers to set toDeposit and toWithdraw.
func (u *unifiedExchangeAdaptor) inventory(assetID uint32, dexLot, cexLot uint64) (b *assetInventory) {
func (u *unifiedExchangeAdaptor) inventory(assetID, feeAssetID uint32, dexLot, cexLot uint64) (b *assetInventory) {
b = new(assetInventory)
u.balancesMtx.RLock()
defer u.balancesMtx.RUnlock()
Expand All @@ -3191,6 +3265,12 @@ func (u *unifiedExchangeAdaptor) inventory(assetID uint32, dexLot, cexLot uint64
b.cex = cexBalance.Available + cexBalance.Reserved + cexBalance.Pending
b.cexLots = b.cex / cexLot
b.total = b.dex + b.cex

if feeAssetID != u.baseID && feeAssetID != u.quoteID {
feeBalance := u.dexBalance(feeAssetID)
b.feeReserves = feeBalance.Available + feeBalance.Locked
}

return
}

Expand Down
116 changes: 115 additions & 1 deletion client/mm/exchange_adaptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,27 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) {
a.baseCexBalances[quoteID] = int64(cexQuote)
}

feeAssetID := func(assetID uint32) uint32 {
token := asset.TokenInfo(assetID)
if token == nil {
return assetID
}
return token.ParentID
}
baseFeeAssetID := feeAssetID(baseID)
quoteFeeAssetID := feeAssetID(quoteID)
// setFeeBal should only be used when the fee asset is not the
// other asset on the market. setLots will set the fee balances
// in order not to require top ups. This should be called again
// to test fee top ups.
setFeeBal := func(base bool, amt int64) {
if base {
a.baseDexBalances[u.baseFeeID] = amt
} else {
a.baseDexBalances[u.quoteFeeID] = amt
}
}

setLots := func(b, s uint64) {
buyLots, sellLots = b, s
a.placementLotsV.Store(&placementLots{
Expand Down Expand Up @@ -628,7 +649,18 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) {
updateInternalTransferBalances(u, dexAvailableBalances, cexAvailableBalances)
}

checkDistribution := func(baseDeposit, baseWithdraw, quoteDeposit, quoteWithdraw uint64, baseInternal, quoteInternal bool) {
// setAvailableFeeBalances should only be used when the fee asset is not the
// other asset on the market.
setAvailableFeeBalance := func(base bool, amt uint64) {
if base {
dexAvailableBalances[baseFeeAssetID] = amt
} else {
dexAvailableBalances[quoteFeeAssetID] = amt
}
updateInternalTransferBalances(u, dexAvailableBalances, cexAvailableBalances)
}

checkDistribution := func(baseDeposit, baseWithdraw, quoteDeposit, quoteWithdraw uint64, baseInternal, quoteInternal bool, feeReserveTopUps ...map[uint32]uint64) {
t.Helper()
dist, err := a.distribution(dexAvailableBalances, cexAvailableBalances)
if err != nil {
Expand Down Expand Up @@ -679,13 +711,95 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) {
if dist.quoteInv.toInternalWithdraw != expQuoteInternalWithdraw {
t.Fatalf("wrong quote internal withrawal size. wanted %d, got %d", expQuoteInternalWithdraw, dist.quoteInv.toInternalWithdraw)
}

if len(feeReserveTopUps) > 0 != (len(dist.feeReserveTopUps) > 0) {
t.Fatalf("expected fee top ups %v, but got %v", len(feeReserveTopUps) > 0, len(dist.feeReserveTopUps) > 0)
}
if len(feeReserveTopUps) > 0 {
feeReserveTopUps := feeReserveTopUps[0]
spew.Dump(feeReserveTopUps)
spew.Dump(dist.feeReserveTopUps)

for assetID, topUp := range feeReserveTopUps {
if topUp == 0 {
delete(feeReserveTopUps, assetID)
}
}

if len(feeReserveTopUps) != len(dist.feeReserveTopUps) {
t.Fatalf("wrong number of fee top ups. wanted %d, got %d", len(feeReserveTopUps), len(dist.feeReserveTopUps))
}
for assetID, exp := range feeReserveTopUps {
if dist.feeReserveTopUps[assetID] != exp {
t.Fatalf("wrong fee top up for asset %d. wanted %d, got %d", assetID, exp, dist.feeReserveTopUps[assetID])
}
}
}
}

setLots(1, 1)
// Base asset - perfect distribution - no action
setBals(minDexBase, minCexBase, minDexQuote, minCexQuote)
checkDistribution(0, 0, 0, 0, false, false)

// Test fee top ups.
baseCouldNeedTopUp := baseID != u.baseFeeID && quoteID != u.baseFeeID
quoteCouldNeedTopUp := baseID != u.quoteFeeID && quoteID != u.quoteFeeID
if baseCouldNeedTopUp || quoteCouldNeedTopUp {
applyTopUpBuffer := func(topUp uint64) uint64 {
return topUp * 102 / 100
}

var minBaseFeeAsset, minQuoteFeeAsset uint64
if baseCouldNeedTopUp {
minBaseFeeAsset = applyTopUpBuffer(sellSwapFees + buyRedeemFees)
setAvailableFeeBalance(true, minBaseFeeAsset)
}
if quoteCouldNeedTopUp {
minQuoteFeeAsset = applyTopUpBuffer(buySwapFees + sellRedeemFees)
setAvailableFeeBalance(false, minQuoteFeeAsset)
}

if u.baseFeeID == u.quoteFeeID {
setAvailableFeeBalance(true, minBaseFeeAsset+minQuoteFeeAsset)
checkDistribution(0, 0, 0, 0, false, false, map[uint32]uint64{baseFeeAssetID: minBaseFeeAsset + minQuoteFeeAsset})
} else {
checkDistribution(0, 0, 0, 0, false, false, map[uint32]uint64{baseFeeAssetID: minBaseFeeAsset, quoteFeeAssetID: minQuoteFeeAsset})
}

if baseCouldNeedTopUp {
setFeeBal(true, 100)
}
if quoteCouldNeedTopUp {
setFeeBal(false, 100)
}
if u.baseFeeID == u.quoteFeeID {
checkDistribution(0, 0, 0, 0, false, false, map[uint32]uint64{baseFeeAssetID: minBaseFeeAsset + minQuoteFeeAsset - 100})
} else {
checkDistribution(0, 0, 0, 0, false, false, map[uint32]uint64{baseFeeAssetID: utils.SafeSub(minBaseFeeAsset, 100), quoteFeeAssetID: utils.SafeSub(minQuoteFeeAsset, 100)})
}

if baseCouldNeedTopUp {
setFeeBal(true, 0)
setAvailableFeeBalance(true, minBaseFeeAsset-50)
}
if quoteCouldNeedTopUp {
setFeeBal(false, 0)
setAvailableFeeBalance(false, minQuoteFeeAsset-50)
}

if u.baseFeeID == u.quoteFeeID {
setAvailableFeeBalance(true, minBaseFeeAsset+minQuoteFeeAsset-50)
checkDistribution(0, 0, 0, 0, false, false, map[uint32]uint64{baseFeeAssetID: minBaseFeeAsset + minQuoteFeeAsset - 50})
} else {
checkDistribution(0, 0, 0, 0, false, false, map[uint32]uint64{baseFeeAssetID: utils.SafeSub(minBaseFeeAsset, 50), quoteFeeAssetID: utils.SafeSub(minQuoteFeeAsset, 50)})
}

setAvailableFeeBalance(true, 0)
setAvailableFeeBalance(false, 0)
setFeeBal(true, 0)
}

// Move all of the base balance to cex and max sure we get a withdraw.
setBals(0, totalBase, minDexQuote, minCexQuote)
checkDistribution(0, minDexBase, 0, 0, false, false)
Expand Down

0 comments on commit c3c29a3

Please sign in to comment.