diff --git a/client/mm/exchange_adaptor.go b/client/mm/exchange_adaptor.go index af448341a6..7c1eded0d1 100644 --- a/client/mm/exchange_adaptor.go +++ b/client/mm/exchange_adaptor.go @@ -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 } @@ -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 @@ -2705,6 +2707,7 @@ 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 @@ -2712,20 +2715,24 @@ func (u *unifiedExchangeAdaptor) lotCosts(sellVWAP, buyVWAP uint64) (*lotCosts, // 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 } @@ -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 { @@ -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") } } @@ -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 @@ -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() @@ -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 } diff --git a/client/mm/exchange_adaptor_test.go b/client/mm/exchange_adaptor_test.go index d71c80646f..278bdfa155 100644 --- a/client/mm/exchange_adaptor_test.go +++ b/client/mm/exchange_adaptor_test.go @@ -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{ @@ -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 { @@ -679,6 +711,30 @@ 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) @@ -686,6 +742,64 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { 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)