From 13a77eab8fefb4d40e1e93a5442f422e3117bb19 Mon Sep 17 00:00:00 2001 From: martonp Date: Wed, 21 May 2025 13:56:19 -0500 Subject: [PATCH] mm: Add live config and balance updates for running bots This change introduces live balance and configuration updates for active bots, along with UI improvements. Starting bots and bot balance allocation now occurs on the 'mmsettings' page (instead of the main 'mm' page). Two allocation options are now available: - Quick config: Set buffers based on the number of lots to match on the same side before funds run low. Also possible to set a slippage buffer for the quote asset and fee reserves for EVM assets. - Manual config: Specify exact asset amounts to allocate to the bot. --- client/core/core.go | 2 +- client/mm/exchange_adaptor.go | 29 +- client/mm/exchange_adaptor_test.go | 25 +- client/mm/mm.go | 47 +- client/mm/mm_test.go | 33 +- client/rpcserver/handlers.go | 4 +- client/rpcserver/types.go | 12 +- client/webserver/api.go | 71 + client/webserver/live_test.go | 16 +- client/webserver/locales/ar.go | 1 - client/webserver/locales/en-us.go | 5 +- client/webserver/locales/pl-pl.go | 1 - client/webserver/site/src/css/icons.scss | 4 + client/webserver/site/src/css/mm.scss | 4 + client/webserver/site/src/html/forms.tmpl | 1 + client/webserver/site/src/html/mm.tmpl | 396 +--- .../webserver/site/src/html/mmsettings.tmpl | 662 +++---- client/webserver/site/src/js/mm.ts | 501 +---- client/webserver/site/src/js/mmsettings.ts | 1715 ++++++++++++----- client/webserver/site/src/js/mmutil.ts | 343 +--- client/webserver/site/src/js/registry.ts | 50 +- client/webserver/webserver.go | 7 + dex/testing/dcrdex/genmarkets.sh | 14 + 23 files changed, 1927 insertions(+), 2016 deletions(-) diff --git a/client/core/core.go b/client/core/core.go index 01b6d32a9c..a72b21875b 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -7274,7 +7274,7 @@ func (c *Core) AssetBalance(assetID uint32) (*WalletBalance, error) { if err != nil { return nil, fmt.Errorf("%d -> %s wallet error: %w", assetID, unbip(assetID), err) } - return c.updateWalletBalance(wallet) + return c.walletBalance(wallet) } func pluralize(n int) string { diff --git a/client/mm/exchange_adaptor.go b/client/mm/exchange_adaptor.go index a35149b37d..796ed50d87 100644 --- a/client/mm/exchange_adaptor.go +++ b/client/mm/exchange_adaptor.go @@ -434,7 +434,7 @@ type unifiedExchangeAdaptor struct { botLoop *dex.ConnectionMaster paused atomic.Bool - autoRebalanceCfg *AutoRebalanceConfig + autoRebalanceCfgV atomic.Value // *AutoRebalanceConfig subscriptionIDMtx sync.RWMutex subscriptionID *int @@ -492,6 +492,13 @@ func (u *unifiedExchangeAdaptor) botCfg() *BotConfig { return u.botCfgV.Load().(*BotConfig) } +func (u *unifiedExchangeAdaptor) autoRebalanceCfg() *AutoRebalanceConfig { + if cfg := u.autoRebalanceCfgV.Load(); cfg != nil { + return cfg.(*AutoRebalanceConfig) + } + return nil +} + // botLooper is just a dex.Connector for a function. type botLooper func(context.Context) (*sync.WaitGroup, error) @@ -2810,7 +2817,7 @@ func (u *unifiedExchangeAdaptor) handleServerConfigUpdate() { cfg := u.botCfg() copy := cfg.copy() copy.updateLotSize(u.lotSize.Load(), coreMkt.LotSize) - err := u.updateConfig(copy) + err := u.updateConfig(copy, u.autoRebalanceCfg()) if err != nil { return err } @@ -2919,13 +2926,14 @@ func (u *unifiedExchangeAdaptor) newDistribution(perLot *lotCosts) *distribution // in the highest matchability score is chosen. func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, dexSellLots, dexBuyLots, maxSellLots, maxBuyLots uint64, dexAdditionalAvailable, cexAdditionalAvailable map[uint32]uint64) { - if u.autoRebalanceCfg == nil { + autoRebalanceCfg := u.autoRebalanceCfg() + if autoRebalanceCfg == nil { return } baseInv, quoteInv := dist.baseInv, dist.quoteInv perLot := dist.perLot - minBaseTransfer, minQuoteTransfer := u.autoRebalanceCfg.MinBaseTransfer, u.autoRebalanceCfg.MinQuoteTransfer + minBaseTransfer, minQuoteTransfer := autoRebalanceCfg.MinBaseTransfer, autoRebalanceCfg.MinQuoteTransfer additionalBaseFees, additionalQuoteFees := perLot.baseFunding, perLot.quoteFunding if u.baseID == u.quoteFeeID { @@ -3146,7 +3154,7 @@ func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, dexSellLo // scoreSplit scores a proposed split using all combinations of transfer // sources. scoreSplit := func(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote uint64) { - if !u.autoRebalanceCfg.InternalOnly { + if !autoRebalanceCfg.InternalOnly { scoreSplitSource(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote, allExternal) scoreSplitSource(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote, onlyBaseInternal) scoreSplitSource(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote, onlyQuoteInternal) @@ -3323,7 +3331,8 @@ func (u *unifiedExchangeAdaptor) doExternalTransfers(dist *distribution, currEpo type distributionFunc func(dexAvail, cexAvail map[uint32]uint64) (*distribution, error) func (u *unifiedExchangeAdaptor) tryTransfers(currEpoch uint64, df distributionFunc) (actionTaken bool, err error) { - if u.autoRebalanceCfg == nil { + autoRebalanceCfg := u.autoRebalanceCfg() + if autoRebalanceCfg == nil { return false, nil } @@ -3345,7 +3354,7 @@ func (u *unifiedExchangeAdaptor) tryTransfers(currEpoch uint64, df distributionF return false, err } - if !u.autoRebalanceCfg.InternalOnly { + if !autoRebalanceCfg.InternalOnly { return u.doExternalTransfers(dist, currEpoch) } @@ -3817,18 +3826,20 @@ func (u *unifiedExchangeAdaptor) applyInventoryDiffs(balanceDiffs *BotInventoryD return mods } -func (u *unifiedExchangeAdaptor) updateConfig(cfg *BotConfig) error { +func (u *unifiedExchangeAdaptor) updateConfig(cfg *BotConfig, autoRebalanceCfg *AutoRebalanceConfig) error { if err := validateConfigUpdate(u.botCfg(), cfg); err != nil { return err } u.botCfgV.Store(cfg) + u.autoRebalanceCfgV.Store(autoRebalanceCfg) u.updateConfigEvent(cfg) return nil } func (u *unifiedExchangeAdaptor) updateInventory(balanceDiffs *BotInventoryDiffs) { u.updateInventoryEvent(u.applyInventoryDiffs(balanceDiffs)) + u.sendStatsUpdate() } func (u *unifiedExchangeAdaptor) Book() (buys, sells []*core.MiniOrder, _ error) { @@ -4061,7 +4072,6 @@ func newUnifiedExchangeAdaptor(cfg *exchangeAdaptorCfg) (*unifiedExchangeAdaptor initialBalances: initialBalances, baseTraits: baseTraits, quoteTraits: quoteTraits, - autoRebalanceCfg: cfg.autoRebalanceConfig, internalTransfer: cfg.internalTransfer, baseDexBalances: baseDEXBalances, @@ -4077,6 +4087,7 @@ func newUnifiedExchangeAdaptor(cfg *exchangeAdaptorCfg) (*unifiedExchangeAdaptor adaptor.fiatRates.Store(map[uint32]float64{}) adaptor.botCfgV.Store(cfg.botCfg) + adaptor.autoRebalanceCfgV.Store(cfg.autoRebalanceConfig) return adaptor, nil } diff --git a/client/mm/exchange_adaptor_test.go b/client/mm/exchange_adaptor_test.go index 9fc2aa2172..43a0924704 100644 --- a/client/mm/exchange_adaptor_test.go +++ b/client/mm/exchange_adaptor_test.go @@ -512,7 +512,7 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { tCore := newTCore() u.CEX = cex u.clientCore = tCore - u.autoRebalanceCfg = &AutoRebalanceConfig{} + u.autoRebalanceCfgV.Store(&AutoRebalanceConfig{}) a := &arbMarketMaker{unifiedExchangeAdaptor: u} u.botCfgV.Store(&BotConfig{ ArbMarketMakerConfig: &ArbMarketMakerConfig{Profit: profit}, @@ -628,8 +628,10 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { if err != nil { t.Fatalf("Error getting lot costs: %v", err) } - a.autoRebalanceCfg.MinBaseTransfer = lotSize - a.autoRebalanceCfg.MinQuoteTransfer = min(perLot.cexQuote, perLot.dexQuote) + a.autoRebalanceCfgV.Store(&AutoRebalanceConfig{ + MinBaseTransfer: lotSize, + MinQuoteTransfer: min(perLot.cexQuote, perLot.dexQuote), + }) } dexAvailableBalances := map[uint32]uint64{} @@ -695,6 +697,15 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { } } + updateMinTransfer := func(asset string, value uint64) { + curr := a.autoRebalanceCfgV.Load().(*AutoRebalanceConfig) + if asset == "base" { + curr.MinBaseTransfer = value + } else { + curr.MinQuoteTransfer = value + } + } + setLots(1, 1) // Base asset - perfect distribution - no action setBals(minDexBase, minCexBase, minDexQuote, minCexQuote) @@ -711,8 +722,8 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { 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 + // Raise the transfer threshold by one atom and it should zero the withdraw. + updateMinTransfer("base", minDexBase+1) checkDistribution(0, 0, 0, 0, false, false) // Same for quote @@ -724,7 +735,7 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { setAvailableBalances(0, 0, minDexQuote-1, 0) checkDistribution(0, 0, 0, minDexQuote, false, false) setAvailableBalances(0, 0, 0, 0) - a.autoRebalanceCfg.MinQuoteTransfer = minDexQuote + 1 + updateMinTransfer("quote", minDexQuote+1) checkDistribution(0, 0, 0, 0, false, false) // Base deposit @@ -790,7 +801,7 @@ func testDistribution(t *testing.T, baseID, quoteID uint32) { // 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 + updateMinTransfer("quote", 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) diff --git a/client/mm/mm.go b/client/mm/mm.go index 37051d9da4..3214512de0 100644 --- a/client/mm/mm.go +++ b/client/mm/mm.go @@ -106,7 +106,7 @@ type bot interface { stats() *RunStats latestEpoch() *EpochReport latestCEXProblems() *CEXProblems - updateConfig(cfg *BotConfig) error + updateConfig(cfg *BotConfig, autoRebalanceCfg *AutoRebalanceConfig) error updateInventory(balanceDiffs *BotInventoryDiffs) withPause(func() error) error timeStart() int64 @@ -477,6 +477,21 @@ func (m *MarketMaker) MarketReport(host string, baseID, quoteID uint32) (*Market }, nil } +// MaxFundingFees returns the maximum funding fees for a bot on a market. +func (m *MarketMaker) MaxFundingFees(mwh *MarketWithHost, maxBuyPlacements, maxSellPlacements uint32, baseOptions, quoteOptions map[string]string) (buyFees, sellFees uint64, err error) { + buyFundingFees, err := m.core.MaxFundingFees(mwh.QuoteID, mwh.Host, maxBuyPlacements, quoteOptions) + if err != nil { + return 0, 0, fmt.Errorf("failed to get buy funding fees: %w", err) + } + + sellFundingFees, err := m.core.MaxFundingFees(mwh.BaseID, mwh.Host, maxSellPlacements, baseOptions) + if err != nil { + return 0, 0, fmt.Errorf("failed to get sell funding fees: %w", err) + } + + return buyFundingFees, sellFundingFees, nil +} + func (m *MarketMaker) loginAndUnlockWallets(pw []byte, cfg *BotConfig) error { err := m.core.Login(pw) if err != nil { @@ -1140,19 +1155,20 @@ func (m *MarketMaker) UpdateRunningBotInventory(mkt *MarketWithHost, balanceDiff } if err := rb.withPause(func() error { - rb.bot.updateInventory(balanceDiffs) + rb.updateInventory(balanceDiffs) return nil }); err != nil { rb.cm.Disconnect() return fmt.Errorf("configuration update error. bot stopped: %w", err) } + return nil } // UpdateRunningBotCfg updates the configuration and balance allocation for a // running bot. If saveUpdate is true, the update configuration will be saved // to the default config file. -func (m *MarketMaker) UpdateRunningBotCfg(cfg *BotConfig, balanceDiffs *BotInventoryDiffs, saveUpdate bool) error { +func (m *MarketMaker) UpdateRunningBotCfg(cfg *BotConfig, balanceDiffs *BotInventoryDiffs, autoRebalanceCfg *AutoRebalanceConfig, saveUpdate bool) error { m.startUpdateMtx.Lock() defer m.startUpdateMtx.Unlock() @@ -1181,12 +1197,15 @@ func (m *MarketMaker) UpdateRunningBotCfg(cfg *BotConfig, balanceDiffs *BotInven var stoppedOracle, startedOracle, updateSuccess bool defer func() { - if updateSuccess { - return + if updateSuccess && saveUpdate { + m.updateDefaultBotConfig(cfg) } - if startedOracle { + + if !updateSuccess && startedOracle { m.oracle.stopAutoSyncingMarket(cfg.BaseID, cfg.QuoteID) - } else if stoppedOracle { + } + + if !updateSuccess && stoppedOracle { err := m.oracle.startAutoSyncingMarket(oldCfg.BaseID, oldCfg.QuoteID) if err != nil { m.log.Errorf("Error restarting oracle for %s: %v", mkt, err) @@ -1206,7 +1225,7 @@ func (m *MarketMaker) UpdateRunningBotCfg(cfg *BotConfig, balanceDiffs *BotInven } if err := rb.withPause(func() error { - if err := rb.updateConfig(cfg); err != nil { + if err := rb.updateConfig(cfg, autoRebalanceCfg); err != nil { return err } if balanceDiffs != nil { @@ -1769,10 +1788,14 @@ func (m *MarketMaker) availableBalances(mkt *MarketWithHost, cexCfg *CEXConfig) // 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. -func (m *MarketMaker) AvailableBalances(mkt *MarketWithHost, alternateConfigPath *string) (dexBalances, cexBalances map[uint32]uint64, _ error) { - _, cexCfg, err := m.configsForMarket(mkt, alternateConfigPath) - if err != nil { - return nil, nil, err +func (m *MarketMaker) AvailableBalances(mkt *MarketWithHost, cexName *string) (dexBalances, cexBalances map[uint32]uint64, _ error) { + var cexCfg *CEXConfig + if cexName != nil && *cexName != "" { + cex := m.cexes[*cexName] + if cex == nil { + return nil, nil, fmt.Errorf("CEX %s not found", *cexName) + } + cexCfg = cex.CEXConfig } return m.availableBalances(mkt, cexCfg) diff --git a/client/mm/mm_test.go b/client/mm/mm_test.go index 53cf77fa7d..42617d2d9c 100644 --- a/client/mm/mm_test.go +++ b/client/mm/mm_test.go @@ -456,6 +456,14 @@ func (c *tCEX) Markets(ctx context.Context) (map[string]*libxc.Market, error) { return nil, nil } func (c *tCEX) Balance(assetID uint32) (*libxc.ExchangeBalance, error) { + if c.balanceErr != nil { + return nil, c.balanceErr + } + + if c.balances[assetID] == nil { + return &libxc.ExchangeBalance{}, nil + } + return c.balances[assetID], c.balanceErr } func (c *tCEX) Trade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64, updaterID int) (*libxc.Trade, error) { @@ -654,7 +662,7 @@ func (t *tExchangeAdaptor) CEXBalance(assetID uint32) *BotBalance { return t.cexBalances[assetID] } func (t *tExchangeAdaptor) stats() *RunStats { return nil } -func (t *tExchangeAdaptor) updateConfig(cfg *BotConfig) error { +func (t *tExchangeAdaptor) updateConfig(cfg *BotConfig, autoRebalanceCfg *AutoRebalanceConfig) error { t.cfg = cfg return nil } @@ -768,9 +776,9 @@ func TestAvailableBalances(t *testing.T) { 60001: 6e5, }) - checkAvailableBalances := func(mkt *MarketWithHost, expDex, expCex map[uint32]uint64) { + checkAvailableBalances := func(mkt *MarketWithHost, cexName *string, expDex, expCex map[uint32]uint64) { t.Helper() - dexBalances, cexBalances, err := mm.AvailableBalances(mkt, nil) + dexBalances, cexBalances, err := mm.AvailableBalances(mkt, cexName) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -782,11 +790,14 @@ func TestAvailableBalances(t *testing.T) { } } + binanceName := libxc.Binance + binanceUSName := libxc.BinanceUS + // No running bots - checkAvailableBalances(dcrBtc, map[uint32]uint64{42: 9e5, 0: 7e5}, map[uint32]uint64{}) - checkAvailableBalances(ethBtc, map[uint32]uint64{60: 8e5, 0: 7e5}, map[uint32]uint64{60: 9e5, 0: 8e5}) - checkAvailableBalances(btcUsdc, map[uint32]uint64{0: 7e5, 60: 8e5, 60001: 6e5}, map[uint32]uint64{0: 8e5, 60001: 6e5}) - checkAvailableBalances(dcrUsdc, map[uint32]uint64{42: 9e5, 60: 8e5, 60001: 6e5}, map[uint32]uint64{42: 7e5, 60001: 6e5}) + checkAvailableBalances(dcrBtc, nil, map[uint32]uint64{42: 9e5, 0: 7e5}, map[uint32]uint64{}) + checkAvailableBalances(ethBtc, &binanceName, map[uint32]uint64{60: 8e5, 0: 7e5}, map[uint32]uint64{60: 9e5, 0: 8e5}) + checkAvailableBalances(btcUsdc, &binanceName, map[uint32]uint64{0: 7e5, 60: 8e5, 60001: 6e5}, map[uint32]uint64{0: 8e5, 60001: 6e5}) + checkAvailableBalances(dcrUsdc, &binanceUSName, map[uint32]uint64{42: 9e5, 60: 8e5, 60001: 6e5}, map[uint32]uint64{42: 7e5, 60001: 6e5}) rb := &runningBot{ bot: &tExchangeAdaptor{ @@ -804,8 +815,8 @@ func TestAvailableBalances(t *testing.T) { } mm.runningBots[*btcUsdc] = rb - checkAvailableBalances(dcrBtc, map[uint32]uint64{42: 9e5, 0: 3e5}, map[uint32]uint64{}) - checkAvailableBalances(ethBtc, map[uint32]uint64{60: 7e5, 0: 3e5}, map[uint32]uint64{60: 9e5, 0: 5e5}) - 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}) + checkAvailableBalances(dcrBtc, nil, map[uint32]uint64{42: 9e5, 0: 3e5}, map[uint32]uint64{}) + checkAvailableBalances(ethBtc, &binanceName, map[uint32]uint64{60: 7e5, 0: 3e5}, map[uint32]uint64{60: 9e5, 0: 5e5}) + checkAvailableBalances(btcUsdc, &binanceName, map[uint32]uint64{0: 3e5, 60: 7e5, 60001: 4e5}, map[uint32]uint64{0: 5e5, 60001: 4e5}) + checkAvailableBalances(dcrUsdc, &binanceUSName, map[uint32]uint64{42: 9e5, 60: 7e5, 60001: 4e5}, map[uint32]uint64{42: 7e5, 60001: 6e5}) } diff --git a/client/rpcserver/handlers.go b/client/rpcserver/handlers.go index 28a0ed9ef0..b94be8b031 100644 --- a/client/rpcserver/handlers.go +++ b/client/rpcserver/handlers.go @@ -872,7 +872,7 @@ func handleMMAvailableBalances(s *RPCServer, params *RawParams) *msgjson.Respons return usage(mmAvailableBalancesRoute, err) } - dexBalances, cexBalances, err := s.mm.AvailableBalances(form.mkt, &form.cfgFilePath) + dexBalances, cexBalances, err := s.mm.AvailableBalances(form.mkt, form.cexName) if err != nil { resErr := msgjson.NewError(msgjson.RPCMMAvailableBalancesError, "unable to get available balances: %v", err) return createResponse(mmAvailableBalancesRoute, nil, resErr) @@ -951,7 +951,7 @@ func handleUpdateRunningBotCfg(s *RPCServer, params *RawParams) *msgjson.Respons return createResponse(updateRunningBotCfgRoute, nil, resErr) } - err = s.mm.UpdateRunningBotCfg(botCfg, form.balances, false) + err = s.mm.UpdateRunningBotCfg(botCfg, form.balances, nil, false) if err != nil { resErr := msgjson.NewError(msgjson.RPCUpdateRunningBotCfgError, "unable to update running bot: %v", err) return createResponse(updateRunningBotCfgRoute, nil, resErr) diff --git a/client/rpcserver/types.go b/client/rpcserver/types.go index 16991f32d4..1aab1ae334 100644 --- a/client/rpcserver/types.go +++ b/client/rpcserver/types.go @@ -196,8 +196,8 @@ type addRemovePeerForm struct { } type mmAvailableBalancesForm struct { - cfgFilePath string - mkt *mm.MarketWithHost + mkt *mm.MarketWithHost + cexName *string } type startBotForm struct { @@ -877,16 +877,18 @@ func parseMktWithHost(host, baseID, quoteID string) (*mm.MarketWithHost, error) } func parseMMAvailableBalancesArgs(params *RawParams) (*mmAvailableBalancesForm, error) { - if err := checkNArgs(params, []int{0}, []int{4}); err != nil { + if err := checkNArgs(params, []int{0}, []int{3}); err != nil { return nil, err } form := new(mmAvailableBalancesForm) - form.cfgFilePath = params.Args[0] - mkt, err := parseMktWithHost(params.Args[1], params.Args[2], params.Args[3]) + mkt, err := parseMktWithHost(params.Args[0], params.Args[1], params.Args[2]) if err != nil { return nil, err } form.mkt = mkt + if len(params.Args) > 3 { + form.cexName = ¶ms.Args[3] + } return form, nil } diff --git a/client/webserver/api.go b/client/webserver/api.go index 9ac83074ab..fe2f941dc5 100644 --- a/client/webserver/api.go +++ b/client/webserver/api.go @@ -1784,6 +1784,58 @@ func (s *WebServer) apiStakeStatus(w http.ResponseWriter, r *http.Request) { }) } +func (s *WebServer) apiAvailableBalances(w http.ResponseWriter, r *http.Request) { + var req struct { + Market *mm.MarketWithHost `json:"market"` + CEXName *string `json:"cexName,omitempty"` + } + if !readPost(w, r, &req) { + return + } + dexBalances, cexBalances, err := s.mm.AvailableBalances(req.Market, req.CEXName) + if err != nil { + s.writeAPIError(w, fmt.Errorf("error fetching available balances: %w", err)) + return + } + + writeJSON(w, &struct { + OK bool `json:"ok"` + DEXBalances map[uint32]uint64 `json:"dexBalances"` + CEXBalances map[uint32]uint64 `json:"cexBalances"` + }{ + OK: true, + DEXBalances: dexBalances, + CEXBalances: cexBalances, + }) +} + +func (s *WebServer) apiMaxFundingFees(w http.ResponseWriter, r *http.Request) { + var req struct { + Market *mm.MarketWithHost `json:"market"` + MaxBuyPlacements uint32 `json:"maxBuyPlacements"` + MaxSellPlacements uint32 `json:"maxSellPlacements"` + BaseOptions map[string]string `json:"baseOptions"` + QuoteOptions map[string]string `json:"quoteOptions"` + } + if !readPost(w, r, &req) { + return + } + buyFees, sellFees, err := s.mm.MaxFundingFees(req.Market, req.MaxBuyPlacements, req.MaxSellPlacements, req.BaseOptions, req.QuoteOptions) + if err != nil { + s.writeAPIError(w, fmt.Errorf("error getting max funding fees: %w", err)) + return + } + writeJSON(w, &struct { + OK bool `json:"ok"` + BuyFees uint64 `json:"buyFees"` + SellFees uint64 `json:"sellFees"` + }{ + OK: true, + BuyFees: buyFees, + SellFees: sellFees, + }) +} + func (s *WebServer) apiSetVSP(w http.ResponseWriter, r *http.Request) { var req struct { AssetID uint32 `json:"assetID"` @@ -2000,6 +2052,25 @@ func (s *WebServer) apiUpdateBotConfig(w http.ResponseWriter, r *http.Request) { writeJSON(w, simpleAck()) } +func (s *WebServer) apiUpdateRunningBot(w http.ResponseWriter, r *http.Request) { + var form struct { + Cfg *mm.BotConfig `json:"cfg"` + AutoRebalanceCfg *mm.AutoRebalanceConfig `json:"autoRebalanceCfg"` + Diffs *mm.BotInventoryDiffs `json:"diffs"` + } + if !readPost(w, r, &form) { + s.writeAPIError(w, fmt.Errorf("failed to read config")) + return + } + + if err := s.mm.UpdateRunningBotCfg(form.Cfg, form.Diffs, form.AutoRebalanceCfg, true); err != nil { + s.writeAPIError(w, err) + return + } + + writeJSON(w, simpleAck()) +} + func (s *WebServer) apiRemoveBotConfig(w http.ResponseWriter, r *http.Request) { var form struct { Host string `json:"host"` diff --git a/client/webserver/live_test.go b/client/webserver/live_test.go index 6923ce649e..308013e64f 100644 --- a/client/webserver/live_test.go +++ b/client/webserver/live_test.go @@ -2271,10 +2271,6 @@ func (m *TMarketMaker) UpdateBotConfig(updatedCfg *mm.BotConfig) error { return nil } -func (m *TMarketMaker) UpdateRunningBot(updatedCfg *mm.BotConfig, balanceDiffs *mm.BotInventoryDiffs, saveUpdate bool) error { - return m.UpdateBotConfig(updatedCfg) -} - func (m *TMarketMaker) RemoveBotConfig(host string, baseID, quoteID uint32) error { for i := 0; i < len(m.cfg.BotConfigs); i++ { botCfg := m.cfg.BotConfigs[i] @@ -2607,6 +2603,18 @@ func (m *TMarketMaker) CEXBook(host string, baseID, quoteID uint32) (buys, sells return book.Buys, book.Sells, nil } +func (m *TMarketMaker) AvailableBalances(mkt *mm.MarketWithHost, cexName *string) (dexBalances, cexBalances map[uint32]uint64, _ error) { + return map[uint32]uint64{mkt.BaseID: 1e6, mkt.QuoteID: 1e6}, map[uint32]uint64{mkt.BaseID: 1e6, mkt.QuoteID: 1e6}, nil +} + +func (m *TMarketMaker) MaxFundingFees(mkt *mm.MarketWithHost, maxBuyPlacements, maxSellPlacements uint32, baseOptions, quoteOptions map[string]string) (buyFees, sellFees uint64, _ error) { + return 1e4, 1e4, nil +} + +func (m *TMarketMaker) UpdateRunningBotCfg(cfg *mm.BotConfig, balanceDiffs *mm.BotInventoryDiffs, autoRebalanceCfg *mm.AutoRebalanceConfig, saveUpdate bool) error { + return nil +} + func makeRequiredAction(assetID uint32, actionID string) *asset.ActionRequiredNote { txID := dex.Bytes(encode.RandomBytes(32)).String() var payload any diff --git a/client/webserver/locales/ar.go b/client/webserver/locales/ar.go index b36fc2bc3f..bc1f36ff68 100644 --- a/client/webserver/locales/ar.go +++ b/client/webserver/locales/ar.go @@ -357,7 +357,6 @@ var Ar = map[string]*intl.Translation{ "Immature tickets": {T: "التذاكر غير الناضجة"}, "app_pw_reg": {Version: 1, T: "أدخل كلمة مرور التطبيق لتأكيد تسجيل منصة المبادلات اللامركزية DEX وإنشاء السندات."}, "treasury spends": {T: "نفقات الخزينة"}, - "bots_running_view_only": {T: "البوتات قيد التشغيل. أنت في وضع العرض فقط."}, "buy_placements_tooltip": {T: "تحديد مواضع الشراء للبوت. سيقوم البوت بوضع الطلبات حسب ترتيب الأولوية إذا لم يكن الرصيد كافيًا لتقديم جميع الطلبات."}, "no_limit_bullet": {T: "ليس هناك حد لعدد إنشاء اللوت"}, "Error": {T: "خطأ"}, diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index 6273c7fdfd..087049c347 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -449,6 +449,7 @@ var EnUS = map[string]*intl.Translation{ "Minimum Balance": {T: "Minimum Balance"}, "Minimum Transfer": {T: "Minimum Transfer"}, "update_settings": {Version: 1, T: "Save Settings"}, + "update_running": {T: "Update Running Bot"}, "create_bot": {T: "Create Bot"}, "reset_settings": {T: "Reset Settings"}, "gap_strategy": {T: "Gap Strategy"}, @@ -549,7 +550,6 @@ var EnUS = map[string]*intl.Translation{ "decred_privacy": {T: "Decred's form of privacy is especially powerful because Decred wallets integrate privacy with staking, which facilitates a consistently large anonymity set, a critical feature for privacy."}, "privacy_optional": {T: "Privacy is completely optional, and can be disabled at any time. There are increased transaction fees associated with privacy, but these fees have historically been relatively negligible."}, "privacy_unlocked": {T: "The wallet must remain unlocked while mixing."}, - "bots_running_view_only": {T: "Bots are running. You are in view-only mode."}, "select_a_cex_prompt": {T: "Select an exchange to enable arbitrage"}, "Market not available": {T: "Market not available"}, "bot_profit_title": {T: "Choose your profit threshold"}, @@ -687,10 +687,13 @@ var EnUS = map[string]*intl.Translation{ "Wallet Balances": {T: "Wallet Balances"}, "Placements": {T: "Placements"}, "delete_bot": {T: "Delete Bot"}, + "bot_running": {T: "This bot is currently running"}, + "Asset Allocations": {T: "Asset Allocations"}, "export_logs": {T: "Export Logs"}, "address has been used": {T: "address has been used"}, "Allow external transfers": {T: "Allow external transfers"}, "external_transfers_tooltip": {T: "When enabled, the bot will be able to transfer funds between the DEX and the CEX."}, "Internal transfers only": {T: "Internal transfers only"}, "internal_only_tooltip": {T: "When enabled, the bot will be able to use any available funds in the wallet to simulate a transfer between the DEX and the CEX, (i.e. increase the bot's DEX balance and decrease the bot's CEX balance when a withdrawal needs to be made) but no actual transfers will be made."}, + "running_bot_allocation_note": {T: "This bot is already running. The numbers below are the allocation adjustments that will be made, not the total balance that will be allocated."}, } diff --git a/client/webserver/locales/pl-pl.go b/client/webserver/locales/pl-pl.go index 147fa170fe..b6cbf27c76 100644 --- a/client/webserver/locales/pl-pl.go +++ b/client/webserver/locales/pl-pl.go @@ -538,7 +538,6 @@ var PlPL = map[string]*intl.Translation{ "privacy_intro": {T: "Po włączeniu prywatności wszystkie środki będą wysyłane za pośrednictwem usługi maskowania historii adresów przy użyciu protokołu o nazwie StakeShuffle."}, "decred_privacy": {T: "Forma prywatności Decred jest szczególnie potężna, ponieważ portfele Decred integrują prywatność ze stakingiem, co daje ciągły dostęp do dużego zbioru anonimowości będącego kluczową cechą prywatności."}, "privacy_optional": {T: "Prywatność jest całkowicie opcjonalna i można ją wyłączyć w dowolnym momencie. Z prywatnością wiążą się zwiększone opłaty transakcyjne, ale w przeszłości były one stosunkowo niewielkie."}, - "bots_running_view_only": {T: "Boty są uruchomione. Jesteś w trybie tylko do podglądu."}, "select_a_cex_prompt": {T: "Wybierz giełdę, aby uruchomić arbitraż"}, "Market not available": {T: "Rynek niedostępny"}, "bot_profit_title": {T: "Wybierz próg zysku"}, diff --git a/client/webserver/site/src/css/icons.scss b/client/webserver/site/src/css/icons.scss index d25d7b963c..99783b26b0 100644 --- a/client/webserver/site/src/css/icons.scss +++ b/client/webserver/site/src/css/icons.scss @@ -226,3 +226,7 @@ .ico-lever::before { content: "\e91c"; } + +.ico-cogs::before { + content: "\e995"; +} diff --git a/client/webserver/site/src/css/mm.scss b/client/webserver/site/src/css/mm.scss index 1136b1c1ed..5b5f637503 100644 --- a/client/webserver/site/src/css/mm.scss +++ b/client/webserver/site/src/css/mm.scss @@ -11,6 +11,10 @@ div[data-handler=mm] { max-width: 75px; } + .mw-500 { + max-width: 500px; + } + [data-tmpl=value].wide { width: 3rem; } diff --git a/client/webserver/site/src/html/forms.tmpl b/client/webserver/site/src/html/forms.tmpl index 22aa1d9649..3a9565ece3 100644 --- a/client/webserver/site/src/html/forms.tmpl +++ b/client/webserver/site/src/html/forms.tmpl @@ -1053,6 +1053,7 @@
+
diff --git a/client/webserver/site/src/html/mm.tmpl b/client/webserver/site/src/html/mm.tmpl index cad9f4e4bb..856e88a45b 100644 --- a/client/webserver/site/src/html/mm.tmpl +++ b/client/webserver/site/src/html/mm.tmpl @@ -30,9 +30,6 @@ - - -
P/L @@ -142,207 +139,76 @@
-
-
- {{- /* PROJECTED ALLOCATIONS */ -}} + {{- /* AVAILABLE BALANCES */ -}}
- Projected Allocation -
-
-
-
USD
+ Available Balances
- {{- /* PROJECTED BASE ASSET ALLOCATIONS */ -}} -
-
- - -
-
-
- + {{- /* BASE ASSET */ -}} +
+
+ +
+
-
-
- - ├─ - Booking Fees - - -
-
- - ├─ - CEX Inventory - - -
-
- - ├─ - Order Inventory - - -
-
- - └─ - Order Reserves - (%) - - -
+
+ |- +
-
- ~ - - USD +
+ |- +
- {{- /* PROJECTED QUOTE ASSET ALLOCATIONS */ -}} -
-
- - -
-
-
- + {{- /* QUOTE ASSET */ -}} +
+
+ +
+
-
-
- - ├─ - Booking Fees - - -
-
- - ├─ - CEX Inventory - - -
-
- - ├─ - Order Inventory - - -
-
- - ├─ - Order Reserves - (%) - - -
-
- - └─ - Slippage Buffer - (%) - - -
+
+ |- +
-
- ~ - - USD +
+ |- +
- {{- /* PROJECTED BASE TOKEN FEE ALLOCATIONS */ -}} -
-
-
- - -
-
-
- -
-
-
-
- - ├─ - Swap Fee Reserves - () - - -
-
-
- - └─ - Booking Fees - - -
-
- ~ - - USD + {{- /* BASE FEE ASSET */ -}} +
+
+ +
+
- {{- /* PROJECTED QUOTE TOKEN FEE ALLOCATIONS */ -}} -
-
-
- - -
-
-
- -
-
-
-
- - ├─ - Swap Fee Reserves - () - - -
-
- - └─ - Booking Fees - - -
-
-
- ~ - - USD + {{- /* QUOTE FEE ASSET */ -}} +
+
+ +
+
+
{{- /* CURRENT MARKET DATA */ -}} @@ -393,20 +259,6 @@
- {{- /* PLACEMENTS */ -}} -
-
-
- - buy placements -
-
- - sell placements -
-
-
-
@@ -426,172 +278,6 @@ settings
- - {{- /* ALLOCATE AND START */ -}} -
- - {{- /* FUNDING STATUS DEPENDENT MESSAGING AND START BUTTON */ -}} -
-
-
- - You have all the funding to run your program immediately. - - You also have some flexibility to determine where funds are sourced. - -
- -
- You have enough funding, but it's not all in the right place. - Luckily, you have withdrawals and deposits enabled, so the problem - could resolve itself. Should we start in this unbalanced condition? -
- -
- You do not appear to have the funding to satisfy your program - configuration. If you'd like, we can still start in this starved - condition, but the bot will probably not perform as intended. -
- -
- - You have existing orders on this market. These will be cancelled - when you start the bot. -
-
- -
- -
- - go back -
-
-
- -
- - {{- /* PROPOSED ALLOCATIONS AND ADJUSTMENTS */ -}} -
- {{- /* CEX AND DEX LOGO COLUMN HEADERS */ -}} -
- -
-
-
- -
-
- - {{- /* PROPOSED BASE ALLOCATIONS */ -}} -
-
- -
-
-
-
-
-
-
-
- - -
-
- - USD -
-
-
-
- - -
-
- - USD -
-
- - {{- /* PROPOSED QUOTE ALLOCATIONS */ -}} -
-
- -
-
-
-
-
-
-
-
- - -
-
- - USD -
-
-
-
- - -
-
- - USD -
-
- - {{- /* BASE TOKEN FEE ALLOCATIONS */ -}} -
-
- -
-
-
-
-
-
- - -
-
- - USD -
-
- - {{- /* QUOTE TOKEN FEE ALLOCATIONS */ -}} -
-
- -
-
-
-
-
-
- - -
-
- - USD -
-
-
-
- ~ - - USD - total -
-
diff --git a/client/webserver/site/src/html/mmsettings.tmpl b/client/webserver/site/src/html/mmsettings.tmpl index c082ecf210..1f59a1f3cc 100644 --- a/client/webserver/site/src/html/mmsettings.tmpl +++ b/client/webserver/site/src/html/mmsettings.tmpl @@ -21,10 +21,10 @@ settings
-
+
{{- /* PLACEMENTS */ -}} -
+
[[[Market Maker Settings]]]
@@ -54,12 +54,13 @@
+ +
+ [[[bot_running]]] +
+ {{- /* MANUAL CONFIG */ -}}
- {{- /* VIEW-ONLY MODE */ -}} -
- [[[bots_running_view_only]]] -
{{- /* STRATEGY SELECTION */ -}}
@@ -182,7 +183,7 @@ {{- /* QUICK CONFIG */ -}}
- [[[Quick Placements]]] + [[[Quick Placements]]] - - +
+ + + +
@@ -354,321 +362,327 @@
- {{- /* ASSET SETTINGS */ -}} -
-
-
-
- - {{- /* LOGO AND TOTAL ALLOCATION */ -}} -
-
- - -
-
-
- - -
-
- ~ USD -
+
+
+
+ + [[[Asset Allocations]]] +
+ +
+ [[[running_bot_allocation_note]]] +
+ +
+ +
+ Sufficient Funds +
+ Insufficient Funds +
+ Sufficient With Rebalance + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+ +
+
+ Buy Buffer + +
+
+ Number of Buys + +
+
+
+
+
+
- {{- /* ALLOCATIONS */ -}} -
- - {{- /* ASSET ALLOCATIONS */ -}} -
- {{- /* DEX BOOKING */ -}} -
-
- Order Inventory - - - lots - - -
-
- x - - - per lot - -
-
- = - - -
-
- - {{- /* CEX COUNTER INVENTORY */ -}} -
- CEX Inventory -
- - -
-
- - {{- /* ORDER RESERVES */ -}} -
-
- Order Reserves -
-
-
- -
-
- x - - -
-
- = - - -
-
- - {{- /* QUOTE SLIPPAGE BUFFER */ -}} -
-
- Slippage Buffer -
-
-
- -
-
- x - - -
-
- = - - -
-
+
+
+ Sell Buffer + +
+
+ Number of Sells + +
+
+
+
- - {{- /* FEE ALLOCATIONS */ -}} -
- - {{- /* TOKEN FEE RESERVES */ -}} -
-
- - Fee Reserves -
-
-
- - -
-
- ~ USD -
-
-
+
+
+
- {{- /* BOOKING FEES */ -}} -
-
- Booking Fees - - - lots - -
-
- x - - - per lot - -
-
- x - - reserves -
-
-
- + - - redeems -
-
- x - - - per redeem - -
-
- x - - reserves -
-
-
- = - - -
-
+
+
+ Slippage Buffer + +
+
+
+ + % +
+
+
+
- {{- /* SWAP RESERVES */ -}} -
-
- Swap Fee Reserves -
-
-
-
- -
- - -
-
- swaps -
-
- x - - - per swap - -
-
- = - - -
-
+
+
+ Buy Fee Reserve + +
+
+
+
+
+
+
+
+
+ Sell Fee Reserve + +
+
+
+ +
+
+
+
- {{- /* ASSET SETTINGS */ -}} -
- {{- /* WALLET SETTINGS */ -}} - -
-
- - [[[Wallet Options]]] -
-
no settings available
-
-
- - - -
-
-
- - -
-
-
- -
-
+
+ + + {{- /* DEX BALANCES */ -}} +
+ +
+
+ +
+
+
+
- -
- {{- /* BALANCES */ -}} -
-
- - [[[Available]]] +
+ +
+
+
-
- - +
+
+
+
+ +
+
+
+
-
+
+
+ +
-
-
- ├─ - - -
-
- └─ - - -
-
+
-
+
-
-
- - [[[Available]]] +
+
+
+ + {{- /* CEX BALANCES */ -}} +
+ +
+
+ +
+
+
-
- - +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
- {{- /* REBALANCE SETTINGS */ -}} -
-
- - [[[cex_rebalance]]] -
-
- [[[Minimum Transfer]]] - -
-
-
- - -
+ {{- /* ASSET SETTINGS */ -}} +
+ + {{- /* WALLET SETTINGS */ -}} +
+
+ + [[[Wallet Options]]] +
+
+
+ + + + +
no settings available
+
+
+ + + +
+
+
+ + +
+
+
+ +
-
+
-
- {{- /* GENERAL SETTINGS */ -}} -
-
+
+ + {{- /* KNOBS */ -}} +
+
+ + Knobs +
+ + {{- /* DRIFT TOLERANCE */ -}} +
+
+ + [[[Drift tolerance]]] + + +
- - Knobs +
+ + %
+
- {{- /* CEX REBALANCE CHECKBOX */ -}} - + {{- /* ORDER PERSISTENCE */ -}} +
+
+ [[[Order persistence]]] + +
+
+
+ + epochs +
+
+
+ + {{- /* AUTO REBALANCE */ -}} +
+
+ + Auto Rebalance +
+ +
+ + + +
+
{{- /* ALLOW EXTERNAL TRANSFERS */ -}} - - {{- /* DRIFT TOLERANCE */ -}} -
-
- - [[[Drift tolerance]]] - - + +
+
+ + Minimum Transfer +
-
-
- - % +
+
+ +
+
- {{- /* ORDER PERSISTENCE */ -}} -
-
- [[[Order persistence]]] - +
+
+ + Minimum Transfer +
-
-
- - epochs +
+
+ +
+
-
-
+
+
diff --git a/client/webserver/site/src/js/mm.ts b/client/webserver/site/src/js/mm.ts index 7eb806959b..4d8517c5fd 100644 --- a/client/webserver/site/src/js/mm.ts +++ b/client/webserver/site/src/js/mm.ts @@ -4,13 +4,9 @@ import { MMBotStatus, RunStatsNote, RunEventNote, - StartConfig, - OrderPlacement, - AutoRebalanceConfig, EpochReportNote, CEXProblemsNote, MarketWithHost, - UIConfig, CEXNotification } from './registry' import { @@ -22,152 +18,18 @@ import { botTypeBasicMM, setMarketElements, setCexElements, - PlacementsChart, BotMarket, hostedMarketID, RunningMarketMakerDisplay, RunningMMDisplayElements } from './mmutil' -import Doc, { MiniSlider } from './doc' +import Doc from './doc' import BasePage from './basepage' import * as OrderUtil from './orderutil' import { Forms, CEXConfigurationForm } from './forms' import * as intl from './locales' -import { StatusBooked } from './orderutil' const mediumBreakpoint = 768 -interface FundingSlider { - left: { - cex: number - dex: number - } - right: { - cex: number - dex: number - } - cexRange: number - dexRange: number -} - -const newSlider = () => { - return { - left: { - cex: 0, - dex: 0 - }, - right: { - cex: 0, - dex: 0 - }, - cexRange: 0, - dexRange: 0 - } -} - -interface FundingSource { - avail: number - req: number - funded: boolean -} - -interface FundingOutlook { - dex: FundingSource - cex: FundingSource - transferable: number - fees: { - avail: number - req: number - funded: boolean - }, - fundedAndBalanced: boolean - fundedAndNotBalanced: boolean -} - -function parseFundingOptions (f: FundingOutlook): [number, number, FundingSlider | undefined] { - const { cex: { avail: cexAvail, req: cexReq }, dex: { avail: dexAvail, req: dexReq }, transferable } = f - - let proposedDex = Math.min(dexAvail, dexReq) - let proposedCex = Math.min(cexAvail, cexReq) - let slider: FundingSlider | undefined - if (f.fundedAndNotBalanced) { - // We have everything we need, but not where we need it, and we can - // deposit and withdraw. - if (dexAvail > dexReq) { - // We have too much dex-side, so we'll have to draw on dex to balance - // cex's shortcomings. - const cexShort = cexReq - cexAvail - const dexRemain = dexAvail - dexReq - if (dexRemain < cexShort) { - // We did something really bad with math to get here. - throw Error('bad math has us with dex surplus + cex underfund invalid remains') - } - proposedDex += cexShort + transferable - } else { - // We don't have enough on dex, but we have enough on cex to cover the - // short. - const dexShort = dexReq - dexAvail - const cexRemain = cexAvail - cexReq - if (cexRemain < dexShort) { - throw Error('bad math got us with cex surplus + dex underfund invalid remains') - } - proposedCex += dexShort + transferable - } - } else if (f.fundedAndBalanced) { - // This asset is fully funded, but the user may choose to fund order - // reserves either cex or dex. - if (transferable > 0) { - const dexRemain = dexAvail - dexReq - const cexRemain = cexAvail - cexReq - - slider = newSlider() - - if (cexRemain > transferable && dexRemain > transferable) { - // Either one could fully fund order reserves. Let the user choose. - slider.left.cex = transferable + cexReq - slider.left.dex = dexReq - slider.right.cex = cexReq - slider.right.dex = transferable + dexReq - } else if (dexRemain < transferable && cexRemain < transferable) { - // => implied that cexRemain + dexRemain > transferable. - // CEX can contribute SOME and DEX can contribute SOME. - slider.left.cex = transferable - dexRemain + cexReq - slider.left.dex = dexRemain + dexReq - slider.right.cex = cexRemain + cexReq - slider.right.dex = transferable - cexRemain + dexReq - } else if (dexRemain > transferable) { - // So DEX has enough to cover reserves, but CEX could potentially - // constribute SOME. NOT ALL. - slider.left.cex = cexReq - slider.left.dex = transferable + dexReq - slider.right.cex = cexRemain + cexReq - slider.right.dex = transferable - cexRemain + dexReq - } else { - // CEX has enough to cover reserves, but DEX could contribute SOME, - // NOT ALL. - slider.left.cex = transferable - dexRemain + cexReq - slider.left.dex = dexRemain + dexReq - slider.right.cex = transferable + cexReq - slider.right.dex = dexReq - } - // We prefer the slider right in the center. - slider.cexRange = slider.right.cex - slider.left.cex - slider.dexRange = slider.right.dex - slider.left.dex - proposedDex = slider.left.dex + (slider.dexRange / 2) - proposedCex = slider.left.cex + (slider.cexRange / 2) - } - } else { // starved - if (cexAvail < cexReq) { - proposedDex = Math.min(dexAvail, dexReq + transferable + (cexReq - cexAvail)) - } else if (dexAvail < dexReq) { - proposedCex = Math.min(cexAvail, cexReq + transferable + (dexReq - dexAvail)) - } else { // just transferable wasn't covered - proposedDex = Math.min(dexAvail, dexReq + transferable) - proposedCex = Math.min(cexAvail, dexReq + cexReq + transferable - proposedDex) - } - } - return [proposedDex, proposedCex, slider] -} - interface CEXRow { cexName: string tr: PageElement @@ -437,16 +299,13 @@ class Bot extends BotMarket { pg: MarketMakerPage div: PageElement page: Record - placementsChart: PlacementsChart - baseAllocSlider: MiniSlider - quoteAllocSlider: MiniSlider row: BotRow runDisplay: RunningMarketMakerDisplay constructor (pg: MarketMakerPage, runningMMElements: RunningMMDisplayElements, status: MMBotStatus) { super(status.config) this.pg = pg - const { baseID, quoteID, host, botType, nBuyPlacements, nSellPlacements, cexName } = this + const { baseID, quoteID, host, botType, cexName } = this this.id = hostedMarketID(host, baseID, quoteID) const div = this.div = pg.page.botTmpl.cloneNode(true) as PageElement @@ -465,32 +324,14 @@ class Bot extends BotMarket { page.botTypeDisplay.textContent = intl.prep(intl.ID_BOTTYPE_BASIC_MM) } - Doc.setVis(botType !== botTypeBasicArb, page.placementsChartBox, page.baseTokenSwapFeesBox) - if (botType !== botTypeBasicArb) { - this.placementsChart = new PlacementsChart(page.placementsChart) - page.buyPlacementCount.textContent = String(nBuyPlacements) - page.sellPlacementCount.textContent = String(nSellPlacements) - } - - Doc.bind(page.startBttn, 'click', () => this.start()) - Doc.bind(page.allocationBttn, 'click', () => this.allocate()) Doc.bind(page.reconfigureBttn, 'click', () => this.reconfigure()) Doc.bind(page.removeBttn, 'click', () => this.pg.confirmRemoveCfg(status.config)) - Doc.bind(page.goBackFromAllocation, 'click', () => this.hideAllocationDialog()) Doc.bind(page.marketLink, 'click', () => app().loadPage('markets', { host, baseID, quoteID })) - this.baseAllocSlider = new MiniSlider(page.baseAllocSlider, () => { /* callback set later */ }) - this.quoteAllocSlider = new MiniSlider(page.quoteAllocSlider, () => { /* callback set later */ }) - const tr = pg.page.botRowTmpl.cloneNode(true) as PageElement setMarketElements(tr, baseID, quoteID, host) const tmpl = Doc.parseTemplate(tr) this.row = { tr, tmpl } - Doc.bind(tmpl.allocateBttn, 'click', (e: MouseEvent) => { - e.stopPropagation() - this.allocate() - pg.showBot(this.id) - }) Doc.bind(tr, 'click', () => pg.showBot(this.id)) this.initialize() @@ -500,38 +341,10 @@ class Bot extends BotMarket { await super.initialize() this.runDisplay.setBotMarket(this) const { - page, host, cexName, botType, div, - cfg: { arbMarketMakingConfig, basicMarketMakingConfig }, mktID, + page, host, cexName, botType, div, mktID, baseFactor, quoteFactor, marketReport: { baseFiatRate } } = this - if (botType !== botTypeBasicArb) { - let buyPlacements: OrderPlacement[] = [] - let sellPlacements: OrderPlacement[] = [] - let profit = 0 - if (arbMarketMakingConfig) { - buyPlacements = arbMarketMakingConfig.buyPlacements.map((p) => ({ lots: p.lots, gapFactor: p.multiplier })) - sellPlacements = arbMarketMakingConfig.sellPlacements.map((p) => ({ lots: p.lots, gapFactor: p.multiplier })) - profit = arbMarketMakingConfig.profit - } else if (basicMarketMakingConfig) { - buyPlacements = basicMarketMakingConfig.buyPlacements - sellPlacements = basicMarketMakingConfig.sellPlacements - let bestBuy: OrderPlacement | undefined - let bestSell : OrderPlacement | undefined - if (buyPlacements.length > 0) bestBuy = buyPlacements.reduce((prev: OrderPlacement, curr: OrderPlacement) => curr.gapFactor < prev.gapFactor ? curr : prev) - if (sellPlacements.length > 0) bestSell = sellPlacements.reduce((prev: OrderPlacement, curr: OrderPlacement) => curr.gapFactor < prev.gapFactor ? curr : prev) - if (bestBuy && bestSell) { - profit = (bestBuy.gapFactor + bestSell.gapFactor) / 2 - } else if (bestBuy) { - profit = bestBuy.gapFactor - } else if (bestSell) { - profit = bestSell.gapFactor - } - } - const marketConfig = { cexName: cexName as string, botType, baseFiatRate: baseFiatRate, dict: { profit, buyPlacements, sellPlacements } } - this.placementsChart.setMarket(marketConfig) - } - Doc.setVis(botType !== botTypeBasicMM, page.cexDataBox) if (botType !== botTypeBasicMM) { const cex = app().mmStatus.cexes[cexName] @@ -564,7 +377,6 @@ class Bot extends BotMarket { const { row: { tmpl } } = this const { running, runStats } = this.status() Doc.setVis(running, tmpl.profitLossBox) - Doc.setVis(!running, tmpl.allocateBttnBox) if (runStats) { tmpl.profitLoss.textContent = Doc.formatFourSigFigs(runStats.profitLoss.profit, 2) } @@ -584,292 +396,55 @@ class Bot extends BotMarket { else this.updateIdleDisplay() } - updateRunningDisplay () { - this.runDisplay.update() - } + async updateIdleDisplay () { + const { page, baseID, quoteID, host, cexName, bui, qui, baseFeeUI, quoteFeeUI, baseFeeID, quoteFeeID } = this - updateIdleDisplay () { - const { - page, proj: { alloc, qProj, bProj }, baseID, quoteID, cexName, bui, qui, baseFeeID, - quoteFeeID, baseFactor, quoteFactor, baseFeeFactor, quoteFeeFactor, - marketReport: { baseFiatRate, quoteFiatRate }, cfg: { uiConfig: { baseConfig, quoteConfig } }, - quoteFeeUI, baseFeeUI - } = this - page.baseAlloc.textContent = Doc.formatFullPrecision(alloc[baseID], bui) - const baseUSD = alloc[baseID] / baseFactor * baseFiatRate - let totalUSD = baseUSD - page.baseAllocUSD.textContent = Doc.formatFourSigFigs(baseUSD) - page.baseBookAlloc.textContent = Doc.formatFullPrecision(bProj.book * baseFactor, bui) - page.baseOrderReservesAlloc.textContent = Doc.formatFullPrecision(bProj.orderReserves * baseFactor, bui) - page.baseOrderReservesPct.textContent = String(Math.round(baseConfig.orderReservesFactor * 100)) - Doc.setVis(cexName, page.baseCexAllocBox) - if (cexName) page.baseCexAlloc.textContent = Doc.formatFullPrecision(bProj.cex * baseFactor, bui) - Doc.setVis(baseFeeID === baseID, page.baseBookingFeesAllocBox) - Doc.setVis(baseFeeID !== baseID, page.baseTokenFeesAllocBox) - if (baseFeeID === baseID) { - const bookingFees = baseID === quoteFeeID ? bProj.bookingFees + qProj.bookingFees : bProj.bookingFees - page.baseBookingFeesAlloc.textContent = Doc.formatFullPrecision(bookingFees * baseFeeFactor, baseFeeUI) - } else { - const feeAlloc = alloc[baseFeeID] - page.baseTokenFeeAlloc.textContent = Doc.formatFullPrecision(feeAlloc, baseFeeUI) - const baseFeeUSD = feeAlloc / baseFeeFactor * app().fiatRatesMap[baseFeeID] - totalUSD += baseFeeUSD - page.baseTokenAllocUSD.textContent = Doc.formatFourSigFigs(baseFeeUSD) - const withQuote = baseFeeID === quoteFeeID - const bookingFees = bProj.bookingFees + (withQuote ? qProj.bookingFees : 0) - page.baseTokenBookingFees.textContent = Doc.formatFullPrecision(bookingFees * baseFeeFactor, baseFeeUI) - page.baseTokenSwapFeeN.textContent = String(baseConfig.swapFeeN + (withQuote ? quoteConfig.swapFeeN : 0)) - const swapReserves = bProj.swapFeeReserves + (withQuote ? qProj.swapFeeReserves : 0) - page.baseTokenSwapFees.textContent = Doc.formatFullPrecision(swapReserves * baseFeeFactor, baseFeeUI) - } + const availableBalances = await MM.availableBalances({ host, baseID, quoteID }, cexName) + const { dexBalances, cexBalances } = availableBalances - page.quoteAlloc.textContent = Doc.formatFullPrecision(alloc[quoteID], qui) - const quoteUSD = alloc[quoteID] / quoteFactor * quoteFiatRate - totalUSD += quoteUSD - page.quoteAllocUSD.textContent = Doc.formatFourSigFigs(quoteUSD) - page.quoteBookAlloc.textContent = Doc.formatFullPrecision(qProj.book * quoteFactor, qui) - page.quoteOrderReservesAlloc.textContent = Doc.formatFullPrecision(qProj.orderReserves * quoteFactor, qui) - page.quoteOrderReservesPct.textContent = String(Math.round(quoteConfig.orderReservesFactor * 100)) - page.quoteSlippageAlloc.textContent = Doc.formatFullPrecision(qProj.slippageBuffer * quoteFactor, qui) - page.slippageBufferFactor.textContent = String(Math.round(quoteConfig.slippageBufferFactor * 100)) - Doc.setVis(cexName, page.quoteCexAllocBox) - if (cexName) page.quoteCexAlloc.textContent = Doc.formatFullPrecision(qProj.cex * quoteFactor, qui) - Doc.setVis(quoteID === quoteFeeID, page.quoteBookingFeesAllocBox) - Doc.setVis(quoteFeeID !== quoteID && quoteFeeID !== baseFeeID, page.quoteTokenFeesAllocBox) - if (quoteID === quoteFeeID) { - const bookingFees = quoteID === baseFeeID ? bProj.bookingFees + qProj.bookingFees : qProj.bookingFees - page.quoteBookingFeesAlloc.textContent = Doc.formatFullPrecision(bookingFees * quoteFeeFactor, quoteFeeUI) - } else if (quoteFeeID !== baseFeeID) { - page.quoteTokenFeeAlloc.textContent = Doc.formatFullPrecision(alloc[quoteFeeID], quoteFeeUI) - const quoteFeeUSD = alloc[quoteFeeID] / quoteFeeFactor * app().fiatRatesMap[quoteFeeID] - totalUSD += quoteFeeUSD - page.quoteTokenAllocUSD.textContent = Doc.formatFourSigFigs(quoteFeeUSD) - page.quoteTokenBookingFees.textContent = Doc.formatFullPrecision(qProj.bookingFees * quoteFeeFactor, quoteFeeUI) - page.quoteTokenSwapFeeN.textContent = String(quoteConfig.swapFeeN) - page.quoteTokenSwapFees.textContent = Doc.formatFullPrecision(qProj.swapFeeReserves * quoteFeeFactor, quoteFeeUI) - } - page.totalAllocUSD.textContent = Doc.formatFourSigFigs(totalUSD) - } + const baseDexBalance = Doc.formatCoinValue(dexBalances[baseID] ?? 0, bui) + page.baseDexBalance.textContent = baseDexBalance + let totalBaseBalance = dexBalances[baseID] ?? 0 - /* - * allocate opens a dialog to choose funding sources (if applicable) and - * confirm allocations and start the bot. - */ - allocate () { - const { - page, marketReport: { baseFiatRate, quoteFiatRate }, baseID, quoteID, - baseFeeID, quoteFeeID, baseFeeFiatRate, quoteFeeFiatRate, cexName, - baseFactor, quoteFactor, baseFeeFactor, quoteFeeFactor, host, mktID - } = this + const quoteDexBalance = Doc.formatCoinValue(dexBalances[quoteID] ?? 0, qui) + page.quoteDexBalance.textContent = quoteDexBalance + let totalQuoteBalance = dexBalances[quoteID] ?? 0 + Doc.setVis(cexName, page.baseCexBalanceSection, page.quoteCexBalanceSection) if (cexName) { - const cex = app().mmStatus.cexes[cexName] - if (!cex || !cex.connected) { - page.offError.textContent = intl.prep(intl.ID_CEX_NOT_CONNECTED, { cexName }) - Doc.showTemporarily(3000, page.offError) - return - } - } - - const f = this.fundingState() - - const [proposedDexBase, proposedCexBase, baseSlider] = parseFundingOptions(f.base) - const [proposedDexQuote, proposedCexQuote, quoteSlider] = parseFundingOptions(f.quote) - - const alloc = this.alloc = { - dex: { - [baseID]: proposedDexBase * baseFactor, - [quoteID]: proposedDexQuote * quoteFactor - }, - cex: { - [baseID]: proposedCexBase * baseFactor, - [quoteID]: proposedCexQuote * quoteFactor - } - } - - alloc.dex[baseFeeID] = Math.min((alloc.dex[baseFeeID] ?? 0) + (f.base.fees.req * baseFeeFactor), f.base.fees.avail * baseFeeFactor) - alloc.dex[quoteFeeID] = Math.min((alloc.dex[quoteFeeID] ?? 0) + (f.quote.fees.req * quoteFeeFactor), f.quote.fees.avail * quoteFeeFactor) - - let totalUSD = (alloc.dex[baseID] / baseFactor * baseFiatRate) + (alloc.dex[quoteID] / quoteFactor * quoteFiatRate) - totalUSD += (alloc.cex[baseID] / baseFactor * baseFiatRate) + (alloc.cex[quoteID] / quoteFactor * quoteFiatRate) - if (baseFeeID !== baseID) totalUSD += alloc.dex[baseFeeID] / baseFeeFactor * baseFeeFiatRate - if (quoteFeeID !== quoteID && quoteFeeID !== baseFeeID) totalUSD += alloc.dex[quoteFeeID] / quoteFeeFactor * quoteFeeFiatRate - page.allocUSD.textContent = Doc.formatFourSigFigs(totalUSD) - - Doc.setVis(cexName, ...Doc.applySelector(page.allocationDialog, '[data-cex-only]')) - Doc.setVis(f.fundedAndBalanced, page.fundedAndBalancedBox) - Doc.setVis(f.base.transferable + f.quote.transferable > 0, page.hasTransferable) - Doc.setVis(f.fundedAndNotBalanced, page.fundedAndNotBalancedBox) - Doc.setVis(f.starved, page.starvedBox) - page.startBttn.classList.toggle('go', f.fundedAndBalanced) - page.startBttn.classList.toggle('warning', !f.fundedAndBalanced) - page.proposedDexBaseAlloc.classList.toggle('text-warning', !(f.base.fundedAndBalanced || f.base.fundedAndNotBalanced)) - page.proposedDexQuoteAlloc.classList.toggle('text-warning', !(f.quote.fundedAndBalanced || f.quote.fundedAndNotBalanced)) - - const setBaseProposal = (dex: number, cex: number) => { - page.proposedDexBaseAlloc.textContent = Doc.formatFourSigFigs(dex) - page.proposedDexBaseAllocUSD.textContent = Doc.formatFourSigFigs(dex * baseFiatRate) - page.proposedCexBaseAlloc.textContent = Doc.formatFourSigFigs(cex) - page.proposedCexBaseAllocUSD.textContent = Doc.formatFourSigFigs(cex * baseFiatRate) - } - setBaseProposal(proposedDexBase, proposedCexBase) - - Doc.setVis(baseSlider, page.baseAllocSlider) - if (baseSlider) { - const dexRange = baseSlider.right.dex - baseSlider.left.dex - const cexRange = baseSlider.right.cex - baseSlider.left.cex - this.baseAllocSlider.setValue(0.5) - this.baseAllocSlider.changed = (r: number) => { - const dexAlloc = baseSlider.left.dex + r * dexRange - const cexAlloc = baseSlider.left.cex + r * cexRange - alloc.dex[baseID] = dexAlloc * baseFactor - alloc.cex[baseID] = cexAlloc * baseFactor - setBaseProposal(dexAlloc, cexAlloc) - } - } - - const setQuoteProposal = (dex: number, cex: number) => { - page.proposedDexQuoteAlloc.textContent = Doc.formatFourSigFigs(dex) - page.proposedDexQuoteAllocUSD.textContent = Doc.formatFourSigFigs(dex * quoteFiatRate) - page.proposedCexQuoteAlloc.textContent = Doc.formatFourSigFigs(cex) - page.proposedCexQuoteAllocUSD.textContent = Doc.formatFourSigFigs(cex * quoteFiatRate) - } - setQuoteProposal(proposedDexQuote, proposedCexQuote) - - Doc.setVis(quoteSlider, page.quoteAllocSlider) - if (quoteSlider) { - const dexRange = quoteSlider.right.dex - quoteSlider.left.dex - const cexRange = quoteSlider.right.cex - quoteSlider.left.cex - this.quoteAllocSlider.setValue(0.5) - this.quoteAllocSlider.changed = (r: number) => { - const dexAlloc = quoteSlider.left.dex + r * dexRange - const cexAlloc = quoteSlider.left.cex + r * cexRange - alloc.dex[quoteID] = dexAlloc * quoteFactor - alloc.cex[quoteID] = cexAlloc * quoteFactor - setQuoteProposal(dexAlloc, cexAlloc) - } - } - - Doc.setVis(baseFeeID !== baseID, ...Doc.applySelector(page.allocationDialog, '[data-base-token-fees]')) - if (baseFeeID !== baseID) { - const reqFees = f.base.fees.req + (baseFeeID === quoteFeeID ? f.quote.fees.req : 0) - const proposedFees = Math.min(reqFees, f.base.fees.avail) - page.proposedDexBaseFeeAlloc.textContent = Doc.formatFourSigFigs(proposedFees) - page.proposedDexBaseFeeAllocUSD.textContent = Doc.formatFourSigFigs(proposedFees * baseFeeFiatRate) - page.proposedDexBaseFeeAlloc.classList.toggle('text-warning', !f.base.fees.funded) - } - - const needQuoteTokenFees = quoteFeeID !== quoteID && quoteFeeID !== baseFeeID - Doc.setVis(needQuoteTokenFees, ...Doc.applySelector(page.allocationDialog, '[data-quote-token-fees]')) - if (needQuoteTokenFees) { - const proposedFees = Math.min(f.quote.fees.req, f.quote.fees.avail) - page.proposedDexQuoteFeeAlloc.textContent = Doc.formatFourSigFigs(proposedFees) - page.proposedDexQuoteFeeAllocUSD.textContent = Doc.formatFourSigFigs(proposedFees * quoteFeeFiatRate) - page.proposedDexQuoteFeeAlloc.classList.toggle('text-warning', !f.quote.fees.funded) - } - - const mkt = app().exchanges[host]?.markets[mktID] - let existingOrders = false - if (mkt && mkt.orders) { - for (let i = 0; i < mkt.orders.length; i++) { - if (mkt.orders[i].status <= StatusBooked) { - existingOrders = true - break - } - } + const baseCexBalance = Doc.formatCoinValue(cexBalances[baseID] ?? 0, bui) + page.baseCexBalance.textContent = baseCexBalance + totalBaseBalance += cexBalances[baseID] ?? 0 + const quoteCexBalance = Doc.formatCoinValue(cexBalances[quoteID] ?? 0, qui) + page.quoteCexBalance.textContent = quoteCexBalance + totalQuoteBalance += cexBalances[quoteID] ?? 0 } - Doc.setVis(existingOrders, page.existingOrdersBox) - Doc.show(page.allocationDialog) - const closeDialog = (e: MouseEvent) => { - if (Doc.mouseInElement(e, page.allocationDialog)) return - this.hideAllocationDialog() - Doc.unbind(document, 'click', closeDialog) - } - Doc.bind(document, 'click', closeDialog) - } - - hideAllocationDialog () { - Doc.hide(this.page.allocationDialog) - } - - async start () { - const { page, alloc, baseID, quoteID, host, cexName, cfg: { uiConfig } } = this - - Doc.hide(page.errMsg) - if (cexName && !app().mmStatus.cexes[cexName]?.connected) { - page.errMsg.textContent = `${cexName} not connected` - Doc.show(page.errMsg) - return - } - - // round allocations values. - for (const m of [alloc.dex, alloc.cex]) { - for (const [assetID, v] of Object.entries(m)) m[parseInt(assetID)] = Math.round(v) - } - - const startConfig: StartConfig = { - baseID: baseID, - quoteID: quoteID, - host: host, - alloc: alloc - } + page.baseTotalBalance.textContent = Doc.formatCoinValue(totalBaseBalance, bui) + page.quoteTotalBalance.textContent = Doc.formatCoinValue(totalQuoteBalance, qui) - startConfig.autoRebalance = this.autoRebalanceSettings(uiConfig) + const baseFeeAssetIsTraded = baseFeeID === baseID || baseFeeID === quoteID + const quoteFeeAssetIsTraded = quoteFeeID === baseID || quoteFeeID === quoteID - try { - app().log('mm', 'starting mm bot', startConfig) - const res = await MM.startBot(startConfig) - if (!app().checkResponse(res)) throw res - } catch (e) { - page.errMsg.textContent = intl.prep(intl.ID_API_ERROR, e) - Doc.show(page.errMsg) - return + if (baseFeeAssetIsTraded) { + Doc.hide(page.baseFeeBalanceSection) + } else { + Doc.show(page.baseFeeBalanceSection) + const baseFeeBalance = Doc.formatCoinValue(dexBalances[baseFeeID] ?? 0, baseFeeUI) + page.baseFeeBalance.textContent = baseFeeBalance } - this.hideAllocationDialog() - } - - minTransferAmounts (): [number, number] { - const { - proj: { bProj, qProj, alloc }, baseFeeID, quoteFeeID, cfg: { uiConfig: { baseConfig, quoteConfig } }, - baseID, quoteID, cexName, mktID - } = this - const totalBase = alloc[baseID] - let dexMinBase = bProj.book - if (baseID === baseFeeID) dexMinBase += bProj.bookingFees - if (baseID === quoteFeeID) dexMinBase += qProj.bookingFees - let dexMinQuote = qProj.book - if (quoteID === quoteFeeID) dexMinQuote += qProj.bookingFees - if (quoteID === baseFeeID) dexMinQuote += bProj.bookingFees - const maxBase = Math.max(totalBase - dexMinBase, totalBase - bProj.cex) - const totalQuote = alloc[quoteID] - const maxQuote = Math.max(totalQuote - dexMinQuote, totalQuote - qProj.cex) - if (maxBase < 0 || maxQuote < 0) { - throw Error(`rebalance math doesn't work: ${JSON.stringify({ bProj, qProj, maxBase, maxQuote })}`) + if (quoteFeeAssetIsTraded) { + Doc.hide(page.quoteFeeBalanceSection) + } else { + Doc.show(page.quoteFeeBalanceSection) + const quoteFeeBalance = Doc.formatCoinValue(dexBalances[quoteFeeID] ?? 0, quoteFeeUI) + page.quoteFeeBalance.textContent = quoteFeeBalance } - const cex = app().mmStatus.cexes[cexName] - const mkt = cex.markets[mktID] - const [minB, maxB] = [mkt.baseMinWithdraw, Math.max(mkt.baseMinWithdraw * 2, maxBase)] - const minBaseTransfer = Math.round(minB + baseConfig.transferFactor * (maxB - minB)) - const [minQ, maxQ] = [mkt.quoteMinWithdraw, Math.max(mkt.quoteMinWithdraw * 2, maxQuote)] - const minQuoteTransfer = Math.round(minQ + quoteConfig.transferFactor * (maxQ - minQ)) - return [minBaseTransfer, minQuoteTransfer] } - autoRebalanceSettings (uiCfg: UIConfig) : AutoRebalanceConfig | undefined { - if (!uiCfg.cexRebalance && !uiCfg.internalTransfers) return - const cfg : AutoRebalanceConfig = { - minBaseTransfer: 0, - minQuoteTransfer: 0, - internalOnly: !uiCfg.cexRebalance - } - if (uiCfg.cexRebalance) { - const [minBaseTransfer, minQuoteTransfer] = this.minTransferAmounts() - cfg.minBaseTransfer = minBaseTransfer - cfg.minQuoteTransfer = minQuoteTransfer - } - return cfg + updateRunningDisplay () { + this.runDisplay.update() } reconfigure () { diff --git a/client/webserver/site/src/js/mmsettings.ts b/client/webserver/site/src/js/mmsettings.ts index 049e71ec8c..ba4982978f 100644 --- a/client/webserver/site/src/js/mmsettings.ts +++ b/client/webserver/site/src/js/mmsettings.ts @@ -15,13 +15,15 @@ import { MarketMakingStatus, MMCEXStatus, BalanceNote, - BotAssetConfig, ApprovalStatus, SupportedAsset, - WalletState, + StartConfig, + MMBotStatus, + RunStats, + UIConfig, UnitInfo, - ProjectedAlloc, - AssetBookingFees + AutoRebalanceConfig, + BotBalanceAllocation } from './registry' import Doc, { NumberInput, @@ -50,8 +52,7 @@ import { GapStrategyAbsolute, GapStrategyAbsolutePlus, GapStrategyPercent, - GapStrategyPercentPlus, - feesAndCommit + GapStrategyPercentPlus } from './mmutil' import { Forms, bind as bindForm, NewWalletForm, TokenApprovalForm, DepositAddress, CEXConfigurationForm } from './forms' import * as intl from './locales' @@ -62,34 +63,6 @@ const lastBotsLK = 'lastBots' const lastArbExchangeLK = 'lastArbExchange' const arbMMRowCacheKey = 'arbmm' -const defaultSwapReserves = { - n: 50, - prec: 0, - inc: 10, - minR: 0, - maxR: 1000, - range: 1000 -} -const defaultOrderReserves = { - factor: 1.0, - minR: 0, - maxR: 3, - range: 3, - prec: 3 -} -const defaultTransfer = { - factor: 0.1, - minR: 0, - maxR: 1, - range: 1 -} -const defaultSlippage = { - factor: 0.05, - minR: 0, - maxR: 0.3, - range: 0.3, - prec: 3 -} const defaultDriftTolerance = { value: 0.002, minV: 0, @@ -141,24 +114,37 @@ const defaultUSDPerSide = { prec: 2 } +function defaultUIConfig (baseMinWithdraw: number, quoteMinWithdraw: number, botType: string) : UIConfig { + const buffer = botType === botTypeBasicArb ? 1 : 0 + return { + allocation: { + dex: {}, + cex: {} + }, + quickBalance: { + buysBuffer: buffer, + sellsBuffer: buffer, + buyFeeReserve: 0, + sellFeeReserve: 0, + slippageBuffer: 5 + }, + usingQuickBalance: true, + internalTransfers: true, + baseMinTransfer: baseMinWithdraw, + quoteMinTransfer: quoteMinWithdraw, + cexRebalance: false + } +} + const defaultMarketMakingConfig: ConfigState = { gapStrategy: GapStrategyPercentPlus, sellPlacements: [], buyPlacements: [], driftTolerance: defaultDriftTolerance.value, profit: 0.02, - orderPersistence: defaultOrderPersistence.value, - cexRebalance: true, - simpleArbLots: 1 + orderPersistence: defaultOrderPersistence.value } as any as ConfigState -const defaultBotAssetConfig: BotAssetConfig = { - swapFeeN: defaultSwapReserves.n, - orderReservesFactor: defaultOrderReserves.factor, - slippageBufferFactor: defaultSlippage.factor, - transferFactor: defaultTransfer.factor -} - // cexButton stores parts of a CEX selection button. interface cexButton { name: string @@ -178,16 +164,11 @@ interface ConfigState { profit: number driftTolerance: number orderPersistence: number // epochs - cexRebalance: boolean - internalTransfers: boolean - disabled: boolean buyPlacements: OrderPlacement[] sellPlacements: OrderPlacement[] baseOptions: Record quoteOptions: Record - baseConfig: BotAssetConfig - quoteConfig: BotAssetConfig - simpleArbLots: number + uiConfig: UIConfig } interface BotSpecs { @@ -217,6 +198,7 @@ export default class MarketMakerSettingsPage extends BasePage { page: Record forms: Forms opts: UIOpts + runningBot: boolean newWalletForm: NewWalletForm approveTokenForm: TokenApprovalForm walletAddrForm: DepositAddress @@ -246,17 +228,47 @@ export default class MarketMakerSettingsPage extends BasePage { marketRows: MarketRow[] lotsPerLevelIncrement: number placementsChart: PlacementsChart - basePane: AssetPane - quotePane: AssetPane + baseSettings: WalletSettings + quoteSettings: WalletSettings driftTolerance: NumberInput driftToleranceSlider: MiniSlider orderPersistence: NumberInput orderPersistenceSlider: MiniSlider + availableDEXBalances: Record + availableCEXBalances: Record + buyBufferSlider: MiniSlider + buyBufferInput: NumberInput + sellBufferSlider: MiniSlider + sellBufferInput: NumberInput + slippageBufferSlider: MiniSlider + slippageBufferInput: NumberInput + buyFeeReserveSlider: MiniSlider + buyFeeReserveInput: NumberInput + sellFeeReserveSlider: MiniSlider + sellFeeReserveInput: NumberInput + baseMinTransferSlider: MiniSlider + baseMinTransferInput: NumberInput + quoteMinTransferSlider: MiniSlider + quoteMinTransferInput: NumberInput + baseDexBalanceSlider: MiniSlider + baseDexBalanceInput: NumberInput + quoteDexBalanceSlider: MiniSlider + quoteDexBalanceInput: NumberInput + baseFeeBalanceSlider: MiniSlider + baseFeeBalanceInput: NumberInput + quoteFeeBalanceSlider: MiniSlider + quoteFeeBalanceInput: NumberInput + baseCexBalanceSlider: MiniSlider + baseCexBalanceInput: NumberInput + quoteCexBalanceSlider: MiniSlider + quoteCexBalanceInput: NumberInput + fundingFeesCache: Record constructor (main: HTMLElement, specs: BotSpecs) { super() this.placementsCache = {} + this.fundingFeesCache = {} this.opts = {} const page = this.page = Doc.idDescendants(main) @@ -271,23 +283,23 @@ export default class MarketMakerSettingsPage extends BasePage { this.approveTokenForm = new TokenApprovalForm(page.approveTokenForm, () => { this.submitBotType() }) this.walletAddrForm = new DepositAddress(page.walletAddrForm) this.cexConfigForm = new CEXConfigurationForm(page.cexConfigForm, (cexName: string) => this.cexConfigured(cexName)) - page.quotePane = page.basePane.cloneNode(true) as PageElement - page.assetPaneBox.appendChild(page.quotePane) - this.basePane = new AssetPane(this, page.basePane) - this.quotePane = new AssetPane(this, page.quotePane) + page.quoteSettings = page.baseSettings.cloneNode(true) as PageElement + page.walletSettingsBox.appendChild(page.quoteSettings) + this.baseSettings = new WalletSettings(this, page.baseSettings, () => { this.updateAllocations() }) + this.quoteSettings = new WalletSettings(this, page.quoteSettings, () => { this.updateAllocations() }) app().headerSpace.appendChild(page.mmTitle) setOptionTemplates(page) Doc.cleanTemplates( page.orderOptTmpl, page.booleanOptTmpl, page.rangeOptTmpl, page.placementRowTmpl, - page.oracleTmpl, page.cexOptTmpl, page.arbBttnTmpl, page.marketRowTmpl, page.needRegTmpl - ) - page.basePane.removeAttribute('id') // don't remove from layout + page.oracleTmpl, page.cexOptTmpl, page.arbBttnTmpl, page.marketRowTmpl, page.needRegTmpl) + page.baseSettings.removeAttribute('id') // don't remove from layout Doc.bind(page.resetButton, 'click', () => { this.setOriginalValues() }) - Doc.bind(page.updateButton, 'click', () => { this.saveSettings() }) - Doc.bind(page.createButton, 'click', async () => { this.saveSettings() }) + Doc.bind(page.updateButton, 'click', () => { this.updateSettings() }) + Doc.bind(page.updateStartButton, 'click', () => { this.saveSettingsAndStart() }) + Doc.bind(page.updateRunningButton, 'click', () => { this.updateSettings() }) Doc.bind(page.deleteBttn, 'click', () => { this.delete() }) bindForm(page.botTypeForm, page.botTypeSubmit, () => { this.submitBotType() }) Doc.bind(page.noMarketBttn, 'click', () => { this.showMarketSelectForm() }) @@ -295,7 +307,6 @@ export default class MarketMakerSettingsPage extends BasePage { Doc.bind(page.botTypeChangeMarket, 'click', () => { this.showMarketSelectForm() }) Doc.bind(page.marketHeader, 'click', () => { this.showMarketSelectForm() }) Doc.bind(page.marketFilterInput, 'input', () => { this.sortMarketRows() }) - Doc.bind(page.cexRebalanceCheckbox, 'change', () => { this.autoRebalanceChanged() }) Doc.bind(page.internalOnlyRadio, 'change', () => { this.internalOnlyChanged() }) Doc.bind(page.externalTransfersRadio, 'change', () => { this.externalTransfersChanged() }) Doc.bind(page.switchToAdvanced, 'click', () => { this.showAdvancedConfig() }) @@ -303,6 +314,10 @@ export default class MarketMakerSettingsPage extends BasePage { Doc.bind(page.qcMatchBuffer, 'change', () => { this.matchBufferChanged() }) Doc.bind(page.switchToUSDPerSide, 'click', () => { this.changeSideCommitmentDialog() }) Doc.bind(page.switchToLotsPerLevel, 'click', () => { this.changeSideCommitmentDialog() }) + Doc.bind(page.manuallyAllocateBttn, 'click', () => { this.setAllocationTechnique(false) }) + Doc.bind(page.quickConfigBttn, 'click', () => { this.setAllocationTechnique(true) }) + Doc.bind(page.enableRebalance, 'change', () => { this.autoRebalanceChanged() }) + // Gap Strategy Doc.bind(page.gapStrategySelect, 'change', () => { if (!page.gapStrategySelect.value) return @@ -465,6 +480,33 @@ export default class MarketMakerSettingsPage extends BasePage { } }) + this.buyBufferSlider = new MiniSlider(page.buyBufferSlider, (amt: number) => this.quickBalanceSliderChanged(amt, 'buyBuffer')) + this.buyBufferInput = new NumberInput(page.buyBuffer, { prec: 0, min: 0, changed: (amt: number) => this.quickBalanceInputChanged(amt, 'buyBuffer') }) + this.sellBufferSlider = new MiniSlider(page.sellBufferSlider, (amt: number) => this.quickBalanceSliderChanged(amt, 'sellBuffer')) + this.sellBufferInput = new NumberInput(page.sellBuffer, { prec: 0, min: 0, changed: (amt: number) => this.quickBalanceInputChanged(amt, 'sellBuffer') }) + this.slippageBufferSlider = new MiniSlider(page.slippageBufferSlider, (amt: number) => this.quickBalanceSliderChanged(amt, 'slippageBuffer')) + this.slippageBufferInput = new NumberInput(page.slippageBuffer, { prec: 3, min: 0, changed: (amt: number) => this.quickBalanceInputChanged(amt, 'slippageBuffer') }) + this.buyFeeReserveSlider = new MiniSlider(page.buyFeeReserveSlider, (amt: number) => this.quickBalanceSliderChanged(amt, 'buyFeeReserve')) + this.buyFeeReserveInput = new NumberInput(page.buyFeeReserve, { prec: 0, min: 0, changed: (amt: number) => this.quickBalanceInputChanged(amt, 'buyFeeReserve') }) + this.sellFeeReserveSlider = new MiniSlider(page.sellFeeReserveSlider, (amt: number) => this.quickBalanceSliderChanged(amt, 'sellFeeReserve')) + this.sellFeeReserveInput = new NumberInput(page.sellFeeReserve, { prec: 0, min: 0, changed: (amt: number) => this.quickBalanceInputChanged(amt, 'sellFeeReserve') }) + this.baseMinTransferSlider = new MiniSlider(page.baseMinTransferSlider, (amt: number) => this.minTransferSliderChanged(amt, 'base')) + this.baseMinTransferInput = new NumberInput(page.baseMinTransfer, { prec: 0, min: 0, changed: (amt: number) => this.minTransferInputChanged(amt, 'base') }) + this.quoteMinTransferSlider = new MiniSlider(page.quoteMinTransferSlider, (amt: number) => this.minTransferSliderChanged(amt, 'quote')) + this.quoteMinTransferInput = new NumberInput(page.quoteMinTransfer, { prec: 0, min: 0, changed: (amt: number) => this.minTransferInputChanged(amt, 'quote') }) + this.baseDexBalanceSlider = new MiniSlider(page.baseDexBalanceSlider, (amt: number) => this.balanceSliderChanged(amt, 'base', 'dex')) + this.baseDexBalanceInput = new NumberInput(page.baseDexBalance, { prec: 0, min: 0, changed: (amt: number) => this.balanceInputChanged(amt, 'base', 'dex') }) + this.quoteDexBalanceSlider = new MiniSlider(page.quoteDexBalanceSlider, (amt: number) => this.balanceSliderChanged(amt, 'quote', 'dex')) + this.quoteDexBalanceInput = new NumberInput(page.quoteDexBalance, { prec: 0, min: 0, changed: (amt: number) => this.balanceInputChanged(amt, 'quote', 'dex') }) + this.baseFeeBalanceSlider = new MiniSlider(page.baseFeeBalanceSlider, (amt: number) => this.balanceSliderChanged(amt, 'baseFee', 'dex')) + this.baseFeeBalanceInput = new NumberInput(page.baseFeeBalance, { prec: 0, min: 0, changed: (amt: number) => this.balanceInputChanged(amt, 'baseFee', 'dex') }) + this.quoteFeeBalanceSlider = new MiniSlider(page.quoteFeeBalanceSlider, (amt: number) => this.balanceSliderChanged(amt, 'quoteFee', 'dex')) + this.quoteFeeBalanceInput = new NumberInput(page.quoteFeeBalance, { prec: 0, min: 0, changed: (amt: number) => this.balanceInputChanged(amt, 'quoteFee', 'dex') }) + this.baseCexBalanceSlider = new MiniSlider(page.baseCexBalanceSlider, (amt: number) => this.balanceSliderChanged(amt, 'base', 'cex')) + this.baseCexBalanceInput = new NumberInput(page.baseCexBalance, { prec: 0, min: 0, changed: (amt: number) => this.balanceInputChanged(amt, 'base', 'cex') }) + this.quoteCexBalanceSlider = new MiniSlider(page.quoteCexBalanceSlider, (amt: number) => this.balanceSliderChanged(amt, 'quote', 'cex')) + this.quoteCexBalanceInput = new NumberInput(page.quoteCexBalance, { prec: 0, min: 0, changed: (amt: number) => this.balanceInputChanged(amt, 'quote', 'cex') }) + const maybeSubmitBuyRow = (e: KeyboardEvent) => { if (e.key !== 'Enter') return if ( @@ -558,58 +600,103 @@ export default class MarketMakerSettingsPage extends BasePage { this.configureUI() } + // clampOriginalAllocations sets the allocations to be within the valid range + // based on the available balances. + clampOriginalAllocations (uiConfig: UIConfig) { + const { baseID, quoteID, baseFeeAssetID, quoteFeeAssetID } = this.walletStuff() + const assetIDs = Array.from(new Set([baseID, quoteID, baseFeeAssetID, quoteFeeAssetID])) + for (const assetID of assetIDs) { + const [dexMin, dexMax] = this.validManualBalanceRange(assetID, 'dex', false) + uiConfig.allocation.dex[assetID] = Math.min(Math.max(uiConfig.allocation.dex[assetID], dexMin), dexMax) + if (this.specs.cexName) { + const [cexMin, cexMax] = this.validManualBalanceRange(assetID, 'cex', false) + uiConfig.allocation.cex[assetID] = Math.min(Math.max(uiConfig.allocation.cex[assetID], cexMin), cexMax) + } + } + } + + async setAvailableBalances () { + const { specs } = this + const availableBalances = await MM.availableBalances({ host: specs.host, baseID: specs.baseID, quoteID: specs.quoteID }, specs.cexName) + this.availableDEXBalances = availableBalances.dexBalances + this.availableCEXBalances = availableBalances.cexBalances + } + async configureUI () { const { page, specs } = this const { host, baseID, quoteID, cexName, botType } = specs + this.fundingFeesCache = {} + const { baseFeeAssetID, quoteFeeAssetID, bui, qui, baseFeeUI, quoteFeeUI } = this.walletStuff() + const baseFeeNotTraded = baseFeeAssetID !== baseID && baseFeeAssetID !== quoteID + const quoteFeeNotTraded = quoteFeeAssetID !== baseID && quoteFeeAssetID !== quoteID + Doc.setVis(baseFeeNotTraded || quoteFeeNotTraded, page.buyFeeReserveSection, page.sellFeeReserveSection) + Doc.setVis(baseFeeNotTraded, page.baseDexFeeBalanceSection) + Doc.setVis(quoteFeeNotTraded && baseFeeAssetID !== quoteFeeAssetID, page.quoteDexFeeBalanceSection) + const [{ symbol: baseSymbol, token: baseToken }, { symbol: quoteSymbol, token: quoteToken }] = [app().assets[baseID], app().assets[quoteID]] this.mktID = `${baseSymbol}_${quoteSymbol}` - Doc.hide( - page.botSettingsContainer, page.marketBox, page.updateButton, page.resetButton, - page.createButton, page.noMarket, page.missingFiatRates - ) + Doc.hide(page.botSettingsContainer, page.marketBox, page.resetButton, page.noMarket, page.missingFiatRates) if ([baseID, quoteID, baseToken?.parentID ?? baseID, quoteToken?.parentID ?? quoteID].some((assetID: number) => !app().fiatRatesMap[assetID])) { Doc.show(page.missingFiatRates) return } + await this.setAvailableBalances() + Doc.show(page.marketLoading) State.storeLocal(specLK, specs) const mmStatus = app().mmStatus - const viewOnly = isViewOnly(specs, mmStatus) + this.runningBot = botIsRunning(specs, mmStatus) + Doc.setVis(this.runningBot, page.runningBotAllocationNote) let botCfg = liveBotConfig(host, baseID, quoteID) if (botCfg) { const oldBotType = botCfg.arbMarketMakingConfig ? botTypeArbMM : botCfg.basicMarketMakingConfig ? botTypeBasicMM : botTypeBasicArb if (oldBotType !== botType) botCfg = undefined } - Doc.setVis(botCfg, page.deleteBttnBox) + Doc.setVis(botCfg && !this.runningBot, page.deleteBttnBox) + page.marketHeader.classList.remove('hoverbg', 'pointer') + page.botTypeHeader.classList.remove('hoverbg', 'pointer') + if (!this.runningBot) { + page.botTypeHeader.classList.add('hoverbg', 'pointer') + page.marketHeader.classList.add('hoverbg', 'pointer') + } + + let [baseMinWithdraw, quoteMinWithdraw] = [0, 0] + if (cexName) { + const mkt = app().mmStatus.cexes[cexName].markets[this.mktID] + baseMinWithdraw = mkt.baseMinWithdraw + quoteMinWithdraw = mkt.quoteMinWithdraw + } const oldCfg = this.originalConfig = Object.assign({}, defaultMarketMakingConfig, { - disabled: viewOnly, baseOptions: this.defaultWalletOptions(baseID), quoteOptions: this.defaultWalletOptions(quoteID), buyPlacements: [], sellPlacements: [], - baseConfig: Object.assign({}, defaultBotAssetConfig), - quoteConfig: Object.assign({}, defaultBotAssetConfig) + uiConfig: defaultUIConfig(baseMinWithdraw, quoteMinWithdraw, botType) }) as ConfigState if (botCfg) { - const { basicMarketMakingConfig: mmCfg, arbMarketMakingConfig: arbMMCfg, simpleArbConfig: arbCfg, uiConfig } = botCfg + const { basicMarketMakingConfig: mmCfg, arbMarketMakingConfig: arbMMCfg, simpleArbConfig: arbCfg } = botCfg this.creatingNewBot = false + // This is kinda sloppy, but we'll copy any relevant issues from the // old config into the originalConfig. const idx = oldCfg as { [k: string]: any } // typescript for (const [k, v] of Object.entries(botCfg)) if (idx[k] !== undefined) idx[k] = v - oldCfg.baseConfig = Object.assign({}, defaultBotAssetConfig, botCfg.uiConfig.baseConfig) - oldCfg.quoteConfig = Object.assign({}, defaultBotAssetConfig, botCfg.uiConfig.quoteConfig) oldCfg.baseOptions = botCfg.baseWalletOptions || {} oldCfg.quoteOptions = botCfg.quoteWalletOptions || {} - oldCfg.cexRebalance = uiConfig.cexRebalance - oldCfg.internalTransfers = uiConfig.internalTransfers + oldCfg.uiConfig = botCfg.uiConfig || defaultUIConfig(baseMinWithdraw, quoteMinWithdraw, botType) + if (this.runningBot && !botCfg.uiConfig.usingQuickBalance) { + // If the bot is running and we are allocating manually, initialize + // the allocations to 0. + oldCfg.uiConfig.allocation = { dex: {}, cex: {} } + } + this.clampOriginalAllocations(oldCfg.uiConfig) if (mmCfg) { oldCfg.buyPlacements = mmCfg.buyPlacements @@ -627,14 +714,15 @@ export default class MarketMakerSettingsPage extends BasePage { // TODO: expose maxActiveArbs oldCfg.profit = arbCfg.profitTrigger oldCfg.orderPersistence = arbCfg.numEpochsLeaveOpen - oldCfg.simpleArbLots = botCfg.uiConfig.simpleArbLots ?? 1 } - Doc.setVis(!viewOnly, page.updateButton, page.resetButton) + Doc.show(page.resetButton) } else { this.creatingNewBot = true - Doc.setVis(!viewOnly, page.createButton) } + Doc.setVis(this.runningBot, page.updateRunningButton) + Doc.setVis(!this.runningBot, page.updateStartButton, page.updateButton) + // Now that we've updated the originalConfig, we'll copy it. this.updatedConfig = JSON.parse(JSON.stringify(oldCfg)) @@ -650,15 +738,45 @@ export default class MarketMakerSettingsPage extends BasePage { } setMarketElements(document.body, baseID, quoteID, host) - Doc.setVis(botType !== botTypeBasicArb, page.driftToleranceBox, page.switchToAdvanced) + Doc.setVis(botType === botTypeBasicArb, page.numBuysLabel, page.numSellsLabel) + Doc.setVis(botType !== botTypeBasicArb, page.driftToleranceBox, page.switchToAdvanced, page.qcTitle, + page.buyBufferLabel, page.sellBufferLabel) Doc.setVis(Boolean(cexName), ...Doc.applySelector(document.body, '[data-cex-show]')) - Doc.setVis(viewOnly, page.viewOnlyRunning) - Doc.setVis(cexName, page.cexRebalanceSettings) - if (!cexName) Doc.hide(page.externalTransfersSettings, page.internalOnlySettings) - if (cexName) setCexElements(document.body, cexName) + Doc.setVis(this.runningBot, page.botRunningMsg) await this.fetchMarketReport() + await this.updateAllocations() + + if (cexName) { + setCexElements(document.body, cexName) + const mkt = app().mmStatus.cexes[cexName].markets[this.mktID] + const { bui, qui } = this.walletStuff() + this.baseMinTransferInput.min = mkt.baseMinWithdraw / bui.conventional.conversionFactor + this.quoteMinTransferInput.min = mkt.quoteMinWithdraw / qui.conventional.conversionFactor + this.baseMinTransferInput.prec = Math.log10(bui.conventional.conversionFactor) + this.quoteMinTransferInput.prec = Math.log10(qui.conventional.conversionFactor) + } + Doc.setVis(cexName, page.rebalanceSection, page.adjustManuallyCexBalances) + + this.baseDexBalanceInput.prec = Math.log10(bui.conventional.conversionFactor) + const [baseDexMin] = this.validManualBalanceRange(baseID, 'dex', false) + this.baseDexBalanceInput.min = baseDexMin / bui.conventional.conversionFactor + this.quoteDexBalanceInput.prec = Math.log10(qui.conventional.conversionFactor) + const [quoteDexMin] = this.validManualBalanceRange(quoteID, 'dex', false) + this.quoteDexBalanceInput.min = quoteDexMin / qui.conventional.conversionFactor + this.baseCexBalanceInput.prec = Math.log10(bui.conventional.conversionFactor) + const [baseCexMin] = this.validManualBalanceRange(baseID, 'cex', false) + this.baseCexBalanceInput.min = baseCexMin / bui.conventional.conversionFactor + this.quoteCexBalanceInput.prec = Math.log10(qui.conventional.conversionFactor) + const [quoteCexMin] = this.validManualBalanceRange(quoteID, 'cex', false) + this.quoteCexBalanceInput.min = quoteCexMin / qui.conventional.conversionFactor + this.baseFeeBalanceInput.prec = Math.log10(baseFeeUI.conventional.conversionFactor) + const [baseFeeMin] = this.validManualBalanceRange(baseFeeAssetID, 'dex', false) + this.baseFeeBalanceInput.min = baseFeeMin / baseFeeUI.conventional.conversionFactor + this.quoteFeeBalanceInput.prec = Math.log10(quoteFeeUI.conventional.conversionFactor) + const [quoteFeeMin] = this.validManualBalanceRange(quoteFeeAssetID, 'dex', false) + this.quoteFeeBalanceInput.min = quoteFeeMin / quoteFeeUI.conventional.conversionFactor const lotSizeUSD = this.lotSizeUSD() this.lotsPerLevelIncrement = Math.round(Math.max(1, defaultLotsPerLevel.usdIncrement / lotSizeUSD)) @@ -666,8 +784,6 @@ export default class MarketMakerSettingsPage extends BasePage { this.qcUSDPerSide.inc = this.lotsPerLevelIncrement * lotSizeUSD this.qcUSDPerSide.min = lotSizeUSD - this.basePane.setAsset(baseID, false) - this.quotePane.setAsset(quoteID, true) const { marketReport: { baseFiatRate } } = this this.placementsChart.setMarket({ cexName: cexName as string, botType, baseFiatRate, dict: this.updatedConfig }) @@ -742,14 +858,384 @@ export default class MarketMakerSettingsPage extends BasePage { return runningBotInventory(assetID) } - adjustedBalances (baseWallet: WalletState, quoteWallet: WalletState) { - const { cexBaseBalance, cexQuoteBalance } = this - const [bInv, qInv] = [this.runningBotInventory(baseWallet.assetID), this.runningBotInventory(quoteWallet.assetID)] - const [cexBaseAvail, cexQuoteAvail] = [(cexBaseBalance?.available || 0) - bInv.cex.total, (cexQuoteBalance?.available || 0) - qInv.cex.total] - const [dexBaseAvail, dexQuoteAvail] = [baseWallet.balance.available - bInv.dex.total, quoteWallet.balance.available - qInv.dex.total] - const baseAvail = dexBaseAvail + cexBaseAvail - const quoteAvail = dexQuoteAvail + cexQuoteAvail - return { baseAvail, quoteAvail, dexBaseAvail, dexQuoteAvail, cexBaseAvail, cexQuoteAvail } + setAllocationTechnique (quick: boolean) { + const { page, updatedConfig } = this + updatedConfig.uiConfig.usingQuickBalance = quick + this.updateAllocations() + Doc.setVis(quick, page.quickAllocateSection) + Doc.setVis(!quick, page.manuallyAllocateSection) + } + + quickBalanceMin (config: 'buyBuffer' | 'sellBuffer' | 'slippageBuffer' | 'buyFeeReserve' | 'sellFeeReserve') : number { + const { botType } = this.marketStuff() + switch (config) { + case 'buyBuffer': return botType === botTypeBasicArb ? 1 : 0 + case 'sellBuffer': return botType === botTypeBasicArb ? 1 : 0 + case 'slippageBuffer': return 0 + case 'buyFeeReserve': return botType === botTypeBasicArb ? 1 : 0 + case 'sellFeeReserve': return botType === botTypeBasicArb ? 1 : 0 + } + } + + quickBalanceMax (config: 'buyBuffer' | 'sellBuffer' | 'slippageBuffer' | 'buyFeeReserve' | 'sellFeeReserve') : number { + const { buyLots, sellLots, botType } = this.marketStuff() + switch (config) { + case 'buyBuffer': return botType === botTypeBasicArb ? 20 : 3 * buyLots + case 'sellBuffer': return botType === botTypeBasicArb ? 20 : 3 * sellLots + case 'slippageBuffer': return 100 + case 'buyFeeReserve': return 1000 + case 'sellFeeReserve': return 1000 + } + } + + quickBalanceInput (config: 'buyBuffer' | 'sellBuffer' | 'slippageBuffer' | 'buyFeeReserve' | 'sellFeeReserve') : NumberInput { + switch (config) { + case 'buyBuffer': return this.buyBufferInput + case 'sellBuffer': return this.sellBufferInput + case 'slippageBuffer': return this.slippageBufferInput + case 'buyFeeReserve': return this.buyFeeReserveInput + case 'sellFeeReserve': return this.sellFeeReserveInput + } + } + + quickBalanceSlider (config: 'buyBuffer' | 'sellBuffer' | 'slippageBuffer' | 'buyFeeReserve' | 'sellFeeReserve') : MiniSlider { + switch (config) { + case 'buyBuffer': return this.buyBufferSlider + case 'sellBuffer': return this.sellBufferSlider + case 'slippageBuffer': return this.slippageBufferSlider + case 'buyFeeReserve': return this.buyFeeReserveSlider + case 'sellFeeReserve': return this.sellFeeReserveSlider + } + } + + // fundingFees fetches the funding fees (fees for split transactions) required + // for a given number of buys and sells. To avoid excessive calls, the results + // are cached. + async fundingFees (numBuys: number, numSells: number) : Promise<[number, number]> { + const { updatedConfig: { baseOptions, quoteOptions }, fundingFeesCache, specs: { host, baseID, quoteID } } = this + const cacheKey = `${numBuys}-${numSells}-${JSON.stringify(baseOptions)}-${JSON.stringify(quoteOptions)}` + if (fundingFeesCache[cacheKey] !== undefined) return fundingFeesCache[cacheKey] + const res = await MM.maxFundingFees({ host, baseID, quoteID }, numBuys, numSells, baseOptions, quoteOptions) + fundingFeesCache[cacheKey] = [res.buyFees, res.sellFees] + return [res.buyFees, res.sellFees] + } + + // updateAllocates updates the required allocations if quick balance config is + // being used. + async updateAllocations () { + const { page, specs, updatedConfig } = this + + Doc.setVis(this.specs.cexName && updatedConfig.uiConfig.cexRebalance, page.baseMinTransferSection, page.quoteMinTransferSection) + + if (!updatedConfig.uiConfig.usingQuickBalance) return + + const { + sellLots, buyLots, baseID, quoteID, baseFeeAssetID, quoteFeeAssetID, + baseIsAccountLocker, quoteIsAccountLocker, bui, qui, baseFeeUI, quoteFeeUI, + numBuys, numSells, lotSize, quoteLot + } = this.marketStuff() + + const { + slippageBuffer, buysBuffer, sellsBuffer, buyFeeReserve, sellFeeReserve + } = this.updatedConfig.uiConfig.quickBalance + const totalBuyLots = buysBuffer + buyLots + const totalSellLots = sellsBuffer + sellLots + + const availableFunds = { dex: this.availableDEXBalances, cex: this.availableCEXBalances } + const [oneTradeBuyFundingFees, oneTradeSellFundingFees] = await this.fundingFees(1, 1) + const [buyFundingFees, sellFundingFees] = await this.fundingFees(numBuys, numSells) + + const canRebalance = !!specs.cexName && updatedConfig.uiConfig.cexRebalance + + let toAlloc : AllocationResult + if (this.runningBot) { + const { runStats } = this.status() + if (!runStats) { + console.error('cannot find run stats for running bot') + return + } + toAlloc = toAllocateRunning(totalBuyLots, totalSellLots, lotSize, quoteLot, slippageBuffer, this.quoteMultiSplitBuffer(), buyFeeReserve, sellFeeReserve, + this.marketReport, availableFunds, canRebalance, baseID, quoteID, baseFeeAssetID, quoteFeeAssetID, + baseIsAccountLocker, quoteIsAccountLocker, runStats, buyFundingFees, sellFundingFees, oneTradeBuyFundingFees, oneTradeSellFundingFees) + } else { + toAlloc = toAllocate(totalBuyLots, totalSellLots, lotSize, quoteLot, slippageBuffer, this.quoteMultiSplitBuffer(), buyFeeReserve, sellFeeReserve, + this.marketReport, availableFunds, canRebalance, baseID, quoteID, baseFeeAssetID, quoteFeeAssetID, + baseIsAccountLocker, quoteIsAccountLocker, buyFundingFees, sellFundingFees, oneTradeBuyFundingFees, oneTradeSellFundingFees) + } + + populateAllocationTable(page.minAllocationTable, baseID, quoteID, baseFeeAssetID, quoteFeeAssetID, this.specs.cexName || '', + toAlloc, bui, qui, baseFeeUI, quoteFeeUI, this.specs.host) + + const assets = Array.from(new Set([baseID, baseFeeAssetID, quoteID, quoteFeeAssetID])) + for (const assetID of assets) { + const dexAlloc = toAlloc.dex[assetID] ? toAlloc.dex[assetID].amount : 0 + const cexAlloc = toAlloc.cex[assetID] ? toAlloc.cex[assetID].amount : 0 + if (assetID === this.specs.baseID) { + this.baseDexBalanceInput.setValue(dexAlloc / bui.conventional.conversionFactor) + this.setManualBalanceSliderValue(dexAlloc, 'base', 'dex') + this.setConfigAllocation(dexAlloc, 'base', 'dex') + + this.baseCexBalanceInput.setValue(cexAlloc / bui.conventional.conversionFactor) + this.setManualBalanceSliderValue(cexAlloc, 'base', 'cex') + this.setConfigAllocation(cexAlloc, 'base', 'cex') + } + if (assetID === quoteID) { + this.quoteDexBalanceInput.setValue(dexAlloc / qui.conventional.conversionFactor) + this.setManualBalanceSliderValue(dexAlloc, 'quote', 'dex') + this.setConfigAllocation(dexAlloc, 'quote', 'dex') + + this.quoteCexBalanceInput.setValue(cexAlloc / qui.conventional.conversionFactor) + this.setManualBalanceSliderValue(cexAlloc, 'quote', 'cex') + this.setConfigAllocation(cexAlloc, 'quote', 'cex') + } + if (assetID === baseFeeAssetID && baseFeeAssetID !== baseID && baseFeeAssetID !== quoteID) { + this.baseFeeBalanceInput.setValue(dexAlloc / baseFeeUI.conventional.conversionFactor) + this.setManualBalanceSliderValue(dexAlloc, 'baseFee', 'dex') + this.setConfigAllocation(dexAlloc, 'baseFee', 'dex') + } + if (assetID === quoteFeeAssetID && quoteFeeAssetID !== quoteID && quoteFeeAssetID !== baseID) { + this.quoteFeeBalanceInput.setValue(dexAlloc / quoteFeeUI.conventional.conversionFactor) + this.setManualBalanceSliderValue(dexAlloc, 'quoteFee', 'dex') + this.setConfigAllocation(dexAlloc, 'quoteFee', 'dex') + } + } + + if (this.specs.cexName) { + this.minTransferInputChanged(updatedConfig.uiConfig.baseMinTransfer / bui.conventional.conversionFactor, 'base') + this.minTransferInputChanged(updatedConfig.uiConfig.quoteMinTransfer / qui.conventional.conversionFactor, 'quote') + } + } + + setQuickBalanceConfig (config: 'buyBuffer' | 'sellBuffer' | 'slippageBuffer' | 'buyFeeReserve' | 'sellFeeReserve', amt: number) { + switch (config) { + case 'buyBuffer': this.updatedConfig.uiConfig.quickBalance.buysBuffer = amt; break + case 'sellBuffer': this.updatedConfig.uiConfig.quickBalance.sellsBuffer = amt; break + case 'slippageBuffer': this.updatedConfig.uiConfig.quickBalance.slippageBuffer = amt; break + case 'buyFeeReserve': this.updatedConfig.uiConfig.quickBalance.buyFeeReserve = amt; break + case 'sellFeeReserve': this.updatedConfig.uiConfig.quickBalance.sellFeeReserve = amt; break + } + } + + quickBalanceSliderChanged (amt: number, config: 'buyBuffer' | 'sellBuffer' | 'slippageBuffer' | 'buyFeeReserve' | 'sellFeeReserve') { + const [min, max] = [this.quickBalanceMin(config), this.quickBalanceMax(config)] + const input = this.quickBalanceInput(config) + const val = Math.floor((max - min) * amt + min) + input.setValue(val) + this.setQuickBalanceConfig(config, val) + this.updateAllocations() + } + + setQuickBalanceSliderValue (amt: number, config: 'buyBuffer' | 'sellBuffer' | 'slippageBuffer' | 'buyFeeReserve' | 'sellFeeReserve') { + const slider = this.quickBalanceSlider(config) + const [min, max] = [this.quickBalanceMin(config), this.quickBalanceMax(config)] + const val = (max - min) === 0 ? 0 : (amt - min) / (max - min) + slider.setValue(val) + } + + quickBalanceInputChanged (amt: number, sliderName: 'buyBuffer' | 'sellBuffer' | 'slippageBuffer' | 'buyFeeReserve' | 'sellFeeReserve') { + this.setQuickBalanceSliderValue(amt, sliderName) + this.setQuickBalanceConfig(sliderName, amt) + this.updateAllocations() + } + + // runningBotAllocations returns the total amount allocated to a running bot. + runningBotAllocations () : BotBalanceAllocation | undefined { + const { baseID, quoteID, baseFeeAssetID, quoteFeeAssetID } = this.walletStuff() + const assetIDs = Array.from(new Set([baseID, quoteID, baseFeeAssetID, quoteFeeAssetID])) + + const botStatus = app().mmStatus.bots.find((s: MMBotStatus) => + s.config.baseID === this.specs.baseID && s.config.quoteID === this.specs.quoteID + ) + if (!botStatus || !botStatus.runStats) { + console.error('cannot find run stats for running bot') + return undefined + } + + const result : BotBalanceAllocation = { dex: {}, cex: {} } + + for (const assetID of assetIDs) { + const { dexBalances, cexBalances } = botStatus.runStats + let totalDEX = 0 + totalDEX += dexBalances[assetID]?.available ?? 0 + totalDEX += dexBalances[assetID]?.locked ?? 0 + totalDEX += dexBalances[assetID]?.pending ?? 0 + totalDEX += dexBalances[assetID]?.reserved ?? 0 + result.dex[assetID] = totalDEX + + if (cexBalances) { + let totalCEX = 0 + totalCEX += cexBalances[assetID]?.available ?? 0 + totalCEX += cexBalances[assetID]?.locked ?? 0 + totalCEX += cexBalances[assetID]?.pending ?? 0 + totalCEX += cexBalances[assetID]?.reserved ?? 0 + result.cex[assetID] = totalCEX + } + } + + return result + } + + minTransferValidRange (asset: 'base' | 'quote') : [number, number] { + const totalAlloc : number = (() => { + const { bui, qui } = this.walletStuff() + const ui = asset === 'base' ? bui : qui + const assetID = asset === 'base' ? this.specs.baseID : this.specs.quoteID + const { dex, cex } = this.updatedConfig.uiConfig.allocation + let total = dex[assetID] + cex[assetID] + + if (!this.runningBot) return total / ui.conventional.conversionFactor + + const botAlloc = this.runningBotAllocations() + if (botAlloc) { + total += botAlloc.dex[assetID] ?? 0 + total += botAlloc.cex[assetID] ?? 0 + } + + return total / ui.conventional.conversionFactor + })() + + const min = asset === 'base' ? this.baseMinTransferInput.min : this.quoteMinTransferInput.min + const max = Math.max(min * 2, totalAlloc) + return [min, max] + } + + setMinTransferCfg (asset: 'base' | 'quote', amt: number) { + const { updatedConfig: cfg } = this + const { bui, qui } = this.walletStuff() + const ui = asset === 'base' ? bui : qui + const msgAmt = Math.floor(amt * ui.conventional.conversionFactor) + if (asset === 'base') cfg.uiConfig.baseMinTransfer = msgAmt + else cfg.uiConfig.quoteMinTransfer = msgAmt + } + + minTransferSliderChanged (r: number, asset: 'base' | 'quote') { + const input = asset === 'base' ? this.baseMinTransferInput : this.quoteMinTransferInput + const [min, max] = this.minTransferValidRange(asset) + const amt = min + (max - min) * r + input.setValue(amt) + this.setMinTransferCfg(asset, amt) + } + + minTransferInputChanged (amt: number, asset: 'base' | 'quote') { + const [min, max] = this.minTransferValidRange(asset) + amt = Math.min(Math.max(amt, min), max) // clamp + const slider = asset === 'base' ? this.baseMinTransferSlider : this.quoteMinTransferSlider + const input = asset === 'base' ? this.baseMinTransferInput : this.quoteMinTransferInput + slider.setValue((amt - min) / (max - min)) + input.setValue(amt) + this.setMinTransferCfg(asset, amt) + } + + manualBalanceSlider (asset: 'base' | 'quote' | 'baseFee' | 'quoteFee', location: 'dex' | 'cex') : MiniSlider | undefined { + switch (asset) { + case 'base': return location === 'dex' ? this.baseDexBalanceSlider : this.baseCexBalanceSlider + case 'quote': return location === 'dex' ? this.quoteDexBalanceSlider : this.quoteCexBalanceSlider + case 'baseFee': return location === 'dex' ? this.baseFeeBalanceSlider : undefined + case 'quoteFee': return location === 'dex' ? this.quoteFeeBalanceSlider : undefined + } + } + + manualBalanceInput (asset: 'base' | 'quote' | 'baseFee' | 'quoteFee', location: 'dex' | 'cex') : NumberInput | undefined { + switch (asset) { + case 'base': return location === 'dex' ? this.baseDexBalanceInput : this.baseCexBalanceInput + case 'quote': return location === 'dex' ? this.quoteDexBalanceInput : this.quoteCexBalanceInput + case 'baseFee': return location === 'dex' ? this.baseFeeBalanceInput : undefined + case 'quoteFee': return location === 'dex' ? this.quoteFeeBalanceInput : undefined + } + } + + assetID (asset: 'base' | 'quote' | 'baseFee' | 'quoteFee') : number { + const { baseFeeAssetID, quoteFeeAssetID } = this.walletStuff() + switch (asset) { + case 'base': return this.specs.baseID + case 'quote': return this.specs.quoteID + case 'baseFee': return baseFeeAssetID + case 'quoteFee': return quoteFeeAssetID + } + } + + // validManualBalanceRange returns the valid range for a manual balance slider. + // For running bots, this ranges from the negative the bot's unused balance to + // the available balance, and for non-running bots, it ranges from 0 to the + // available balance. + validManualBalanceRange (assetID: number, location: 'dex' | 'cex', conventional: boolean) : [number, number] { + const conventionalRange = (min: number, max: number): [number, number] => { + if (!conventional) return [min, max] + const ui = app().assets[assetID].unitInfo + return [min / ui.conventional.conversionFactor, max / ui.conventional.conversionFactor] + } + + const max = location === 'cex' + ? this.availableCEXBalances[assetID] ?? 0 + : this.availableDEXBalances[assetID] ?? 0 + + if (!this.runningBot) return conventionalRange(0, max) + + const botStatus = app().mmStatus.bots.find((s: MMBotStatus) => + s.config.baseID === this.specs.baseID && s.config.quoteID === this.specs.quoteID + ) + + if (!botStatus?.runStats) return conventionalRange(0, max) + + const min = location === 'cex' + ? -botStatus.runStats.cexBalances?.[assetID]?.available ?? 0 + : -botStatus.runStats.dexBalances?.[assetID]?.available ?? 0 + + return conventionalRange(min, max) + } + + setConfigAllocation (amt: number, asset: 'base' | 'quote' | 'baseFee' | 'quoteFee', location: 'dex' | 'cex') { + const { updatedConfig: cfg } = this + const assetID = this.assetID(asset) + if (location === 'dex') { + cfg.uiConfig.allocation.dex[assetID] = amt + } else { + cfg.uiConfig.allocation.cex[assetID] = amt + } + } + + balanceSliderChanged (amt: number, asset: 'base' | 'quote' | 'baseFee' | 'quoteFee', location: 'dex' | 'cex') { + const assetID = this.assetID(asset) + const [min, max] = this.validManualBalanceRange(assetID, location, false) + const input = this.manualBalanceInput(asset, location) + const ui = app().assets[assetID].unitInfo + if (input) { + amt = (max - min) * amt + min + if (amt < 0) amt = Math.ceil(amt) + else amt = Math.floor(amt) + input.setValue(amt / ui.conventional.conversionFactor) + } + this.setConfigAllocation(amt, asset, location) + } + + setManualBalanceSliderValue (amt: number, asset: 'base' | 'quote' | 'baseFee' | 'quoteFee', location: 'dex' | 'cex') { + const assetID = this.assetID(asset) + const [min, max] = this.validManualBalanceRange(assetID, location, false) + const slider = this.manualBalanceSlider(asset, location) + if (slider) slider.setValue((amt - min) / (max - min)) + } + + balanceInputChanged (amt: number, asset: 'base' | 'quote' | 'baseFee' | 'quoteFee', location: 'dex' | 'cex') { + const assetID = this.assetID(asset) + const [min, max] = this.validManualBalanceRange(assetID, location, false) + const ui = app().assets[assetID].unitInfo + amt = amt * ui.conventional.conversionFactor + if (amt > max || amt < min) { + if (amt > max) amt = max + else amt = min + const input = this.manualBalanceInput(asset, location) + if (input) input.setValue(amt / ui.conventional.conversionFactor) + } + this.setManualBalanceSliderValue(amt, asset, location) + this.setConfigAllocation(amt, asset, location) + } + + status () { + const { specs: { baseID, quoteID } } = this + const botStatus = app().mmStatus.bots.find((s: MMBotStatus) => s.config.baseID === baseID && s.config.quoteID === quoteID) + if (!botStatus) return { botCfg: {} as BotConfig, running: false, runStats: {} as RunStats } + const { config: botCfg, running, runStats, latestEpoch, cexProblems } = botStatus + return { botCfg, running, runStats, latestEpoch, cexProblems } } lotSizeUSD () { @@ -761,14 +1247,20 @@ export default class MarketMakerSettingsPage extends BasePage { return lotSize / ui.conventional.conversionFactor * baseFiatRate } + quoteMultiSplitBuffer () : number { + if (!this.updatedConfig.quoteOptions) return 0 + if (this.updatedConfig.quoteOptions.multisplit !== 'true') return 0 + return Number(this.updatedConfig.quoteOptions.multisplitbuffer || '0') + } + /* * marketStuff is just a bunch of useful properties for the current specs * gathered in one place and with preferable names. */ marketStuff () { const { - page, specs: { host, baseID, quoteID, cexName, botType }, basePane, quotePane, - marketReport: { baseFiatRate, quoteFiatRate, baseFees, quoteFees }, + page, specs: { host, baseID, quoteID, cexName, botType }, + marketReport: { baseFiatRate, quoteFiatRate }, lotsPerLevelIncrement, updatedConfig: cfg, originalConfig: oldCfg, mktID } = this const { symbol: baseSymbol, unitInfo: bui } = app().assets[baseID] @@ -785,46 +1277,19 @@ export default class MarketMakerSettingsPage extends BasePage { spot } - let [dexBaseLots, dexQuoteLots] = [cfg.simpleArbLots, cfg.simpleArbLots] + let [sellLots, buyLots, numBuys, numSells] = [0, 0, 0, 0] if (botType !== botTypeBasicArb) { - dexBaseLots = this.updatedConfig.sellPlacements.reduce((lots: number, p: OrderPlacement) => lots + p.lots, 0) - dexQuoteLots = this.updatedConfig.buyPlacements.reduce((lots: number, p: OrderPlacement) => lots + p.lots, 0) + sellLots = this.updatedConfig.sellPlacements.reduce((lots: number, p: OrderPlacement) => lots + p.lots, 0) + buyLots = this.updatedConfig.buyPlacements.reduce((lots: number, p: OrderPlacement) => lots + p.lots, 0) + numBuys = this.updatedConfig.buyPlacements.length + numSells = this.updatedConfig.sellPlacements.length } const quoteLot = calculateQuoteLot(lotSize, baseID, quoteID, spot) - const walletStuff = this.walletStuff() - const { baseFeeAssetID, quoteFeeAssetID, baseIsAccountLocker, quoteIsAccountLocker } = walletStuff - - const { commit, fees } = feesAndCommit( - baseID, quoteID, baseFees, quoteFees, lotSize, dexBaseLots, dexQuoteLots, - baseFeeAssetID, quoteFeeAssetID, baseIsAccountLocker, quoteIsAccountLocker, - cfg.baseConfig.orderReservesFactor, cfg.quoteConfig.orderReservesFactor - ) return { - page, - cfg, - oldCfg, - host, - xc, - baseID, - quoteID, - botType, - cexName, - baseFiatRate, - quoteFiatRate, - xcRate, - baseSymbol, - quoteSymbol, - mktID, - lotSize, - lotSizeUSD, - lotsPerLevelIncrement, - quoteLot, - commit, - basePane, - quotePane, - fees, - ...walletStuff + page, cfg, oldCfg, host, xc, botType, cexName, baseFiatRate, quoteFiatRate, + xcRate, baseSymbol, quoteSymbol, mktID, lotSize, lotSizeUSD, lotsPerLevelIncrement, + quoteLot, sellLots, buyLots, numBuys, numSells, ...this.walletStuff() } } @@ -839,19 +1304,9 @@ export default class MarketMakerSettingsPage extends BasePage { const baseIsAccountLocker = (baseWallet.traits & traitAccountLocker) > 0 const quoteIsAccountLocker = (quoteWallet.traits & traitAccountLocker) > 0 return { - baseWallet, - quoteWallet, - baseFeeUI, - quoteFeeUI, - baseToken, - quoteToken, - bui, - qui, - baseFeeAssetID, - quoteFeeAssetID, - baseIsAccountLocker, - quoteIsAccountLocker, - ...this.adjustedBalances(baseWallet, quoteWallet) + baseWallet, quoteWallet, baseFeeUI, quoteFeeUI, baseToken, quoteToken, + bui, qui, baseFeeAssetID, quoteFeeAssetID, baseIsAccountLocker, quoteIsAccountLocker, + baseID, quoteID } } @@ -903,7 +1358,7 @@ export default class MarketMakerSettingsPage extends BasePage { this.qcUSDPerSide.setValue(lotsPerLevel * levelsPerSide * lotSizeUSD) this.qcLevelsPerSide.setValue(levelsPerSide) } else if (botType === botTypeBasicArb) { - this.qcLotsPerLevel.setValue(cfg.simpleArbLots) + this.qcLotsPerLevel.setValue(1) } this.showQuickConfig() this.quickConfigUpdated() @@ -936,29 +1391,30 @@ export default class MarketMakerSettingsPage extends BasePage { const { page, opts: { usingUSDPerSide } } = this Doc.hide( page.matchMultiplierBox, page.placementsChartBox, page.placementChartLegend, - page.lotsPerLevelLabel, page.levelSpacingBox, page.arbLotsLabel, page.qcLevelPerSideBox + page.lotsPerLevelLabel, page.levelSpacingBox, page.arbLotsLabel, page.qcLevelPerSideBox, + page.qcUSDPerSideBox, page.qcLotsBox ) - Doc.setVis(usingUSDPerSide, page.qcUSDPerSideBox) - Doc.setVis(!usingUSDPerSide, page.qcLotsBox) switch (botType) { case botTypeArbMM: Doc.show( page.qcLevelPerSideBox, page.matchMultiplierBox, page.placementsChartBox, page.placementChartLegend, page.lotsPerLevelLabel ) + Doc.setVis(usingUSDPerSide, page.qcUSDPerSideBox) + Doc.setVis(!usingUSDPerSide, page.qcLotsBox) break case botTypeBasicMM: Doc.show( page.qcLevelPerSideBox, page.levelSpacingBox, page.placementsChartBox, page.lotsPerLevelLabel ) + Doc.setVis(usingUSDPerSide, page.qcUSDPerSideBox) + Doc.setVis(!usingUSDPerSide, page.qcLotsBox) break - case botTypeBasicArb: - Doc.show(page.arbLotsLabel) } } - quickConfigUpdated () { + async quickConfigUpdated () { const { page, cfg, botType, cexName } = this.marketStuff() Doc.hide(page.qcError) @@ -996,7 +1452,6 @@ export default class MarketMakerSettingsPage extends BasePage { const levelSpacingDisabled = levelsPerSide === 1 page.levelSpacingBox.classList.toggle('disabled', levelSpacingDisabled) page.qcLevelSpacing.disabled = levelSpacingDisabled - cfg.simpleArbLots = lotsPerLevel if (botType !== botTypeBasicArb) { this.clearPlacements(cexName ? arbMMRowCacheKey : cfg.gapStrategy) @@ -1013,26 +1468,7 @@ export default class MarketMakerSettingsPage extends BasePage { this.placementsChart.render() } - this.updateAllocations() - } - - updateAllocations () { - this.updateBaseAllocations() - this.updateQuoteAllocations() - } - - updateBaseAllocations () { - const { commit, lotSize, basePane, fees } = this.marketStuff() - - basePane.updateInventory(commit.dex.base.lots, commit.dex.quote.lots, lotSize, commit.dex.base.val, commit.cex.base.val, fees.base) - basePane.updateCommitTotal() - } - - updateQuoteAllocations () { - const { commit, quoteLot: lotSize, quotePane, fees } = this.marketStuff() - - quotePane.updateInventory(commit.dex.quote.lots, commit.dex.base.lots, lotSize, commit.dex.quote.val, commit.cex.quote.val, fees.quote) - quotePane.updateCommitTotal() + await this.updateAllocations() } matchBufferChanged () { @@ -1056,8 +1492,8 @@ export default class MarketMakerSettingsPage extends BasePage { async showBotTypeForm (host: string, baseID: number, quoteID: number, botType?: string, configuredCEX?: string) { const { page } = this this.formSpecs = { host, baseID, quoteID, botType: '' } - const viewOnly = isViewOnly(this.formSpecs, app().mmStatus) - if (viewOnly) { + const botRunning = botIsRunning(this.formSpecs, app().mmStatus) + if (botRunning) { const botCfg = liveBotConfig(host, baseID, quoteID) const specs = this.specs = this.formSpecs switch (true) { @@ -1136,7 +1572,7 @@ export default class MarketMakerSettingsPage extends BasePage { } reshowBotTypeForm () { - if (isViewOnly(this.specs, app().mmStatus)) this.showMarketSelectForm() + if (this.runningBot) return const { baseID, quoteID, host, cexName, botType } = this.specs this.showBotTypeForm(host, baseID, quoteID, botType, cexName) } @@ -1177,6 +1613,7 @@ export default class MarketMakerSettingsPage extends BasePage { } showMarketSelectForm () { + if (this.runningBot) return this.page.marketFilterInput.value = '' this.sortMarketRows() this.forms.show(this.page.marketSelectForm) @@ -1193,57 +1630,56 @@ export default class MarketMakerSettingsPage extends BasePage { } } - handleBalanceNote (n: BalanceNote) { - this.approveTokenForm.handleBalanceNote(n) + async handleBalanceNote (note: BalanceNote) { if (!this.marketReport) return - const { baseID, quoteID, quoteToken, baseToken } = this.marketStuff() - if (n.assetID === baseID || n.assetID === baseToken?.parentID) { - this.basePane.updateBalances() - } else if (n.assetID === quoteID || n.assetID === quoteToken?.parentID) { - this.quotePane.updateBalances() + const { assetID } = note + const { baseID, quoteID, baseFeeAssetID, quoteFeeAssetID } = this.walletStuff() + if ([baseID, quoteID, baseFeeAssetID, quoteFeeAssetID].indexOf(assetID) >= 0) { + await this.setAvailableBalances() + this.updateAllocations() } } internalOnlyChanged () { const checked = Boolean(this.page.internalOnlyRadio.checked) this.page.externalTransfersRadio.checked = !checked - this.updatedConfig.cexRebalance = !checked - this.updatedConfig.internalTransfers = checked + this.updatedConfig.uiConfig.cexRebalance = !checked + this.updatedConfig.uiConfig.internalTransfers = checked this.updateAllocations() } externalTransfersChanged () { const checked = Boolean(this.page.externalTransfersRadio.checked) this.page.internalOnlyRadio.checked = !checked - this.updatedConfig.cexRebalance = checked - this.updatedConfig.internalTransfers = !checked + this.updatedConfig.uiConfig.cexRebalance = checked + this.updatedConfig.uiConfig.internalTransfers = !checked this.updateAllocations() } autoRebalanceChanged () { const { page, updatedConfig: cfg } = this - const checked = page.cexRebalanceCheckbox?.checked + const checked = page.enableRebalance.checked Doc.setVis(checked, page.internalOnlySettings, page.externalTransfersSettings) - if (checked && !cfg.cexRebalance && !cfg.internalTransfers) { + if (checked && !cfg.uiConfig.cexRebalance && !cfg.uiConfig.internalTransfers) { // default to external transfers - cfg.cexRebalance = true + cfg.uiConfig.cexRebalance = true page.externalTransfersRadio.checked = true page.internalOnlyRadio.checked = false } else if (!checked) { - cfg.cexRebalance = false - cfg.internalTransfers = false + cfg.uiConfig.cexRebalance = false + cfg.uiConfig.internalTransfers = false page.externalTransfersRadio.checked = false page.internalOnlyRadio.checked = false - } else if (cfg.cexRebalance && cfg.internalTransfers) { + } else if (cfg.uiConfig.cexRebalance && cfg.uiConfig.internalTransfers) { // should not happen.. set to default - cfg.internalTransfers = false + cfg.uiConfig.internalTransfers = false page.externalTransfersRadio.checked = true page.internalOnlyRadio.checked = false } else { // set to current values. This case should only be called when the form // is loaded. - page.externalTransfersRadio.checked = cfg.cexRebalance - page.internalOnlyRadio.checked = cfg.internalTransfers + page.externalTransfersRadio.checked = cfg.uiConfig.cexRebalance + page.internalOnlyRadio.checked = cfg.uiConfig.internalTransfers } this.updateAllocations() @@ -1695,9 +2131,9 @@ export default class MarketMakerSettingsPage extends BasePage { this.qcProfitSlider.setValue((profit - defaultProfit.minV) / defaultProfit.range) if (cexName) { - page.cexRebalanceCheckbox.checked = cfg.cexRebalance || cfg.internalTransfers - page.internalOnlyRadio.checked = cfg.internalTransfers - page.externalTransfersRadio.checked = cfg.cexRebalance + page.enableRebalance.checked = cfg.uiConfig.cexRebalance || cfg.uiConfig.internalTransfers + page.internalOnlyRadio.checked = cfg.uiConfig.internalTransfers + page.externalTransfersRadio.checked = cfg.uiConfig.cexRebalance this.autoRebalanceChanged() } @@ -1720,8 +2156,44 @@ export default class MarketMakerSettingsPage extends BasePage { oldCfg.buyPlacements.forEach((p) => { this.addPlacement(true, p) }) oldCfg.sellPlacements.forEach((p) => { this.addPlacement(false, p) }) - this.basePane.setupWalletSettings() - this.quotePane.setupWalletSettings() + // Quick balance + this.buyBufferInput.setValue(cfg.uiConfig.quickBalance.buysBuffer) + this.sellBufferInput.setValue(cfg.uiConfig.quickBalance.sellsBuffer) + this.buyFeeReserveInput.setValue(cfg.uiConfig.quickBalance.buyFeeReserve) + this.sellFeeReserveInput.setValue(cfg.uiConfig.quickBalance.sellFeeReserve) + this.slippageBufferInput.setValue(cfg.uiConfig.quickBalance.slippageBuffer) + this.setQuickBalanceSliderValue(cfg.uiConfig.quickBalance.buysBuffer, 'buyBuffer') + this.setQuickBalanceSliderValue(cfg.uiConfig.quickBalance.sellsBuffer, 'sellBuffer') + this.setQuickBalanceSliderValue(cfg.uiConfig.quickBalance.buyFeeReserve, 'buyFeeReserve') + this.setQuickBalanceSliderValue(cfg.uiConfig.quickBalance.sellFeeReserve, 'sellFeeReserve') + this.setQuickBalanceSliderValue(cfg.uiConfig.quickBalance.slippageBuffer, 'slippageBuffer') + + // Manual balance + const { bui, qui, baseFeeUI, quoteFeeUI } = this.walletStuff() + const { baseID, quoteID, baseFeeAssetID, quoteFeeAssetID } = this.marketStuff() + this.baseDexBalanceInput.setValue(cfg.uiConfig.allocation.dex[baseID] / bui.conventional.conversionFactor) + this.quoteDexBalanceInput.setValue(cfg.uiConfig.allocation.dex[quoteID] / qui.conventional.conversionFactor) + this.baseCexBalanceInput.setValue(cfg.uiConfig.allocation.cex[baseID] / bui.conventional.conversionFactor) + this.quoteCexBalanceInput.setValue(cfg.uiConfig.allocation.cex[quoteID] / qui.conventional.conversionFactor) + this.baseFeeBalanceInput.setValue(cfg.uiConfig.allocation.dex[baseFeeAssetID] / baseFeeUI.conventional.conversionFactor) + this.quoteFeeBalanceInput.setValue(cfg.uiConfig.allocation.dex[quoteFeeAssetID] / quoteFeeUI.conventional.conversionFactor) + this.setManualBalanceSliderValue(cfg.uiConfig.allocation.dex[quoteID], 'quote', 'dex') + this.setManualBalanceSliderValue(cfg.uiConfig.allocation.cex[baseID], 'base', 'cex') + this.setManualBalanceSliderValue(cfg.uiConfig.allocation.cex[quoteID], 'quote', 'cex') + this.setManualBalanceSliderValue(cfg.uiConfig.allocation.dex[baseFeeAssetID], 'base', 'dex') + this.setManualBalanceSliderValue(cfg.uiConfig.allocation.dex[quoteFeeAssetID], 'quote', 'dex') + + this.setAllocationTechnique(cfg.uiConfig.usingQuickBalance) + + if (cfg.uiConfig.cexRebalance) { + this.minTransferInputChanged(cfg.uiConfig.baseMinTransfer / bui.conventional.conversionFactor, 'base') + this.minTransferInputChanged(cfg.uiConfig.quoteMinTransfer / qui.conventional.conversionFactor, 'quote') + } + + this.baseSettings.clear() + this.quoteSettings.clear() + this.baseSettings.init(cfg.baseOptions, this.specs.baseID, false) + this.quoteSettings.init(cfg.quoteOptions, this.specs.quoteID, true) this.updateModifiedMarkers() if (Doc.isDisplayed(page.quickConfig)) this.switchToQuickConfig() @@ -1759,11 +2231,17 @@ export default class MarketMakerSettingsPage extends BasePage { return ok } - /* - * saveSettings updates the settings in the backend, and sets the originalConfig - * to be equal to the updatedConfig. - */ - async saveSettings () { + autoRebalanceSettings () : AutoRebalanceConfig | undefined { + const { updatedConfig: cfg } = this + if (!cfg.uiConfig.cexRebalance && !cfg.uiConfig.internalTransfers) return + return { + minBaseTransfer: cfg.uiConfig.baseMinTransfer, + minQuoteTransfer: cfg.uiConfig.quoteMinTransfer, + internalOnly: !cfg.uiConfig.cexRebalance + } + } + + async doSave () { // Make a copy and delete either the basic mm config or the arb-mm config, // depending on whether a cex is selected. if (!this.validateFields(true)) return @@ -1774,13 +2252,7 @@ export default class MarketMakerSettingsPage extends BasePage { baseID: baseID, quoteID: quoteID, cexName: cexName ?? '', - uiConfig: { - simpleArbLots: cfg.simpleArbLots, - baseConfig: cfg.baseConfig, - quoteConfig: cfg.quoteConfig, - cexRebalance: cfg.cexRebalance, - internalTransfers: cfg.internalTransfers - }, + uiConfig: cfg.uiConfig, baseWalletOptions: cfg.baseOptions, quoteWalletOptions: cfg.quoteOptions } @@ -1796,7 +2268,22 @@ export default class MarketMakerSettingsPage extends BasePage { } app().log('mm', 'saving bot config', botCfg) - await MM.updateBotConfig(botCfg) + + // When loading a running bot with balances configured manually, we set + // all the diffs initially to 0. However, we save the UI with the total + // allocations for each asset, so that if the bot is stopped and then the + // settings are reloaded, the total allocations will be shown. + const updatedAllocation = cfg.uiConfig.allocation + if (!botCfg.uiConfig.usingQuickBalance && this.runningBot) { + const botAlloc = this.runningBotAllocations() + if (botAlloc) { + botCfg.uiConfig.allocation = combineBotAllocations(botAlloc, updatedAllocation) + } + } + + if (this.runningBot) await MM.updateRunningBot(botCfg, updatedAllocation, this.autoRebalanceSettings()) + else await MM.updateBotConfig(botCfg) + await app().fetchMMStatus() this.originalConfig = JSON.parse(JSON.stringify(cfg)) this.updateModifiedMarkers() @@ -1804,6 +2291,26 @@ export default class MarketMakerSettingsPage extends BasePage { lastBots[`${baseID}_${quoteID}_${host}`] = this.specs State.storeLocal(lastBotsLK, lastBots) if (cexName) State.storeLocal(lastArbExchangeLK, cexName) + } + + async updateSettings () { + await this.doSave() + app().loadPage('mm') + } + + async saveSettingsAndStart () { + const { specs: { host, baseID, quoteID }, updatedConfig: cfg } = this + await this.doSave() + + const startConfig: StartConfig = { + baseID: baseID, + quoteID: quoteID, + host: host, + alloc: cfg.uiConfig.allocation, + autoRebalance: this.autoRebalanceSettings() + } + + await MM.startBot(startConfig) app().loadPage('mm') } @@ -2028,7 +2535,7 @@ export default class MarketMakerSettingsPage extends BasePage { } } -function isViewOnly (specs: BotSpecs, mmStatus: MarketMakingStatus): boolean { +function botIsRunning (specs: BotSpecs, mmStatus: MarketMakingStatus): boolean { const botStatus = mmStatus.bots.find(({ config: cfg }) => cfg.host === specs.host && cfg.baseID === specs.baseID && cfg.quoteID === specs.quoteID) return Boolean(botStatus?.running) } @@ -2066,247 +2573,32 @@ function tokenAssetApprovalStatuses (host: string, b: SupportedAsset, q: Support ] } -class AssetPane { +class WalletSettings { pg: MarketMakerSettingsPage div: PageElement page: Record - assetID: number - ui: UnitInfo - walletConfig: Record - feeAssetID: number - feeUI: UnitInfo - isQuote: boolean - isToken: boolean - lotSize: number // might be quote converted - lotSizeConv: number - cfg: BotAssetConfig - inv: ProjectedAlloc - nSwapFees: IncrementalInput - nSwapFeesSlider: MiniSlider - orderReserves: NumberInput - orderReservesSlider: MiniSlider - slippageBuffer: NumberInput - slippageBufferSlider: MiniSlider - minTransfer: NumberInput - minTransferSlider: MiniSlider + updated: () => void + optElements: Record - constructor (pg: MarketMakerSettingsPage, div: PageElement) { + constructor (pg: MarketMakerSettingsPage, div: PageElement, updated: () => void) { this.pg = pg this.div = div - const page = this.page = Doc.parseTemplate(div) - - this.nSwapFees = new IncrementalInput(page.nSwapFees, { - prec: defaultSwapReserves.prec, - inc: defaultSwapReserves.inc, - changed: (v: number) => { - const { minR, range } = defaultSwapReserves - this.cfg.swapFeeN = v - this.nSwapFeesSlider.setValue((v - minR) / range) - this.pg.updateAllocations() - } - }) + this.page = Doc.parseTemplate(div) + this.updated = updated + } - this.nSwapFeesSlider = new MiniSlider(page.nSwapFeesSlider, (r: number) => { - const { minR, range, prec } = defaultSwapReserves - const [v] = toPrecision(minR + r * range, prec) - this.cfg.swapFeeN = v - this.nSwapFees.setValue(v) - this.pg.updateAllocations() - }) - this.orderReserves = new NumberInput(page.orderReservesFactor, { - prec: defaultOrderReserves.prec, - min: 0, - changed: (v: number) => { - const { minR, range } = defaultOrderReserves - this.cfg.orderReservesFactor = v - this.orderReservesSlider.setValue((v - minR) / range) - this.pg.updateAllocations() - } - }) - this.orderReservesSlider = new MiniSlider(page.orderReservesSlider, (r: number) => { - const { minR, range, prec } = defaultOrderReserves - const [v] = toPrecision(minR + r * range, prec) - this.orderReserves.setValue(v) - this.cfg.orderReservesFactor = v - this.pg.updateAllocations() - }) - this.slippageBuffer = new NumberInput(page.slippageBufferFactor, { - prec: defaultSlippage.prec, - min: 0, - changed: (v: number) => { - const { minR, range } = defaultSlippage - this.cfg.slippageBufferFactor = v - this.slippageBufferSlider.setValue((v - minR) / range) - this.pg.updateAllocations() - } - }) - this.slippageBufferSlider = new MiniSlider(page.slippageBufferSlider, (r: number) => { - const { minR, range, prec } = defaultSlippage - const [v] = toPrecision(minR + r * range, prec) - this.slippageBuffer.setValue(minR + r * range) - this.cfg.slippageBufferFactor = v - this.pg.updateAllocations() - }) - this.minTransfer = new NumberInput(page.minTransfer, { - sigFigs: true, - min: 0, - changed: (v: number) => { - const { cfg } = this - const totalInventory = this.commit() - const [minV, maxV] = [this.minTransfer.min, Math.max(this.minTransfer.min * 2, totalInventory)] - cfg.transferFactor = (v - minV) / (maxV - minV) - this.minTransferSlider.setValue(cfg.transferFactor) - } - }) - this.minTransferSlider = new MiniSlider(page.minTransferSlider, (r: number) => { - const { cfg } = this - const totalInventory = this.commit() - const [minV, maxV] = [this.minTransfer.min, Math.max(this.minTransfer.min, totalInventory)] - cfg.transferFactor = r - this.minTransfer.setValue(minV + r * (maxV - minV)) - }) + clear () { + Doc.empty(this.page.walletSettings) + } - Doc.bind(page.showBalance, 'click', () => { pg.showAddress(this.assetID) }) - } - - // lot size can change if this is the quote asset, keep it updated. - setLotSize (lotSize: number) { - const { ui } = this - this.lotSize = lotSize - this.lotSizeConv = lotSize / ui.conventional.conversionFactor - } - - setAsset (assetID: number, isQuote: boolean) { - this.assetID = assetID - this.isQuote = isQuote - const cfg = this.cfg = isQuote ? this.pg.updatedConfig.quoteConfig : this.pg.updatedConfig.baseConfig - const { page, div, pg: { specs: { botType, baseID, cexName }, mktID, updatedConfig: { baseOptions, quoteOptions } } } = this - const { symbol, name, token, unitInfo: ui } = app().assets[assetID] - this.ui = ui - this.walletConfig = assetID === baseID ? baseOptions : quoteOptions - const { conventional: { unit: ticker } } = ui - this.feeAssetID = token ? token.parentID : assetID - const { unitInfo: feeUI, name: feeName, symbol: feeSymbol } = app().assets[this.feeAssetID] - this.feeUI = feeUI - this.inv = { book: 0, bookingFees: 0, swapFeeReserves: 0, cex: 0, orderReserves: 0, slippageBuffer: 0 } - this.isToken = Boolean(token) - Doc.setVis(this.isToken, page.feeTotalBox, page.feeReservesBox, page.feeBalances) - Doc.setVis(isQuote, page.slippageBufferBox) - Doc.setSrc(div, '[data-logo]', Doc.logoPath(symbol)) - Doc.setText(div, '[data-name]', name) - Doc.setText(div, '[data-ticker]', ticker) - const { conventional: { unit: feeTicker } } = feeUI - Doc.setText(div, '[data-fee-ticker]', feeTicker) - Doc.setText(div, '[data-fee-name]', feeName) - Doc.setSrc(div, '[data-fee-logo]', Doc.logoPath(feeSymbol)) - Doc.setVis(botType !== botTypeBasicMM, page.cexMinInvBox) - Doc.setVis(botType !== botTypeBasicArb, page.orderReservesBox) - this.nSwapFees.setValue(cfg.swapFeeN ?? defaultSwapReserves.n) - this.nSwapFeesSlider.setValue(cfg.swapFeeN / defaultSwapReserves.maxR) - if (botType !== botTypeBasicArb) { - const [v] = toPrecision(cfg.orderReservesFactor ?? defaultOrderReserves.factor, defaultOrderReserves.prec) - this.orderReserves.setValue(v) - this.orderReservesSlider.setValue((v - defaultOrderReserves.minR) / defaultOrderReserves.range) - } - if (botType !== botTypeBasicMM) { - this.minTransfer.prec = Math.log10(ui.conventional.conversionFactor) - const mkt = app().mmStatus.cexes[cexName as string].markets[mktID] - this.minTransfer.min = ((isQuote ? mkt.quoteMinWithdraw : mkt.baseMinWithdraw) / ui.conventional.conversionFactor) - } - this.slippageBuffer.setValue(cfg.slippageBufferFactor) - const { minR, range } = defaultSlippage - this.slippageBufferSlider.setValue((cfg.slippageBufferFactor - minR) / range) - this.setupWalletSettings() - this.updateBalances() - } - - commit () { - const { inv, isToken } = this - let commit = inv.book + inv.cex + inv.orderReserves + inv.slippageBuffer - if (!isToken) commit += inv.bookingFees + inv.swapFeeReserves - return commit - } - - updateInventory (lots: number, counterLots: number, lotSize: number, dexCommit: number, cexCommit: number, fees: AssetBookingFees) { - this.setLotSize(lotSize) - const { page, cfg, lotSizeConv, inv, ui, feeUI, isToken, isQuote, pg: { specs: { cexName, botType } } } = this - page.bookLots.textContent = String(lots) - page.bookLotSize.textContent = Doc.formatFourSigFigs(lotSizeConv) - inv.book = lots * lotSizeConv - page.bookCommitment.textContent = Doc.formatFourSigFigs(inv.book) - const feesPerLotConv = fees.bookingFeesPerLot / feeUI.conventional.conversionFactor - page.bookingFeesPerLot.textContent = Doc.formatFourSigFigs(feesPerLotConv) - page.swapReservesFactor.textContent = fees.swapReservesFactor.toFixed(2) - page.bookingFeesLots.textContent = String(lots) - inv.bookingFees = fees.bookingFees / feeUI.conventional.conversionFactor - page.bookingFees.textContent = Doc.formatFourSigFigs(inv.bookingFees) - if (cexName) { - inv.cex = cexCommit / ui.conventional.conversionFactor - page.cexMinInv.textContent = Doc.formatFourSigFigs(inv.cex) - } - if (botType !== botTypeBasicArb) { - const totalInventory = Math.max(cexCommit, dexCommit) / ui.conventional.conversionFactor - page.orderReservesBasis.textContent = Doc.formatFourSigFigs(totalInventory) - const orderReserves = totalInventory * cfg.orderReservesFactor - inv.orderReserves = orderReserves - page.orderReserves.textContent = Doc.formatFourSigFigs(orderReserves) - } - if (isToken) { - const feesPerSwapConv = fees.tokenFeesPerSwap / feeUI.conventional.conversionFactor - page.feeReservesPerSwap.textContent = Doc.formatFourSigFigs(feesPerSwapConv) - inv.swapFeeReserves = feesPerSwapConv * cfg.swapFeeN - page.feeReserves.textContent = Doc.formatFourSigFigs(inv.swapFeeReserves) - } - if (isQuote) { - const basis = inv.book + inv.cex + inv.orderReserves - page.slippageBufferBasis.textContent = Doc.formatCoinValue(basis * ui.conventional.conversionFactor, ui) - inv.slippageBuffer = basis * cfg.slippageBufferFactor - page.slippageBuffer.textContent = Doc.formatCoinValue(inv.slippageBuffer * ui.conventional.conversionFactor, ui) - } - Doc.setVis(fees.bookingFeesPerCounterLot > 0, page.redemptionFeesBox) - if (fees.bookingFeesPerCounterLot > 0) { - const feesPerLotConv = fees.bookingFeesPerCounterLot / feeUI.conventional.conversionFactor - page.redemptionFeesPerLot.textContent = Doc.formatFourSigFigs(feesPerLotConv) - page.redemptionFeesLots.textContent = String(counterLots) - page.redeemReservesFactor.textContent = fees.redeemReservesFactor.toFixed(2) - } - this.updateCommitTotal() - this.updateTokenFees() - this.updateRebalance() - } - - updateCommitTotal () { - const { page, assetID, ui } = this - const commit = this.commit() - page.commitTotal.textContent = Doc.formatCoinValue(Math.round(commit * ui.conventional.conversionFactor), ui) - page.commitTotalFiat.textContent = Doc.formatFourSigFigs(commit * app().fiatRatesMap[assetID]) - } - - updateTokenFees () { - const { page, inv, feeAssetID, feeUI, isToken } = this - if (!isToken) return - const feeReserves = inv.bookingFees + inv.swapFeeReserves - page.feeTotal.textContent = Doc.formatCoinValue(feeReserves * feeUI.conventional.conversionFactor, feeUI) - page.feeTotalFiat.textContent = Doc.formatFourSigFigs(feeReserves * app().fiatRatesMap[feeAssetID]) - } - - updateRebalance () { - const { page, cfg, pg: { updatedConfig: { cexRebalance }, specs: { cexName } } } = this - const showRebalance = cexName && cexRebalance - Doc.setVis(showRebalance, page.rebalanceOpts) - if (!showRebalance) return - const totalInventory = this.commit() - const [minV, maxV] = [this.minTransfer.min, Math.max(this.minTransfer.min * 2, totalInventory)] - const rangeV = maxV - minV - this.minTransfer.setValue(minV + cfg.transferFactor * rangeV) - this.minTransferSlider.setValue((cfg.transferFactor - defaultTransfer.minR) / defaultTransfer.range) - } - - setupWalletSettings () { - const { page, assetID, walletConfig } = this + init (walletConfig: Record, assetID: number, isQuote: boolean) { + const { page } = this const walletSettings = app().currentWalletDefinition(assetID) Doc.empty(page.walletSettings) Doc.setVis(!walletSettings.multifundingopts, page.walletSettingsNone) + const { symbol } = app().assets[assetID] + page.ticker.textContent = symbol.toUpperCase() + page.logo.src = Doc.logoPath(symbol) if (!walletSettings.multifundingopts) return const optToDiv: Record = {} const dependentOpts: Record = {} @@ -2320,8 +2612,9 @@ class AssetPane { if (!optKeys) return for (const optKey of optKeys) Doc.setVis(vis, optToDiv[optKey]) } + this.optElements = {} const addOpt = (opt: OrderOption) => { - if (opt.quoteAssetOnly && !this.isQuote) return + if (opt.quoteAssetOnly && !isQuote) return const currVal = walletConfig[opt.key] let div: PageElement | undefined if (opt.isboolean) { @@ -2332,8 +2625,10 @@ class AssetPane { Doc.bind(tmpl.input, 'change', () => { walletConfig[opt.key] = tmpl.input.checked ? 'true' : 'false' setDependentOptsVis(opt.key, Boolean(tmpl.input.checked)) + this.updated() }) if (opt.description) tmpl.tooltip.dataset.tooltip = opt.description + this.optElements[opt.key] = tmpl.input } else if (opt.xyRange) { const { start, end, xUnit } = opt.xyRange const range = end.x - start.x @@ -2350,6 +2645,7 @@ class AssetPane { const [v, s] = toFourSigFigs(rawV, 1) walletConfig[opt.key] = s slider.setValue((v - start.x) / range) + this.updated() } }) const slider = new MiniSlider(tmpl.slider, (r: number) => { @@ -2357,6 +2653,7 @@ class AssetPane { const [v, s] = toFourSigFigs(rawV, 1) walletConfig[opt.key] = s input.setValue(v) + this.updated() }) // TODO: default value should be smaller or none for base asset. const [v, s] = toFourSigFigs(parseFloatDefault(currVal, start.x), 3) @@ -2364,6 +2661,7 @@ class AssetPane { slider.setValue((v - start.x) / range) input.setValue(v) tmpl.value.textContent = s + this.optElements[opt.key] = input } if (!div) return console.error("don't know how to handle opt", opt) page.walletSettings.appendChild(div) @@ -2373,29 +2671,522 @@ class AssetPane { Doc.setVis(parentOptVal === 'true', div) } } - if (walletSettings.multifundingopts && walletSettings.multifundingopts.length > 0) { for (const opt of walletSettings.multifundingopts) addOpt(opt) } app().bindTooltips(page.walletSettings) } +} - updateBalances () { - const { page, assetID, ui, feeAssetID, feeUI, pg: { specs: { cexName, baseID }, cexBaseBalance, cexQuoteBalance } } = this - const { balance: { available } } = app().walletMap[assetID] - const botInv = this.pg.runningBotInventory(assetID) - const dexAvail = available - botInv.dex.total - let cexAvail = 0 - Doc.setVis(cexName, page.balanceBreakdown) - if (cexName) { - page.dexAvail.textContent = Doc.formatFourSigFigs(dexAvail / ui.conventional.conversionFactor) - const { available: cexRawAvail } = assetID === baseID ? cexBaseBalance : cexQuoteBalance - cexAvail = cexRawAvail - botInv.cex.total - page.cexAvail.textContent = Doc.formatFourSigFigs(cexAvail / ui.conventional.conversionFactor) +function populateAllocationTable ( + div: PageElement, baseID: number, quoteID: number, baseFeeID: number, + quoteFeeID: number, cexName: string, allocationResult: AllocationResult, + baseUI: UnitInfo, quoteUI: UnitInfo, baseFeeUI: UnitInfo, + quoteFeeUI: UnitInfo, host: string) { + const dexBalances: Record = {} + const cexBalances: Record = {} + const page = Doc.parseTemplate(div) + + const setColor = (el: PageElement, status: AllocationStatus) => { + el.classList.remove('text-buycolor', 'text-danger', 'text-warning') + switch (status) { + case 'sufficient': el.classList.add('text-buycolor'); break + case 'insufficient': el.classList.add('text-danger'); break + case 'sufficient-with-rebalance': el.classList.add('text-warning'); break } - page.avail.textContent = Doc.formatFourSigFigs((dexAvail + cexAvail) / ui.conventional.conversionFactor) - if (assetID === feeAssetID) return - const { balance: { available: feeAvail } } = app().walletMap[feeAssetID] - page.feeAvail.textContent = Doc.formatFourSigFigs(feeAvail / feeUI.conventional.conversionFactor) } + + for (const [key, value] of Object.entries(allocationResult.dex)) { + const assetID = Number(key) + if (assetID === baseID) setColor(page.dexBaseAlloc, value.status) + if (assetID === quoteID) setColor(page.dexQuoteAlloc, value.status) + if (assetID === baseFeeID) setColor(page.dexBaseFeeAlloc, value.status) + if (assetID === quoteFeeID) setColor(page.dexQuoteFeeAlloc, value.status) + dexBalances[assetID] = value.amount + } + for (const [key, value] of Object.entries(allocationResult.cex)) { + const assetID = Number(key) + if (assetID === baseID) setColor(page.cexBaseAlloc, value.status) + if (assetID === quoteID) setColor(page.cexQuoteAlloc, value.status) + cexBalances[assetID] = value.amount + } + + const baseFeeNotTraded = baseFeeID !== baseID && baseFeeID !== quoteID + const quoteFeeNotTraded = quoteFeeID !== quoteID && quoteFeeID !== baseID + + Doc.setVis(baseFeeNotTraded, page.baseFeeHeader, page.dexBaseFeeAlloc) + Doc.setVis(quoteFeeNotTraded && baseFeeID !== quoteFeeID, page.quoteFeeHeader, page.dexQuoteFeeAlloc) + Doc.setVis(cexName, page.cexRow) + + const format = (v: number, unitInfo: UnitInfo) => v ? Doc.formatCoinValue(v, unitInfo) : '0' + + page.dexBaseAlloc.textContent = format(dexBalances[baseID], baseUI) + page.dexQuoteAlloc.textContent = format(dexBalances[quoteID], quoteUI) + if (baseFeeNotTraded) page.dexBaseFeeAlloc.textContent = format(dexBalances[baseFeeID], baseFeeUI) + if (quoteFeeNotTraded) page.dexQuoteFeeAlloc.textContent = format(dexBalances[quoteFeeID], quoteFeeUI) + + if (cexBalances && cexName) { + page.cexBaseAlloc.textContent = format(cexBalances[baseID], baseUI) + page.cexQuoteAlloc.textContent = format(cexBalances[quoteID], quoteUI) + setCexElements(div, cexName) + } + + setMarketElements(div, baseID, quoteID, host) +} + +type Fees = { + swap: number + redeem: number + refund: number + funding: number +} + +interface PerLotBreakdown { + totalAmount: number + tradedAmount: number + fees: Fees + slippageBuffer: number + multiSplitBuffer: number +} + +function newPerLotBreakdown () : PerLotBreakdown { + return { + totalAmount: 0, + tradedAmount: 0, + fees: { swap: 0, redeem: 0, refund: 0, funding: 0 }, + slippageBuffer: 0, + multiSplitBuffer: 0 + } +} + +interface PerLot { + cex: Record + dex: Record +} + +interface FeeReserveBreakdown { + buyReserves: Fees + sellReserves: Fees +} + +type AllocationStatus = 'sufficient' | 'insufficient' | 'sufficient-with-rebalance' + +interface CalculationBreakdown { + totalRequired: number + + feeReserves: FeeReserveBreakdown + numBuyFeeReserves: number + numSellFeeReserves: number + + numBuyLots: number + buyLot: PerLotBreakdown + numSellLots: number + sellLot: PerLotBreakdown + + // initialFundingFees are the fees to initially place + // every buy and sell lot. + initialBuyFundingFees: number + initialSellFundingFees: number + + available: number + allocated: number + rebalanceAdjustment: number + + // For running bots only + runningBotAvailable: number + runningBotTotal: number +} + +function newCalculationBreakdown () : CalculationBreakdown { + return { + buyLot: newPerLotBreakdown(), + sellLot: newPerLotBreakdown(), + feeReserves: { + buyReserves: { swap: 0, redeem: 0, refund: 0, funding: 0 }, + sellReserves: { swap: 0, redeem: 0, refund: 0, funding: 0 } + }, + numBuyFeeReserves: 0, + numSellFeeReserves: 0, + numBuyLots: 0, + numSellLots: 0, + initialBuyFundingFees: 0, + initialSellFundingFees: 0, + totalRequired: 0, + available: 0, + allocated: 0, + rebalanceAdjustment: 0, + runningBotAvailable: 0, + runningBotTotal: 0 + } +} + +interface AllocationDetail { + amount: number + status: AllocationStatus + calculation: CalculationBreakdown +} + +function newAllocationDetail () : AllocationDetail { + return { + amount: 0, + status: 'sufficient', + calculation: newCalculationBreakdown() + } +} + +type AllocationResult = { + dex: Record + cex: Record +} + +export type AvailableFunds = { + dex: Record + cex?: Record +} + +// perLotRequirements calculates the funding requirements for a single buy and sell lot. +function perLotRequirements ( + baseID: number, + quoteID: number, + baseFeeID: number, + quoteFeeID: number, + lotSize: number, + quoteLot: number, + marketReport: MarketReport, + slippageBuffer: number, + multiSplitBuffer: number, + oneTradeBuyFundingFees: number, + oneTradeSellFundingFees: number, + baseIsAccountLocker: boolean, + quoteIsAccountLocker: boolean): { perSellLot: PerLot, perBuyLot: PerLot } { + const perSellLot: PerLot = { cex: {}, dex: {} } + const perBuyLot: PerLot = { cex: {}, dex: {} } + const assetIDs = Array.from(new Set([baseID, quoteID, baseFeeID, quoteFeeID])) + for (const assetID of assetIDs) { + perSellLot.dex[assetID] = newPerLotBreakdown() + perBuyLot.dex[assetID] = newPerLotBreakdown() + perSellLot.cex[assetID] = newPerLotBreakdown() + perBuyLot.cex[assetID] = newPerLotBreakdown() + } + + perSellLot.dex[baseID].tradedAmount = lotSize + perSellLot.dex[baseFeeID].fees.swap = marketReport.baseFees.max.swap + perSellLot.cex[quoteID].tradedAmount = quoteLot + perSellLot.cex[quoteID].slippageBuffer = slippageBuffer + perSellLot.dex[baseFeeID].fees.funding = oneTradeSellFundingFees + if (baseIsAccountLocker) perSellLot.dex[baseFeeID].fees.refund = marketReport.baseFees.max.refund + if (quoteIsAccountLocker) perSellLot.dex[quoteFeeID].fees.redeem = marketReport.quoteFees.max.redeem + + perBuyLot.dex[quoteID].tradedAmount = quoteLot + perBuyLot.dex[quoteID].multiSplitBuffer = multiSplitBuffer + perBuyLot.dex[quoteID].slippageBuffer = slippageBuffer + perBuyLot.cex[baseID].tradedAmount = lotSize + perBuyLot.dex[quoteFeeID].fees.swap = marketReport.quoteFees.max.swap + perBuyLot.dex[quoteFeeID].fees.funding = oneTradeBuyFundingFees + if (baseIsAccountLocker) perBuyLot.dex[baseFeeID].fees.redeem = marketReport.baseFees.max.redeem + if (quoteIsAccountLocker) perBuyLot.dex[quoteFeeID].fees.refund = marketReport.quoteFees.max.refund + + const calculateTotalAmount = (perLot: PerLotBreakdown) : number => { + let total = perLot.tradedAmount + const slippagePercentage = perLot.slippageBuffer / 100 + const multiSplitPercentage = perLot.multiSplitBuffer / 100 + total *= (1 + slippagePercentage + multiSplitPercentage) + total = Math.floor(total) + total += perLot.fees.swap + perLot.fees.redeem + perLot.fees.refund + perLot.fees.funding + return total + } + + for (const assetID of assetIDs) { + perSellLot.dex[assetID].totalAmount = calculateTotalAmount(perSellLot.dex[assetID]) + perBuyLot.dex[assetID].totalAmount = calculateTotalAmount(perBuyLot.dex[assetID]) + perSellLot.cex[assetID].totalAmount = calculateTotalAmount(perSellLot.cex[assetID]) + perBuyLot.cex[assetID].totalAmount = calculateTotalAmount(perBuyLot.cex[assetID]) + } + + return { perSellLot, perBuyLot } +} + +// requiredFunds calculates the total funds required for a bot based on the quick +// allocation settings. +function requiredFunds ( + numBuyLots: number, + numSellLots: number, + lotSize: number, + quoteLot: number, + slippageBuffer: number, + multiSplitBuffer: number, + buyFeeBuffer: number, + sellFeeBuffer: number, + marketReport: MarketReport, + baseIsAccountLocker: boolean, + quoteIsAccountLocker: boolean, + baseID: number, + quoteID: number, + baseFeeID: number, + quoteFeeID: number, + buyFundingFees: number, + sellFundingFees: number, + oneTradeBuyFundingFees: number, + oneTradeSellFundingFees: number) : AllocationResult { + const toAllocate: AllocationResult = { dex: {}, cex: {} } + const assetIDs = Array.from(new Set([baseID, quoteID, baseFeeID, quoteFeeID])) + + for (const assetID of assetIDs) { + toAllocate.dex[assetID] = newAllocationDetail() + toAllocate.cex[assetID] = newAllocationDetail() + } + + const { perBuyLot, perSellLot } = perLotRequirements(baseID, quoteID, baseFeeID, quoteFeeID, + lotSize, quoteLot, marketReport, slippageBuffer, multiSplitBuffer, oneTradeBuyFundingFees, + oneTradeSellFundingFees, baseIsAccountLocker, quoteIsAccountLocker) + + for (const assetID of assetIDs) { + toAllocate.dex[assetID].calculation.buyLot = perBuyLot.dex[assetID] + toAllocate.dex[assetID].calculation.sellLot = perSellLot.dex[assetID] + toAllocate.cex[assetID].calculation.buyLot = perBuyLot.cex[assetID] + toAllocate.cex[assetID].calculation.sellLot = perSellLot.cex[assetID] + toAllocate.dex[assetID].calculation.numBuyLots = numBuyLots + toAllocate.dex[assetID].calculation.numSellLots = numSellLots + toAllocate.cex[assetID].calculation.numBuyLots = numBuyLots + toAllocate.cex[assetID].calculation.numSellLots = numSellLots + + if (assetID === baseFeeID) { + toAllocate.dex[assetID].calculation.feeReserves.sellReserves.swap = marketReport.baseFees.estimated.swap + if (baseIsAccountLocker) { + toAllocate.dex[assetID].calculation.feeReserves.buyReserves.redeem = marketReport.baseFees.estimated.redeem + toAllocate.dex[assetID].calculation.feeReserves.sellReserves.refund = marketReport.baseFees.estimated.refund + } + toAllocate.dex[assetID].calculation.initialSellFundingFees = sellFundingFees + } + + if (assetID === quoteFeeID) { + toAllocate.dex[assetID].calculation.feeReserves.buyReserves.swap = marketReport.quoteFees.estimated.swap + if (quoteIsAccountLocker) { + toAllocate.dex[assetID].calculation.feeReserves.sellReserves.redeem = marketReport.quoteFees.estimated.redeem + toAllocate.dex[assetID].calculation.feeReserves.buyReserves.refund = marketReport.quoteFees.estimated.refund + } + toAllocate.dex[assetID].calculation.initialBuyFundingFees = buyFundingFees + toAllocate.dex[assetID].calculation.initialSellFundingFees = sellFundingFees + } + toAllocate.dex[assetID].calculation.numBuyFeeReserves = buyFeeBuffer + toAllocate.dex[assetID].calculation.numSellFeeReserves = sellFeeBuffer + } + + const totalFees = (fees: Fees) : number => { + return fees.swap + fees.redeem + fees.refund + fees.funding + } + + const calculateTotalRequired = (breakdown: CalculationBreakdown) : number => { + let total = 0 + total += breakdown.buyLot.totalAmount * breakdown.numBuyLots + total += breakdown.sellLot.totalAmount * breakdown.numSellLots + total += totalFees(breakdown.feeReserves.buyReserves) * breakdown.numBuyFeeReserves + total += totalFees(breakdown.feeReserves.sellReserves) * breakdown.numSellFeeReserves + total += breakdown.initialBuyFundingFees + total += breakdown.initialSellFundingFees + return total + } + + for (const assetID of assetIDs) { + toAllocate.dex[assetID].calculation.totalRequired = calculateTotalRequired(toAllocate.dex[assetID].calculation) + toAllocate.cex[assetID].calculation.totalRequired = calculateTotalRequired(toAllocate.cex[assetID].calculation) + } + + return toAllocate +} + +// toAllocation calculates the quick allocations for a bot that is not running. +function toAllocate ( + numBuyLots: number, + numSellLots: number, + lotSize: number, + quoteLot: number, + slippageBuffer: number, + multiSplitBuffer: number, + buyFeeBuffer: number, + sellFeeBuffer: number, + marketReport: MarketReport, + availableFunds: AvailableFunds, + canRebalance: boolean, + baseID: number, + quoteID: number, + baseFeeID: number, + quoteFeeID: number, + baseIsAccountLocker: boolean, + quoteIsAccountLocker: boolean, + buyFundingFees: number, + sellFundingFees: number, + oneTradeBuyFundingFees: number, + oneTradeSellFundingFees: number +) : AllocationResult { + const result = requiredFunds(numBuyLots, numSellLots, lotSize, quoteLot, slippageBuffer, multiSplitBuffer, + buyFeeBuffer, sellFeeBuffer, marketReport, baseIsAccountLocker, quoteIsAccountLocker, baseID, quoteID, + baseFeeID, quoteFeeID, buyFundingFees, sellFundingFees, oneTradeBuyFundingFees, oneTradeSellFundingFees) + + const assetIDs = Array.from(new Set([baseID, quoteID, baseFeeID, quoteFeeID])) + + // For each asset, check if allocation is sufficient and set status + for (const assetID of assetIDs) { + result.dex[assetID].calculation.available = availableFunds.dex[assetID] ?? 0 + result.cex[assetID].calculation.available = availableFunds.cex?.[assetID] ?? 0 + + // dexSurplus / cexSurplus may be negative + const dexSurplus = result.dex[assetID].calculation.available - result.dex[assetID].calculation.totalRequired + const cexSurplus = result.cex[assetID].calculation.available - result.cex[assetID].calculation.totalRequired + + if (dexSurplus >= 0) { + result.dex[assetID].amount = result.dex[assetID].calculation.totalRequired + } else { + result.dex[assetID].status = 'insufficient' + result.dex[assetID].amount = result.dex[assetID].calculation.available + } + + if (cexSurplus >= 0) { + result.cex[assetID].amount = result.cex[assetID].calculation.totalRequired + } else { + result.cex[assetID].status = 'insufficient' + result.cex[assetID].amount = result.cex[assetID].calculation.available + } + + if (canRebalance && dexSurplus < 0 && cexSurplus > 0) { + const dexDeficit = -dexSurplus + const additionalCEX = Math.min(dexDeficit, cexSurplus) + result.cex[assetID].calculation.rebalanceAdjustment = additionalCEX + result.cex[assetID].amount += additionalCEX + if (cexSurplus >= dexDeficit) result.dex[assetID].status = 'sufficient-with-rebalance' + } + + // If cex is insufficient, increase dex allocation + if (canRebalance && cexSurplus < 0 && dexSurplus > 0) { + const cexDeficit = -cexSurplus + const additionalDEX = Math.min(cexDeficit, dexSurplus) + result.dex[assetID].calculation.rebalanceAdjustment = additionalDEX + result.dex[assetID].amount += additionalDEX + if (dexSurplus >= cexDeficit) result.cex[assetID].status = 'sufficient-with-rebalance' + } + + if (canRebalance && dexSurplus < 0 && cexSurplus > 0) { + const dexDeficit = -dexSurplus + const additionalCEX = Math.min(dexDeficit, cexSurplus) + result.cex[assetID].calculation.rebalanceAdjustment = additionalCEX + result.cex[assetID].amount += additionalCEX + if (cexSurplus >= dexDeficit) result.dex[assetID].status = 'sufficient-with-rebalance' + } + } + + return result +} + +// toAllocateRunning calculates the quick allocations for a running bot. +function toAllocateRunning ( + numBuyLots: number, + numSellLots: number, + lotSize: number, + quoteLot: number, + slippageBuffer: number, + multiSplitBuffer: number, + buyFeeBuffer: number, + sellFeeBuffer: number, + marketReport: MarketReport, + availableFunds: AvailableFunds, + canRebalance: boolean, + baseID: number, + quoteID: number, + baseFeeID: number, + quoteFeeID: number, + baseIsAccountLocker: boolean, + quoteIsAccountLocker: boolean, + runStats: RunStats, + buyFundingFees: number, + sellFundingFees: number, + oneTradeBuyFundingFees: number, + oneTradeSellFundingFees: number) : AllocationResult { + const result = requiredFunds(numBuyLots, numSellLots, lotSize, quoteLot, slippageBuffer, multiSplitBuffer, buyFeeBuffer, sellFeeBuffer, + marketReport, baseIsAccountLocker, quoteIsAccountLocker, baseID, quoteID, baseFeeID, quoteFeeID, + buyFundingFees, sellFundingFees, oneTradeBuyFundingFees, oneTradeSellFundingFees) + + const assetIDs = Array.from(new Set([baseID, quoteID, baseFeeID, quoteFeeID])) + + const totalBotBalance = (source: 'cex' | 'dex', assetID: number) => { + let bals + if (source === 'dex') { + bals = runStats.dexBalances[assetID] ?? { available: 0, locked: 0, pending: 0, reserved: 0 } + } else { + bals = runStats.cexBalances[assetID] ?? { available: 0, locked: 0, pending: 0, reserved: 0 } + } + return bals.available + bals.locked + bals.pending + bals.reserved + } + + for (const assetID of assetIDs) { + result.dex[assetID].calculation.runningBotTotal = totalBotBalance('dex', assetID) + result.cex[assetID].calculation.runningBotTotal = totalBotBalance('cex', assetID) + result.dex[assetID].calculation.runningBotAvailable = runStats.dexBalances[assetID]?.available ?? 0 + result.cex[assetID].calculation.runningBotAvailable = runStats.cexBalances[assetID]?.available ?? 0 + result.dex[assetID].calculation.available = availableFunds.dex[assetID] ?? 0 + result.cex[assetID].calculation.available = availableFunds.cex?.[assetID] ?? 0 + + const dexTotalAvailable = result.dex[assetID].calculation.runningBotTotal + result.dex[assetID].calculation.available + const dexSurplus = dexTotalAvailable - result.dex[assetID].calculation.totalRequired + + const cexTotalAvailable = result.cex[assetID].calculation.runningBotTotal + result.cex[assetID].calculation.available + const cexSurplus = cexTotalAvailable - result.cex[assetID].calculation.totalRequired + + if (dexSurplus >= 0) { + result.dex[assetID].amount = result.dex[assetID].calculation.totalRequired - result.dex[assetID].calculation.runningBotTotal + if (result.dex[assetID].amount < 0) result.dex[assetID].amount = -Math.min(-result.dex[assetID].amount, result.dex[assetID].calculation.runningBotAvailable) + } else { + result.dex[assetID].status = 'insufficient' + result.dex[assetID].amount = result.dex[assetID].calculation.available + } + + if (cexSurplus >= 0) { + result.cex[assetID].amount = result.cex[assetID].calculation.totalRequired - result.cex[assetID].calculation.runningBotTotal + if (result.cex[assetID].amount < 0) result.cex[assetID].amount = -Math.min(-result.cex[assetID].amount, result.cex[assetID].calculation.runningBotAvailable) + } else { + result.cex[assetID].status = 'insufficient' + result.cex[assetID].amount = result.cex[assetID].calculation.available + } + + if (canRebalance && dexSurplus < 0 && cexSurplus > 0) { + const dexDeficit = -dexSurplus + const additionalCEX = Math.min(dexDeficit, cexSurplus) + result.cex[assetID].calculation.rebalanceAdjustment = additionalCEX + result.cex[assetID].amount += additionalCEX + if (cexSurplus >= dexDeficit) result.dex[assetID].status = 'sufficient-with-rebalance' + } + + if (canRebalance && dexSurplus < 0 && cexSurplus > 0) { + const dexDeficit = -dexSurplus + const additionalCEX = Math.min(dexDeficit, cexSurplus) + result.cex[assetID].calculation.rebalanceAdjustment = additionalCEX + result.cex[assetID].amount += additionalCEX + if (cexSurplus >= dexDeficit) result.dex[assetID].status = 'sufficient-with-rebalance' + } + } + + return result +} + +// combineBotAllocations combines two allocations. If the result of an allocation +// is negative, it is set to 0. +function combineBotAllocations (alloc1: BotBalanceAllocation, alloc2: BotBalanceAllocation) : BotBalanceAllocation { + const result: BotBalanceAllocation = { dex: {}, cex: {} } + + for (const assetIDStr of Object.keys(alloc1.dex)) { + const assetID = Number(assetIDStr) + result.dex[assetID] = (alloc1.dex?.[assetID] ?? 0) + (alloc2.dex?.[assetID] ?? 0) + if (result.dex[assetID] < 0) { + result.dex[assetID] = 0 + } + } + + for (const assetIDStr of Object.keys(alloc1.cex)) { + const assetID = Number(assetIDStr) + result.cex[assetID] = (alloc1.cex?.[assetID] ?? 0) + (alloc2.cex?.[assetID] ?? 0) + if (result.cex[assetID] < 0) { + result.cex[assetID] = 0 + } + } + + return result } diff --git a/client/webserver/site/src/js/mmutil.ts b/client/webserver/site/src/js/mmutil.ts index 1f720b5a2a..be2632d1c3 100644 --- a/client/webserver/site/src/js/mmutil.ts +++ b/client/webserver/site/src/js/mmutil.ts @@ -16,12 +16,9 @@ import { UnitInfo, MarketReport, BotBalanceAllocation, - ProjectedAlloc, BalanceNote, BotBalance, Order, - LotFeeRange, - BookingFees, BotProblems, EpochReportNote, OrderReport, @@ -29,7 +26,8 @@ import { TradePlacement, SupportedAsset, CEXProblemsNote, - CEXProblems + CEXProblems, + AutoRebalanceConfig } from './registry' import { getJSON, postJSON } from './http' import Doc, { clamp } from './doc' @@ -82,6 +80,15 @@ class MarketMakerBot { return postJSON('/api/updatebotconfig', cfg) } + /* + * updateRunningBot updates the BotConfig and inventory for a running bot. + */ + async updateRunningBot (cfg: BotConfig, diffs: BotBalanceAllocation, autoRebalanceCfg?: AutoRebalanceConfig) { + const req: any = { cfg, diffs } + if (autoRebalanceCfg) req.autoRebalanceCfg = autoRebalanceCfg + return postJSON('/api/updaterunningbot', req) + } + /* * updateCEXConfig appends or updates the specified CEXConfig. */ @@ -97,6 +104,10 @@ class MarketMakerBot { return postJSON('/api/marketreport', { host, baseID, quoteID }) } + async maxFundingFees (market: MarketWithHost, maxBuyPlacements: number, maxSellPlacements: number, baseOptions: Record, quoteOptions: Record) { + return postJSON('/api/maxfundingfees', { market, maxBuyPlacements, maxSellPlacements, baseOptions, quoteOptions }) + } + async startBot (config: StartConfig) { return await postJSON('/api/startmarketmakingbot', { config }) } @@ -131,6 +142,10 @@ class MarketMakerBot { this.cexBalanceCache[cexName][assetID] = cexBalance return cexBalance } + + async availableBalances (market: MarketWithHost, cexName?: string) : Promise<{ dexBalances: Record, cexBalances: Record }> { + return await postJSON('/api/availablebalances', { market, cexName }) + } } // MM is the front end representation of the server's mm.MarketMaker. @@ -363,24 +378,6 @@ export function liveBotStatus (host: string, baseID: number, quoteID: number): M if (statuses.length) return statuses[0] } -interface Lotter { - lots: number -} - -function sumLots (lots: number, p: Lotter) { - return lots + p.lots -} - -interface AllocationProjection { - bProj: ProjectedAlloc - qProj: ProjectedAlloc - alloc: Record -} - -function emptyProjection (): ProjectedAlloc { - return { book: 0, bookingFees: 0, swapFeeReserves: 0, cex: 0, orderReserves: 0, slippageBuffer: 0 } -} - export class BotMarket { cfg: BotConfig host: string @@ -404,7 +401,6 @@ export class BotMarket { cexName: string dinfo: CEXDisplayInfo alloc: BotBalanceAllocation - proj: AllocationProjection bui: UnitInfo baseFactor: number baseFeeUI: UnitInfo @@ -424,11 +420,7 @@ export class BotMarket { rateStep: number baseFeeFiatRate: number quoteFeeFiatRate: number - baseLots: number - quoteLots: number marketReport: MarketReport - nBuyPlacements: number - nSellPlacements: number constructor (cfg: BotConfig) { const host = this.host = cfg.host @@ -484,20 +476,10 @@ export class BotMarket { if (cfg.arbMarketMakingConfig) { this.botType = botTypeArbMM - this.baseLots = cfg.arbMarketMakingConfig.sellPlacements.reduce(sumLots, 0) - this.quoteLots = cfg.arbMarketMakingConfig.buyPlacements.reduce(sumLots, 0) - this.nBuyPlacements = cfg.arbMarketMakingConfig.buyPlacements.length - this.nSellPlacements = cfg.arbMarketMakingConfig.sellPlacements.length } else if (cfg.simpleArbConfig) { this.botType = botTypeBasicArb - this.baseLots = cfg.uiConfig.simpleArbLots as number - this.quoteLots = cfg.uiConfig.simpleArbLots as number - } else if (cfg.basicMarketMakingConfig) { // basicmm + } else if (cfg.basicMarketMakingConfig) { this.botType = botTypeBasicMM - this.baseLots = cfg.basicMarketMakingConfig.sellPlacements.reduce(sumLots, 0) - this.quoteLots = cfg.basicMarketMakingConfig.buyPlacements.reduce(sumLots, 0) - this.nBuyPlacements = cfg.basicMarketMakingConfig.buyPlacements.length - this.nSellPlacements = cfg.basicMarketMakingConfig.sellPlacements.length } } @@ -507,7 +489,6 @@ export class BotMarket { const r = this.marketReport = res.report as MarketReport this.lotSizeUSD = lotSizeConv * r.baseFiatRate this.quoteLotUSD = quoteLotConv * r.quoteFiatRate - this.proj = this.projectedAllocations() } status () { @@ -580,197 +561,6 @@ export class BotMarket { cexQuoteFeeAvail: cexQuoteFeeAvail / quoteFeeFactor } } - - /* - * feesAndCommit generates a snapshot of current market fees, as well as a - * "commit", which is the funding dedicated to being on order. The commit - * values do not include booking fees, order reserves, etc. just the order - * quantity. - */ - feesAndCommit () { - const { - baseID, quoteID, marketReport: { baseFees, quoteFees }, lotSize, - baseLots, quoteLots, baseFeeID, quoteFeeID, baseIsAccountLocker, quoteIsAccountLocker, - cfg: { uiConfig: { baseConfig, quoteConfig } } - } = this - - return feesAndCommit( - baseID, quoteID, baseFees, quoteFees, lotSize, baseLots, quoteLots, - baseFeeID, quoteFeeID, baseIsAccountLocker, quoteIsAccountLocker, - baseConfig.orderReservesFactor, quoteConfig.orderReservesFactor - ) - } - - /* - * projectedAllocations calculates the required asset allocations from the - * user's configuration settings and the current market state. - */ - projectedAllocations () { - const { - cfg: { uiConfig: { quoteConfig, baseConfig } }, - baseFactor, quoteFactor, baseID, quoteID, lotSizeConv, quoteLotConv, - baseFeeFactor, quoteFeeFactor, baseFeeID, quoteFeeID, baseToken, - quoteToken, cexName - } = this - const { commit, fees } = this.feesAndCommit() - - const bProj = emptyProjection() - const qProj = emptyProjection() - - bProj.book = commit.dex.base.lots * lotSizeConv - qProj.book = commit.cex.base.lots * quoteLotConv - - bProj.orderReserves = Math.max(commit.cex.base.val, commit.dex.base.val) * baseConfig.orderReservesFactor / baseFactor - qProj.orderReserves = Math.max(commit.cex.quote.val, commit.dex.quote.val) * quoteConfig.orderReservesFactor / quoteFactor - - if (cexName) { - bProj.cex = commit.cex.base.lots * lotSizeConv - qProj.cex = commit.cex.quote.lots * quoteLotConv - } - - bProj.bookingFees = fees.base.bookingFees / baseFeeFactor - qProj.bookingFees = fees.quote.bookingFees / quoteFeeFactor - - if (baseToken) bProj.swapFeeReserves = fees.base.tokenFeesPerSwap * baseConfig.swapFeeN / baseFeeFactor - if (quoteToken) qProj.swapFeeReserves = fees.quote.tokenFeesPerSwap * quoteConfig.swapFeeN / quoteFeeFactor - qProj.slippageBuffer = (qProj.book + qProj.cex + qProj.orderReserves) * quoteConfig.slippageBufferFactor - - const alloc: Record = {} - const addAlloc = (assetID: number, amt: number) => { alloc[assetID] = (alloc[assetID] ?? 0) + amt } - addAlloc(baseID, Math.round((bProj.book + bProj.cex + bProj.orderReserves) * baseFactor)) - addAlloc(baseFeeID, Math.round((bProj.bookingFees + bProj.swapFeeReserves) * baseFeeFactor)) - addAlloc(quoteID, Math.round((qProj.book + qProj.cex + qProj.orderReserves + qProj.slippageBuffer) * quoteFactor)) - addAlloc(quoteFeeID, Math.round((qProj.bookingFees + qProj.swapFeeReserves) * quoteFeeFactor)) - - return { qProj, bProj, alloc } - } - - /* - * fundingState examines the projected allocations and the user's wallet - * balances to determine whether the user can fund the bot fully, unbalanced, - * or starved, and what funding source options might be available. - */ - fundingState () { - const { - proj: { bProj, qProj }, baseID, quoteID, baseFeeID, quoteFeeID, - cfg: { uiConfig: { cexRebalance } }, cexName - } = this - const { - baseAvail, quoteAvail, dexBaseAvail, dexQuoteAvail, cexBaseAvail, cexQuoteAvail, - dexBaseFeeAvail, dexQuoteFeeAvail - } = this.adjustedBalances() - - const canRebalance = Boolean(cexName && cexRebalance) - - // Three possible states. - // 1. We have the funding in the projection, and its in the right places. - // Give them some options for which wallet to pull order reserves from, - // but they can start immediately.. - // 2. We have the funding, but it's in the wrong place or the wrong asset, - // but we have deposits and withdraws enabled. We can offer them the - // option to start in an unbalanced state. - // 3. We don't have the funds. We offer them an option to start in a - // starved state. - const cexMinBaseAlloc = bProj.cex - let [dexMinBaseAlloc, transferableBaseAlloc, dexBaseFeeReq] = [bProj.book, 0, 0] - // Only add booking fees if this is the fee asset. - if (baseID === baseFeeID) dexMinBaseAlloc += bProj.bookingFees - // Base asset is a token. - else dexBaseFeeReq += bProj.bookingFees + bProj.swapFeeReserves - // If we can rebalance, the order reserves could potentially be withdrawn. - if (canRebalance) transferableBaseAlloc += bProj.orderReserves - // If we can't rebalance, order reserves are required in dex balance. - else dexMinBaseAlloc += bProj.orderReserves - // Handle the special case where the base asset it the quote asset's fee - // asset. - if (baseID === quoteFeeID) { - if (canRebalance) transferableBaseAlloc += qProj.bookingFees + qProj.swapFeeReserves - else dexMinBaseAlloc += qProj.bookingFees + qProj.swapFeeReserves - } - - let [dexMinQuoteAlloc, cexMinQuoteAlloc, transferableQuoteAlloc, dexQuoteFeeReq] = [qProj.book, qProj.cex, 0, 0] - if (quoteID === quoteFeeID) dexMinQuoteAlloc += qProj.bookingFees - else dexQuoteFeeReq += qProj.bookingFees + qProj.swapFeeReserves - if (canRebalance) transferableQuoteAlloc += qProj.orderReserves + qProj.slippageBuffer - else { - // The slippage reserves reserves should be split between cex and dex. - dexMinQuoteAlloc += qProj.orderReserves - const basis = qProj.book + qProj.cex + qProj.orderReserves - dexMinQuoteAlloc += (qProj.book + qProj.orderReserves) / basis * qProj.slippageBuffer - cexMinQuoteAlloc += qProj.cex / basis * qProj.slippageBuffer - } - if (quoteID === baseFeeID) { - if (canRebalance) transferableQuoteAlloc += bProj.bookingFees + bProj.swapFeeReserves - else dexMinQuoteAlloc += bProj.bookingFees + bProj.swapFeeReserves - } - - const dexBaseFunded = dexBaseAvail >= dexMinBaseAlloc - const cexBaseFunded = cexBaseAvail >= cexMinBaseAlloc - const dexQuoteFunded = dexQuoteAvail >= dexMinQuoteAlloc - const cexQuoteFunded = cexQuoteAvail >= cexMinQuoteAlloc - const totalBaseReq = dexMinBaseAlloc + cexMinBaseAlloc + transferableBaseAlloc - const totalQuoteReq = dexMinQuoteAlloc + cexMinQuoteAlloc + transferableQuoteAlloc - const baseFundedAndBalanced = dexBaseFunded && cexBaseFunded && baseAvail >= totalBaseReq - const quoteFundedAndBalanced = dexQuoteFunded && cexQuoteFunded && quoteAvail >= totalQuoteReq - const baseFeesFunded = dexBaseFeeAvail >= dexBaseFeeReq - const quoteFeesFunded = dexQuoteFeeAvail >= dexQuoteFeeReq - - const fundedAndBalanced = baseFundedAndBalanced && quoteFundedAndBalanced && baseFeesFunded && quoteFeesFunded - - // Are we funded but not balanced, but able to rebalance with a cex? - let fundedAndNotBalanced = !fundedAndBalanced - if (!fundedAndBalanced) { - const ordersFunded = baseAvail >= totalBaseReq && quoteAvail >= totalQuoteReq - const feesFunded = baseFeesFunded && quoteFeesFunded - fundedAndNotBalanced = ordersFunded && feesFunded && canRebalance - } - - return { - base: { - dex: { - avail: dexBaseAvail, - req: dexMinBaseAlloc, - funded: dexBaseFunded - }, - cex: { - avail: cexBaseAvail, - req: cexMinBaseAlloc, - funded: cexBaseFunded - }, - transferable: transferableBaseAlloc, - fees: { - avail: dexBaseFeeAvail, - req: dexBaseFeeReq, - funded: baseFeesFunded - }, - fundedAndBalanced: baseFundedAndBalanced, - fundedAndNotBalanced: !baseFundedAndBalanced && baseAvail >= totalBaseReq && canRebalance - }, - quote: { - dex: { - avail: dexQuoteAvail, - req: dexMinQuoteAlloc, - funded: dexQuoteFunded - }, - cex: { - avail: cexQuoteAvail, - req: cexMinQuoteAlloc, - funded: cexQuoteFunded - }, - transferable: transferableQuoteAlloc, - fees: { - avail: dexQuoteFeeAvail, - req: dexQuoteFeeReq, - funded: quoteFeesFunded - }, - fundedAndBalanced: quoteFundedAndBalanced, - fundedAndNotBalanced: !quoteFundedAndBalanced && quoteAvail >= totalQuoteReq && canRebalance - }, - fundedAndBalanced, - fundedAndNotBalanced, - starved: !fundedAndBalanced && !fundedAndNotBalanced - } - } } export type RunningMMDisplayElements = { @@ -812,6 +602,10 @@ export class RunningMarketMakerDisplay { const { mkt: { baseID, quoteID, host }, startTime } = this app().loadPage('mmlogs', { baseID, quoteID, host, startTime, returnPage: page }) }) + Doc.bind(this.page.settingsBttn, 'click', () => { + const { mkt: { baseID, quoteID, host } } = this + app().loadPage('mmsettings', { baseID, quoteID, host }) + }) Doc.bind(this.page.buyOrdersBttn, 'click', () => this.showOrderReport('buys')) Doc.bind(this.page.sellOrdersBttn, 'click', () => this.showOrderReport('sells')) } @@ -1227,95 +1021,6 @@ function setSignedValue (v: number, vEl: PageElement, signEl: PageElement, maxDe // signEl.classList.toggle('ico-minus', v < 0) } -export function feesAndCommit ( - baseID: number, quoteID: number, baseFees: LotFeeRange, quoteFees: LotFeeRange, - lotSize: number, baseLots: number, quoteLots: number, baseFeeID: number, quoteFeeID: number, - baseIsAccountLocker: boolean, quoteIsAccountLocker: boolean, baseOrderReservesFactor: number, - quoteOrderReservesFactor: number -) { - const quoteLot = calculateQuoteLot(lotSize, baseID, quoteID) - const [cexBaseLots, cexQuoteLots] = [quoteLots, baseLots] - const commit = { - dex: { - base: { - lots: baseLots, - val: baseLots * lotSize - }, - quote: { - lots: quoteLots, - val: quoteLots * quoteLot - } - }, - cex: { - base: { - lots: cexBaseLots, - val: cexBaseLots * lotSize - }, - quote: { - lots: cexQuoteLots, - val: cexQuoteLots * quoteLot - } - } - } - - let baseTokenFeesPerSwap = 0 - let baseRedeemReservesPerLot = 0 - if (baseID !== baseFeeID) { // token - baseTokenFeesPerSwap += baseFees.estimated.swap - if (baseFeeID === quoteFeeID) baseTokenFeesPerSwap += quoteFees.estimated.redeem - } - let baseBookingFeesPerLot = baseFees.max.swap - if (baseID === quoteFeeID) baseBookingFeesPerLot += quoteFees.max.redeem - if (baseIsAccountLocker) { - baseBookingFeesPerLot += baseFees.max.refund - if (!quoteIsAccountLocker && baseFeeID !== quoteFeeID) baseRedeemReservesPerLot = baseFees.max.redeem - } - - let quoteTokenFeesPerSwap = 0 - let quoteRedeemReservesPerLot = 0 - if (quoteID !== quoteFeeID) { - quoteTokenFeesPerSwap += quoteFees.estimated.swap - if (quoteFeeID === baseFeeID) quoteTokenFeesPerSwap += baseFees.estimated.redeem - } - let quoteBookingFeesPerLot = quoteFees.max.swap - if (quoteID === baseFeeID) quoteBookingFeesPerLot += baseFees.max.redeem - if (quoteIsAccountLocker) { - quoteBookingFeesPerLot += quoteFees.max.refund - if (!baseIsAccountLocker && quoteFeeID !== baseFeeID) quoteRedeemReservesPerLot = quoteFees.max.redeem - } - - const baseReservesFactor = 1 + baseOrderReservesFactor - const quoteReservesFactor = 1 + quoteOrderReservesFactor - - const baseBookingFees = (baseBookingFeesPerLot * baseLots) * baseReservesFactor - const baseRedeemFees = (baseRedeemReservesPerLot * quoteLots) * quoteReservesFactor - const quoteBookingFees = (quoteBookingFeesPerLot * quoteLots) * quoteReservesFactor - const quoteRedeemFees = (quoteRedeemReservesPerLot * baseLots) * baseReservesFactor - - const fees: BookingFees = { - base: { - ...baseFees, - bookingFeesPerLot: baseBookingFeesPerLot, - bookingFeesPerCounterLot: baseRedeemReservesPerLot, - bookingFees: baseBookingFees + baseRedeemFees, - swapReservesFactor: baseReservesFactor, - redeemReservesFactor: quoteReservesFactor, - tokenFeesPerSwap: baseTokenFeesPerSwap - }, - quote: { - ...quoteFees, - bookingFeesPerLot: quoteBookingFeesPerLot, - bookingFeesPerCounterLot: quoteRedeemReservesPerLot, - bookingFees: quoteBookingFees + quoteRedeemFees, - swapReservesFactor: quoteReservesFactor, - redeemReservesFactor: baseReservesFactor, - tokenFeesPerSwap: quoteTokenFeesPerSwap - } - } - - return { commit, fees } -} - function botProblemMessages (problems: BotProblems | undefined, cexName: string, dexHost: string): string[] { if (!problems) return [] const msgs: string[] = [] diff --git a/client/webserver/site/src/js/registry.ts b/client/webserver/site/src/js/registry.ts index a981a4919b..10129a9868 100644 --- a/client/webserver/site/src/js/registry.ts +++ b/client/webserver/site/src/js/registry.ts @@ -800,17 +800,20 @@ export interface BotBalanceAllocation { cex: Record } -export interface BotAssetConfig { - swapFeeN: number - orderReservesFactor: number - slippageBufferFactor: number - transferFactor: number +export interface QuickBalanceConfig { + buysBuffer: number + sellsBuffer: number + buyFeeReserve: number + sellFeeReserve: number + slippageBuffer: number } export interface UIConfig { - baseConfig: BotAssetConfig - quoteConfig: BotAssetConfig - simpleArbLots?: number + quickBalance: QuickBalanceConfig + allocation: BotBalanceAllocation + usingQuickBalance: boolean + baseMinTransfer: number + quoteMinTransfer: number cexRebalance: boolean internalTransfers: boolean } @@ -908,37 +911,6 @@ export interface FeeEstimates extends LotFeeRange { tokenFeesPerSwap: number } -export interface ProjectedAlloc { - // book is inventory dedicated either to active orders for basicmm and arbmm, - // or on reserve for orders in the case of basicarb. book + bookingFees is the - // starvation threshold for DEX, meaning it's impossible to start a bot - // unstarved if there no way to get book + bookingFees to Bison Wallet. A user - // could potentially adjust order reserves or swap fee reserves to free up - // more funds, but with possible degradation of bot performance. - book: number - // booking fees is funding dedicated to covering the fees for funded orders. - // bookingFees are in the units of the parent chain for token assets. - bookingFees: number - // swapFeeReserves is only required for token assets. These are fees - // reserved for funding swaps. These fees are only debited, so will definitely - // run out eventually, but we'll get a UI that enabled manual and/or auto - // refill soon. swapFeeReserves are in the units of the parent chain. - swapFeeReserves: number - // cex is the inventory dedicated to funding counter-orders on cex for an - // arbmm or simplearb bot. cex is the starvation threshold for CEX. - cex: number - // orderReserves is inventory reserved for facilitating withdraws and - // deposits or for replacing matched orders. It's a good idea to have a - // little extra around, otherwise a trade sequence gone wrong could put - // the bot in a starved or unbalanced state. - orderReserves: number - // slippageBuffer is only required for the quote asset. This accounts for - // variations in rate, because the quote asset's "lot size" varies with - // rate. If the rate goes down, the quote-converted lot size goes up, so - // we'll let the user choose to reserve a little extra for this case. - slippageBuffer: number -} - export interface FeeGapStats { basisPrice: number feeGap: number diff --git a/client/webserver/webserver.go b/client/webserver/webserver.go index 98adcf82f8..1f4faf2264 100644 --- a/client/webserver/webserver.go +++ b/client/webserver/webserver.go @@ -188,6 +188,9 @@ type MMCore interface { RunOverview(startTime int64, mkt *mm.MarketWithHost) (*mm.MarketMakingRunOverview, error) RunLogs(startTime int64, mkt *mm.MarketWithHost, n uint64, refID *uint64, filter *mm.RunLogFilters) (events, updatedEvents []*mm.MarketMakingEvent, overview *mm.MarketMakingRunOverview, err error) CEXBook(host string, baseID, quoteID uint32) (buys, sells []*core.MiniOrder, _ error) + UpdateRunningBotCfg(cfg *mm.BotConfig, balanceDiffs *mm.BotInventoryDiffs, autoRebalanceCfg *mm.AutoRebalanceConfig, saveUpdate bool) error + AvailableBalances(mkt *mm.MarketWithHost, cexName *string) (dexBalances, cexBalances map[uint32]uint64, _ error) + MaxFundingFees(mkt *mm.MarketWithHost, maxBuyPlacements, maxSellPlacements uint32, baseOptions, quoteOptions map[string]string) (buyFees, sellFees uint64, err error) } // genCertPair generates a key/cert pair to the paths provided. @@ -598,6 +601,7 @@ func New(cfg *Config) (*WebServer, error) { apiAuth.Post("/startmarketmakingbot", s.apiStartMarketMakingBot) apiAuth.Post("/stopmarketmakingbot", s.apiStopMarketMakingBot) apiAuth.Post("/updatebotconfig", s.apiUpdateBotConfig) + apiAuth.Post("/updaterunningbot", s.apiUpdateRunningBot) apiAuth.Post("/updatecexconfig", s.apiUpdateCEXConfig) apiAuth.Post("/removebotconfig", s.apiRemoveBotConfig) apiAuth.Get("/marketmakingstatus", s.apiMarketMakingStatus) @@ -606,6 +610,9 @@ func New(cfg *Config) (*WebServer, error) { apiAuth.Get("/archivedmmruns", s.apiArchivedRuns) apiAuth.Post("/mmrunlogs", s.apiRunLogs) apiAuth.Post("/cexbook", s.apiCEXBook) + apiAuth.Post("/availablebalances", s.apiAvailableBalances) + apiAuth.Post("/maxfundingfees", s.apiMaxFundingFees) + }) }) diff --git a/dex/testing/dcrdex/genmarkets.sh b/dex/testing/dcrdex/genmarkets.sh index e534152af7..d7dc132b06 100755 --- a/dex/testing/dcrdex/genmarkets.sh +++ b/dex/testing/dcrdex/genmarkets.sh @@ -203,6 +203,20 @@ EOF else echo "Polygon is not running. Configuring dcrdex markets without Polygon." fi +if [ $ETH_ON -eq 0 ] && [ $POLYGON_ON -eq 0 ]; then + cat << EOF >> "${FILEPATH}" + }, + { + "base": "USDC.ETH_simnet", + "quote": "USDC.POLYGON_simnet", + "lotSize": 1000000, + "rateStep": 10000, + "epochDuration": ${EPOCH_DURATION}, + "marketBuyBuffer": 1.2, + "parcelSize": 4 +EOF +fi + if [ $DOGE_ON -eq 0 ]; then cat << EOF >> "${FILEPATH}" },