From 6c0d7cfed0d9352016288546611b807927de91be Mon Sep 17 00:00:00 2001 From: martonp Date: Sat, 27 Jul 2024 10:57:28 +0200 Subject: [PATCH] mm: Internal transfers This diff updates the market maker to attempt to reallocate available funds to bots before doing a deposit or a withdrawal. For example, if a deposit is required, and the user has additional available funds on the CEX that are not allocated to any bots, funds will be allocated to the bot on the CEX and unallocated from the bot on the wallet to simulate a deposit. Bots may be configured to only attempt these "internal transfers", to attempt both internal and external transfers, or to do no rebalancing at all. --- client/mm/config.go | 4 + client/mm/exchange_adaptor.go | 283 +++++++++++---- client/mm/exchange_adaptor_test.go | 324 +++++++++++++----- client/mm/mm.go | 24 ++ client/mm/mm_arb_market_maker.go | 15 +- client/mm/mm_arb_market_maker_test.go | 9 + client/mm/mm_simple_arb.go | 15 +- client/webserver/locales/en-us.go | 4 + .../webserver/site/src/html/mmsettings.tmpl | 19 + client/webserver/site/src/js/mm.ts | 29 +- client/webserver/site/src/js/mmsettings.ts | 57 ++- client/webserver/site/src/js/registry.ts | 2 + 12 files changed, 605 insertions(+), 180 deletions(-) diff --git a/client/mm/config.go b/client/mm/config.go index ea11a2c2dd..f14db2e0d3 100644 --- a/client/mm/config.go +++ b/client/mm/config.go @@ -47,6 +47,10 @@ type CEXConfig struct { type AutoRebalanceConfig struct { MinBaseTransfer uint64 `json:"minBaseTransfer"` MinQuoteTransfer uint64 `json:"minQuoteTransfer"` + // InternalOnly means that the bot will only simulate transfers by + // allocating unallocated funds to the bot's balance and never actually + // perform deposits and withdrawals with the CEX. + InternalOnly bool `json:"internalOnly"` } // BotBalanceAllocation is the initial allocation of funds for a bot. diff --git a/client/mm/exchange_adaptor.go b/client/mm/exchange_adaptor.go index a41d4a05eb..90f800641f 100644 --- a/client/mm/exchange_adaptor.go +++ b/client/mm/exchange_adaptor.go @@ -400,6 +400,8 @@ func (m *market) msgRate(convRate float64) uint64 { return calc.MessageRate(convRate, m.bui, m.qui) } +type doInternalTransferFunc func(dexAvailable, cexAvailable map[uint32]uint64) error + // unifiedExchangeAdaptor implements both botCoreAdaptor and botCexAdaptor. type unifiedExchangeAdaptor struct { *market @@ -418,6 +420,9 @@ type unifiedExchangeAdaptor struct { initialBalances map[uint32]uint64 baseTraits asset.WalletTrait quoteTraits asset.WalletTrait + // ** IMPORTANT ** No mutexes should be locked when calling this + // function. + internalTransfer func(*MarketWithHost, doInternalTransferFunc) error botLooper dex.Connector botLoop *dex.ConnectionMaster @@ -524,7 +529,7 @@ func (u *unifiedExchangeAdaptor) withPause(f func() error) error { // // balancesMtx must be read locked when calling this function. func (u *unifiedExchangeAdaptor) logBalanceAdjustments(dexDiffs, cexDiffs map[uint32]int64, reason string) { - if u.log.Level() > dex.LevelTrace { + if u.log.Level() > dex.LevelDebug { return } @@ -608,7 +613,7 @@ func (u *unifiedExchangeAdaptor) logBalanceAdjustments(dexDiffs, cexDiffs map[ui } writeLine("") - u.log.Tracef(msg.String()) + u.log.Debugf(msg.String()) } // SufficientBalanceForDEXTrade returns whether the bot has sufficient balance @@ -2599,12 +2604,12 @@ func dexOrderEffects(o *core.Order, swaps, redeems, refunds map[string]*asset.Wa dex.Settled[fromFeeAsset] -= int64(tx.Fees) } - var reedeemIsDynamicSwapper, refundIsDynamicSwapper bool + var redeemIsDynamicSwapper, refundIsDynamicSwapper bool if o.Sell { - reedeemIsDynamicSwapper = quoteTraits.IsDynamicSwapper() + redeemIsDynamicSwapper = quoteTraits.IsDynamicSwapper() refundIsDynamicSwapper = baseTraits.IsDynamicSwapper() } else { - reedeemIsDynamicSwapper = baseTraits.IsDynamicSwapper() + redeemIsDynamicSwapper = baseTraits.IsDynamicSwapper() refundIsDynamicSwapper = quoteTraits.IsDynamicSwapper() } @@ -2616,7 +2621,7 @@ func dexOrderEffects(o *core.Order, swaps, redeems, refunds map[string]*asset.Wa } dex.Pending[toAsset] += tx.Amount - if reedeemIsDynamicSwapper { + if redeemIsDynamicSwapper { dex.Settled[toFeeAsset] -= int64(tx.Fees) } else if dex.Pending[toFeeAsset] >= tx.Fees { dex.Pending[toFeeAsset] -= tx.Fees @@ -2827,16 +2832,17 @@ func (u *unifiedExchangeAdaptor) newDistribution(perLot *lotCosts) *distribution } // optimizeTransfers populates the toDeposit and toWithdraw fields of the base -// and quote assetDistribution. To find the best asset distribution, a series -// of possible target configurations are tested and the distribution that -// results in the highest matchability is chosen. -func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, dexSellLots, dexBuyLots, maxSellLots, maxBuyLots uint64) { - baseInv, quoteInv := dist.baseInv, dist.quoteInv - perLot := dist.perLot - +// and quote assetDistributions. To find the best asset distribution, a series +// of possible target configurations are tested and the distribution that results +// in the highest matchability score is chosen. +func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, dexSellLots, dexBuyLots, maxSellLots, maxBuyLots uint64, + dexAdditionalAvailable, cexAdditionalAvailable map[uint32]uint64) { if u.autoRebalanceCfg == nil { return } + + baseInv, quoteInv := dist.baseInv, dist.quoteInv + perLot := dist.perLot minBaseTransfer, minQuoteTransfer := u.autoRebalanceCfg.MinBaseTransfer, u.autoRebalanceCfg.MinQuoteTransfer additionalBaseFees, additionalQuoteFees := perLot.baseFunding, perLot.quoteFunding @@ -2854,6 +2860,11 @@ func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, dexSellLo quoteAvail = quoteInv.total - additionalQuoteFees } + baseDEXAdditionalAvailable := dexAdditionalAvailable[u.baseID] + quoteDEXAdditionalAvailable := dexAdditionalAvailable[u.quoteID] + baseCEXAdditionalAvailable := cexAdditionalAvailable[u.baseID] + quoteCEXAdditionalAvailable := cexAdditionalAvailable[u.quoteID] + // matchability is the number of lots that can be matched with a specified // asset distribution. matchability := func(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots uint64) uint64 { @@ -2901,6 +2912,8 @@ func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, dexSellLo fees uint64 baseDeposit, baseWithdraw uint64 quoteDeposit, quoteWithdraw uint64 + baseTransferInternal bool + quoteTransferInternal bool } baseSplits := [][2]uint64{ {baseInv.dex, baseInv.cex}, // current @@ -2913,74 +2926,155 @@ func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, dexSellLo {baseInv.cex, baseInv.dex}, } - splits := make([]*scoredSplit, 0) - // scoreSplit gets a score for the proposed asset distribution and, if the - // score is higher than currentScore, saves the result to the splits slice. - scoreSplit := func(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote uint64) { - score := matchability(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots) - if score <= currentScore { - return + type transferSource uint16 + const ( + allExternal transferSource = iota + allInternal + onlyBaseInternal + onlyQuoteInternal + ) + + isETHToken := func(assetID uint32) bool { + token := asset.TokenInfo(assetID) + if token == nil { + return false } + return token.ParentID == 60 + } + splits := make([]*scoredSplit, 0) + // scoreSplitSource gets a score for the proposed asset distribution using + // the specified sources for the transfer, and, if the score is higher than + // currentScore, saves the result to the splits slice. If the source is + // internal, and the available balance is less than the target, the full + // available internal balance will be used. + scoreSplitSource := func(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote uint64, source transferSource) { var fees uint64 + incrementFees := func(base bool) { + // TODO: use actual fees + fees++ + if base && (u.baseID == 0 || u.baseID == 60 || isETHToken(u.baseID)) { + fees++ + } + if !base && (u.quoteID == 0 || u.quoteID == 60 || isETHToken(u.quoteID)) { + fees++ + } + } + + baseInternal := source == allInternal || source == onlyBaseInternal + quoteInternal := source == allInternal || source == onlyQuoteInternal + var baseDeposit, baseWithdraw, quoteDeposit, quoteWithdraw uint64 + + // actual lots may change if the source is internal and the available + // balance is less than the target. + actualDexBaseLots, actualDexQuoteLots, actualCexBaseLots, actualCexQuoteLots := dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots + if dexBaseLots != baseInv.dexLots || cexBaseLots != baseInv.cexLots { - fees++ dexTarget := dexBaseLots*perLot.dexBase + additionalBaseFees + extraBase cexTarget := cexBaseLots * perLot.cexBase if dexTarget > baseInv.dex { - if withdraw := dexTarget - baseInv.dex; withdraw >= minBaseTransfer { - baseWithdraw = withdraw + toWithdraw := dexTarget - baseInv.dex + if baseInternal { + baseWithdraw = utils.Min(baseDEXAdditionalAvailable, toWithdraw, dist.baseInv.cexAvail) + if toWithdraw > baseDEXAdditionalAvailable { + dexTotal := utils.SafeSub(dist.baseInv.dex+baseWithdraw, additionalBaseFees) + actualDexBaseLots = dexTotal / perLot.dexBase + actualCexBaseLots = utils.SafeSub(dist.baseInv.cex, baseWithdraw) / perLot.cexBase + } + } else if toWithdraw >= minBaseTransfer { + baseWithdraw = toWithdraw + incrementFees(true) } else { return } } else if cexTarget > baseInv.cex { - if deposit := cexTarget - baseInv.cex; deposit >= minBaseTransfer { - baseDeposit = deposit + toDeposit := cexTarget - baseInv.cex + if baseInternal { + baseDeposit = utils.Min(baseCEXAdditionalAvailable, toDeposit, dist.baseInv.dexAvail) + if toDeposit > baseCEXAdditionalAvailable { + dexTotal := utils.SafeSub(dist.baseInv.dex, baseDeposit) + dexTotal = utils.SafeSub(dexTotal, additionalBaseFees) + actualDexBaseLots = dexTotal / perLot.dexBase + actualCexBaseLots = (dist.baseInv.cex + baseDeposit) / perLot.cexBase + } + } else if toDeposit >= minBaseTransfer { + baseDeposit = toDeposit + incrementFees(true) } else { return } } - // TODO: Use actual fee estimates. - if u.baseID == 0 || u.baseID == 42 { - fees++ - } } + if dexQuoteLots != quoteInv.dexLots || cexQuoteLots != quoteInv.cexLots { - fees++ dexTarget := dexQuoteLots*perLot.dexQuote + additionalQuoteFees + (extraQuote / 2) cexTarget := cexQuoteLots*perLot.cexQuote + (extraQuote / 2) + if dexTarget > quoteInv.dex { - if withdraw := dexTarget - quoteInv.dex; withdraw >= minQuoteTransfer { - quoteWithdraw = withdraw + toWithdraw := dexTarget - quoteInv.dex + if quoteInternal { + quoteWithdraw = utils.Min(quoteDEXAdditionalAvailable, toWithdraw, dist.quoteInv.cexAvail) + if toWithdraw > quoteDEXAdditionalAvailable { + dexTotal := utils.SafeSub(dist.quoteInv.dex+quoteWithdraw, additionalQuoteFees) + actualDexQuoteLots = dexTotal / perLot.dexQuote + actualCexQuoteLots = (dist.quoteInv.cex - quoteWithdraw) / perLot.cexQuote + } + } else if toWithdraw >= minQuoteTransfer { + quoteWithdraw = toWithdraw + incrementFees(false) } else { return } - } else if cexTarget > quoteInv.cex { - if deposit := cexTarget - quoteInv.cex; deposit >= minQuoteTransfer { - quoteDeposit = deposit + toDeposit := cexTarget - quoteInv.cex + if quoteInternal { + quoteDeposit = utils.Min(quoteCEXAdditionalAvailable, toDeposit, dist.quoteInv.dexAvail) + if toDeposit > quoteCEXAdditionalAvailable { + dexTotal := utils.SafeSub(dist.quoteInv.dex, quoteDeposit) + dexTotal = utils.SafeSub(dexTotal, additionalQuoteFees) + actualDexQuoteLots = dexTotal / perLot.dexQuote + actualCexQuoteLots = (dist.quoteInv.cex + quoteDeposit) / perLot.cexQuote + } + } else if toDeposit >= minQuoteTransfer { + quoteDeposit = toDeposit + incrementFees(false) } else { return } } - if u.quoteID == 0 || u.quoteID == 60 { - fees++ - } + } + + score := matchability(actualDexBaseLots, actualDexQuoteLots, actualCexBaseLots, actualCexQuoteLots) + if score <= currentScore { + return } splits = append(splits, &scoredSplit{ - score: score, - fees: fees, - baseDeposit: baseDeposit, - baseWithdraw: baseWithdraw, - quoteDeposit: quoteDeposit, - quoteWithdraw: quoteWithdraw, - spread: utils.Min(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots), + score: score, + fees: fees, + baseDeposit: baseDeposit, + baseWithdraw: baseWithdraw, + quoteDeposit: quoteDeposit, + quoteWithdraw: quoteWithdraw, + spread: utils.Min(actualDexBaseLots, actualDexQuoteLots, actualCexBaseLots, actualCexQuoteLots), + baseTransferInternal: baseInternal, + quoteTransferInternal: quoteInternal, }) } + // scoreSplit scores a proposed split using all combinations of transfer + // sources. + scoreSplit := func(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote uint64) { + if !u.autoRebalanceCfg.InternalOnly { + scoreSplitSource(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote, allExternal) + scoreSplitSource(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote, onlyBaseInternal) + scoreSplitSource(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote, onlyQuoteInternal) + } + scoreSplitSource(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote, allInternal) + } + // Try to hit all possible combinations. for _, b := range baseSplits { dexBaseLots, cexBaseLots, extraBase := baseSplit(b[0], b[1]) @@ -2991,6 +3085,7 @@ func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, dexSellLo scoreSplit(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote) } } + // Try in both directions. for _, q := range quoteSplits { dexQuoteLots, cexQuoteLots, extraQuote := quoteSplit(q[0], q[1]) @@ -3012,14 +3107,45 @@ func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, dexSellLo return i.score > j.score || (i.score == j.score && (i.fees < j.fees || i.spread > j.spread)) }) split := splits[0] - baseInv.toDeposit = split.baseDeposit - baseInv.toWithdraw = split.baseWithdraw - quoteInv.toDeposit = split.quoteDeposit - quoteInv.toWithdraw = split.quoteWithdraw + + if split.baseTransferInternal { + baseInv.toInternalDeposit = split.baseDeposit + baseInv.toInternalWithdraw = split.baseWithdraw + } else { + baseInv.toDeposit = split.baseDeposit + baseInv.toWithdraw = split.baseWithdraw + } + + if split.quoteTransferInternal { + quoteInv.toInternalDeposit = split.quoteDeposit + quoteInv.toInternalWithdraw = split.quoteWithdraw + } else { + quoteInv.toDeposit = split.quoteDeposit + quoteInv.toWithdraw = split.quoteWithdraw + } +} + +func (u *unifiedExchangeAdaptor) doInternalTransfers(baseDeposit, baseWithdraw, quoteDeposit, quoteWithdraw uint64) { + u.balancesMtx.Lock() + dexDiffs, cexDiffs := make(map[uint32]int64), make(map[uint32]int64) + dexDiffs[u.baseID] = int64(baseWithdraw - baseDeposit) + cexDiffs[u.baseID] = -int64(baseWithdraw - baseDeposit) + dexDiffs[u.quoteID] = int64(quoteWithdraw - quoteDeposit) + cexDiffs[u.quoteID] = -int64(quoteWithdraw - quoteDeposit) + for assetID, diff := range dexDiffs { + u.baseDexBalances[assetID] += diff + } + for assetID, diff := range cexDiffs { + u.baseCexBalances[assetID] += diff + } + u.balancesMtx.Unlock() + + if baseDeposit > 0 || baseWithdraw > 0 || quoteDeposit > 0 || quoteWithdraw > 0 { + u.logBalanceAdjustments(dexDiffs, cexDiffs, "internal transfers") + } } -// transfer attempts to perform the transers specified in the distribution. -func (u *unifiedExchangeAdaptor) transfer(dist *distribution, currEpoch uint64) (actionTaken bool, err error) { +func (u *unifiedExchangeAdaptor) doExternalTransfers(dist *distribution, currEpoch uint64) (actionTaken bool, err error) { baseInv, quoteInv := dist.baseInv, dist.quoteInv if baseInv.toDeposit+baseInv.toWithdraw+quoteInv.toDeposit+quoteInv.toWithdraw == 0 { return false, nil @@ -3115,26 +3241,56 @@ func (u *unifiedExchangeAdaptor) transfer(dist *distribution, currEpoch uint64) return true, nil } +type distributionFunc func(dexAvail, cexAvail map[uint32]uint64) (*distribution, error) + +func (u *unifiedExchangeAdaptor) tryTransfers(currEpoch uint64, df distributionFunc) (actionTaken bool, err error) { + if u.autoRebalanceCfg == nil { + return false, nil + } + + var dist *distribution + err = u.internalTransfer(u.mwh, func(dexAvail, cexAvail map[uint32]uint64) error { + dist, err = df(dexAvail, cexAvail) + if err != nil { + return fmt.Errorf("distribution calculation error: %w", err) + } + + baseDeposit := dist.baseInv.toInternalDeposit + baseWithdraw := dist.baseInv.toInternalWithdraw + quoteDeposit := dist.quoteInv.toInternalDeposit + quoteWithdraw := dist.quoteInv.toInternalWithdraw + u.doInternalTransfers(baseDeposit, baseWithdraw, quoteDeposit, quoteWithdraw) + return nil + }) + if err != nil { + return false, err + } + + if !u.autoRebalanceCfg.InternalOnly { + return u.doExternalTransfers(dist, currEpoch) + } + + return false, nil +} + // assetInventory is an accounting of the distribution of base- or quote-asset // funding. type assetInventory struct { dex uint64 dexAvail uint64 dexPending uint64 - dexLocked uint64 dexLots uint64 - cex uint64 - cexAvail uint64 - cexPending uint64 - cexReserved uint64 - cexLocked uint64 - cexLots uint64 + cex uint64 + cexAvail uint64 + cexLots uint64 total uint64 - toDeposit uint64 - toWithdraw uint64 + toDeposit uint64 + toWithdraw uint64 + toInternalDeposit uint64 + toInternalWithdraw uint64 } // inventory generates a current view of the the bot's asset distribution. @@ -3147,17 +3303,14 @@ func (u *unifiedExchangeAdaptor) inventory(assetID uint32, dexLot, cexLot uint64 dexBalance := u.dexBalance(assetID) b.dexAvail = dexBalance.Available b.dexPending = dexBalance.Pending - b.dexLocked = dexBalance.Locked b.dex = dexBalance.Available + dexBalance.Locked + dexBalance.Pending b.dexLots = b.dex / dexLot cexBalance := u.cexBalance(assetID) b.cexAvail = cexBalance.Available - b.cexPending = cexBalance.Pending - b.cexReserved = cexBalance.Reserved - b.cexLocked = cexBalance.Locked b.cex = cexBalance.Available + cexBalance.Reserved + cexBalance.Pending b.cexLots = b.cex / cexLot b.total = b.dex + b.cex + return } @@ -3764,6 +3917,7 @@ type exchangeAdaptorCfg struct { log dex.Logger eventLogDB eventLogDB botCfg *BotConfig + internalTransfer func(*MarketWithHost, doInternalTransferFunc) error } // newUnifiedExchangeAdaptor is the constructor for a unifiedExchangeAdaptor. @@ -3815,6 +3969,7 @@ func newUnifiedExchangeAdaptor(cfg *exchangeAdaptorCfg) (*unifiedExchangeAdaptor baseTraits: baseTraits, quoteTraits: quoteTraits, autoRebalanceCfg: cfg.autoRebalanceConfig, + internalTransfer: cfg.internalTransfer, baseDexBalances: baseDEXBalances, baseCexBalances: baseCEXBalances, diff --git a/client/mm/exchange_adaptor_test.go b/client/mm/exchange_adaptor_test.go index c568c4541c..1a29badcef 100644 --- a/client/mm/exchange_adaptor_test.go +++ b/client/mm/exchange_adaptor_test.go @@ -467,23 +467,31 @@ func TestFreeUpFunds(t *testing.T) { } func TestDistribution(t *testing.T) { - // utxo/utxo - testDistribution(t, 42, 0) - // utxo/account-locker - testDistribution(t, 42, 60) - testDistribution(t, 60, 42) - // token/parent - testDistribution(t, 60001, 60) - testDistribution(t, 60, 60001) - // token/token - same chain - testDistribution(t, 966002, 966001) - testDistribution(t, 966001, 966002) - // token/token - different chains - testDistribution(t, 60001, 966003) - testDistribution(t, 966003, 60001) - // utxo/token - testDistribution(t, 42, 966003) - testDistribution(t, 966003, 42) + tests := [][2]uint32{ + // utxo/utxo + {42, 0}, + // utxo/account-locker + {42, 60}, + {60, 42}, + // token/parent + {60001, 60}, + {60, 60001}, + // token/token - same chain + {966002, 966001}, + {966001, 966002}, + // token/token - different chains + {60001, 966003}, + {966003, 60001}, + // utxo/token + {42, 966003}, + {966003, 42}, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%d/%d", test[0], test[1]), func(t *testing.T) { + testDistribution(t, test[0], test[1]) + }) + } } func testDistribution(t *testing.T, baseID, quoteID uint32) { @@ -616,58 +624,136 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { a.autoRebalanceCfg.MinQuoteTransfer = utils.Min(perLot.cexQuote, perLot.dexQuote) } - checkDistribution := func(baseDeposit, baseWithdraw, quoteDeposit, quoteWithdraw uint64) { + dexAvailableBalances := map[uint32]uint64{} + cexAvailableBalances := map[uint32]uint64{} + setAvailableBalances := func(dexBase, cexBase, dexQuote, cexQuote uint64) { + dexAvailableBalances[baseID] = dexBase + dexAvailableBalances[quoteID] = dexQuote + cexAvailableBalances[baseID] = cexBase + cexAvailableBalances[quoteID] = cexQuote + updateInternalTransferBalances(u, dexAvailableBalances, cexAvailableBalances) + } + + checkDistribution := func(baseDeposit, baseWithdraw, quoteDeposit, quoteWithdraw uint64, baseInternal, quoteInternal bool) { t.Helper() - dist, err := a.distribution() + dist, err := a.distribution(dexAvailableBalances, cexAvailableBalances) if err != nil { t.Fatalf("distribution error: %v", err) } - if dist.baseInv.toDeposit != baseDeposit { - t.Fatalf("wrong base deposit size. wanted %d, got %d", baseDeposit, dist.baseInv.toDeposit) + + var expBaseExternalDeposit, expBaseExternalWithdraw, expQuoteExternalDeposit, expQuoteExternalWithdraw uint64 + var expBaseInternalDeposit, expBaseInternalWithdraw, expQuoteInternalDeposit, expQuoteInternalWithdraw uint64 + + if baseInternal { + expBaseInternalDeposit = baseDeposit + expBaseInternalWithdraw = baseWithdraw + } else { + expBaseExternalDeposit = baseDeposit + expBaseExternalWithdraw = baseWithdraw + } + + if quoteInternal { + expQuoteInternalDeposit = quoteDeposit + expQuoteInternalWithdraw = quoteWithdraw + } else { + expQuoteExternalDeposit = quoteDeposit + expQuoteExternalWithdraw = quoteWithdraw + } + + if dist.baseInv.toDeposit != expBaseExternalDeposit { + t.Fatalf("wrong base deposit size. wanted %d, got %d", expBaseExternalDeposit, dist.baseInv.toDeposit) } - if dist.baseInv.toWithdraw != baseWithdraw { - t.Fatalf("wrong base withrawal size. wanted %d, got %d", baseWithdraw, dist.baseInv.toWithdraw) + if dist.baseInv.toWithdraw != expBaseExternalWithdraw { + t.Fatalf("wrong base withrawal size. wanted %d, got %d", expBaseExternalWithdraw, dist.baseInv.toWithdraw) } - if dist.quoteInv.toDeposit != quoteDeposit { - t.Fatalf("wrong quote deposit size. wanted %d, got %d", quoteDeposit, dist.quoteInv.toDeposit) + if dist.quoteInv.toDeposit != expQuoteExternalDeposit { + t.Fatalf("wrong quote deposit size. wanted %d, got %d", expQuoteExternalDeposit, dist.quoteInv.toDeposit) } - if dist.quoteInv.toWithdraw != quoteWithdraw { - t.Fatalf("wrong quote withrawal size. wanted %d, got %d", quoteWithdraw, dist.quoteInv.toWithdraw) + if dist.quoteInv.toWithdraw != expQuoteExternalWithdraw { + t.Fatalf("wrong quote withrawal size. wanted %d, got %d", expQuoteExternalWithdraw, dist.quoteInv.toWithdraw) + } + + if dist.baseInv.toInternalDeposit != expBaseInternalDeposit { + t.Fatalf("wrong base internal deposit size. wanted %d, got %d", expBaseInternalDeposit, dist.baseInv.toInternalDeposit) + } + if dist.baseInv.toInternalWithdraw != expBaseInternalWithdraw { + t.Fatalf("wrong base internal withrawal size. wanted %d, got %d", expBaseInternalWithdraw, dist.baseInv.toInternalWithdraw) + } + if dist.quoteInv.toInternalDeposit != expQuoteInternalDeposit { + t.Fatalf("wrong quote internal deposit size. wanted %d, got %d", expQuoteInternalDeposit, dist.quoteInv.toInternalDeposit) + } + if dist.quoteInv.toInternalWithdraw != expQuoteInternalWithdraw { + t.Fatalf("wrong quote internal withrawal size. wanted %d, got %d", expQuoteInternalWithdraw, dist.quoteInv.toInternalWithdraw) } } setLots(1, 1) // Base asset - perfect distribution - no action setBals(minDexBase, minCexBase, minDexQuote, minCexQuote) - checkDistribution(0, 0, 0, 0) + checkDistribution(0, 0, 0, 0, false, false) // 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) + checkDistribution(0, minDexBase, 0, 0, false, false) + // Set available balance enough to cover the withdraw. + setAvailableBalances(minDexBase, 0, 0, 0) + checkDistribution(0, minDexBase, 0, 0, true, false) + setAvailableBalances(0, 0, 0, 0) + // One less available balance causes withdrawal to happen + setAvailableBalances(minDexBase-1, 0, 0, 0) + checkDistribution(0, minDexBase, 0, 0, false, false) + setAvailableBalances(0, 0, 0, 0) // Raise the transfer theshold by one atom and it should zero the withdraw. a.autoRebalanceCfg.MinBaseTransfer = minDexBase + 1 - checkDistribution(0, 0, 0, 0) - a.autoRebalanceCfg.MinBaseTransfer = 0 + checkDistribution(0, 0, 0, 0, false, false) // Same for quote + setLots(1, 1) setBals(minDexBase, minCexBase, 0, totalQuote) - checkDistribution(0, 0, 0, minDexQuote) + checkDistribution(0, 0, 0, minDexQuote, false, false) + setAvailableBalances(0, 0, minDexQuote, 0) + checkDistribution(0, 0, 0, minDexQuote, false, true) + setAvailableBalances(0, 0, minDexQuote-1, 0) + checkDistribution(0, 0, 0, minDexQuote, false, false) + setAvailableBalances(0, 0, 0, 0) a.autoRebalanceCfg.MinQuoteTransfer = minDexQuote + 1 - checkDistribution(0, 0, 0, 0) - a.autoRebalanceCfg.MinQuoteTransfer = 0 + checkDistribution(0, 0, 0, 0, false, false) + // Base deposit + setLots(1, 1) setBals(totalBase, 0, minDexQuote, minCexQuote) + checkDistribution(minCexBase, 0, 0, 0, false, false) + setAvailableBalances(0, minCexBase, 0, 0) + checkDistribution(minCexBase, 0, 0, 0, true, false) + setAvailableBalances(0, minCexBase-1, 0, 0) + checkDistribution(minCexBase, 0, 0, 0, false, false) + setAvailableBalances(0, 0, 0, 0) - checkDistribution(minCexBase, 0, 0, 0) // Quote deposit setBals(minDexBase, minCexBase, totalQuote, 0) - checkDistribution(0, 0, minCexQuote, 0) + checkDistribution(0, 0, minCexQuote, 0, false, false) + setAvailableBalances(0, 0, 0, minCexQuote) + checkDistribution(0, 0, minCexQuote, 0, false, true) + setAvailableBalances(0, 0, 0, minCexQuote-1) + checkDistribution(0, 0, minCexQuote, 0, false, false) + setAvailableBalances(0, 0, 0, 0) + // Doesn't have to be symmetric. setLots(1, 3) setBals(totalBase, 0, minDexQuote, minCexQuote) - checkDistribution(minCexBase, 0, 0, 0) + checkDistribution(minCexBase, 0, 0, 0, false, false) + setAvailableBalances(0, minCexBase, 0, 0) + checkDistribution(minCexBase, 0, 0, 0, true, false) + setAvailableBalances(0, minCexBase-1, 0, 0) + checkDistribution(minCexBase, 0, 0, 0, false, false) + setAvailableBalances(0, 0, 0, 0) setBals(minDexBase, minCexBase, 0, totalQuote) - checkDistribution(0, 0, 0, minDexQuote) + checkDistribution(0, 0, 0, minDexQuote, false, false) + setAvailableBalances(minDexQuote, minDexQuote, minDexQuote, minDexQuote) + checkDistribution(0, 0, 0, minDexQuote, false, true) + setAvailableBalances(0, 0, minDexQuote-1, 0) + checkDistribution(0, 0, 0, minDexQuote, false, false) + setAvailableBalances(0, 0, 0, 0) // Even if there's extra, if neither side has too low of balance, nothing // will happen. The extra will be split evenly between dex and cex. @@ -675,39 +761,47 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { setLots(5, 3) // Base OK setBals(minDexBase, minCexBase*10, minDexQuote, minCexQuote) - checkDistribution(0, 0, 0, 0) + checkDistribution(0, 0, 0, 0, false, false) // Base withdraw. Extra goes to dex for base asset. setBals(0, minDexBase+minCexBase+extra, minDexQuote, minCexQuote) - checkDistribution(0, minDexBase+extra, 0, 0) + checkDistribution(0, minDexBase+extra, 0, 0, false, false) + + setBals(0, minCexBase*10, minDexQuote, minCexQuote) + setAvailableBalances(minDexBase, 0, 0, 0) + checkDistribution(0, minDexBase, 0, 0, true, false) + setAvailableBalances(minDexBase-1, 0, 0, 0) + checkDistribution(0, 950000000, 0, 0, false, false) + setAvailableBalances(0, 0, 0, 0) + // Base deposit. setBals(minDexBase+minCexBase, extra, minDexQuote, minCexQuote) - checkDistribution(minCexBase-extra, 0, 0, 0) + checkDistribution(minCexBase-extra, 0, 0, 0, false, false) // Quote OK setBals(minDexBase, minCexBase, minDexQuote*100, minCexQuote*100) - checkDistribution(0, 0, 0, 0) + checkDistribution(0, 0, 0, 0, false, false) // Quote withdraw. Extra is split for the quote asset. Gotta lower the min // transfer a little bit to make this one happen. setBals(minDexBase, minCexBase, minDexQuote-perLot.dexQuote+extra, minCexQuote+perLot.dexQuote) a.autoRebalanceCfg.MinQuoteTransfer = perLot.dexQuote - extra/2 - checkDistribution(0, 0, 0, perLot.dexQuote-extra/2) + checkDistribution(0, 0, 0, perLot.dexQuote-extra/2, false, false) // Quote deposit setBals(minDexBase, minCexBase, minDexQuote+perLot.cexQuote+extra, minCexQuote-perLot.cexQuote) - checkDistribution(0, 0, perLot.cexQuote+extra/2, 0) + checkDistribution(0, 0, perLot.cexQuote+extra/2, 0, false, false) // Deficit math. // Since cex lot is smaller, dex can't use this extra. setBals(addBaseFees+perLot.dexBase*3+perLot.cexBase, 0, addQuoteFees+minDexQuote, minCexQuote) - checkDistribution(2*perLot.cexBase, 0, 0, 0) + checkDistribution(2*perLot.cexBase, 0, 0, 0, false, false) // Same thing, but with enough for fees, and there's no reason to transfer // because it doesn't improve our matchability. setBals(perLot.dexBase*3, extra, minDexQuote, minCexQuote) - checkDistribution(0, 0, 0, 0) + checkDistribution(0, 0, 0, 0, false, false) setBals(addBaseFees+minDexBase, minCexBase, addQuoteFees+perLot.dexQuote*5+perLot.cexQuote*2+extra, 0) - checkDistribution(0, 0, perLot.cexQuote*2+extra/2, 0) + checkDistribution(0, 0, perLot.cexQuote*2+extra/2, 0, false, false) setBals(addBaseFees+perLot.dexBase, 5*perLot.cexBase+2*perLot.dexBase+extra, addQuoteFees+minDexQuote, minCexQuote) - checkDistribution(0, 2*perLot.dexBase+extra, 0, 0) + checkDistribution(0, 2*perLot.dexBase+extra, 0, 0, false, false) setBals(addBaseFees+perLot.dexBase*2, perLot.cexBase*2, addQuoteFees+perLot.dexQuote, perLot.cexQuote*2+perLot.dexQuote+extra) - checkDistribution(0, 0, 0, perLot.dexQuote+extra/2) + checkDistribution(0, 0, 0, perLot.dexQuote+extra/2, false, false) var epok uint64 epoch := func() uint64 { @@ -715,7 +809,7 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { return epok } - checkTransfers := func(expActionTaken bool, expBaseDeposit, expBaseWithdraw, expQuoteDeposit, expQuoteWithdraw uint64) { + checkTransfers := func(expActionTaken bool, expBaseDeposit, expBaseWithdraw, expQuoteDeposit, expQuoteWithdraw uint64, baseInternal, quoteInternal bool) { t.Helper() defer func() { u.wg.Wait() @@ -726,13 +820,36 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { u.pendingDEXOrders = make(map[order.OrderID]*pendingDEXOrder) }() - actionTaken, err := a.tryTransfers(epoch()) + var expBaseExternalDeposit, expBaseExternalWithdraw, expQuoteExternalDeposit, expQuoteExternalWithdraw uint64 + var expBaseInternalDeposit, expBaseInternalWithdraw, expQuoteInternalDeposit, expQuoteInternalWithdraw uint64 + if baseInternal { + expBaseInternalDeposit = expBaseDeposit + expBaseInternalWithdraw = expBaseWithdraw + } else { + expBaseExternalDeposit = expBaseDeposit + expBaseExternalWithdraw = expBaseWithdraw + } + if quoteInternal { + expQuoteInternalDeposit = expQuoteDeposit + expQuoteInternalWithdraw = expQuoteWithdraw + } else { + expQuoteExternalDeposit = expQuoteDeposit + expQuoteExternalWithdraw = expQuoteWithdraw + } + + u.balancesMtx.RLock() + initialDexBase, initialCexBase := u.baseDexBalances[baseID], u.baseCexBalances[baseID] + initialDexQuote, initialCexQuote := u.baseDexBalances[quoteID], u.baseCexBalances[quoteID] + u.balancesMtx.RUnlock() + + actionTaken, err := a.tryTransfers(epoch(), a.distribution) if err != nil { t.Fatalf("Unexpected error: %v", err) } if actionTaken != expActionTaken { t.Fatalf("wrong actionTaken result. wanted %t, got %t", expActionTaken, actionTaken) } + var baseDeposit, quoteDeposit *sendArgs for _, s := range tCore.sends { if s.assetID == baseID { @@ -749,51 +866,75 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { quoteWithdrawal = w } } - if expBaseDeposit > 0 { + + u.balancesMtx.RLock() + dexBaseDiff := u.baseDexBalances[baseID] - initialDexBase + cexBaseDiff := u.baseCexBalances[baseID] - initialCexBase + dexQuoteDiff := u.baseDexBalances[quoteID] - initialDexQuote + cexQuoteDiff := u.baseCexBalances[quoteID] - initialCexQuote + u.balancesMtx.RUnlock() + + if expBaseExternalDeposit > 0 { if baseDeposit == nil { t.Fatalf("Missing base deposit") } - if baseDeposit.value != expBaseDeposit { - t.Fatalf("Wrong value for base deposit. wanted %d, got %d", expBaseDeposit, baseDeposit.value) + if baseDeposit.value != expBaseExternalDeposit { + t.Fatalf("Wrong value for base deposit. wanted %d, got %d", expBaseExternalDeposit, baseDeposit.value) } } else if baseDeposit != nil { t.Fatalf("Unexpected base deposit") } - if expQuoteDeposit > 0 { - if quoteDeposit == nil { - t.Fatalf("Missing quote deposit") - } - if quoteDeposit.value != expQuoteDeposit { - t.Fatalf("Wrong value for quote deposit. wanted %d, got %d", expQuoteDeposit, quoteDeposit.value) - } - } else if quoteDeposit != nil { - t.Fatalf("Unexpected quote deposit") - } - if expBaseWithdraw > 0 { + + if expBaseExternalWithdraw > 0 { if baseWithdrawal == nil { t.Fatalf("Missing base withdrawal") } - if baseWithdrawal.amt != expBaseWithdraw { - t.Fatalf("Wrong value for base withdrawal. wanted %d, got %d", expBaseWithdraw, baseWithdrawal.amt) + if baseWithdrawal.amt != expBaseExternalWithdraw { + t.Fatalf("Wrong value for base withdrawal. wanted %d, got %d", expBaseExternalWithdraw, baseWithdrawal.amt) } } else if baseWithdrawal != nil { t.Fatalf("Unexpected base withdrawal") } - if expQuoteWithdraw > 0 { + + if expQuoteExternalDeposit > 0 { + if quoteDeposit == nil { + t.Fatalf("Missing quote deposit") + } + if quoteDeposit.value != expQuoteExternalDeposit { + t.Fatalf("Wrong value for quote deposit. wanted %d, got %d", expQuoteExternalDeposit, quoteDeposit.value) + } + } else if quoteDeposit != nil { + t.Fatalf("Unexpected quote deposit") + } + + if expQuoteExternalWithdraw > 0 { if quoteWithdrawal == nil { t.Fatalf("Missing quote withdrawal") } - if quoteWithdrawal.amt != expQuoteWithdraw { - t.Fatalf("Wrong value for quote withdrawal. wanted %d, got %d", expQuoteWithdraw, quoteWithdrawal.amt) + if quoteWithdrawal.amt != expQuoteExternalWithdraw { + t.Fatalf("Wrong value for quote withdrawal. wanted %d, got %d", expQuoteExternalWithdraw, quoteWithdrawal.amt) } } else if quoteWithdrawal != nil { t.Fatalf("Unexpected quote withdrawal") } + + if dexBaseDiff != int64(expBaseInternalWithdraw-expBaseInternalDeposit) { + t.Fatalf("wrong dex base diff. wanted %d, got %d", int64(expBaseInternalWithdraw-expBaseInternalDeposit), dexBaseDiff) + } + if cexBaseDiff != int64(expBaseInternalDeposit-expBaseInternalWithdraw) { + t.Fatalf("wrong cex base diff. wanted %d, got %d", int64(expBaseInternalDeposit-expBaseInternalWithdraw), cexBaseDiff) + } + if dexQuoteDiff != int64(expQuoteInternalWithdraw-expQuoteInternalDeposit) { + t.Fatalf("wrong dex quote diff. wanted %d, got %d", int64(expQuoteInternalWithdraw-expQuoteInternalDeposit), dexQuoteDiff) + } + if cexQuoteDiff != int64(expQuoteInternalDeposit-expQuoteInternalWithdraw) { + t.Fatalf("wrong cex quote diff. wanted %d, got %d", int64(expQuoteInternalDeposit-expQuoteInternalWithdraw), cexQuoteDiff) + } } setLots(1, 1) setBals(minDexBase, minCexBase, minDexQuote, minCexQuote) - checkTransfers(false, 0, 0, 0, 0) + checkTransfers(false, 0, 0, 0, 0, false, false) coinID := []byte{0xa0} coin := &tCoin{coinID: coinID, value: 1} @@ -804,20 +945,40 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { // Base deposit. setBals(totalBase, 0, minDexQuote, minCexQuote) - checkTransfers(true, minCexBase, 0, 0, 0) + checkTransfers(true, minCexBase, 0, 0, 0, false, false) + // Base internal deposit. + setBals(totalBase, 0, minDexQuote, minCexQuote) + setAvailableBalances(0, minCexBase, 0, 0) + checkTransfers(false, minCexBase, 0, 0, 0, true, false) + setAvailableBalances(0, 0, 0, 0) // Base withdrawal cex.confirmWithdrawal = &withdrawArgs{txID: txID} setBals(0, totalBase, minDexQuote, minCexQuote) - checkTransfers(true, 0, minDexBase, 0, 0) + checkTransfers(true, 0, minDexBase, 0, 0, false, false) + // Base internal withdrawal + setBals(0, totalBase, minDexQuote, minCexQuote) + setAvailableBalances(minDexBase, 0, 0, 0) + checkTransfers(false, 0, minDexBase, 0, 0, true, false) + setAvailableBalances(0, 0, 0, 0) // Quote deposit setBals(minDexBase, minCexBase, totalQuote, 0) - checkTransfers(true, 0, 0, minCexQuote, 0) + checkTransfers(true, 0, 0, minCexQuote, 0, false, false) + // Quote internal deposit + setBals(minDexBase, minCexBase, totalQuote, 0) + setAvailableBalances(0, 0, 0, minCexQuote) + checkTransfers(false, 0, 0, minCexQuote, 0, false, true) + setAvailableBalances(0, 0, 0, 0) // Quote withdrawal setBals(minDexBase, minCexBase, 0, totalQuote) - checkTransfers(true, 0, 0, 0, minDexQuote) + checkTransfers(true, 0, 0, 0, minDexQuote, false, false) + // Quote internal withdrawal + setBals(minDexBase, minCexBase, 0, totalQuote) + setAvailableBalances(0, 0, minDexQuote, 0) + checkTransfers(false, 0, 0, 0, minDexQuote, false, true) + setAvailableBalances(0, 0, 0, 0) // Base deposit, but we need to cancel an order to free up the funds. setBals(totalBase, 0, minDexQuote, minCexQuote) @@ -843,18 +1004,19 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { u.pendingDEXOrders[oid] = po } checkCancel := func() { + t.Helper() if len(tCore.cancelsPlaced) != 1 || tCore.cancelsPlaced[0] != oid { t.Fatalf("No cancels placed") } tCore.cancelsPlaced = nil } addLocked(baseID, totalBase) - checkTransfers(true, 0, 0, 0, 0) + checkTransfers(true, 0, 0, 0, 0, false, false) checkCancel() setBals(minDexBase, minCexBase, totalQuote, 0) addLocked(quoteID, totalQuote) - checkTransfers(true, 0, 0, 0, 0) + checkTransfers(true, 0, 0, 0, 0, false, false) checkCancel() setBals(0, totalBase /* being withdrawn */, minDexQuote, minCexQuote) @@ -863,17 +1025,17 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { amtWithdrawn: totalBase, } // Distribution should indicate a deposit. - checkDistribution(minCexBase, 0, 0, 0) + checkDistribution(minCexBase, 0, 0, 0, false, false) // But freeUpFunds will come up short. No action taken. - checkTransfers(false, 0, 0, 0, 0) + checkTransfers(false, 0, 0, 0, 0, false, false) setBals(minDexBase, minCexBase, 0, totalQuote) u.pendingWithdrawals["a"] = &pendingWithdrawal{ assetID: quoteID, amtWithdrawn: totalQuote, } - checkDistribution(0, 0, minCexQuote, 0) - checkTransfers(false, 0, 0, 0, 0) + checkDistribution(0, 0, minCexQuote, 0, false, false) + checkTransfers(false, 0, 0, 0, 0, false, false) u.market = mustParseMarket(&core.Market{}) } diff --git a/client/mm/mm.go b/client/mm/mm.go index f06ba59653..ec136160ce 100644 --- a/client/mm/mm.go +++ b/client/mm/mm.go @@ -874,6 +874,7 @@ func (m *MarketMaker) startBot(startCfg *StartConfig, botCfg *BotConfig, cexCfg log: m.botSubLogger(botCfg), botCfg: botCfg, eventLogDB: m.eventLogDB, + internalTransfer: m.internalTransfer, } bot, err := m.newBot(botCfg, adaptorCfg) @@ -1068,6 +1069,29 @@ func validRunningBotCfgUpdate(oldCfg, newCfg *BotConfig) error { return nil } +// internalTransfer is called from the exchange adaptor when attempting an +// internal transfer. +// +// ** IMPORTANT ** No mutexes in exchangeAdaptor should be locked when calling this +// function. +func (m *MarketMaker) internalTransfer(mkt *MarketWithHost, doTransfer doInternalTransferFunc) error { + m.startUpdateMtx.Lock() + defer m.startUpdateMtx.Unlock() + + runningBots := m.runningBotsLookup() + rb, found := runningBots[*mkt] + if !found { + return fmt.Errorf("internalTransfer called for non-running bot %s", mkt) + } + + dex, cex, err := m.availableBalances(mkt, rb.cexCfg) + if err != nil { + return fmt.Errorf("error getting available balances: %v", err) + } + + return doTransfer(dex, cex) +} + // UpdateRunningBotInventory updates the inventory of a running bot. func (m *MarketMaker) UpdateRunningBotInventory(mkt *MarketWithHost, balanceDiffs *BotInventoryDiffs) error { m.startUpdateMtx.Lock() diff --git a/client/mm/mm_arb_market_maker.go b/client/mm/mm_arb_market_maker.go index 174ad291f7..7edb3f7c2a 100644 --- a/client/mm/mm_arb_market_maker.go +++ b/client/mm/mm_arb_market_maker.go @@ -290,7 +290,7 @@ func (a *arbMarketMaker) ordersToPlace() (buys, sells []*TradePlacement, err err // distribution parses the current inventory distribution and checks if better // distributions are possible via deposit or withdrawal. -func (a *arbMarketMaker) distribution() (dist *distribution, err error) { +func (a *arbMarketMaker) distribution(additionalDEX, additionalCEX map[uint32]uint64) (dist *distribution, err error) { cfgI := a.placementLotsV.Load() if cfgI == nil { return nil, errors.New("no placements?") @@ -318,7 +318,7 @@ func (a *arbMarketMaker) distribution() (dist *distribution, err error) { return nil, fmt.Errorf("error getting lot costs: %w", err) } dist = a.newDistribution(perLot) - a.optimizeTransfers(dist, dexSellLots, dexBuyLots, dexSellLots, dexBuyLots) + a.optimizeTransfers(dist, dexSellLots, dexBuyLots, dexSellLots, dexBuyLots, additionalDEX, additionalCEX) return dist, nil } @@ -346,7 +346,7 @@ func (a *arbMarketMaker) rebalance(epoch uint64, book *orderbook.OrderBook) { return } - actionTaken, err := a.tryTransfers(currEpoch) + actionTaken, err := a.tryTransfers(currEpoch, a.distribution) if err != nil { a.log.Errorf("Error performing transfers: %v", err) } else if actionTaken { @@ -386,15 +386,6 @@ func (a *arbMarketMaker) rebalance(epoch uint64, book *orderbook.OrderBook) { a.registerFeeGap() } -func (a *arbMarketMaker) tryTransfers(currEpoch uint64) (actionTaken bool, err error) { - dist, err := a.distribution() - if err != nil { - a.log.Errorf("distribution calculation error: %v", err) - return - } - return a.transfer(dist, currEpoch) -} - func feeGap(core botCoreAdaptor, cex libxc.CEX, baseID, quoteID uint32, lotSize uint64) (*FeeGapStats, error) { s := &FeeGapStats{ BasisPrice: cex.MidGap(baseID, quoteID), diff --git a/client/mm/mm_arb_market_maker_test.go b/client/mm/mm_arb_market_maker_test.go index dbf232739c..cd8629a5d1 100644 --- a/client/mm/mm_arb_market_maker_test.go +++ b/client/mm/mm_arb_market_maker_test.go @@ -457,6 +457,9 @@ func mustParseAdaptorFromMarket(m *core.Market) *unifiedExchangeAdaptor { pendingWithdrawals: make(map[string]*pendingWithdrawal), clientCore: tCore, cexProblems: newCEXProblems(), + internalTransfer: func(mwh *MarketWithHost, fn doInternalTransferFunc) error { + return fn(map[uint32]uint64{}, map[uint32]uint64{}) + }, } u.botCfgV.Store(&BotConfig{ @@ -468,6 +471,12 @@ func mustParseAdaptorFromMarket(m *core.Market) *unifiedExchangeAdaptor { return u } +func updateInternalTransferBalances(u *unifiedExchangeAdaptor, baseBal, quoteBal map[uint32]uint64) { + u.internalTransfer = func(mwh *MarketWithHost, fn doInternalTransferFunc) error { + return fn(baseBal, quoteBal) + } +} + func mustParseAdaptor(cfg *exchangeAdaptorCfg) *unifiedExchangeAdaptor { if cfg.core.(*tCore).market == nil { cfg.core.(*tCore).market = &core.Market{ diff --git a/client/mm/mm_simple_arb.go b/client/mm/mm_simple_arb.go index b32ce3282f..50e3e99099 100644 --- a/client/mm/mm_simple_arb.go +++ b/client/mm/mm_simple_arb.go @@ -380,7 +380,7 @@ func (a *simpleArbMarketMaker) rebalance(newEpoch uint64) { defer a.rebalanceRunning.Store(false) a.log.Tracef("rebalance: epoch %d", newEpoch) - actionTaken, err := a.tryTransfers(newEpoch) + actionTaken, err := a.tryTransfers(newEpoch, a.distribution) if err != nil { a.log.Errorf("Error performing transfers: %v", err) } else if actionTaken { @@ -416,7 +416,7 @@ func (a *simpleArbMarketMaker) rebalance(newEpoch uint64) { a.registerFeeGap() } -func (a *simpleArbMarketMaker) distribution() (dist *distribution, err error) { +func (a *simpleArbMarketMaker) distribution(additionalDEX, additionalCEX map[uint32]uint64) (dist *distribution, err error) { sellVWAP, buyVWAP, err := a.cexCounterRates(1, 1) if err != nil { return nil, fmt.Errorf("error getting cex counter-rates: %w", err) @@ -442,19 +442,10 @@ func (a *simpleArbMarketMaker) distribution() (dist *distribution, err error) { avgBaseLot, avgQuoteLot := float64(perLot.dexBase+perLot.cexBase)/2, float64(perLot.dexQuote+perLot.cexQuote)/2 baseLots := uint64(math.Round(float64(dist.baseInv.total) / avgBaseLot / 2)) quoteLots := uint64(math.Round(float64(dist.quoteInv.total) / avgQuoteLot / 2)) - a.optimizeTransfers(dist, baseLots, quoteLots, baseLots*2, quoteLots*2) + a.optimizeTransfers(dist, baseLots, quoteLots, baseLots*2, quoteLots*2, additionalDEX, additionalCEX) return dist, nil } -func (a *simpleArbMarketMaker) tryTransfers(currEpoch uint64) (actionTaken bool, err error) { - dist, err := a.distribution() - if err != nil { - a.log.Errorf("distribution calculation error: %v", err) - return - } - return a.transfer(dist, currEpoch) -} - func (a *simpleArbMarketMaker) botLoop(ctx context.Context) (*sync.WaitGroup, error) { book, bookFeed, err := a.core.SyncBook(a.host, a.baseID, a.quoteID) if err != nil { diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index 2db4ce7c0a..0be3f38912 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -678,4 +678,8 @@ var EnUS = map[string]*intl.Translation{ "Priority": {T: "Priority"}, "Wallet Balances": {T: "Wallet Balances"}, "Placements": {T: "Placements"}, + "Allow external transfers": {T: "Allow external transfers"}, + "external_transfers_tooltip": {T: "When enabled, the bot will be able to transfer funds between the DEX and the CEX."}, + "Internal transfers only": {T: "Internal transfers only"}, + "internal_only_tooltip": {T: "When enabled, the bot will be able to use any available funds in the wallet to simulate a transfer between the DEX and the CEX, (i.e. increase the bot's DEX balance and decrease the bot's CEX balance when a withdrawal needs to be made) but no actual transfers will be made."}, } diff --git a/client/webserver/site/src/html/mmsettings.tmpl b/client/webserver/site/src/html/mmsettings.tmpl index 2402fef851..c082ecf210 100644 --- a/client/webserver/site/src/html/mmsettings.tmpl +++ b/client/webserver/site/src/html/mmsettings.tmpl @@ -659,6 +659,7 @@ Knobs + {{- /* CEX REBALANCE CHECKBOX */ -}}