diff --git a/client/mm/exchange_adaptor.go b/client/mm/exchange_adaptor.go
index 3688781d71..a41d4a05eb 100644
--- a/client/mm/exchange_adaptor.go
+++ b/client/mm/exchange_adaptor.go
@@ -71,7 +71,7 @@ type botCoreAdaptor interface {
ExchangeRateFromFiatSources() uint64
OrderFeesInUnits(sell, base bool, rate uint64) (uint64, error) // estimated fees, not max
SubscribeOrderUpdates() (updates <-chan *core.Order)
- SufficientBalanceForDEXTrade(rate, qty uint64, sell bool) (bool, map[uint32]uint64, error)
+ SufficientBalanceForDEXTrade(rate, qty uint64, sell bool) (bool, error)
}
// botCexAdaptor is an interface used by bots to access CEX related
@@ -84,7 +84,7 @@ type botCexAdaptor interface {
SubscribeMarket(ctx context.Context, baseID, quoteID uint32) error
SubscribeTradeUpdates() <-chan *libxc.Trade
CEXTrade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64) (*libxc.Trade, error)
- SufficientBalanceForCEXTrade(baseID, quoteID uint32, sell bool, rate, qty uint64) (bool, map[uint32]uint64)
+ SufficientBalanceForCEXTrade(baseID, quoteID uint32, sell bool, rate, qty uint64) bool
MidGap(baseID, quoteID uint32) uint64
Book() (buys, sells []*core.MiniOrder, _ error)
}
@@ -613,7 +613,7 @@ func (u *unifiedExchangeAdaptor) logBalanceAdjustments(dexDiffs, cexDiffs map[ui
// SufficientBalanceForDEXTrade returns whether the bot has sufficient balance
// to place a DEX trade.
-func (u *unifiedExchangeAdaptor) SufficientBalanceForDEXTrade(rate, qty uint64, sell bool) (bool, map[uint32]uint64, error) {
+func (u *unifiedExchangeAdaptor) SufficientBalanceForDEXTrade(rate, qty uint64, sell bool) (bool, error) {
fromAsset, fromFeeAsset, toAsset, toFeeAsset := orderAssets(u.baseID, u.quoteID, sell)
balances := map[uint32]uint64{}
for _, assetID := range []uint32{fromAsset, fromFeeAsset, toAsset, toFeeAsset} {
@@ -625,57 +625,53 @@ func (u *unifiedExchangeAdaptor) SufficientBalanceForDEXTrade(rate, qty uint64,
buyFees, sellFees, err := u.orderFees()
if err != nil {
- return false, nil, err
+ return false, err
}
-
- reqBals := make(map[uint32]uint64)
-
- // Funding Fees
fees, fundingFees := buyFees.Max, buyFees.Funding
if sell {
fees, fundingFees = sellFees.Max, sellFees.Funding
}
- reqBals[fromFeeAsset] += fundingFees
- // Trade Qty
+ if balances[fromFeeAsset] < fundingFees {
+ return false, nil
+ }
+ balances[fromFeeAsset] -= fundingFees
+
fromQty := qty
if !sell {
fromQty = calc.BaseToQuote(rate, qty)
}
- reqBals[fromAsset] += fromQty
+ if balances[fromAsset] < fromQty {
+ return false, nil
+ }
+ balances[fromAsset] -= fromQty
- // Swap Fees
numLots := qty / u.lotSize
- reqBals[fromFeeAsset] += numLots * fees.Swap
+ if balances[fromFeeAsset] < numLots*fees.Swap {
+ return false, nil
+ }
+ balances[fromFeeAsset] -= numLots * fees.Swap
- // Refund Fees
if u.isAccountLocker(fromAsset) {
- reqBals[fromFeeAsset] += numLots * fees.Refund
+ if balances[fromFeeAsset] < numLots*fees.Refund {
+ return false, nil
+ }
+ balances[fromFeeAsset] -= numLots * fees.Refund
}
- // Redeem Fees
if u.isAccountLocker(toAsset) {
- reqBals[toFeeAsset] += numLots * fees.Redeem
- }
-
- sufficient := true
- deficiencies := make(map[uint32]uint64)
-
- for assetID, reqBal := range reqBals {
- if bal, found := balances[assetID]; found && bal >= reqBal {
- continue
- } else {
- deficiencies[assetID] = reqBal - bal
- sufficient = false
+ if balances[toFeeAsset] < numLots*fees.Redeem {
+ return false, nil
}
+ balances[toFeeAsset] -= numLots * fees.Redeem
}
- return sufficient, deficiencies, nil
+ return true, nil
}
// SufficientBalanceOnCEXTrade returns whether the bot has sufficient balance
// to place a CEX trade.
-func (u *unifiedExchangeAdaptor) SufficientBalanceForCEXTrade(baseID, quoteID uint32, sell bool, rate, qty uint64) (bool, map[uint32]uint64) {
+func (u *unifiedExchangeAdaptor) SufficientBalanceForCEXTrade(baseID, quoteID uint32, sell bool, rate, qty uint64) bool {
var fromAssetID uint32
var fromAssetQty uint64
if sell {
@@ -687,12 +683,7 @@ func (u *unifiedExchangeAdaptor) SufficientBalanceForCEXTrade(baseID, quoteID ui
}
fromAssetBal := u.CEXBalance(fromAssetID)
-
- if fromAssetBal.Available < fromAssetQty {
- return false, map[uint32]uint64{fromAssetID: fromAssetQty - fromAssetBal.Available}
- }
-
- return true, nil
+ return fromAssetBal.Available >= fromAssetQty
}
// dexOrderInfo is used by MultiTrade to keep track of the placement index
@@ -1042,10 +1033,11 @@ type TradePlacement struct {
RequiredCEX uint64 `json:"requiredCex"`
UsedDEX map[uint32]uint64 `json:"usedDex"`
UsedCEX uint64 `json:"usedCex"`
- Order *core.Order `json:"order"`
Error *BotProblems `json:"error"`
}
+// setError sets the error field of the TradePlacement and updates the fields
+// that indicate that the trade was placed to 0.
func (tp *TradePlacement) setError(err error) {
if err == nil {
tp.Error = nil
@@ -1082,6 +1074,10 @@ type OrderReport struct {
}
func (or *OrderReport) setError(err error) {
+ if err == nil {
+ or.Error = nil
+ return
+ }
if or.Error == nil {
or.Error = &BotProblems{}
}
@@ -1089,14 +1085,15 @@ func (or *OrderReport) setError(err error) {
}
func newOrderReport(placements []*TradePlacement) *OrderReport {
- for _, p := range placements {
- p.StandingLots = 0
- p.OrderedLots = 0
- p.RequiredDEX = make(map[uint32]uint64)
- p.UsedDEX = make(map[uint32]uint64)
- p.UsedCEX = 0
- p.Order = nil
- p.Error = nil
+ cpPlacements := make([]*TradePlacement, len(placements))
+ for i, p := range placements {
+ cpPlacements[i] = &TradePlacement{
+ Rate: p.Rate,
+ Lots: p.Lots,
+ CounterTradeRate: p.CounterTradeRate,
+ RequiredDEX: make(map[uint32]uint64),
+ UsedDEX: make(map[uint32]uint64),
+ }
}
return &OrderReport{
@@ -1104,7 +1101,7 @@ func newOrderReport(placements []*TradePlacement) *OrderReport {
RequiredDEXBals: make(map[uint32]uint64),
RemainingDEXBals: make(map[uint32]uint64),
UsedDEXBals: make(map[uint32]uint64),
- Placements: placements,
+ Placements: cpPlacements,
}
}
@@ -1362,7 +1359,7 @@ func (u *unifiedExchangeAdaptor) multiTrade(
// DEXTrade places a single order on the DEX order book.
func (u *unifiedExchangeAdaptor) DEXTrade(rate, qty uint64, sell bool) (*core.Order, error) {
- enough, _, err := u.SufficientBalanceForDEXTrade(rate, qty, sell)
+ enough, err := u.SufficientBalanceForDEXTrade(rate, qty, sell)
if err != nil {
return nil, err
}
@@ -2174,8 +2171,7 @@ func (w *unifiedExchangeAdaptor) SubscribeTradeUpdates() <-chan *libxc.Trade {
// Trade executes a trade on the CEX. The trade will be executed using the
// bot's CEX balance.
func (u *unifiedExchangeAdaptor) CEXTrade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64) (*libxc.Trade, error) {
- sufficient, _ := u.SufficientBalanceForCEXTrade(baseID, quoteID, sell, rate, qty)
- if !sufficient {
+ if !u.SufficientBalanceForCEXTrade(baseID, quoteID, sell, rate, qty) {
return nil, fmt.Errorf("insufficient balance")
}
@@ -2345,7 +2341,7 @@ func (u *unifiedExchangeAdaptor) OrderFeesInUnits(sell, base bool, rate uint64)
// threshold. If cancelCEXOrders is true, it will also cancel CEX orders. True
// is returned if all orders have been cancelled. If cancelCEXOrders is false,
// false will always be returned.
-func (u *unifiedExchangeAdaptor) tryCancelOrders(ctx context.Context, epoch *uint64, cancelCEXOrders bool) ([]dex.Bytes, bool) {
+func (u *unifiedExchangeAdaptor) tryCancelOrders(ctx context.Context, epoch *uint64, cancelCEXOrders bool) bool {
u.balancesMtx.RLock()
defer u.balancesMtx.RUnlock()
@@ -2384,7 +2380,7 @@ func (u *unifiedExchangeAdaptor) tryCancelOrders(ctx context.Context, epoch *uin
}
if !cancelCEXOrders {
- return cancels, false
+ return false
}
for _, pendingOrder := range u.pendingCEXOrders {
@@ -2411,7 +2407,7 @@ func (u *unifiedExchangeAdaptor) tryCancelOrders(ctx context.Context, epoch *uin
}
}
- return cancels, done
+ return done
}
func (u *unifiedExchangeAdaptor) cancelAllOrders(ctx context.Context) {
@@ -2431,7 +2427,7 @@ func (u *unifiedExchangeAdaptor) cancelAllOrders(ctx context.Context) {
}
currentEpoch := book.CurrentEpoch()
- if _, done := u.tryCancelOrders(ctx, ¤tEpoch, true); done {
+ if u.tryCancelOrders(ctx, ¤tEpoch, true) {
return
}
@@ -2445,7 +2441,7 @@ func (u *unifiedExchangeAdaptor) cancelAllOrders(ctx context.Context) {
case ni := <-bookFeed.Next():
switch epoch := ni.Payload.(type) {
case *core.ResolvedEpoch:
- if _, done := u.tryCancelOrders(ctx, &epoch.Current, true); done {
+ if u.tryCancelOrders(ctx, &epoch.Current, true) {
return
}
timer.Reset(timeout)
@@ -3116,7 +3112,6 @@ func (u *unifiedExchangeAdaptor) transfer(dist *distribution, currEpoch uint64)
return false, fmt.Errorf("error withdrawing quote: %w", err)
}
}
-
return true, nil
}
@@ -3171,6 +3166,12 @@ func (u *unifiedExchangeAdaptor) inventory(assetID uint32, dexLot, cexLot uint64
// of lots, a 1-lot estimate will be attempted too.
func (u *unifiedExchangeAdaptor) cexCounterRates(cexBuyLots, cexSellLots uint64) (dexBuyRate, dexSellRate uint64, err error) {
tryLots := func(b, s uint64) (uint64, uint64, bool, error) {
+ if b == 0 {
+ b = 1
+ }
+ if s == 0 {
+ s = 1
+ }
buyRate, _, filled, err := u.CEX.VWAP(u.baseID, u.quoteID, true, u.lotSize*s)
if err != nil {
return 0, 0, false, fmt.Errorf("error calculating dex buy price for quote conversion: %w", err)
@@ -3656,51 +3657,44 @@ const (
cexWithdrawProblem
)
-func (u *unifiedExchangeAdaptor) updateCEXProblems(typ cexProblemType, assetID uint32, err error) {
- u.cexProblemsMtx.RLock()
- existingErrNil := func() bool {
+// updateCEXProblemState updates the state of a cex problem. It returns true
+// if the problem state was updated. It is always updated if the error is
+// non-nil.
+func (u *unifiedExchangeAdaptor) updateCEXProblemState(typ cexProblemType, assetID uint32, err error) bool {
+ if err != nil {
switch typ {
case cexTradeProblem:
- return u.cexProblems.TradeErr == nil
+ u.cexProblems.TradeErr = newStampedError(err)
case cexDepositProblem:
- return u.cexProblems.DepositErr[assetID] == nil
+ u.cexProblems.DepositErr[assetID] = newStampedError(err)
case cexWithdrawProblem:
- return u.cexProblems.WithdrawErr[assetID] == nil
- default:
- return true
+ u.cexProblems.WithdrawErr[assetID] = newStampedError(err)
}
+ return true
}
- if existingErrNil() && err == nil {
- u.cexProblemsMtx.RUnlock()
- return
- }
- u.cexProblemsMtx.RUnlock()
-
- u.cexProblemsMtx.Lock()
- defer u.cexProblemsMtx.Unlock()
+ var updated bool
switch typ {
case cexTradeProblem:
- if err == nil {
- u.cexProblems.TradeErr = nil
- } else {
- u.cexProblems.TradeErr = newStampedError(err)
- }
+ updated = u.cexProblems.TradeErr != nil
+ u.cexProblems.TradeErr = nil
case cexDepositProblem:
- if err == nil {
- delete(u.cexProblems.DepositErr, assetID)
- } else {
- u.cexProblems.DepositErr[assetID] = newStampedError(err)
- }
+ updated = u.cexProblems.DepositErr[assetID] != nil
+ delete(u.cexProblems.DepositErr, assetID)
case cexWithdrawProblem:
- if err == nil {
- delete(u.cexProblems.WithdrawErr, assetID)
- } else {
- u.cexProblems.WithdrawErr[assetID] = newStampedError(err)
- }
+ updated = u.cexProblems.WithdrawErr[assetID] != nil
+ delete(u.cexProblems.WithdrawErr, assetID)
}
+ return updated
+}
- u.clientCore.Broadcast(newCexProblemsNote(u.host, u.baseID, u.quoteID, u.cexProblems))
+func (u *unifiedExchangeAdaptor) updateCEXProblems(typ cexProblemType, assetID uint32, err error) {
+ u.cexProblemsMtx.Lock()
+ defer u.cexProblemsMtx.Unlock()
+
+ if u.updateCEXProblemState(typ, assetID, err) {
+ u.clientCore.Broadcast(newCexProblemsNote(u.host, u.baseID, u.quoteID, u.cexProblems))
+ }
}
// checkBotHealth returns true if the bot is healthy and can continue trading.
diff --git a/client/mm/exchange_adaptor_test.go b/client/mm/exchange_adaptor_test.go
index 6936415f55..c568c4541c 100644
--- a/client/mm/exchange_adaptor_test.go
+++ b/client/mm/exchange_adaptor_test.go
@@ -18,6 +18,7 @@ import (
"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/calc"
"decred.org/dcrdex/dex/encode"
+ "decred.org/dcrdex/dex/msgjson"
"decred.org/dcrdex/dex/order"
"decred.org/dcrdex/dex/utils"
"github.com/davecgh/go-spew/spew"
@@ -213,7 +214,7 @@ func TestSufficientBalanceForDEXTrade(t *testing.T) {
if err != nil {
t.Fatalf("Connect error: %v", err)
}
- sufficient, _, err := adaptor.SufficientBalanceForDEXTrade(test.rate, test.qty, test.sell)
+ sufficient, err := adaptor.SufficientBalanceForDEXTrade(test.rate, test.qty, test.sell)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -280,7 +281,7 @@ func TestSufficientBalanceForCEXTrade(t *testing.T) {
QuoteID: quoteID,
},
})
- sufficient, _ := adaptor.SufficientBalanceForCEXTrade(baseID, quoteID, test.sell, test.rate, test.qty)
+ sufficient := adaptor.SufficientBalanceForCEXTrade(baseID, quoteID, test.sell, test.rate, test.qty)
if sufficient != expSufficient {
t.Fatalf("expected sufficient=%v, got %v", expSufficient, sufficient)
}
@@ -1532,6 +1533,157 @@ func TestMultiTrade(t *testing.T) {
orderIDs[4], orderIDs[5],
},
},
+ {
+ name: "not enough bonding for last placement",
+ baseID: 42,
+ quoteID: 0,
+
+ // ---- Sell ----
+ sellDexBalances: map[uint32]uint64{
+ 42: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
+ 0: 0,
+ },
+ sellCexBalances: map[uint32]uint64{
+ 42: 0,
+ 0: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
+ b2q(sellPlacements[1].CounterTradeRate, 2*lotSize) +
+ b2q(sellPlacements[2].CounterTradeRate, 3*lotSize) +
+ b2q(sellPlacements[3].CounterTradeRate, 2*lotSize),
+ },
+ sellPlacements: sellPlacements,
+ sellPendingOrders: pendingOrders(true, 42, 0),
+ expectedSellPlacements: []*core.QtyRate{
+ {Qty: lotSize, Rate: sellPlacements[1].Rate},
+ {Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
+ {Qty: lotSize, Rate: sellPlacements[3].Rate},
+ },
+ expectedSellOrderReport: &OrderReport{
+ Placements: []*TradePlacement{
+ {Lots: 1, Rate: sps[0].Rate, CounterTradeRate: sps[0].CounterTradeRate,
+ StandingLots: 1,
+ RequiredDEX: map[uint32]uint64{},
+ UsedDEX: map[uint32]uint64{},
+ RequiredCEX: 0,
+ UsedCEX: 0,
+ },
+ {Lots: 2, Rate: sps[1].Rate, CounterTradeRate: sps[1].CounterTradeRate,
+ StandingLots: 1,
+ RequiredDEX: map[uint32]uint64{
+ 42: lotSize + sellFees.Max.Swap,
+ },
+ UsedDEX: map[uint32]uint64{
+ 42: lotSize + sellFees.Max.Swap,
+ },
+ RequiredCEX: b2q(sellPlacements[1].CounterTradeRate, lotSize),
+ UsedCEX: b2q(sellPlacements[1].CounterTradeRate, lotSize),
+ OrderedLots: 1,
+ },
+ {Lots: 3, Rate: sps[2].Rate, CounterTradeRate: sps[2].CounterTradeRate,
+ StandingLots: 1,
+ RequiredDEX: map[uint32]uint64{
+ 42: 2 * (lotSize + sellFees.Max.Swap),
+ },
+ UsedDEX: map[uint32]uint64{
+ 42: 2 * (lotSize + sellFees.Max.Swap),
+ },
+ RequiredCEX: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
+ UsedCEX: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
+ OrderedLots: 2,
+ },
+ {Lots: 2, Rate: sps[3].Rate, CounterTradeRate: sps[3].CounterTradeRate,
+ StandingLots: 1,
+ RequiredDEX: map[uint32]uint64{
+ 42: lotSize + sellFees.Max.Swap,
+ },
+ UsedDEX: map[uint32]uint64{},
+ RequiredCEX: b2q(sellPlacements[3].CounterTradeRate, lotSize),
+ UsedCEX: 0,
+ OrderedLots: 0,
+ Error: &BotProblems{
+ UserLimitTooLow: true,
+ },
+ },
+ },
+ Fees: sellFees,
+ AvailableDEXBals: map[uint32]*BotBalance{
+ 42: {
+ Available: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
+ },
+ 0: {},
+ },
+ RequiredDEXBals: map[uint32]uint64{
+ 42: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
+ },
+ RemainingDEXBals: map[uint32]uint64{
+ 42: 0,
+ 0: 0,
+ },
+ AvailableCEXBal: &BotBalance{
+ Available: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
+ b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
+ b2q(sellPlacements[3].CounterTradeRate, lotSize),
+ Reserved: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
+ b2q(sellPlacements[1].CounterTradeRate, lotSize) +
+ b2q(sellPlacements[2].CounterTradeRate, lotSize) +
+ b2q(sellPlacements[3].CounterTradeRate, lotSize),
+ },
+ RequiredCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
+ b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
+ b2q(sellPlacements[3].CounterTradeRate, lotSize),
+ RemainingCEXBal: 0,
+ UsedDEXBals: map[uint32]uint64{
+ 42: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
+ },
+ UsedCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
+ b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
+ b2q(sellPlacements[3].CounterTradeRate, lotSize),
+ },
+ expectedSellPlacementsWithDecrement: []*core.QtyRate{
+ {Qty: lotSize, Rate: sellPlacements[1].Rate},
+ {Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
+ },
+
+ // ---- Buy ----
+ buyDexBalances: map[uint32]uint64{
+ 42: 0,
+ 0: b2q(buyPlacements[1].Rate, lotSize) +
+ b2q(buyPlacements[2].Rate, 2*lotSize) +
+ b2q(buyPlacements[3].Rate, lotSize) +
+ 4*buyFees.Max.Swap + buyFees.Funding,
+ },
+ buyCexBalances: map[uint32]uint64{
+ 42: 8 * lotSize,
+ 0: 0,
+ },
+ buyPlacements: buyPlacements,
+ buyPendingOrders: pendingOrders(false, 42, 0),
+ expectedBuyPlacements: []*core.QtyRate{
+ {Qty: lotSize, Rate: buyPlacements[1].Rate},
+ {Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
+ {Qty: lotSize, Rate: buyPlacements[3].Rate},
+ },
+ expectedBuyPlacementsWithDecrement: []*core.QtyRate{
+ {Qty: lotSize, Rate: buyPlacements[1].Rate},
+ {Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
+ },
+ expectedCancels: []order.OrderID{orderIDs[2]},
+ expectedCancelsWithDecrement: []order.OrderID{orderIDs[2]},
+ multiTradeResult: []*core.MultiTradeResult{
+ {Order: &core.Order{ID: orderIDs[4][:]}},
+ {Order: &core.Order{ID: orderIDs[5][:]}},
+ {Error: &msgjson.Error{Code: msgjson.OrderQuantityTooHigh}},
+ },
+ multiTradeResultWithDecrement: []*core.MultiTradeResult{
+ {Order: &core.Order{ID: orderIDs[4][:]}},
+ {Order: &core.Order{ID: orderIDs[5][:]}},
+ },
+ expectedOrderIDs: []order.OrderID{
+ orderIDs[4], orderIDs[5],
+ },
+ expectedOrderIDsWithDecrement: []order.OrderID{
+ orderIDs[4], orderIDs[5],
+ },
+ },
{
name: "non account locker, reconfig to less placements",
baseID: 42,
diff --git a/client/mm/libxc/orderbook.go b/client/mm/libxc/orderbook.go
index 78eced2cf3..7da5d3bb37 100644
--- a/client/mm/libxc/orderbook.go
+++ b/client/mm/libxc/orderbook.go
@@ -115,6 +115,10 @@ func (ob *orderbook) clear() {
}
func (ob *orderbook) vwap(bids bool, qty uint64) (vwap, extrema uint64, filled bool) {
+ if qty == 0 { // avoid division by zero
+ return 0, 0, false
+ }
+
ob.mtx.RLock()
defer ob.mtx.RUnlock()
diff --git a/client/mm/mm_simple_arb.go b/client/mm/mm_simple_arb.go
index 726127f134..b32ce3282f 100644
--- a/client/mm/mm_simple_arb.go
+++ b/client/mm/mm_simple_arb.go
@@ -82,57 +82,39 @@ func (a *simpleArbMarketMaker) cfg() *SimpleArbConfig {
}
// arbExists checks if an arbitrage opportunity exists.
-func (a *simpleArbMarketMaker) arbExists() (exists, sellOnDex bool, lotsToArb, dexRate, cexRate uint64, dexDefs, cexDefs map[uint32]uint64, err error) {
+func (a *simpleArbMarketMaker) arbExists() (exists, sellOnDex bool, lotsToArb, dexRate, cexRate uint64, err error) {
sellOnDex = false
- exists, lotsToArb, dexRate, cexRate, buyDexDefs, buyCexDefs, err := a.arbExistsOnSide(sellOnDex)
+ exists, lotsToArb, dexRate, cexRate, err = a.arbExistsOnSide(sellOnDex)
if err != nil || exists {
return
}
sellOnDex = true
- exists, lotsToArb, dexRate, cexRate, sellDexDefs, sellCexDefs, err := a.arbExistsOnSide(sellOnDex)
+ exists, lotsToArb, dexRate, cexRate, err = a.arbExistsOnSide(sellOnDex)
if err != nil || exists {
return
}
- dexDefs = make(map[uint32]uint64)
- cexDefs = make(map[uint32]uint64)
- for assetID, qty := range buyDexDefs {
- dexDefs[assetID] += qty
- }
- for assetID, qty := range sellDexDefs {
- dexDefs[assetID] += qty
- }
- for assetID, qty := range buyCexDefs {
- cexDefs[assetID] += qty
- }
- for assetID, qty := range sellCexDefs {
- cexDefs[assetID] += qty
- }
-
return
}
// arbExistsOnSide checks if an arbitrage opportunity exists either when
// buying or selling on the dex.
-func (a *simpleArbMarketMaker) arbExistsOnSide(sellOnDEX bool) (exists bool, lotsToArb, dexRate, cexRate uint64, dexDefs, cexDefs map[uint32]uint64, err error) {
+func (a *simpleArbMarketMaker) arbExistsOnSide(sellOnDEX bool) (exists bool, lotsToArb, dexRate, cexRate uint64, err error) {
lotSize := a.lotSize
var prevProfit uint64
for numLots := uint64(1); ; numLots++ {
dexAvg, dexExtrema, dexFilled, err := a.book.VWAP(numLots, a.lotSize, !sellOnDEX)
if err != nil {
- return false, 0, 0, 0, nil, nil, fmt.Errorf("error calculating dex VWAP: %w", err)
- }
- if !dexFilled {
- break
+ return false, 0, 0, 0, fmt.Errorf("error calculating dex VWAP: %w", err)
}
-
cexAvg, cexExtrema, cexFilled, err := a.CEX.VWAP(a.baseID, a.quoteID, sellOnDEX, numLots*lotSize)
if err != nil {
- return false, 0, 0, 0, nil, nil, fmt.Errorf("error calculating cex VWAP: %w", err)
+ return false, 0, 0, 0, fmt.Errorf("error calculating cex VWAP: %w", err)
}
- if !cexFilled {
+
+ if !dexFilled || !cexFilled {
break
}
@@ -154,15 +136,15 @@ func (a *simpleArbMarketMaker) arbExistsOnSide(sellOnDEX bool) (exists bool, lot
break
}
- dexSufficient, dexDefs, err := a.core.SufficientBalanceForDEXTrade(dexExtrema, numLots*lotSize, sellOnDEX)
+ dexSufficient, err := a.core.SufficientBalanceForDEXTrade(dexExtrema, numLots*lotSize, sellOnDEX)
if err != nil {
- return false, 0, 0, 0, nil, nil, fmt.Errorf("error checking dex balance: %w", err)
+ return false, 0, 0, 0, fmt.Errorf("error checking dex balance: %w", err)
}
- cexSufficient, cexDefs := a.cex.SufficientBalanceForCEXTrade(a.baseID, a.quoteID, !sellOnDEX, cexExtrema, numLots*lotSize)
+ cexSufficient := a.cex.SufficientBalanceForCEXTrade(a.baseID, a.quoteID, !sellOnDEX, cexExtrema, numLots*lotSize)
if !dexSufficient || !cexSufficient {
if numLots == 1 {
- return false, 0, 0, 0, dexDefs, cexDefs, nil
+ return false, 0, 0, 0, nil
} else {
break
}
@@ -174,7 +156,7 @@ func (a *simpleArbMarketMaker) arbExistsOnSide(sellOnDEX bool) (exists bool, lot
feesInQuoteUnits, err := a.core.OrderFeesInUnits(sellOnDEX, false, dexAvg)
if err != nil {
- return false, 0, 0, 0, nil, nil, fmt.Errorf("error getting fees: %w", err)
+ return false, 0, 0, 0, fmt.Errorf("error getting fees: %w", err)
}
qty := numLots * lotSize
@@ -198,10 +180,10 @@ func (a *simpleArbMarketMaker) arbExistsOnSide(sellOnDEX bool) (exists bool, lot
if lotsToArb > 0 {
a.log.Infof("arb opportunity - sellOnDex: %t, lotsToArb: %d, dexRate: %s, cexRate: %s: profit: %s",
sellOnDEX, lotsToArb, a.fmtRate(dexRate), a.fmtRate(cexRate), a.fmtBase(prevProfit))
- return true, lotsToArb, dexRate, cexRate, nil, nil, nil
+ return true, lotsToArb, dexRate, cexRate, nil
}
- return false, 0, 0, 0, nil, nil, nil
+ return false, 0, 0, 0, nil
}
// executeArb will execute an arbitrage sequence by placing orders on the dex
@@ -368,12 +350,15 @@ func (a *simpleArbMarketMaker) handleDEXOrderUpdate(o *core.Order) {
}
}
-func (a *simpleArbMarketMaker) tryArb(newEpoch uint64) (exists, sellOnDEX bool) {
+func (a *simpleArbMarketMaker) tryArb(newEpoch uint64) (exists, sellOnDEX bool, err error) {
if !(a.checkBotHealth(newEpoch) && a.tradingLimitNotReached(newEpoch)) {
- return false, false
+ return false, false, nil
}
- exists, sellOnDex, lotsToArb, dexRate, cexRate, _, _, _ := a.arbExists()
+ exists, sellOnDex, lotsToArb, dexRate, cexRate, err := a.arbExists()
+ if err != nil {
+ return false, false, err
+ }
if a.log.Level() == dex.LevelTrace {
a.log.Tracef("%s rebalance. exists = %t, %s on dex, lots = %d, dex rate = %s, cex rate = %s",
a.name, exists, sellStr(sellOnDex), lotsToArb, a.fmtRate(dexRate), a.fmtRate(cexRate))
@@ -383,7 +368,7 @@ func (a *simpleArbMarketMaker) tryArb(newEpoch uint64) (exists, sellOnDEX bool)
a.executeArb(sellOnDex, lotsToArb, dexRate, cexRate, newEpoch)
}
- return exists, sellOnDex
+ return exists, sellOnDex, nil
}
// rebalance checks if there is an arbitrage opportunity between the dex and cex,
@@ -402,7 +387,16 @@ func (a *simpleArbMarketMaker) rebalance(newEpoch uint64) {
return
}
- exists, sellOnDex := a.tryArb(newEpoch)
+ epochReport := &EpochReport{EpochNum: newEpoch}
+
+ exists, sellOnDex, err := a.tryArb(newEpoch)
+ if err != nil {
+ epochReport.setPreOrderProblems(err)
+ a.unifiedExchangeAdaptor.updateEpochReport(epochReport)
+ return
+ }
+
+ a.unifiedExchangeAdaptor.updateEpochReport(epochReport)
a.activeArbsMtx.Lock()
remainingArbs := make([]*arbSequence, 0, len(a.activeArbs))
diff --git a/client/mm/mm_test.go b/client/mm/mm_test.go
index 722e0b4b6f..9b4b078cd5 100644
--- a/client/mm/mm_test.go
+++ b/client/mm/mm_test.go
@@ -281,7 +281,6 @@ type tBotCoreAdaptor struct {
maxSellQty uint64
lastTradePlaced *dexOrder
tradeResult *core.Order
- balanceDefs map[uint32]uint64
}
func (c *tBotCoreAdaptor) DEXBalance(assetID uint32) (*BotBalance, error) {
@@ -322,11 +321,11 @@ func (c *tBotCoreAdaptor) OrderFeesInUnits(sell, base bool, rate uint64) (uint64
return c.buyFeesInQuote, nil
}
-func (c *tBotCoreAdaptor) SufficientBalanceForDEXTrade(rate, qty uint64, sell bool) (bool, map[uint32]uint64, error) {
+func (c *tBotCoreAdaptor) SufficientBalanceForDEXTrade(rate, qty uint64, sell bool) (bool, error) {
if sell {
- return qty <= c.maxSellQty, c.balanceDefs, nil
+ return qty <= c.maxSellQty, nil
}
- return qty <= c.maxBuyQty, c.balanceDefs, nil
+ return qty <= c.maxBuyQty, nil
}
func (c *tBotCoreAdaptor) DEXTrade(rate, qty uint64, sell bool) (*core.Order, error) {
@@ -571,7 +570,6 @@ type tBotCexAdaptor struct {
tradeUpdates chan *libxc.Trade
maxBuyQty uint64
maxSellQty uint64
- balanceDefs map[uint32]uint64
}
func newTBotCEXAdaptor() *tBotCexAdaptor {
@@ -621,11 +619,11 @@ func (c *tBotCexAdaptor) FreeUpFunds(assetID uint32, cex bool, amt uint64, currE
}
func (c *tBotCexAdaptor) MidGap(baseID, quoteID uint32) uint64 { return 0 }
-func (c *tBotCexAdaptor) SufficientBalanceForCEXTrade(baseID, quoteID uint32, sell bool, rate, qty uint64) (bool, map[uint32]uint64) {
+func (c *tBotCexAdaptor) SufficientBalanceForCEXTrade(baseID, quoteID uint32, sell bool, rate, qty uint64) bool {
if sell {
- return qty <= c.maxSellQty, c.balanceDefs
+ return qty <= c.maxSellQty
}
- return qty <= c.maxBuyQty, c.balanceDefs
+ return qty <= c.maxBuyQty
}
func (c *tBotCexAdaptor) Book() (_, _ []*core.MiniOrder, _ error) { return nil, nil, nil }
diff --git a/client/webserver/site/src/html/forms.tmpl b/client/webserver/site/src/html/forms.tmpl
index d5d4344b59..38e3fc9352 100644
--- a/client/webserver/site/src/html/forms.tmpl
+++ b/client/webserver/site/src/html/forms.tmpl
@@ -1069,10 +1069,11 @@
-
-
-
+
+
+
+
[[[Asset]]] |
@@ -1095,13 +1096,14 @@
|
|
|
- |
+ |
|
-
-
-
-
+
+
+
+
+
Placements
@@ -1114,8 +1116,8 @@
[[[Required DEX]]] |
[[[Used DEX]]] |
- [[[Required CEX]]] |
- [[[Used CEX]]] |
+
+
diff --git a/client/webserver/site/src/js/forms.ts b/client/webserver/site/src/js/forms.ts
index 92ed0950de..631f207d96 100644
--- a/client/webserver/site/src/js/forms.ts
+++ b/client/webserver/site/src/js/forms.ts
@@ -69,6 +69,7 @@ interface FormsConfig {
export class Forms {
formsDiv: PageElement
currentForm: PageElement | undefined
+ currentFormID: string | undefined
keyup: (e: KeyboardEvent) => void
closed?: () => void
@@ -94,8 +95,9 @@ export class Forms {
}
/* showForm shows a modal form with a little animation. */
- async show (form: HTMLElement): Promise {
+ async show (form: HTMLElement, id?: string): Promise {
this.currentForm = form
+ this.currentFormID = id
Doc.hide(...Array.from(this.formsDiv.children))
form.style.right = '10000px'
Doc.show(this.formsDiv, form)
@@ -108,8 +110,9 @@ export class Forms {
close (): void {
Doc.hide(this.formsDiv)
- if (this.closed) this.closed()
this.currentForm = undefined
+ this.currentFormID = undefined
+ if (this.closed) this.closed()
}
exit () {
diff --git a/client/webserver/site/src/js/markets.ts b/client/webserver/site/src/js/markets.ts
index 8121cd0831..0c55b22a68 100644
--- a/client/webserver/site/src/js/markets.ts
+++ b/client/webserver/site/src/js/markets.ts
@@ -70,7 +70,7 @@ import {
CEXProblemsNote
} from './registry'
import { setOptionTemplates } from './opts'
-import { RunningMarketMakerDisplay } from './mmutil'
+import { RunningMarketMakerDisplay, RunningMMDisplayElements } from './mmutil'
const bind = Doc.bind
@@ -274,7 +274,14 @@ export default class MarketsPage extends BasePage {
this.depositAddrForm = new DepositAddress(page.deposit)
}
- this.mm = new RunningMarketMakerDisplay(page.mmRunning, this.forms, page.orderReportForm, 'markets')
+ const runningMMDisplayElements: RunningMMDisplayElements = {
+ orderReportForm: page.orderReportForm,
+ dexBalancesRowTmpl: page.dexBalancesRowTmpl,
+ placementRowTmpl: page.placementRowTmpl,
+ placementAmtRowTmpl: page.placementAmtRowTmpl
+ }
+ Doc.cleanTemplates(page.dexBalancesRowTmpl, page.placementRowTmpl, page.placementAmtRowTmpl)
+ this.mm = new RunningMarketMakerDisplay(page.mmRunning, this.forms, runningMMDisplayElements, 'markets')
this.reputationMeter = new ReputationMeter(page.reputationMeter)
@@ -283,6 +290,7 @@ export default class MarketsPage extends BasePage {
// Prepare templates for the buy and sell tables and the user's order table.
setOptionTemplates(page)
+
Doc.cleanTemplates(
page.orderRowTmpl, page.durBttnTemplate, page.booleanOptTmpl, page.rangeOptTmpl,
page.orderOptTmpl, page.userOrderTmpl, page.recentMatchesTemplate
diff --git a/client/webserver/site/src/js/mm.ts b/client/webserver/site/src/js/mm.ts
index 31b8bdc3eb..a5b048239b 100644
--- a/client/webserver/site/src/js/mm.ts
+++ b/client/webserver/site/src/js/mm.ts
@@ -24,7 +24,8 @@ import {
PlacementsChart,
BotMarket,
hostedMarketID,
- RunningMarketMakerDisplay
+ RunningMarketMakerDisplay,
+ RunningMMDisplayElements
} from './mmutil'
import Doc, { MiniSlider } from './doc'
import BasePage from './basepage'
@@ -32,7 +33,6 @@ import * as OrderUtil from './orderutil'
import { Forms, CEXConfigurationForm } from './forms'
import * as intl from './locales'
import { StatusBooked } from './orderutil'
-
const mediumBreakpoint = 768
interface FundingSlider {
@@ -184,6 +184,7 @@ export default class MarketMakerPage extends BasePage {
sortedBots: Bot[]
cexes: Record
twoColumn: boolean
+ runningMMDisplayElements: RunningMMDisplayElements
constructor (main: HTMLElement) {
super()
@@ -198,6 +199,13 @@ export default class MarketMakerPage extends BasePage {
this.forms = new Forms(page.forms)
this.cexConfigForm = new CEXConfigurationForm(page.cexConfigForm, (cexName: string) => this.cexConfigured(cexName))
+ this.runningMMDisplayElements = {
+ orderReportForm: page.orderReportForm,
+ dexBalancesRowTmpl: page.dexBalancesRowTmpl,
+ placementRowTmpl: page.placementRowTmpl,
+ placementAmtRowTmpl: page.placementAmtRowTmpl
+ }
+ Doc.cleanTemplates(page.dexBalancesRowTmpl, page.placementRowTmpl, page.placementAmtRowTmpl)
Doc.bind(page.newBot, 'click', () => { this.newBot() })
Doc.bind(page.archivedLogsBtn, 'click', () => { app().loadPage('mmarchives') })
@@ -306,7 +314,7 @@ export default class MarketMakerPage extends BasePage {
const [baseSymbol, quoteSymbol] = [app().assets[baseID].symbol, app().assets[quoteID].symbol]
const mktID = `${baseSymbol}_${quoteSymbol}`
if (!app().exchanges[host]?.markets[mktID]) return
- const bot = new Bot(this, botStatus, startupBalanceCache)
+ const bot = new Bot(this, this.runningMMDisplayElements, botStatus, startupBalanceCache)
page.botRows.appendChild(bot.row.tr)
sortedBots.push(bot)
bots[bot.id] = bot
@@ -411,7 +419,7 @@ class Bot extends BotMarket {
row: BotRow
runDisplay: RunningMarketMakerDisplay
- constructor (pg: MarketMakerPage, status: MMBotStatus, startupBalanceCache?: Record>) {
+ constructor (pg: MarketMakerPage, runningMMElements: RunningMMDisplayElements, status: MMBotStatus, startupBalanceCache?: Record>) {
super(status.config)
startupBalanceCache = startupBalanceCache ?? {}
this.pg = pg
@@ -421,7 +429,7 @@ class Bot extends BotMarket {
const div = this.div = pg.page.botTmpl.cloneNode(true) as PageElement
const page = this.page = Doc.parseTemplate(div)
- this.runDisplay = new RunningMarketMakerDisplay(page.onBox, pg.forms, pg.page.orderReportForm, 'mm')
+ this.runDisplay = new RunningMarketMakerDisplay(page.onBox, pg.forms, runningMMElements, 'mm')
setMarketElements(div, baseID, quoteID, host)
if (cexName) setCexElements(div, cexName)
diff --git a/client/webserver/site/src/js/mmutil.ts b/client/webserver/site/src/js/mmutil.ts
index 485b97e3f2..e6c16fef3c 100644
--- a/client/webserver/site/src/js/mmutil.ts
+++ b/client/webserver/site/src/js/mmutil.ts
@@ -766,6 +766,13 @@ export class BotMarket {
}
}
+export type RunningMMDisplayElements = {
+ orderReportForm: PageElement
+ dexBalancesRowTmpl: PageElement
+ placementRowTmpl: PageElement
+ placementAmtRowTmpl: PageElement
+}
+
export class RunningMarketMakerDisplay {
div: PageElement
page: Record
@@ -778,19 +785,19 @@ export class RunningMarketMakerDisplay {
cexProblems?: CEXProblems
orderReportFormEl: PageElement
orderReportForm: Record
+ displayedOrderReportFormSide: 'buys' | 'sells'
dexBalancesRowTmpl: PageElement
placementRowTmpl: PageElement
placementAmtRowTmpl: PageElement
- displayedSide: 'buys' | 'sells'
- constructor (div: PageElement, forms: Forms, orderReportForm: PageElement, page: string) {
+ constructor (div: PageElement, forms: Forms, elements: RunningMMDisplayElements, page: string) {
this.div = div
this.page = Doc.parseTemplate(div)
- this.orderReportFormEl = orderReportForm
- this.orderReportForm = Doc.idDescendants(orderReportForm)
- this.dexBalancesRowTmpl = this.orderReportForm.dexBalancesRowTmpl
- this.placementRowTmpl = this.orderReportForm.placementRowTmpl
- this.placementAmtRowTmpl = this.orderReportForm.placementAmtRowTmpl
+ this.orderReportFormEl = elements.orderReportForm
+ this.orderReportForm = Doc.idDescendants(elements.orderReportForm)
+ this.dexBalancesRowTmpl = elements.dexBalancesRowTmpl
+ this.placementRowTmpl = elements.placementRowTmpl
+ this.placementAmtRowTmpl = elements.placementAmtRowTmpl
Doc.cleanTemplates(this.dexBalancesRowTmpl, this.placementRowTmpl, this.placementAmtRowTmpl)
this.forms = forms
Doc.bind(this.page.stopBttn, 'click', () => this.stop())
@@ -880,9 +887,9 @@ export class RunningMarketMakerDisplay {
if (n.baseID !== baseID || n.quoteID !== quoteID || n.host !== host) return
if (!n.report) return
this.latestEpoch = n.report
- if (this.forms.currentForm === this.orderReportFormEl) {
- const orderReport = this.displayedSide === 'buys' ? n.report.buysReport : n.report.sellsReport
- if (orderReport) this.updateOrderReport(orderReport, this.displayedSide, n.report.epochNum)
+ if (this.forms.currentForm === this.orderReportFormEl && this.forms.currentFormID === this.mkt.id) {
+ const orderReport = this.displayedOrderReportFormSide === 'buys' ? n.report.buysReport : n.report.sellsReport
+ if (orderReport) this.updateOrderReport(orderReport, this.displayedOrderReportFormSide, n.report.epochNum)
else this.forms.close()
}
this.update()
@@ -909,8 +916,8 @@ export class RunningMarketMakerDisplay {
} = this
// Get fresh stats
const { botCfg: { cexName, basicMarketMakingConfig: bmmCfg }, runStats, latestEpoch, cexProblems } = this.mkt.status()
- if (latestEpoch) this.latestEpoch = latestEpoch
- if (cexProblems) this.cexProblems = cexProblems
+ this.latestEpoch = latestEpoch
+ this.cexProblems = cexProblems
Doc.hide(page.stats, page.cexRow, page.pendingDepositBox, page.pendingWithdrawalBox)
@@ -1035,8 +1042,6 @@ export class RunningMarketMakerDisplay {
return
}
- form.cexLogo.src = CEXDisplayInfos[this.mkt.cexName].logo
- form.cexBalancesTitle.textContent = intl.prep(intl.ID_CEX_BALANCES, { cexName: CEXDisplayInfos[this.mkt.cexName].name })
Doc.empty(form.dexBalancesBody, form.placementsBody)
const createRow = (assetID: number): [PageElement, number] => {
const row = this.dexBalancesRowTmpl.cloneNode(true) as HTMLElement
@@ -1085,9 +1090,16 @@ export class RunningMarketMakerDisplay {
}
setDeficiencyVisibility(totalDeficiency > 0, rows)
- Doc.setVis(this.mkt.cexName, form.cexBalancesTable, form.counterTradeRateHeader)
+ Doc.setVis(this.mkt.cexName, form.cexSection, form.counterTradeRateHeader, form.requiredCEXHeader, form.usedCEXHeader)
let cexAsset: SupportedAsset
if (this.mkt.cexName) {
+ const cexDisplayInfo = CEXDisplayInfos[this.mkt.cexName]
+ if (cexDisplayInfo) {
+ form.cexLogo.src = cexDisplayInfo.logo
+ form.cexBalancesTitle.textContent = intl.prep(intl.ID_CEX_BALANCES, { cexName: cexDisplayInfo.name })
+ } else {
+ console.error(`CEXDisplayInfo not found for ${this.mkt.cexName}`)
+ }
const cexAssetID = side === 'buys' ? this.mkt.baseID : this.mkt.quoteID
cexAsset = app().assets[cexAssetID]
form.cexAsset.textContent = cexAsset.symbol.toUpperCase()
@@ -1112,6 +1124,8 @@ export class RunningMarketMakerDisplay {
if (deficient) {
form.cexDeficiency.textContent = Doc.formatCoinValue(deficiencyCexBal, cexAsset.unitInfo)
form.cexDeficiencyWithPending.textContent = Doc.formatCoinValue(deficiencyWithPendingCexBal, cexAsset.unitInfo)
+ if (deficiencyWithPendingCexBal > 0) form.cexDeficiencyWithPending.classList.add('text-warning')
+ else form.cexDeficiencyWithPending.classList.remove('text-warning')
}
}
@@ -1177,8 +1191,8 @@ export class RunningMarketMakerDisplay {
const report = side === 'buys' ? this.latestEpoch.buysReport : this.latestEpoch.sellsReport
if (!report) return
this.updateOrderReport(report, side, this.latestEpoch.epochNum)
- this.displayedSide = side
- this.forms.show(this.orderReportFormEl)
+ this.displayedOrderReportFormSide = side
+ this.forms.show(this.orderReportFormEl, this.mkt.id)
}
readBook () {
diff --git a/client/webserver/site/src/js/registry.ts b/client/webserver/site/src/js/registry.ts
index 644d65f078..d4832abee4 100644
--- a/client/webserver/site/src/js/registry.ts
+++ b/client/webserver/site/src/js/registry.ts
@@ -982,9 +982,7 @@ export interface TradePlacement {
requiredCex: number
usedDex: Record
usedCex: number
- causesSelfMatch: boolean
error?: BotProblems
- reason: any
}
export interface OrderReport {