diff --git a/client/mm/mm.go b/client/mm/mm.go index dd0b33cbca..587552a589 100644 --- a/client/mm/mm.go +++ b/client/mm/mm.go @@ -49,6 +49,18 @@ type dexOrderBook interface { var _ dexOrderBook = (*orderbook.OrderBook)(nil) +// botBalance keeps track of the amount of funds available for a +// bot's use, and the amount that is currently locked/pending for +// various reasons. Only the Available balance matters for the +// behavior of the bots. The others are just tracked to inform the +// user. +type botBalance struct { + Available uint64 `json:"available"` + FundingOrder uint64 `json:"fundingOrder"` + PendingRedeem uint64 `json:"pendingRedeem"` + PendingRefund uint64 `json:"pendingRefund"` +} + // botBalance keeps track of the bot balances. // When the MarketMaker is created, it will allocate the proper amount of // funds for each bot. Then, as the bot makes trades, each bot's balances @@ -98,9 +110,9 @@ var _ dexOrderBook = (*orderbook.OrderBook)(nil) // - ToAsset: // INCREASE: if isAccountLocker, ExcessRedeemFees (RedeemFeesLockedFunds - ActualRedeemFees) // else ExcessRedeemFees (MaxRedeemFeesForLotsRedeemed - ActualRedeemFees) -type botBalance struct { +type botBalances struct { mtx sync.RWMutex - balances map[uint32]uint64 + balances map[uint32]*botBalance } // orderInfo stores the necessary information the MarketMaker needs for a @@ -119,6 +131,7 @@ type orderInfo struct { singleLotRedeemFees uint64 unusedLockedFundsReturned bool excessFeesReturned bool + matchesSeen map[order.MatchID]struct{} matchesRefunded map[order.MatchID]struct{} } @@ -149,7 +162,7 @@ type MarketMaker struct { log dex.Logger core clientCore doNotKillWhenBotsStop bool // used for testing - botBalances map[string]*botBalance + botBalances map[string]*botBalances ordersMtx sync.RWMutex orders map[order.OrderID]*orderInfo @@ -246,7 +259,7 @@ func validateAndFilterEnabledConfigs(cfgs []*BotConfig) ([]*BotConfig, error) { } func (m *MarketMaker) setupBalances(cfgs []*BotConfig) error { - m.botBalances = make(map[string]*botBalance, len(cfgs)) + m.botBalances = make(map[string]*botBalances, len(cfgs)) type trackedBalance struct { balanceAvailable uint64 @@ -309,10 +322,14 @@ func (m *MarketMaker) setupBalances(cfgs []*BotConfig) error { quoteBalance.balanceReserved += quoteRequired mktID := dexMarketID(cfg.Host, cfg.BaseAsset, cfg.QuoteAsset) - m.botBalances[mktID] = &botBalance{ - balances: map[uint32]uint64{ - cfg.BaseAsset: baseRequired, - cfg.QuoteAsset: quoteRequired, + m.botBalances[mktID] = &botBalances{ + balances: map[uint32]*botBalance{ + cfg.BaseAsset: { + Available: baseRequired, + }, + cfg.QuoteAsset: { + Available: quoteRequired, + }, }, } } @@ -331,52 +348,92 @@ func (m *MarketMaker) isAccountLocker(assetID uint32) bool { return walletState.Traits.IsAccountLocker() } -// increaseBotBalance increases a bot's balance of an asset. -func (m *MarketMaker) increaseBotBalance(botID string, assetID uint32, amount uint64, oidB dex.Bytes) { - bb := m.botBalances[botID] - if bb == nil { - m.log.Errorf("increaseBotBalance: bot %s not found", botID) - return - } +type botBalanceType uint8 - bb.mtx.Lock() - defer bb.mtx.Unlock() +const ( + balTypeAvailable botBalanceType = iota + balTypeFundingOrder + balTypePendingRedeem + balTypePendingRefund +) - if _, found := bb.balances[assetID]; found { - m.log.Debugf("botID: %s, increase asset %d, amount %d. before: %d, after: %d", botID, assetID, amount, bb.balances[assetID], bb.balances[assetID]+amount) - bb.balances[assetID] += amount - } else { - m.log.Errorf("increaseBotBalance: asset %d not found for bot %s", assetID, botID) - } +// balanceMod is passed to modifyBotBalance to increase or decrease one +// of the bot's balances for an asset. +type balanceMod struct { + increase bool + assetID uint32 + typ botBalanceType + amount uint64 } -// decreaseBotBalance decreases a bot's balance of an asset. -func (m *MarketMaker) decreaseBotBalance(botID string, assetID uint32, amount uint64, oidB dex.Bytes) { +// modifyBotBalance does modifications to the various bot balances. +func (m *MarketMaker) modifyBotBalance(botID string, mods []*balanceMod) { bb := m.botBalances[botID] if bb == nil { - m.log.Errorf("decreaseBalance: bot %s not found", botID) + m.log.Errorf("increaseBotBalance: bot %s not found", botID) return } bb.mtx.Lock() defer bb.mtx.Unlock() - if _, found := bb.balances[assetID]; found { - if bb.balances[assetID] < amount { - m.log.Errorf("decreaseBalance: bot %s has insufficient balance for asset %d. "+ - "balance: %d, amount: %d", botID, assetID, bb.balances[assetID], amount) - bb.balances[assetID] = 0 - return - } - for botID, bb := range m.botBalances { - m.log.Debugf("botID: %s", botID) - for assetID, balance := range bb.balances { - m.log.Debugf(" asset %d, balance: %d", assetID, balance) + for _, mod := range mods { + assetBalance, found := bb.balances[mod.assetID] + if !found { + m.log.Errorf("modifyBotBalance: asset %d not found for bot %s", mod.assetID, botID) + continue + } + + switch mod.typ { + case balTypeAvailable: + if mod.increase { + assetBalance.Available += mod.amount + } else { + if assetBalance.Available < mod.amount { + m.log.Errorf("modifyBotBalance: bot %s has insufficient balance for asset %d. "+ + "balance: %d, amount: %d", botID, mod.assetID, assetBalance.Available, mod.amount) + assetBalance.Available = 0 + return + } + assetBalance.Available -= mod.amount + } + case balTypeFundingOrder: + if mod.increase { + assetBalance.FundingOrder += mod.amount + } else { + if assetBalance.FundingOrder < mod.amount { + m.log.Errorf("modifyBotBalance: bot %s has insufficient funding order for asset %d. "+ + "balance: %d, amount: %d", botID, mod.assetID, assetBalance.FundingOrder, mod.amount) + assetBalance.FundingOrder = 0 + return + } + assetBalance.FundingOrder -= mod.amount + } + case balTypePendingRedeem: + if mod.increase { + assetBalance.PendingRedeem += mod.amount + } else { + if assetBalance.PendingRedeem < mod.amount { + m.log.Errorf("modifyBotBalance: bot %s has insufficient pending redeem for asset %d. "+ + "balance: %d, amount: %d", botID, mod.assetID, assetBalance.PendingRedeem, mod.amount) + assetBalance.PendingRedeem = 0 + return + } + assetBalance.PendingRedeem -= mod.amount + } + case balTypePendingRefund: + if mod.increase { + assetBalance.PendingRefund += mod.amount + } else { + if assetBalance.PendingRefund < mod.amount { + m.log.Errorf("modifyBotBalance: bot %s has insufficient pending refund for asset %d. "+ + "balance: %d, amount: %d", botID, mod.assetID, assetBalance.PendingRefund, mod.amount) + assetBalance.PendingRefund = 0 + return + } + assetBalance.PendingRefund -= mod.amount } } - bb.balances[assetID] -= amount - } else { - m.log.Errorf("decreaseBalance: asset %d not found for bot %s", assetID, botID) } } @@ -392,7 +449,7 @@ func (m *MarketMaker) botBalance(botID string, assetID uint32) uint64 { defer bb.mtx.RUnlock() if _, found := bb.balances[assetID]; found { - return bb.balances[assetID] + return bb.balances[assetID].Available } m.log.Errorf("balance: asset %d not found for bot %s", assetID, botID) @@ -422,9 +479,8 @@ func (m *MarketMaker) removeOrderInfo(id dex.Bytes) { // handleMatchUpdate adds the redeem/refund amount to the bot's balance if the // match is in the confirmed state. func (m *MarketMaker) handleMatchUpdate(match *core.Match, oid dex.Bytes) { - if match.Status != order.MatchConfirmed && match.Refund == nil { - return - } + var matchID order.MatchID + copy(matchID[:], match.MatchID) orderInfo := m.getOrderInfo(oid) if orderInfo == nil { @@ -432,8 +488,35 @@ func (m *MarketMaker) handleMatchUpdate(match *core.Match, oid dex.Bytes) { return } - var matchID order.MatchID - copy(matchID[:], match.MatchID) + if _, seen := orderInfo.matchesSeen[matchID]; !seen { + orderInfo.matchesSeen[matchID] = struct{}{} + + var maxRedeemFees uint64 + if orderInfo.initialRedeemFeesLocked == 0 { + numLots := match.Qty / orderInfo.lotSize + maxRedeemFees = numLots * orderInfo.singleLotRedeemFees + } + + var balanceMods []*balanceMod + if orderInfo.order.Sell { + balanceMods = []*balanceMod{ + {false, orderInfo.order.BaseID, balTypeFundingOrder, match.Qty}, + {true, orderInfo.order.QuoteID, balTypePendingRedeem, calc.BaseToQuote(match.Rate, match.Qty) - maxRedeemFees}, + } + } else { + balanceMods = []*balanceMod{ + {false, orderInfo.order.QuoteID, balTypeFundingOrder, calc.BaseToQuote(match.Rate, match.Qty)}, + {true, orderInfo.order.BaseID, balTypePendingRedeem, match.Qty - maxRedeemFees}, + } + } + + m.modifyBotBalance(orderInfo.bot, balanceMods) + } + + if match.Status != order.MatchConfirmed && match.Refund == nil { + return + } + if _, handled := orderInfo.matchesRefunded[matchID]; handled { return } @@ -444,15 +527,31 @@ func (m *MarketMaker) handleMatchUpdate(match *core.Match, oid dex.Bytes) { // TODO: Currently refunds are not handled properly. Core gives no way to // retrieve the refund fee. Core will need to make this information available, // and then the fee will need to be taken into account before increasing the - // bot's balance. - refundAsset := orderInfo.order.BaseID - refundQty := match.Qty - if !orderInfo.order.Sell { - refundAsset = orderInfo.order.QuoteID - refundQty = calc.BaseToQuote(match.Rate, refundQty) + // bot's balance. Also, currently we are not detecting that a refund will happen, + // only that it has already happened. When a match has been revoked, the bot's + // PendingRefund balance must be increased, and the PendingRedeem amount must be + // decreased. + + var maxRedeemFees uint64 + if orderInfo.initialRedeemFeesLocked == 0 { + numLots := match.Qty / orderInfo.lotSize + maxRedeemFees = numLots * orderInfo.singleLotRedeemFees + } + + var balanceMods []*balanceMod + if orderInfo.order.Sell { + balanceMods = []*balanceMod{ + {false, orderInfo.order.QuoteID, balTypePendingRedeem, calc.BaseToQuote(match.Rate, match.Qty) - maxRedeemFees}, + {true, orderInfo.order.BaseID, balTypeAvailable, match.Qty}, + } + } else { + balanceMods = []*balanceMod{ + {false, orderInfo.order.BaseID, balTypePendingRedeem, match.Qty - maxRedeemFees}, + {true, orderInfo.order.QuoteID, balTypeAvailable, calc.BaseToQuote(match.Rate, match.Qty)}, + } } m.log.Tracef("oid: %s, increasing balance due to refund") - m.increaseBotBalance(orderInfo.bot, refundAsset, refundQty, oid) + m.modifyBotBalance(orderInfo.bot, balanceMods) } else { redeemAsset := orderInfo.order.BaseID redeemQty := match.Qty @@ -467,7 +566,12 @@ func (m *MarketMaker) handleMatchUpdate(match *core.Match, oid dex.Bytes) { maxRedeemFees = numLots * orderInfo.singleLotRedeemFees } m.log.Tracef("oid: %s, increasing balance due to redeem, redeemQty - %v, maxRedeemFees - %v", oid, redeemQty, maxRedeemFees) - m.increaseBotBalance(orderInfo.bot, redeemAsset, redeemQty-maxRedeemFees, oid) + + balanceMods := []*balanceMod{ + {false, redeemAsset, balTypePendingRedeem, redeemQty - maxRedeemFees}, + {true, redeemAsset, balTypeAvailable, redeemQty - maxRedeemFees}, + } + m.modifyBotBalance(orderInfo.bot, balanceMods) } if orderInfo.finishedProcessing() { @@ -530,7 +634,12 @@ func (m *MarketMaker) handleOrderUpdate(o *core.Order) { if usedFunds < orderInfo.initialFundsLocked { m.log.Tracef("oid: %s, returning unused locked funds, initialFundsLocked %v, filledQty %v, filledLots %v, maxSwapFees %v", o.ID, orderInfo.initialFundsLocked, filledQty, filledLots, maxSwapFees) - m.increaseBotBalance(orderInfo.bot, fromAsset, orderInfo.initialFundsLocked-usedFunds, o.ID) + + balanceMods := []*balanceMod{ + {true, fromAsset, balTypeAvailable, orderInfo.initialFundsLocked - usedFunds}, + {false, fromAsset, balTypeFundingOrder, orderInfo.initialFundsLocked - usedFunds}, + } + m.modifyBotBalance(orderInfo.bot, balanceMods) } else { m.log.Errorf("oid: %v - usedFunds %d >= initialFundsLocked %d", hex.EncodeToString(o.ID), orderInfo.initialFundsLocked) @@ -545,7 +654,11 @@ func (m *MarketMaker) handleOrderUpdate(o *core.Order) { maxSwapFees := filledLots * orderInfo.singleLotSwapFees if maxSwapFees > o.FeesPaid.Swap { m.log.Tracef("oid: %s, return excess swap fees, maxSwapFees %v, swap fees %v", o.ID, maxSwapFees, o.FeesPaid.Swap) - m.increaseBotBalance(orderInfo.bot, fromAsset, maxSwapFees-o.FeesPaid.Swap, o.ID) + balanceMods := []*balanceMod{ + {true, fromAsset, balTypeAvailable, maxSwapFees - o.FeesPaid.Swap}, + {false, fromAsset, balTypeFundingOrder, maxSwapFees}, + } + m.modifyBotBalance(orderInfo.bot, balanceMods) } else if maxSwapFees < o.FeesPaid.Swap { m.log.Errorf("oid: %v - maxSwapFees %d < swap fees %d", hex.EncodeToString(o.ID), maxSwapFees, o.FeesPaid.Swap) } @@ -555,7 +668,11 @@ func (m *MarketMaker) handleOrderUpdate(o *core.Order) { if orderInfo.initialRedeemFeesLocked > o.FeesPaid.Redemption { m.log.Tracef("oid: %s, return excess redeem fees (accountLocker), initialRedeemFeesLocked %v, redemption fees %v", o.ID, orderInfo.initialRedeemFeesLocked, o.FeesPaid.Redemption) - m.increaseBotBalance(orderInfo.bot, toAsset, orderInfo.initialRedeemFeesLocked-o.FeesPaid.Redemption, o.ID) + balanceMods := []*balanceMod{ + {true, toAsset, balTypeAvailable, orderInfo.initialRedeemFeesLocked - o.FeesPaid.Redemption}, + {false, toAsset, balTypeFundingOrder, orderInfo.initialRedeemFeesLocked}, + } + m.modifyBotBalance(orderInfo.bot, balanceMods) } else { m.log.Errorf("oid: %v - initialRedeemFeesLocked %d > redemption fees %d", hex.EncodeToString(o.ID), orderInfo.initialRedeemFeesLocked, o.FeesPaid.Redemption) @@ -564,7 +681,10 @@ func (m *MarketMaker) handleOrderUpdate(o *core.Order) { maxRedeemFees := filledLots * orderInfo.singleLotRedeemFees if maxRedeemFees > o.FeesPaid.Redemption { m.log.Tracef("oid: %s, return excess redeem fees, maxRedeemFees %v, redemption fees %v", o.ID, maxRedeemFees, o.FeesPaid.Redemption) - m.increaseBotBalance(orderInfo.bot, toAsset, maxRedeemFees-o.FeesPaid.Redemption, o.ID) + balanceMods := []*balanceMod{ + {true, toAsset, balTypeAvailable, maxRedeemFees - o.FeesPaid.Redemption}, + } + m.modifyBotBalance(orderInfo.bot, balanceMods) } else if maxRedeemFees < o.FeesPaid.Redemption { m.log.Errorf("oid: %v - maxRedeemFees %d < redemption fees %d", hex.EncodeToString(o.ID), maxRedeemFees, o.FeesPaid.Redemption) diff --git a/client/mm/mm_test.go b/client/mm/mm_test.go index 95427c03aa..ddbcd75864 100644 --- a/client/mm/mm_test.go +++ b/client/mm/mm_test.go @@ -441,14 +441,10 @@ func TestSetupBalances(t *testing.T) { t.Fatalf("%s: unexpected error: %v", test.name, err) } - for id, reserves := range mm.botBalances { - fmt.Printf("bot %s reserves: %d \n", id, reserves) - } - for botID, wantReserve := range test.wantReserves { botReserves := mm.botBalances[botID] for assetID, wantReserve := range wantReserve { - if botReserves.balances[assetID] != wantReserve { + if botReserves.balances[assetID].Available != wantReserve { t.Fatalf("%s: unexpected reserve for bot %s, asset %d. "+ "want %d, got %d", test.name, botID, assetID, wantReserve, botReserves.balances[assetID]) @@ -923,11 +919,11 @@ func TestSegregatedCoreMaxBuy(t *testing.T) { } } -func assetBalancesMatch(expected map[uint32]uint64, botName string, mm *MarketMaker) error { +func assetBalancesMatch(expected map[uint32]*botBalance, botName string, mm *MarketMaker) error { for assetID, exp := range expected { - actual := mm.botBalance(botName, assetID) - if actual != exp { - return fmt.Errorf("asset %d expected %d != actual %d\n", assetID, exp, actual) + actual := mm.botBalances[botName].balances[assetID] + if !reflect.DeepEqual(exp, actual) { + return fmt.Errorf("asset %d expected %+v != actual %+v\n", assetID, exp, actual) } } return nil @@ -957,7 +953,7 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { type noteAndBalances struct { note core.Notification - balance map[uint32]uint64 + balance map[uint32]*botBalance } type test struct { @@ -968,7 +964,7 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { multiTrade *core.MultiTradeForm trade *core.TradeForm assetBalances map[uint32]uint64 - postTradeBalances map[uint32]uint64 + postTradeBalances map[uint32]*botBalance market *core.Market swapFees uint64 redeemFees uint64 @@ -1018,9 +1014,15 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { RedeemLockedAmt: 2000, Sell: true, }, - postTradeBalances: map[uint32]uint64{ - 0: (1e7 / 2) - 2000, - 42: (1e7 / 2) - 2e6 - 2000, + postTradeBalances: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000, + FundingOrder: 2000, + }, + 42: { + Available: (1e7 / 2) - 2e6 - 2000, + FundingOrder: 2e6 + 2000, + }, }, notifications: []*noteAndBalances{ { @@ -1043,9 +1045,16 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { }, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 2000, - 42: (1e7 / 2) - 2e6 - 2000, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000, + PendingRedeem: calc.BaseToQuote(5e7, 1e6), + FundingOrder: 2000, + }, + 42: { + Available: (1e7 / 2) - 2e6 - 2000, + FundingOrder: 1e6 + 2000, + }, }, }, { @@ -1058,9 +1067,16 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Status: order.MatchComplete, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 2000, - 42: (1e7 / 2) - 2e6 - 2000, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000, + PendingRedeem: calc.BaseToQuote(5e7, 1e6), + FundingOrder: 2000, + }, + 42: { + Available: (1e7 / 2) - 2e6 - 2000, + FundingOrder: 1e6 + 2000, + }, }, }, { @@ -1074,9 +1090,15 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Redeem: &core.Coin{}, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 2000 + calc.BaseToQuote(5e7, 1e6), - 42: (1e7 / 2) - 2e6 - 2000, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 + calc.BaseToQuote(5e7, 1e6), + FundingOrder: 2000, + }, + 42: { + Available: (1e7 / 2) - 2e6 - 2000, + FundingOrder: 1e6 + 2000, + }, }, }, { @@ -1104,9 +1126,13 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { }, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 800 + calc.BaseToQuote(5e7, 1e6), - 42: (1e7 / 2) - 1e6 - 800, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) + calc.BaseToQuote(5e7, 1e6) - 800, + }, + 42: { + Available: (1e7 / 2) - 1e6 - 800, + }, }, }, }, @@ -1142,14 +1168,18 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { swapFees: 1000, redeemFees: 1000, tradeRes: &core.Order{ - ID: id, - LockedAmt: calc.BaseToQuote(5e7, 2e6) + 2000, - RedeemLockedAmt: 2000, - Sell: false, - }, - postTradeBalances: map[uint32]uint64{ - 0: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), - 42: (1e7 / 2) - 2000, + ID: id, + LockedAmt: calc.BaseToQuote(5e7, 2e6) + 2000, + Sell: false, + }, + postTradeBalances: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), + FundingOrder: calc.BaseToQuote(5e7, 2e6) + 2000, + }, + 42: { + Available: (1e7 / 2), + }, }, notifications: []*noteAndBalances{ { @@ -1172,9 +1202,15 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { }, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), - 42: (1e7 / 2) - 2000, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), + FundingOrder: calc.BaseToQuote(5e7, 1e6) + 2000, + }, + 42: { + Available: (1e7 / 2), + PendingRedeem: 1e6 - 1000, + }, }, }, { @@ -1187,9 +1223,15 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Status: order.MatchComplete, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), - 42: (1e7 / 2) - 2000, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), + FundingOrder: calc.BaseToQuote(5e7, 1e6) + 2000, + }, + 42: { + Available: (1e7 / 2), + PendingRedeem: 1e6 - 1000, + }, }, }, { @@ -1203,9 +1245,14 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Redeem: &core.Coin{}, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), - 42: (1e7 / 2) - 2000 + 1e6, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), + FundingOrder: calc.BaseToQuote(5e7, 1e6) + 2000, + }, + 42: { + Available: (1e7 / 2) + 1e6 - 1000, + }, }, }, { @@ -1234,9 +1281,13 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { }, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 800 - calc.BaseToQuote(5e7, 1e6), - 42: (1e7 / 2) - 800 + 1e6, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 800 - calc.BaseToQuote(5e7, 1e6), + }, + 42: { + Available: (1e7 / 2) + 1e6 - 800, + }, }, }, }, @@ -1277,9 +1328,15 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { RedeemLockedAmt: 2000, Sell: true, }, - postTradeBalances: map[uint32]uint64{ - 0: (1e7 / 2) - 2000, - 42: (1e7 / 2) - 2e6 - 2000, + postTradeBalances: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000, + FundingOrder: 2000, + }, + 42: { + Available: (1e7 / 2) - 2e6 - 2000, + FundingOrder: 2e6 + 2000, + }, }, notifications: []*noteAndBalances{ { @@ -1302,9 +1359,16 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { }, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 2000, - 42: (1e7 / 2) - 2e6 - 2000, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000, + FundingOrder: 2000, + PendingRedeem: calc.BaseToQuote(5e7, 1e6), + }, + 42: { + Available: (1e7 / 2) - 2e6 - 2000, + FundingOrder: 1e6 + 2000, + }, }, }, { @@ -1317,9 +1381,16 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Status: order.MatchComplete, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 2000, - 42: (1e7 / 2) - 2e6 - 2000, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000, + FundingOrder: 2000, + PendingRedeem: calc.BaseToQuote(5e7, 1e6), + }, + 42: { + Available: (1e7 / 2) - 2e6 - 2000, + FundingOrder: 1e6 + 2000, + }, }, }, { @@ -1333,22 +1404,27 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Redeem: &core.Coin{}, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 2000 + calc.BaseToQuote(5e7, 1e6), - 42: (1e7 / 2) - 2e6 - 2000, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 + calc.BaseToQuote(5e7, 1e6), + FundingOrder: 2000, + }, + 42: { + Available: (1e7 / 2) - 2e6 - 2000, + FundingOrder: 1e6 + 2000, + }, }, }, { note: &core.OrderNote{ Order: &core.Order{ - ID: id, - Status: order.OrderStatusExecuted, - BaseID: 42, - QuoteID: 0, - Qty: 2e6, - Sell: true, - Filled: 2e6, - AllFeesConfirmed: true, + ID: id, + Status: order.OrderStatusExecuted, + BaseID: 42, + QuoteID: 0, + Qty: 2e6, + Sell: true, + Filled: 2e6, FeesPaid: &core.FeeBreakdown{ Swap: 1600, Redemption: 1600, @@ -1369,9 +1445,16 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { }, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 1600 + calc.BaseToQuote(5e7, 1e6), - 42: (1e7 / 2) - 2e6 - 1600, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 + calc.BaseToQuote(5e7, 1e6), + FundingOrder: 2000, + PendingRedeem: calc.BaseToQuote(55e6, 1e6), + }, + 42: { + Available: (1e7 / 2) - 2e6 - 2000, + FundingOrder: 2000, + }, }, }, { @@ -1385,9 +1468,16 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Redeem: &core.Coin{}, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 1600 + calc.BaseToQuote(5e7, 1e6), - 42: (1e7 / 2) - 2e6 - 1600, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 + calc.BaseToQuote(5e7, 1e6), + FundingOrder: 2000, + PendingRedeem: calc.BaseToQuote(55e6, 1e6), + }, + 42: { + Available: (1e7 / 2) - 2e6 - 2000, + FundingOrder: 2000, + }, }, }, { @@ -1401,9 +1491,55 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Redeem: &core.Coin{}, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 1600 + calc.BaseToQuote(525e5, 2e6), - 42: (1e7 / 2) - 2e6 - 1600, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 + calc.BaseToQuote(5e7, 1e6) + calc.BaseToQuote(55e6, 1e6), + FundingOrder: 2000, + }, + 42: { + Available: (1e7 / 2) - 2e6 - 2000, + FundingOrder: 2000, + }, + }, + }, + { + note: &core.OrderNote{ + Order: &core.Order{ + ID: id, + Status: order.OrderStatusExecuted, + BaseID: 42, + QuoteID: 0, + Qty: 2e6, + Sell: true, + Filled: 2e6, + AllFeesConfirmed: true, + FeesPaid: &core.FeeBreakdown{ + Swap: 1600, + Redemption: 1600, + }, + Matches: []*core.Match{ + { + MatchID: matchIDs[0][:], + Qty: 1e6, + Rate: 5e7, + Status: order.MatchConfirmed, + }, + { + MatchID: matchIDs[1][:], + Qty: 1e6, + Rate: 55e6, + Status: order.MatchConfirmed, + }, + }, + }, + }, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 1600 + calc.BaseToQuote(5e7, 1e6) + calc.BaseToQuote(55e6, 1e6), + }, + 42: { + Available: (1e7 / 2) - 2e6 - 1600, + }, }, }, }, @@ -1439,14 +1575,18 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { swapFees: 1000, redeemFees: 1000, tradeRes: &core.Order{ - ID: id, - LockedAmt: calc.BaseToQuote(5e7, 2e6) + 2000, - RedeemLockedAmt: 2000, - Sell: true, - }, - postTradeBalances: map[uint32]uint64{ - 0: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), - 42: (1e7 / 2) - 2000, + ID: id, + LockedAmt: calc.BaseToQuote(5e7, 2e6) + 2000, + Sell: true, + }, + postTradeBalances: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), + FundingOrder: calc.BaseToQuote(5e7, 2e6) + 2000, + }, + 42: { + Available: (1e7 / 2), + }, }, notifications: []*noteAndBalances{ { @@ -1469,9 +1609,15 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { }, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), - 42: (1e7 / 2) - 2000, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), + FundingOrder: calc.BaseToQuote(5e7, 1e6) + 2000, + }, + 42: { + Available: (1e7 / 2), + PendingRedeem: 1e6 - 1000, + }, }, }, { @@ -1484,9 +1630,15 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Status: order.MatchComplete, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), - 42: (1e7 / 2) - 2000, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), + FundingOrder: calc.BaseToQuote(5e7, 1e6) + 2000, + }, + 42: { + Available: (1e7 / 2), + PendingRedeem: 1e6 - 1000, + }, }, }, { @@ -1500,23 +1652,27 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Redeem: &core.Coin{}, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), - 42: (1e7 / 2) - 2000 + 1e6, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 2e6), + FundingOrder: calc.BaseToQuote(5e7, 1e6) + 2000, + }, + 42: { + Available: (1e7 / 2) - 1000 + 1e6, + }, }, }, { note: &core.OrderNote{ Order: &core.Order{ - ID: id, - Status: order.OrderStatusExecuted, - BaseID: 42, - QuoteID: 0, - Qty: 2e6, - Rate: 5e7, - Sell: false, - Filled: 2e6, - AllFeesConfirmed: true, + ID: id, + Status: order.OrderStatusExecuted, + BaseID: 42, + QuoteID: 0, + Qty: 2e6, + Rate: 5e7, + Sell: false, + Filled: 2e6, FeesPaid: &core.FeeBreakdown{ Swap: 1600, Redemption: 1600, @@ -1537,9 +1693,15 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { }, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 1600 - calc.BaseToQuote(5e7, 1e6) - calc.BaseToQuote(45e6, 1e6), - 42: (1e7 / 2) - 1600 + 1e6, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 1e6) - calc.BaseToQuote(45e6, 1e6), + FundingOrder: 2000, + }, + 42: { + Available: (1e7 / 2) + 1e6 - 1000, + PendingRedeem: 1e6 - 1000, + }, }, }, { @@ -1553,9 +1715,15 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Redeem: &core.Coin{}, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 1600 - calc.BaseToQuote(5e7, 1e6) - calc.BaseToQuote(45e6, 1e6), - 42: (1e7 / 2) - 1600 + 1e6, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 1e6) - calc.BaseToQuote(45e6, 1e6), + FundingOrder: 2000, + }, + 42: { + Available: (1e7 / 2) + 1e6 - 1000, + PendingRedeem: 1e6 - 1000, + }, }, }, { @@ -1569,9 +1737,14 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Redeem: &core.Coin{}, }, }, - balance: map[uint32]uint64{ - 0: (1e7 / 2) - 1600 - calc.BaseToQuote(5e7, 1e6) - calc.BaseToQuote(45e6, 1e6), - 42: (1e7 / 2) - 1600 + 2e6, + balance: map[uint32]*botBalance{ + 0: { + Available: (1e7 / 2) - 2000 - calc.BaseToQuote(5e7, 1e6) - calc.BaseToQuote(45e6, 1e6), + FundingOrder: 2000, + }, + 42: { + Available: (1e7 / 2) + 2e6 - 2000, + }, }, }, }, @@ -1608,17 +1781,21 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { redeemFees: 1000, maxFundingFees: 500, tradeRes: &core.Order{ - ID: id, - LockedAmt: calc.BaseToQuote(5e7, 5e6) + 1000, - RedeemLockedAmt: 0, - Sell: true, + ID: id, + LockedAmt: calc.BaseToQuote(5e7, 5e6) + 1000, + Sell: false, FeesPaid: &core.FeeBreakdown{ Funding: 400, }, }, - postTradeBalances: map[uint32]uint64{ - 0: 100, - 42: 5e6, + postTradeBalances: map[uint32]*botBalance{ + 0: { + Available: 100, + FundingOrder: calc.BaseToQuote(5e7, 5e6) + 1400, + }, + 42: { + Available: 5e6, + }, }, }, // "edge not enough balance for single buy, with maxFundingFee > 0" @@ -1694,9 +1871,15 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Funding: 400, }, }, - postTradeBalances: map[uint32]uint64{ - 0: 5e6, - 42: 100, + + postTradeBalances: map[uint32]*botBalance{ + 0: { + Available: 5e6, + }, + 42: { + Available: 100, + FundingOrder: 5e6 + 1400, + }, }, }, // "edge not enough balance for single sell" @@ -1766,9 +1949,15 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { LockedAmt: calc.BaseToQuote(52e7, 5e6) + 1000, RedeemLockedAmt: 1000, }, - postTradeBalances: map[uint32]uint64{ - 0: 0, - 42: 0, + postTradeBalances: map[uint32]*botBalance{ + 0: { + Available: 0, + FundingOrder: calc.BaseToQuote(52e7, 5e6) + 1000, + }, + 42: { + Available: 0, + FundingOrder: 1000, + }, }, isAccountLocker: map[uint32]bool{42: true}, }, @@ -1839,9 +2028,15 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { LockedAmt: 5e6 + 1000, RedeemLockedAmt: 1000, }, - postTradeBalances: map[uint32]uint64{ - 0: 0, - 42: 0, + postTradeBalances: map[uint32]*botBalance{ + 0: { + Available: 0, + FundingOrder: 1000, + }, + 42: { + Available: 0, + FundingOrder: 5e6 + 1000, + }, }, isAccountLocker: map[uint32]bool{0: true}, }, @@ -1931,9 +2126,14 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Sell: true, }, }, - postTradeBalances: map[uint32]uint64{ - 0: 100, - 42: 5e6, + postTradeBalances: map[uint32]*botBalance{ + 0: { + Available: 100, + FundingOrder: calc.BaseToQuote(5e7, 5e6) + calc.BaseToQuote(52e7, 5e6) + 2400, + }, + 42: { + Available: 5e6, + }, }, }, // "edge not enough balance for multi buy" @@ -2030,9 +2230,14 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Sell: true, }, }, - postTradeBalances: map[uint32]uint64{ - 0: 5e6, - 42: 100, + postTradeBalances: map[uint32]*botBalance{ + 0: { + Available: 5e6, + }, + 42: { + Available: 100, + FundingOrder: 1e7 + 2400, + }, }, }, // "edge not enough balance for multi sell" @@ -2125,9 +2330,15 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { Sell: true, }, }, - postTradeBalances: map[uint32]uint64{ - 0: 0, - 42: 0, + postTradeBalances: map[uint32]*botBalance{ + 0: { + Available: 0, + FundingOrder: calc.BaseToQuote(5e7, 5e6) + calc.BaseToQuote(52e7, 5e6) + 2000, + }, + 42: { + Available: 0, + FundingOrder: 2000, + }, }, isAccountLocker: map[uint32]bool{42: true}, }, @@ -2222,9 +2433,15 @@ func testSegregatedCoreTrade(t *testing.T, testMultiTrade bool) { }, }, isAccountLocker: map[uint32]bool{0: true}, - postTradeBalances: map[uint32]uint64{ - 0: 0, - 42: 0, + postTradeBalances: map[uint32]*botBalance{ + 0: { + Available: 0, + FundingOrder: 2000, + }, + 42: { + Available: 0, + FundingOrder: 1e7 + 2000, + }, }, }, // "edge not enough balance for multi sell due to redeem fees" diff --git a/client/mm/wrapped_core.go b/client/mm/wrapped_core.go index 0f8de73299..c79d42abc5 100644 --- a/client/mm/wrapped_core.go +++ b/client/mm/wrapped_core.go @@ -82,6 +82,7 @@ func (c *wrappedCore) Trade(pw []byte, form *core.TradeForm) (*core.Order, error singleLotRedeemFees: singleLotRedeemFees, lotSize: mkt.LotSize, matchesRefunded: make(map[order.MatchID]struct{}), + matchesSeen: make(map[order.MatchID]struct{}), } c.mm.ordersMtx.Unlock() @@ -95,10 +96,15 @@ func (c *wrappedCore) Trade(pw []byte, form *core.TradeForm) (*core.Order, error fundingFees = o.FeesPaid.Funding } - c.mm.decreaseBotBalance(c.botID, fromAsset, o.LockedAmt+fundingFees, o.ID) + balMods := []*balanceMod{ + {false, fromAsset, balTypeAvailable, o.LockedAmt + fundingFees}, + {true, fromAsset, balTypeFundingOrder, o.LockedAmt + fundingFees}, + } if o.RedeemLockedAmt > 0 { - c.mm.decreaseBotBalance(c.botID, toAsset, o.RedeemLockedAmt, o.ID) + balMods = append(balMods, &balanceMod{false, toAsset, balTypeAvailable, o.RedeemLockedAmt}) + balMods = append(balMods, &balanceMod{true, toAsset, balTypeFundingOrder, o.RedeemLockedAmt}) } + c.mm.modifyBotBalance(c.botID, balMods) return o, nil } @@ -129,8 +135,10 @@ func (c *wrappedCore) MultiTrade(pw []byte, form *core.MultiTradeForm) ([]*core. } fromAsset := form.Quote + toAsset := form.Base if form.Sell { fromAsset = form.Base + toAsset = form.Quote } form.MaxLock = c.mm.botBalance(c.botID, fromAsset) @@ -139,6 +147,7 @@ func (c *wrappedCore) MultiTrade(pw []byte, form *core.MultiTradeForm) ([]*core. return nil, err } + var totalFromLocked, totalToLocked, fundingFeesPaid uint64 for _, o := range orders { var orderID order.OrderID copy(orderID[:], o.ID) @@ -154,24 +163,26 @@ func (c *wrappedCore) MultiTrade(pw []byte, form *core.MultiTradeForm) ([]*core. singleLotRedeemFees: singleLotRedeemFees, lotSize: mkt.LotSize, matchesRefunded: make(map[order.MatchID]struct{}), + matchesSeen: make(map[order.MatchID]struct{}), } c.mm.ordersMtx.Unlock() - fromAsset, toAsset := form.Quote, form.Base - if form.Sell { - fromAsset, toAsset = toAsset, fromAsset - } - - var fundingFees uint64 + totalFromLocked += o.LockedAmt + totalToLocked += o.RedeemLockedAmt if o.FeesPaid != nil { - fundingFees = o.FeesPaid.Funding + totalFromLocked += o.FeesPaid.Funding } + } - c.mm.decreaseBotBalance(c.botID, fromAsset, o.LockedAmt+fundingFees, o.ID) - if o.RedeemLockedAmt > 0 { - c.mm.decreaseBotBalance(c.botID, toAsset, o.RedeemLockedAmt, o.ID) - } + balMods := []*balanceMod{ + {false, fromAsset, balTypeAvailable, totalFromLocked + fundingFeesPaid}, + {true, fromAsset, balTypeFundingOrder, totalFromLocked}, + } + if totalToLocked > 0 { + balMods = append(balMods, &balanceMod{false, toAsset, balTypeAvailable, totalToLocked}) + balMods = append(balMods, &balanceMod{true, toAsset, balTypeFundingOrder, totalToLocked}) } + c.mm.modifyBotBalance(c.botID, balMods) return orders, nil }