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 */ -}}