Skip to content

Commit

Permalink
Decrease bot balance after bond creation
Browse files Browse the repository at this point in the history
If a bond creation transaction makes the bots have more balance than is
available in the wallet, decrease the bot balances.
  • Loading branch information
martonp committed Apr 27, 2024
1 parent 656911e commit 4ee1e28
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 42 deletions.
6 changes: 3 additions & 3 deletions client/mm/exchange_adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -799,7 +799,7 @@ func (u *unifiedExchangeAdaptor) MultiTrade(placements []*multiTradePlacement, s
availableBalance := bal.Available
if dexReserves != nil {
if dexReserves[assetID] > availableBalance {
u.log.Errorf("MultiTrade: insufficient dex balance for reserves. required: %d, have: %d", dexReserves[assetID], availableBalance)
u.log.Errorf("MultiTrade: insufficient dex balance for %s reserves. required: %d, have: %d", dex.BipIDSymbol(assetID), dexReserves[assetID], availableBalance)
return nil
}
availableBalance -= dexReserves[assetID]
Expand All @@ -808,7 +808,7 @@ func (u *unifiedExchangeAdaptor) MultiTrade(placements []*multiTradePlacement, s
}
}
if remainingBalances[fromFeeAsset] < fundingFees {
u.log.Debugf("MultiTrade: insufficient balance for funding fees. required: %d, have: %d", fundingFees, remainingBalances[fromFeeAsset])
u.log.Debugf("MultiTrade: insufficient balance for %s funding fees. required: %d, have: %d", dex.BipIDSymbol(fromFeeAsset), fundingFees, remainingBalances[fromFeeAsset])
return nil
}
remainingBalances[fromFeeAsset] -= fundingFees
Expand All @@ -822,7 +822,7 @@ func (u *unifiedExchangeAdaptor) MultiTrade(placements []*multiTradePlacement, s
remainingCEXBal = cexBal.Available
reserves := cexReserves[toAsset]
if remainingCEXBal < reserves {
u.log.Errorf("MultiTrade: insufficient CEX balance for reserves. required: %d, have: %d", cexReserves, remainingCEXBal)
u.log.Errorf("MultiTrade: insufficient CEX balance for %s reserves. required: %d, have: %d", dex.BipIDSymbol(toAsset), cexReserves, remainingCEXBal)
return nil
}
remainingCEXBal -= reserves
Expand Down
164 changes: 128 additions & 36 deletions client/mm/mm.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,54 @@ func (m *MarketMaker) defaultConfig() *MarketMakingConfig {
return m.defaultCfg.Copy()
}

func (m *MarketMaker) ensureSufficientBalanceOnDEX(assetID uint32) {
dexBals, _, err := m.availableBalances(map[uint32]interface{}{assetID: struct{}{}}, nil, nil)
if err != nil {
m.log.Errorf("Error getting available balances: %v", err)
return
}

if dexBals[assetID] >= 0 {
return
}

amtToDecrease := -dexBals[assetID]

m.log.Warnf("Insufficient balance for %s on DEX. Attempting to decrease bot balances.", dex.BipIDSymbol(assetID))

m.startUpdateMtx.Lock()
defer m.startUpdateMtx.Unlock()

runningBots := m.runningBotsLookup()

botsToAdjustBalance := make([]*runningBot, 0)
for _, rb := range runningBots {
if _, found := rb.assets()[assetID]; found {
botsToAdjustBalance = append(botsToAdjustBalance, rb)
}
}

amtToDecreasePerBot := amtToDecrease / int64(len(botsToAdjustBalance))
var additionalOnFirstBot int64
if amtToDecrease > amtToDecreasePerBot*int64(len(botsToAdjustBalance)) {
additionalOnFirstBot = amtToDecrease - amtToDecreasePerBot*int64(len(botsToAdjustBalance))
}

m.log.Infof("Decreasing balance for %s by %d on %d bots", dex.BipIDSymbol(assetID), amtToDecrease, len(botsToAdjustBalance))

for i, rb := range botsToAdjustBalance {
balDiff := -amtToDecreasePerBot
if i == 0 {
balDiff -= additionalOnFirstBot
}
rb.botCfgMtx.RLock()
rb.adaptor.updateConfig(rb.botCfg, &BotBalanceDiffs{
DEX: map[uint32]int64{assetID: balDiff},
})
rb.botCfgMtx.RUnlock()
}
}

func (m *MarketMaker) Connect(ctx context.Context) (*sync.WaitGroup, error) {
m.ctx = ctx
cfg := m.defaultConfig()
Expand All @@ -450,6 +498,32 @@ func (m *MarketMaker) Connect(ctx context.Context) (*sync.WaitGroup, error) {
}
var wg sync.WaitGroup

wg.Add(1)
go func() {
defer wg.Done()
noteFeed := m.core.NotificationFeed()
defer noteFeed.ReturnFeed()

for {
select {
case <-ctx.Done():
return
case note := <-noteFeed.C:
walletNote, is := note.(*core.WalletNote)
if !is {
continue
}
txNote, is := walletNote.Payload.(*asset.TransactionNote)
if !is {
continue
}
if txNote.Transaction.Type == asset.CreateBond {
m.ensureSufficientBalanceOnDEX(txNote.AssetID)
}
}
}
}()

wg.Add(1)
go func() {
defer wg.Done()
Expand All @@ -468,7 +542,7 @@ func (m *MarketMaker) Connect(ctx context.Context) (*sync.WaitGroup, error) {
}

func (m *MarketMaker) balancesSufficient(balances *BotBalanceAllocation, mkt *MarketWithHost, cexCfg *CEXConfig) error {
availableDEXBalances, availableCEXBalances, err := m.availableBalances(mkt, cexCfg)
availableDEXBalances, availableCEXBalances, err := m.availableBalancesForMarket(mkt, cexCfg)
if err != nil {
return fmt.Errorf("error getting available balances: %v", err)
}
Expand Down Expand Up @@ -950,30 +1024,17 @@ func marketFees(c clientCore, host string, baseID, quoteID uint32, useMaxFeeRate
}, nil
}

func (m *MarketMaker) availableBalances(mkt *MarketWithHost, cexCfg *CEXConfig) (dexBalances, cexBalances map[uint32]uint64, _ error) {
dexAssets := make(map[uint32]interface{})
cexAssets := make(map[uint32]interface{})

dexAssets[mkt.BaseID] = struct{}{}
dexAssets[mkt.QuoteID] = struct{}{}
dexAssets[feeAsset(mkt.BaseID)] = struct{}{}
dexAssets[feeAsset(mkt.QuoteID)] = struct{}{}

if cexCfg != nil {
cexAssets[mkt.BaseID] = struct{}{}
cexAssets[mkt.QuoteID] = struct{}{}
}

checkTotalBalances := func() (dexBals, cexBals map[uint32]uint64, err error) {
dexBals = make(map[uint32]uint64, len(dexAssets))
cexBals = make(map[uint32]uint64, len(cexAssets))
func (m *MarketMaker) availableBalances(dexAssets, cexAssets map[uint32]interface{}, cexCfg *CEXConfig) (dexBalances, cexBalances map[uint32]int64, _ error) {
checkTotalBalances := func() (dexBals, cexBals map[uint32]int64, err error) {
dexBals = make(map[uint32]int64, len(dexAssets))
cexBals = make(map[uint32]int64, len(cexAssets))

for assetID := range dexAssets {
bal, err := m.core.AssetBalance(assetID)
if err != nil {
return nil, nil, err
}
dexBals[assetID] = bal.Available
dexBals[assetID] = int64(bal.Available)
}

if cexCfg != nil {
Expand All @@ -988,9 +1049,7 @@ func (m *MarketMaker) availableBalances(mkt *MarketWithHost, cexCfg *CEXConfig)
return nil, nil, err
}

m.log.Infof("CEX BALANCE FOR %d: %+v", assetID, balance)

cexBals[assetID] = balance.Available
cexBals[assetID] = int64(balance.Available)
}
}

Expand All @@ -1007,7 +1066,7 @@ func (m *MarketMaker) availableBalances(mkt *MarketWithHost, cexCfg *CEXConfig)
return false
}

balancesEqual := func(bal1, bal2 map[uint32]uint64) bool {
balancesEqual := func(bal1, bal2 map[uint32]int64) bool {
if len(bal1) != len(bal2) {
return false
}
Expand Down Expand Up @@ -1062,20 +1121,10 @@ func (m *MarketMaker) availableBalances(mkt *MarketWithHost, cexCfg *CEXConfig)

if balancesEqual(updatedDEXBalances, totalDEXBalances) && balancesEqual(updatedCEXBalances, totalCEXBalances) {
for assetID, bal := range reservedDEXBalances {
if bal > totalDEXBalances[assetID] {
m.log.Warnf("reserved DEX balance for %s exceeds available balance: %d > %d", dex.BipIDSymbol(assetID), bal, totalDEXBalances[assetID])
totalDEXBalances[assetID] = 0
} else {
totalDEXBalances[assetID] -= bal
}
totalDEXBalances[assetID] -= int64(bal)
}
for assetID, bal := range reservedCEXBalances {
if bal > totalCEXBalances[assetID] {
m.log.Warnf("reserved CEX balance for %s exceeds available balance: %d > %d", dex.BipIDSymbol(assetID), bal, totalCEXBalances[assetID])
totalCEXBalances[assetID] = 0
} else {
totalCEXBalances[assetID] -= bal
}
totalCEXBalances[assetID] -= int64(bal)
}
return totalDEXBalances, totalCEXBalances, nil
}
Expand All @@ -1087,6 +1136,49 @@ func (m *MarketMaker) availableBalances(mkt *MarketWithHost, cexCfg *CEXConfig)
return nil, nil, fmt.Errorf("failed to get available balances after %d tries", maxTries)
}

func (m *MarketMaker) availableBalancesForMarket(mkt *MarketWithHost, cexCfg *CEXConfig) (dexBalances, cexBalances map[uint32]uint64, _ error) {
dexAssets := make(map[uint32]interface{})
cexAssets := make(map[uint32]interface{})

dexAssets[mkt.BaseID] = struct{}{}
dexAssets[mkt.QuoteID] = struct{}{}
dexAssets[feeAsset(mkt.BaseID)] = struct{}{}
dexAssets[feeAsset(mkt.QuoteID)] = struct{}{}

if cexCfg != nil {
cexAssets[mkt.BaseID] = struct{}{}
cexAssets[mkt.QuoteID] = struct{}{}
}

dexBals, cexBals, err := m.availableBalances(dexAssets, cexAssets, cexCfg)
if err != nil {
return nil, nil, err
}

dexBalances = make(map[uint32]uint64, len(dexBals))
cexBalances = make(map[uint32]uint64, len(cexBals))

for assetID, bal := range dexBals {
if bal >= 0 {
dexBalances[assetID] = uint64(bal)
} else {
m.log.Warnf("Negative DEX balance for %s: %d", dex.BipIDSymbol(assetID), bal)
dexBalances[assetID] = 0
}
}

for assetID, bal := range cexBals {
if bal >= 0 {
cexBalances[assetID] = uint64(bal)
} else {
m.log.Warnf("Negative CEX balance for %s: %d", dex.BipIDSymbol(assetID), bal)
cexBalances[assetID] = 0
}
}

return dexBalances, cexBalances, nil
}

// AvailableBalances returns the available balances of assets relevant to
// market making on the specified market on the DEX (including fee assets),
// and optionally a CEX depending on the configured strategy.
Expand All @@ -1096,5 +1188,5 @@ func (m *MarketMaker) AvailableBalances(mkt *MarketWithHost, alternateConfigPath
return nil, nil, err
}

return m.availableBalances(mkt, cexCfg)
return m.availableBalancesForMarket(mkt, cexCfg)
}
92 changes: 89 additions & 3 deletions client/mm/mm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,8 @@ func (c *tBotCexAdaptor) PrepareRebalance(ctx context.Context, assetID uint32) (
type tExchangeAdaptor struct {
dexBalances map[uint32]*BotBalance
cexBalances map[uint32]*BotBalance

lastBalanceDiff *BotBalanceDiffs
}

var _ exchangeAdaptor = (*tExchangeAdaptor)(nil)
Expand All @@ -693,9 +695,11 @@ func (t *tExchangeAdaptor) CEXBalance(assetID uint32) *BotBalance {
}
return t.cexBalances[assetID]
}
func (t *tExchangeAdaptor) stats() *RunStats { return nil }
func (t *tExchangeAdaptor) updateConfig(cfg *BotConfig, balanceDiffs *BotBalanceDiffs) {}
func (t *tExchangeAdaptor) timeStart() int64 { return 0 }
func (t *tExchangeAdaptor) stats() *RunStats { return nil }
func (t *tExchangeAdaptor) updateConfig(cfg *BotConfig, balanceDiffs *BotBalanceDiffs) {
t.lastBalanceDiff = balanceDiffs
}
func (t *tExchangeAdaptor) timeStart() int64 { return 0 }

func TestAvailableBalances(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
Expand Down Expand Up @@ -842,3 +846,85 @@ func TestAvailableBalances(t *testing.T) {
checkAvailableBalances(btcUsdc, map[uint32]uint64{0: 3e5, 60: 7e5, 60001: 4e5}, map[uint32]uint64{0: 5e5, 60001: 4e5})
checkAvailableBalances(dcrUsdc, map[uint32]uint64{42: 9e5, 60: 7e5, 60001: 4e5}, map[uint32]uint64{42: 7e5, 60001: 6e5})
}

func TestEnsureSufficientBalance(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

tCore := newTCore()

dcrBtc := &MarketWithHost{
Host: "dex.com",
BaseID: 42,
QuoteID: 0,
}

dcrUsdc := &MarketWithHost{
Host: "dex.com",
BaseID: 42,
QuoteID: 60001,
}

cfg := &MarketMakingConfig{
BotConfigs: []*BotConfig{
{
Host: "dex.com",
BaseID: 42,
QuoteID: 0,
},
{
Host: "dex.com",
BaseID: 42,
QuoteID: 60001,
},
},
}

mm := MarketMaker{
ctx: ctx,
log: tLogger,
core: tCore,
defaultCfg: cfg,
runningBots: make(map[MarketWithHost]*runningBot),
}

tCore.setAssetBalances(map[uint32]uint64{
42: 8e5,
})

mm.runningBots[*dcrBtc] = &runningBot{
adaptor: &tExchangeAdaptor{
dexBalances: map[uint32]*BotBalance{
42: {Available: 4.5e5},
},
},
botCfg: cfg.BotConfigs[0],
}

mm.runningBots[*dcrUsdc] = &runningBot{
adaptor: &tExchangeAdaptor{
dexBalances: map[uint32]*BotBalance{
42: {Available: 4.5e5},
},
},
botCfg: cfg.BotConfigs[1],
}

mm.ensureSufficientBalanceOnDEX(42)

dcrBTCAdaptor := mm.runningBots[*dcrBtc].adaptor.(*tExchangeAdaptor)
if dcrBTCAdaptor.lastBalanceDiff == nil {
t.Fatalf("no balance diff")
}
if dcrBTCAdaptor.lastBalanceDiff.DEX[42] != -0.5e5 {
t.Fatalf("unexpected balance diff. wanted -0.5e5, got %d", dcrBTCAdaptor.lastBalanceDiff.DEX[42])
}

dcrUSDCAdaptor := mm.runningBots[*dcrUsdc].adaptor.(*tExchangeAdaptor)
if dcrUSDCAdaptor.lastBalanceDiff == nil {
t.Fatalf("no balance diff")
}
if dcrUSDCAdaptor.lastBalanceDiff.DEX[42] != -0.5e5 {
t.Fatalf("unexpected balance diff. wanted -0.5e5, got %d", dcrUSDCAdaptor.lastBalanceDiff.DEX[42])
}
}

0 comments on commit 4ee1e28

Please sign in to comment.