diff --git a/client/mm/config.go b/client/mm/config.go index ea11a2c2dd..6d12ba7727 100644 --- a/client/mm/config.go +++ b/client/mm/config.go @@ -44,6 +44,9 @@ type CEXConfig struct { // total amount allocated for trading. // The way these are configured will probably be changed to better capture the // reasoning above. +// +// If MinBaseTransfer or MinQuoteTransfer are set to 0, the bot will only +// attempt internal transfers. type AutoRebalanceConfig struct { MinBaseTransfer uint64 `json:"minBaseTransfer"` MinQuoteTransfer uint64 `json:"minQuoteTransfer"` diff --git a/client/mm/exchange_adaptor.go b/client/mm/exchange_adaptor.go index fd5f15fee9..af448341a6 100644 --- a/client/mm/exchange_adaptor.go +++ b/client/mm/exchange_adaptor.go @@ -404,7 +404,7 @@ func (m *market) msgRate(convRate float64) uint64 { return calc.MessageRate(convRate, m.bui, m.qui) } -type doTransferFunc func(dexAvailable, cexAvailable map[uint32]uint64) bool +type doTransferFunc func(dexAvailable, cexAvailable map[uint32]uint64) error // unifiedExchangeAdaptor implements both botCoreAdaptor and botCexAdaptor. type unifiedExchangeAdaptor struct { @@ -424,9 +424,9 @@ type unifiedExchangeAdaptor struct { initialBalances map[uint32]uint64 baseTraits asset.WalletTrait quoteTraits asset.WalletTrait - // ** IMPORTANT ** No mutexes is should be locked when calling this + // ** IMPORTANT ** No mutexes should be locked when calling this // function. - internalTransfer func(*MarketWithHost, doTransferFunc) bool + internalTransfer func(*MarketWithHost, doTransferFunc) error botLooper dex.Connector botLoop *dex.ConnectionMaster @@ -528,7 +528,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 } @@ -612,7 +612,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 @@ -2717,31 +2717,31 @@ type distribution struct { perLot *lotCosts } -func (u *unifiedExchangeAdaptor) newDistribution(perLot *lotCosts) *distribution { - return &distribution{ +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, } + dist.baseInv.dexAdditionalAvailable = additionalDEX[u.baseID] + dist.baseInv.cexAdditionalAvailable = additionalCEX[u.baseID] + dist.quoteInv.dexAdditionalAvailable = additionalDEX[u.quoteID] + dist.quoteInv.cexAdditionalAvailable = additionalCEX[u.quoteID] + return dist } // 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, useMinTransfer bool) { +func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, dexSellLots, dexBuyLots, maxSellLots, maxBuyLots uint64) { baseInv, quoteInv := dist.baseInv, dist.quoteInv perLot := dist.perLot - if u.autoRebalanceCfg == nil && useMinTransfer { + if u.autoRebalanceCfg == nil { return } - - var minBaseTransfer, minQuoteTransfer uint64 - if useMinTransfer { - minBaseTransfer, minQuoteTransfer = u.autoRebalanceCfg.MinBaseTransfer, u.autoRebalanceCfg.MinQuoteTransfer - } + minBaseTransfer, minQuoteTransfer := u.autoRebalanceCfg.MinBaseTransfer, u.autoRebalanceCfg.MinQuoteTransfer additionalBaseFees, additionalQuoteFees := perLot.baseFunding, perLot.quoteFunding if u.baseID == u.quoteFeeID { @@ -2805,6 +2805,8 @@ func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, fees uint64 baseDeposit, baseWithdraw uint64 quoteDeposit, quoteWithdraw uint64 + baseTransferInternal bool + quoteTransferInternal bool } baseSplits := [][2]uint64{ {baseInv.dex, baseInv.cex}, // current @@ -2817,74 +2819,153 @@ func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, {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(dist.baseInv.dexAdditionalAvailable, toWithdraw, dist.baseInv.cexAvail) + if toWithdraw > dist.baseInv.dexAdditionalAvailable { + 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(dist.baseInv.cexAdditionalAvailable, toDeposit, dist.baseInv.dexAvail) + if toDeposit > dist.baseInv.cexAdditionalAvailable { + 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(dist.quoteInv.dexAdditionalAvailable, toWithdraw, dist.quoteInv.cexAvail) + if toWithdraw > dist.quoteInv.dexAdditionalAvailable { + 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(dist.quoteInv.cexAdditionalAvailable, toDeposit, dist.quoteInv.dexAvail) + if toDeposit > dist.quoteInv.cexAdditionalAvailable { + 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) { + scoreSplitSource(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote, allExternal) + scoreSplitSource(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote, allInternal) + scoreSplitSource(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote, onlyBaseInternal) + scoreSplitSource(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote, onlyQuoteInternal) + } + // Try to hit all possible combinations. for _, b := range baseSplits { dexBaseLots, cexBaseLots, extraBase := baseSplit(b[0], b[1]) @@ -2895,6 +2976,7 @@ func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, scoreSplit(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote) } } + // Try in both directions. for _, q := range quoteSplits { dexQuoteLots, cexQuoteLots, extraQuote := quoteSplit(q[0], q[1]) @@ -2916,85 +2998,49 @@ func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, 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 -} - -// tryInternalTransfers attempts to use available funds that are not reserved -// by any bots to rebalance between the DEX and the CEX instead of doing an -// actual deposit or withdrawal. True is returned if the deposits and/or -// withdrawals were completely covered using available funds. -func (u *unifiedExchangeAdaptor) tryInternalTransfers(dist *distribution) bool { - doTransfer := func(dexAvail, cexAvail map[uint32]uint64) bool { - u.balancesMtx.Lock() - defer u.balancesMtx.Unlock() - baseInv, quoteInv := dist.baseInv, dist.quoteInv - dexDiffs, cexDiffs := make(map[uint32]int64), make(map[uint32]int64) - - complete := true - transferDone := false + if split.baseTransferInternal { + baseInv.toInternalDeposit = split.baseDeposit + baseInv.toInternalWithdraw = split.baseWithdraw + } else { + baseInv.toDeposit = split.baseDeposit + baseInv.toWithdraw = split.baseWithdraw + } - if baseInv.toDeposit > 0 { - botDEXBal := u.dexBalance(u.baseID).Available - toDeposit := utils.Min(baseInv.toDeposit, cexAvail[u.baseID], botDEXBal) - complete = complete && toDeposit == baseInv.toDeposit - transferDone = transferDone || toDeposit > 0 - u.baseCexBalances[u.baseID] += int64(toDeposit) - u.baseDexBalances[u.baseID] -= int64(toDeposit) - dexDiffs = map[uint32]int64{u.baseID: -int64(toDeposit)} - cexDiffs = map[uint32]int64{u.baseID: int64(toDeposit)} - } + if split.quoteTransferInternal { + quoteInv.toInternalDeposit = split.quoteDeposit + quoteInv.toInternalWithdraw = split.quoteWithdraw + } else { + quoteInv.toDeposit = split.quoteDeposit + quoteInv.toWithdraw = split.quoteWithdraw + } +} - if baseInv.toWithdraw > 0 { - botCEXBal := u.cexBalance(u.baseID).Available - toWithdraw := utils.Min(baseInv.toWithdraw, dexAvail[u.baseID], botCEXBal) - complete = complete && toWithdraw == baseInv.toWithdraw - transferDone = transferDone || toWithdraw > 0 - u.baseCexBalances[u.baseID] -= int64(toWithdraw) - u.baseDexBalances[u.baseID] += int64(toWithdraw) - dexDiffs = map[uint32]int64{u.baseID: int64(toWithdraw)} - cexDiffs = map[uint32]int64{u.baseID: -int64(toWithdraw)} - } +type distributionFunc func(dexAvail, cexAvail map[uint32]uint64) (*distribution, error) - if quoteInv.toDeposit > 0 { - botDEXBal := u.dexBalance(u.quoteID).Available - toDeposit := utils.Min(quoteInv.toDeposit, cexAvail[u.quoteID], botDEXBal) - complete = complete && toDeposit == quoteInv.toDeposit - transferDone = transferDone || toDeposit > 0 - u.baseCexBalances[u.quoteID] += int64(toDeposit) - u.baseDexBalances[u.quoteID] -= int64(toDeposit) - dexDiffs[u.quoteID] = -int64(toDeposit) - cexDiffs[u.quoteID] = int64(toDeposit) - } +func (u *unifiedExchangeAdaptor) doInternalTransfers(dist *distribution) { + u.balancesMtx.Lock() + dexDiffs, cexDiffs := make(map[uint32]int64), make(map[uint32]int64) - if quoteInv.toWithdraw > 0 { - botCEXBal := u.cexBalance(u.quoteID).Available - toWithdraw := utils.Min(quoteInv.toWithdraw, dexAvail[u.quoteID], botCEXBal) - complete = complete && toWithdraw == quoteInv.toWithdraw - transferDone = transferDone || toWithdraw > 0 - u.baseCexBalances[u.quoteID] -= int64(toWithdraw) - u.baseDexBalances[u.quoteID] += int64(toWithdraw) - dexDiffs[u.quoteID] = int64(toWithdraw) - cexDiffs[u.quoteID] = -int64(toWithdraw) - } + 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) - if transferDone { - u.logBalanceAdjustments(dexDiffs, cexDiffs, "Internal transfer") - } + 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] + u.balancesMtx.Unlock() - return complete + if dexDiffs[u.baseID] != 0 || dexDiffs[u.quoteID] != 0 { + u.logBalanceAdjustments(dexDiffs, cexDiffs, "internal transfers") } - - return u.internalTransfer(u.mwh, doTransfer) } -// tryExternalTransfers attempts to perform the transfers specified in the -// distribution by doing an actual deposit or withdrawal. -func (u *unifiedExchangeAdaptor) tryExternalTransfers(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 } @@ -3024,6 +3070,7 @@ func (u *unifiedExchangeAdaptor) tryExternalTransfers(dist *distribution, currEp } cancels = cs } + if quoteInv.toDeposit > 0 || baseInv.toWithdraw > 0 { var toFree uint64 if quoteInv.dexAvail < quoteInv.toDeposit { @@ -3078,36 +3125,30 @@ func (u *unifiedExchangeAdaptor) tryExternalTransfers(dist *distribution, currEp return false, fmt.Errorf("error withdrawing quote: %w", err) } } + return true, nil } -// tryTransfers attempts to optimize the asset distribution by moving funds -// between the DEX and the CEX. If the distribution is already optimal, no -// transfers are made. If the distribution is not optimal, the bot will first -// attempt to use available funds to rebalance the distribution. If this is -// not sufficient, an actual deposit or withdrawal will be done. -func (u *unifiedExchangeAdaptor) tryTransfers(currEpoch uint64, distribution func(bool) (*distribution, error)) (actionTaken bool, err error) { - dist, err := distribution(false) - if err != nil { - return false, fmt.Errorf("distribution calculation error: %w", err) - } - - baseInv, quoteInv := dist.baseInv, dist.quoteInv - if baseInv.toDeposit+baseInv.toWithdraw+quoteInv.toDeposit+quoteInv.toWithdraw == 0 { +func (u *unifiedExchangeAdaptor) tryTransfers(currEpoch uint64, df distributionFunc) (actionTaken bool, err error) { + if u.autoRebalanceCfg == nil { return false, nil } - complete := u.tryInternalTransfers(dist) - if complete || 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) + } - dist, err = distribution(true) + u.doInternalTransfers(dist) + return nil + }) if err != nil { - return false, fmt.Errorf("distribution calculation error: %w", err) + return false, err } - return u.tryExternalTransfers(dist, currEpoch) + return u.doExternalTransfers(dist, currEpoch) } // assetInventory is an accounting of the distribution of base- or quote-asset @@ -3116,20 +3157,21 @@ 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 + dexAdditionalAvailable uint64 + cexAdditionalAvailable uint64 + + toDeposit uint64 + toWithdraw uint64 + toInternalDeposit uint64 + toInternalWithdraw uint64 } // inventory generates a current view of the the bot's asset distribution. @@ -3142,14 +3184,10 @@ 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 @@ -3268,6 +3306,7 @@ func (u *unifiedExchangeAdaptor) updateFeeRates() error { funding: buyFundingFees, bookingFeesPerLot: buyBookingFeesPerLot, } + u.sellFees = &orderFees{ LotFeeRange: &LotFeeRange{ Max: maxSellFees, @@ -3586,7 +3625,7 @@ type exchangeAdaptorCfg struct { log dex.Logger eventLogDB eventLogDB botCfg *BotConfig - internalTransfer func(*MarketWithHost, doTransferFunc) bool + internalTransfer func(*MarketWithHost, doTransferFunc) error } // newUnifiedExchangeAdaptor is the constructor for a unifiedExchangeAdaptor. diff --git a/client/mm/exchange_adaptor_test.go b/client/mm/exchange_adaptor_test.go index ff633e549f..d71c80646f 100644 --- a/client/mm/exchange_adaptor_test.go +++ b/client/mm/exchange_adaptor_test.go @@ -468,164 +468,6 @@ func TestFreeUpFunds(t *testing.T) { check(quoteID, false, quoteLot+1, lotSize) } -func TestInternalTransfer(t *testing.T) { - const baseID, quoteID = 42, 0 - - type test struct { - name string - dist *distribution - dexAvailable map[uint32]uint64 - cexAvailable map[uint32]uint64 - dexBotBalance map[uint32]uint64 - cexBotBalance map[uint32]uint64 - expDEXDiffs map[uint32]int64 - expCEXDiffs map[uint32]int64 - expComplete bool - } - - tests := []*test{ - { - name: "base deposit, quote withdraw, complete", - dist: &distribution{ - baseInv: &assetInventory{ - toDeposit: 1e9, - }, - quoteInv: &assetInventory{ - toWithdraw: 1e8, - }, - }, - dexAvailable: map[uint32]uint64{ - quoteID: 1.1e8, - }, - cexAvailable: map[uint32]uint64{ - baseID: 1e9, - }, - dexBotBalance: map[uint32]uint64{ - baseID: 1e9, - }, - cexBotBalance: map[uint32]uint64{ - quoteID: 1e8, - }, - expDEXDiffs: map[uint32]int64{ - baseID: -1e9, - quoteID: 1e8, - }, - expCEXDiffs: map[uint32]int64{ - baseID: 1e9, - quoteID: -1e8, - }, - expComplete: true, - }, - { - name: "base withdraw, quote deposit, incomplete due to available balance", - dist: &distribution{ - baseInv: &assetInventory{ - toWithdraw: 1e9, - }, - quoteInv: &assetInventory{ - toDeposit: 1e8, - }, - }, - dexAvailable: map[uint32]uint64{ - baseID: 1e8, - }, - cexAvailable: map[uint32]uint64{ - quoteID: 1e7, - }, - dexBotBalance: map[uint32]uint64{ - quoteID: 1e8, - }, - cexBotBalance: map[uint32]uint64{ - baseID: 1e9, - }, - expDEXDiffs: map[uint32]int64{ - baseID: 1e8, - quoteID: -1e7, - }, - expCEXDiffs: map[uint32]int64{ - baseID: -1e8, - quoteID: 1e7, - }, - expComplete: false, - }, - { - name: "base withdraw, quote deposit, incomplete due to bot balance", - dist: &distribution{ - baseInv: &assetInventory{ - toWithdraw: 1e9, - }, - quoteInv: &assetInventory{ - toDeposit: 1e8, - }, - }, - dexAvailable: map[uint32]uint64{ - baseID: 1e9, - }, - cexAvailable: map[uint32]uint64{ - quoteID: 1e8, - }, - dexBotBalance: map[uint32]uint64{ - quoteID: 1e6, - }, - cexBotBalance: map[uint32]uint64{ - baseID: 1e7, - }, - expDEXDiffs: map[uint32]int64{ - baseID: 1e7, - quoteID: -1e6, - }, - expCEXDiffs: map[uint32]int64{ - baseID: -1e7, - quoteID: 1e6, - }, - expComplete: false, - }, - } - - runTest := func(test *test) { - u := mustParseAdaptorFromMarket(&core.Market{ - LotSize: 1e8, - BaseID: baseID, - QuoteID: quoteID, - RateStep: 1e2, - }) - u.internalTransfer = func(_ *MarketWithHost, doTransfer doTransferFunc) bool { - return doTransfer(test.dexAvailable, test.cexAvailable) - } - - for assetID, bal := range test.dexBotBalance { - u.baseDexBalances[assetID] = int64(bal) - } - for assetID, bal := range test.cexBotBalance { - u.baseCexBalances[assetID] = int64(bal) - } - - complete := u.tryInternalTransfers(test.dist) - - if complete != test.expComplete { - t.Fatalf("%s: expected complete %v, got %v", test.name, test.expComplete, complete) - } - - for assetID, balance := range u.baseDexBalances { - if test.expDEXDiffs[assetID]+int64(test.dexBotBalance[assetID]) != balance { - t.Fatalf("%s: expected dex diff for asset %d = %d, got %d", - test.name, assetID, test.expDEXDiffs[assetID], balance-int64(test.dexBotBalance[assetID])) - } - } - - for assetID, balance := range u.baseCexBalances { - if test.expCEXDiffs[assetID]+int64(test.cexBotBalance[assetID]) != balance { - t.Fatalf("%s: expected cex diff for asset %d = %d, got %d", - test.name, assetID, test.expDEXDiffs[assetID], balance-int64(test.cexBotBalance[assetID])) - } - } - } - - for _, test := range tests { - runTest(test) - } -} - func TestDistribution(t *testing.T) { // utxo/utxo testDistribution(t, 42, 0) @@ -776,58 +618,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(true) + 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 != expBaseExternalWithdraw { + t.Fatalf("wrong base withrawal size. wanted %d, got %d", expBaseExternalWithdraw, dist.baseInv.toWithdraw) } - if dist.baseInv.toWithdraw != baseWithdraw { - t.Fatalf("wrong base withdrawal size. wanted %d, got %d", baseWithdraw, dist.baseInv.toWithdraw) + if dist.quoteInv.toDeposit != expQuoteExternalDeposit { + t.Fatalf("wrong quote deposit size. wanted %d, got %d", expQuoteExternalDeposit, dist.quoteInv.toDeposit) } - if dist.quoteInv.toDeposit != quoteDeposit { - t.Fatalf("wrong quote deposit size. wanted %d, got %d", quoteDeposit, dist.quoteInv.toDeposit) + if dist.quoteInv.toWithdraw != expQuoteExternalWithdraw { + t.Fatalf("wrong quote withrawal size. wanted %d, got %d", expQuoteExternalWithdraw, dist.quoteInv.toWithdraw) } - if dist.quoteInv.toWithdraw != quoteWithdraw { - t.Fatalf("wrong quote withdrawal size. wanted %d, got %d", quoteWithdraw, 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) + checkDistribution(0, 0, 0, 0, false, false) a.autoRebalanceCfg.MinBaseTransfer = 0 // Same for quote 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) + checkDistribution(0, 0, 0, 0, false, false) a.autoRebalanceCfg.MinQuoteTransfer = 0 + // Base deposit 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. @@ -835,39 +755,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 { @@ -875,7 +803,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() @@ -886,13 +814,36 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { u.pendingDEXOrders = make(map[order.OrderID]*pendingDEXOrder) }() - actionTaken, err := u.tryTransfers(epoch(), a.distribution) + 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 { @@ -909,51 +860,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} @@ -964,20 +939,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) @@ -1003,18 +998,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) @@ -1023,17 +1019,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 4e55e500f7..40c33f502f 100644 --- a/client/mm/mm.go +++ b/client/mm/mm.go @@ -926,30 +926,27 @@ func validRunningBotCfgUpdate(oldCfg, newCfg *BotConfig) error { return nil } -// internalTransfer is called from the exchange adaptor to attempt an internal -// transfer rather than a withdrawal / deposit. +// 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 doTransferFunc) (complete bool) { +func (m *MarketMaker) internalTransfer(mkt *MarketWithHost, doTransfer doTransferFunc) error { m.startUpdateMtx.Lock() defer m.startUpdateMtx.Unlock() runningBots := m.runningBotsLookup() rb, found := runningBots[*mkt] if !found { - m.log.Errorf("internalTransfer called for non-running bot %s", mkt) - return false + return fmt.Errorf("internalTransfer called for non-running bot %s", mkt) } if rb.cexCfg == nil { - m.log.Errorf("internalTransfer called for bot without CEX config %s", mkt) - return false + return fmt.Errorf("internalTransfer called for bot without CEX config %s", mkt) } dex, cex, err := m.availableBalances(mkt, rb.cexCfg) if err != nil { - m.log.Errorf("error getting available balances: %v", err) - return false + return fmt.Errorf("error getting available balances: %v", err) } return doTransfer(dex, cex) diff --git a/client/mm/mm_arb_market_maker.go b/client/mm/mm_arb_market_maker.go index f75014725a..2691e98f81 100644 --- a/client/mm/mm_arb_market_maker.go +++ b/client/mm/mm_arb_market_maker.go @@ -293,7 +293,7 @@ func (a *arbMarketMaker) ordersToPlace() (buys, sells []*multiTradePlacement) { // distribution parses the current inventory distribution and checks if better // distributions are possible via deposit or withdrawal. -func (a *arbMarketMaker) distribution(useMinTransfer bool) (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?") @@ -320,8 +320,8 @@ func (a *arbMarketMaker) distribution(useMinTransfer bool) (dist *distribution, if perLot == nil { return nil, fmt.Errorf("error getting lot costs: %w", err) } - dist = a.newDistribution(perLot) - a.optimizeTransfers(dist, dexSellLots, dexBuyLots, dexSellLots, dexBuyLots, useMinTransfer) + dist = a.newDistribution(perLot, additionalDEX, additionalCEX) + a.optimizeTransfers(dist, dexSellLots, dexBuyLots, dexSellLots, dexBuyLots) return dist, nil } diff --git a/client/mm/mm_arb_market_maker_test.go b/client/mm/mm_arb_market_maker_test.go index 968e1ea6d4..5ebd4763dd 100644 --- a/client/mm/mm_arb_market_maker_test.go +++ b/client/mm/mm_arb_market_maker_test.go @@ -443,12 +443,18 @@ func mustParseAdaptorFromMarket(m *core.Market) *unifiedExchangeAdaptor { eventLogDB: newTEventLogDB(), pendingDeposits: make(map[string]*pendingDeposit), pendingWithdrawals: make(map[string]*pendingWithdrawal), - internalTransfer: func(*MarketWithHost, doTransferFunc) bool { - return false + internalTransfer: func(mwh *MarketWithHost, fn doTransferFunc) error { + return fn(map[uint32]uint64{}, map[uint32]uint64{}) }, } } +func updateInternalTransferBalances(u *unifiedExchangeAdaptor, baseBal, quoteBal map[uint32]uint64) { + u.internalTransfer = func(mwh *MarketWithHost, fn doTransferFunc) 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 6a53169b91..01d2fd1366 100644 --- a/client/mm/mm_simple_arb.go +++ b/client/mm/mm_simple_arb.go @@ -400,7 +400,7 @@ func (a *simpleArbMarketMaker) rebalance(newEpoch uint64) { a.registerFeeGap() } -func (a *simpleArbMarketMaker) distribution(useMinTransfer bool) (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) @@ -422,11 +422,11 @@ func (a *simpleArbMarketMaker) distribution(useMinTransfer bool) (dist *distribu if perLot == nil { return nil, fmt.Errorf("error getting lot costs: %w", err) } - dist = a.newDistribution(perLot) + dist = a.newDistribution(perLot, additionalDEX, additionalCEX) 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, useMinTransfer) + a.optimizeTransfers(dist, baseLots, quoteLots, baseLots*2, quoteLots*2) return dist, nil } diff --git a/dex/utils/generics.go b/dex/utils/generics.go index 572442b603..d5c56ae4da 100644 --- a/dex/utils/generics.go +++ b/dex/utils/generics.go @@ -35,6 +35,13 @@ func MapKeys[K comparable, V any](m map[K]V) []K { return ks } +func SafeSub[I constraints.Unsigned](a I, b I) I { + if a < b { + return 0 + } + return a - b +} + func Min[I constraints.Ordered](m I, ns ...I) I { min := m for _, n := range ns {