diff --git a/client/mm/config.go b/client/mm/config.go index ea11a2c2dd..267586841c 100644 --- a/client/mm/config.go +++ b/client/mm/config.go @@ -11,6 +11,15 @@ type MarketMakingConfig struct { CexConfigs []*CEXConfig `json:"cexConfigs"` } +func (m *MarketMakingConfig) botConfig(mwh *MarketWithHost) *BotConfig { + for _, cfg := range m.BotConfigs { + if cfg.Host == mwh.Host && cfg.BaseID == mwh.BaseID && cfg.QuoteID == mwh.QuoteID { + return cfg + } + } + return nil +} + func (cfg *MarketMakingConfig) Copy() *MarketMakingConfig { c := &MarketMakingConfig{ BotConfigs: make([]*BotConfig, len(cfg.BotConfigs)), diff --git a/client/mm/exchange_adaptor.go b/client/mm/exchange_adaptor.go index 813775759c..9f0b4afe05 100644 --- a/client/mm/exchange_adaptor.go +++ b/client/mm/exchange_adaptor.go @@ -3590,6 +3590,7 @@ func (u *unifiedExchangeAdaptor) updateConfig(cfg *BotConfig) { func (u *unifiedExchangeAdaptor) updateInventory(balanceDiffs *BotInventoryDiffs) { u.updateInventoryEvent(u.applyInventoryDiffs(balanceDiffs)) + u.sendStatsUpdate() } func (u *unifiedExchangeAdaptor) Book() (buys, sells []*core.MiniOrder, _ error) { diff --git a/client/mm/mm.go b/client/mm/mm.go index b9c3cba0dd..ea5a7706e5 100644 --- a/client/mm/mm.go +++ b/client/mm/mm.go @@ -477,6 +477,31 @@ func (m *MarketMaker) MarketReport(host string, baseID, quoteID uint32) (*Market }, nil } +// MaxFundingFees returns the maximum funding fees for a bot on a market. +// There must be a bot config saved in the default config file for the +// market. +func (m *MarketMaker) MaxFundingFees(mwh *MarketWithHost) (buyFees, sellFees uint64, err error) { + cfg := m.defaultConfig() + botCfg := cfg.botConfig(mwh) + if botCfg == nil { + return 0, 0, fmt.Errorf("no bot config found for %s", mwh) + } + + maxBuyPlacements, maxSellPlacements := botCfg.maxPlacements() + + buyFundingFees, err := m.core.MaxFundingFees(mwh.QuoteID, mwh.Host, maxBuyPlacements, botCfg.QuoteWalletOptions) + 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, botCfg.BaseWalletOptions) + 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 { @@ -1085,12 +1110,13 @@ 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 } @@ -1126,12 +1152,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) @@ -1711,10 +1740,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/rpcserver/handlers.go b/client/rpcserver/handlers.go index 8417584557..2ae6e0148d 100644 --- a/client/rpcserver/handlers.go +++ b/client/rpcserver/handlers.go @@ -861,7 +861,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) 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 23bb0067fe..03f6dfdefc 100644 --- a/client/webserver/api.go +++ b/client/webserver/api.go @@ -19,6 +19,7 @@ import ( "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/config" "decred.org/dcrdex/dex/encode" + "github.com/davecgh/go-spew/spew" ) var zero = encode.ClearBytes @@ -1749,6 +1750,54 @@ 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"` + } + if !readPost(w, r, &req) { + return + } + buyFees, sellFees, err := s.mm.MaxFundingFees(req.Market) + 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"` @@ -1965,6 +2014,39 @@ func (s *WebServer) apiUpdateBotConfig(w http.ResponseWriter, r *http.Request) { writeJSON(w, simpleAck()) } +func (s *WebServer) apiUpdateRunningBotConfig(w http.ResponseWriter, r *http.Request) { + var updatedCfg *mm.BotConfig + if !readPost(w, r, &updatedCfg) { + s.writeAPIError(w, fmt.Errorf("failed to read config")) + return + } + + spew.Dump("apiUpdateRunningBotConfig", updatedCfg) + + if err := s.mm.UpdateRunningBotCfg(updatedCfg, nil, true); err != nil { + s.writeAPIError(w, err) + return + } + + writeJSON(w, simpleAck()) +} + +func (s *WebServer) apiUpdateBotInventory(w http.ResponseWriter, r *http.Request) { + var form struct { + Market *mm.MarketWithHost `json:"market"` + Diffs *mm.BotInventoryDiffs `json:"diffs"` + } + if !readPost(w, r, &form) { + s.writeAPIError(w, fmt.Errorf("failed to read form")) + return + } + if err := s.mm.UpdateRunningBotInventory(form.Market, form.Diffs); 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/jsintl.go b/client/webserver/jsintl.go index 072b03393e..4f139b5b08 100644 --- a/client/webserver/jsintl.go +++ b/client/webserver/jsintl.go @@ -219,6 +219,7 @@ const ( idCausesSelfMatch = "CAUSES_SELF_MATCH" idCexNotConnected = "CEX_NOT_CONNECTED" idDeleteBot = "DELETE_BOT" + idAllocationFormTitle = "ALLOCATION_FORM_TITLE" ) var enUS = map[string]*intl.Translation{ @@ -437,6 +438,7 @@ var enUS = map[string]*intl.Translation{ idCausesSelfMatch: {T: "This order would cause a self-match"}, idCexNotConnected: {T: "{{ cexName }} not connected"}, idDeleteBot: {T: "Are you sure you want to delete this bot for the {{ baseTicker }}-{{ quoteTicker }} market on {{ host }}?"}, + idAllocationFormTitle: {T: "Allocate funds for {{ baseSymbol }} - {{ quoteSymbol }} on {{ host }}"}, } var ptBR = map[string]*intl.Translation{ diff --git a/client/webserver/locales/ar.go b/client/webserver/locales/ar.go index 27f7c264ac..bdce048eab 100644 --- a/client/webserver/locales/ar.go +++ b/client/webserver/locales/ar.go @@ -356,7 +356,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 8d15eef0cc..c286891da9 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -541,7 +541,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"}, @@ -679,4 +678,5 @@ 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"}, } diff --git a/client/webserver/locales/pl-pl.go b/client/webserver/locales/pl-pl.go index c1f7be9a99..f5f5452ddf 100644 --- a/client/webserver/locales/pl-pl.go +++ b/client/webserver/locales/pl-pl.go @@ -537,7 +537,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/html/forms.tmpl b/client/webserver/site/src/html/forms.tmpl index 08cc9edfb5..c7fa377962 100644 --- a/client/webserver/site/src/html/forms.tmpl +++ b/client/webserver/site/src/html/forms.tmpl @@ -1028,6 +1028,8 @@
+ +
@@ -1146,3 +1148,335 @@ {{end}} + +{{define "allocationForm"}} +
+
+ + {{- /* FUNDING STATUS DEPENDENT MESSAGING AND START BUTTON */ -}} +
+
+
+ Before starting the bot, you need to allocate funds for trading. The minimum required allocation for all the configured placements + is set by default, and you can add additional funds by adjusting the configurations below. Alternatively, you can manually set each + of the balances individually. +
+
+
+ +
+
+
+ +
+ + {{- /* PROPOSED ALLOCATIONS AND ADJUSTMENTS */ -}} + +
+ {{- /* LEFT COLUMN */ -}} +
+ {{- /* TO ALLOCATE */ -}} +
+
+
To Allocate
+ +
+ Sufficient Funds +
+ Insufficient Funds +
+ Sufficient With Rebalance + +
+ {{ template "balancesTable" }} +
+
+ + {{- /* BALANCE CONFIGURATION */ -}} +
+
+
Balance Configuration
+ +
+ + {{- /* DEX BALANCES */ -}} +
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ + {{- /* CEX BALANCES */ -}} +
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+ {{- /* AVAILABLE BALANCES */ -}} +
+
Available Balances
+
+ {{template "balancesTable"}} +
+
+ + {{- /* RUNNING BOT BALANCES */ -}} +
+
Running Bot Balances
+
+ {{template "balancesTable"}} +
+
+ + {{- /* REQUIRED PER SELL LOT */ -}} +
+
Required Per Sell Lot
+
+ {{template "balancesTable"}} +
+
+ + {{- /* REQUIRED PER BUY LOT */ -}} +
+
Required Per Buy Lot
+
+ {{template "balancesTable"}} +
+
+
+
+ + {{- /* RIGHT COLUMN - ADJUSTMENTS */ -}} +
+
+
+
Adjustments
+
+ +
+
+ Buy Buffer + +
+
+
+ +
+
+
+
+ +
+
+ Sell Buffer + +
+
+
+ +
+
+
+
+ +
+
+ Slippage Buffer + +
+
+
+ + % +
+
+
+
+ +
+
+ Buy Fee Reserve + +
+
+
+ +
+
+
+
+ +
+
+ Sell Fee Reserve + +
+
+
+ +
+
+
+
+
+ +
+
+
Auto Rebalance
+
+ +
+ + + +
+ +
+
+
+ + Minimum Transfer + +
+
+
+ +
+
+
+
+ +
+
+ + Minimum Transfer + +
+
+
+ +
+
+
+
+
+
+
+
+
+{{end}} + +{{ define "balancesTable" }} + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+{{end}} \ No newline at end of file diff --git a/client/webserver/site/src/html/mm.tmpl b/client/webserver/site/src/html/mm.tmpl index cad9f4e4bb..f8b0f92f46 100644 --- a/client/webserver/site/src/html/mm.tmpl +++ b/client/webserver/site/src/html/mm.tmpl @@ -160,7 +160,7 @@
- Projected Allocation + Minimum Allocation
@@ -180,35 +180,20 @@
-
+
├─ - Booking Fees + DEX - +
├─ - CEX Inventory + CEX
-
- - ├─ - Order Inventory - - -
-
- - └─ - Order Reserves - (%) - - -
~ @@ -228,43 +213,20 @@
-
+
├─ - Booking Fees + DEX - +
├─ - CEX Inventory + CEX
-
- - ├─ - Order Inventory - - -
-
- - ├─ - Order Reserves - (%) - - -
-
- - └─ - Slippage Buffer - (%) - - -
~ @@ -273,7 +235,7 @@
{{- /* PROJECTED BASE TOKEN FEE ALLOCATIONS */ -}} -
+
@@ -284,22 +246,12 @@
-
-
- - ├─ - Swap Fee Reserves - () - - -
-
-
+
- └─ - Booking Fees + ├─ + DEX - +
~ @@ -309,7 +261,7 @@
{{- /* PROJECTED QUOTE TOKEN FEE ALLOCATIONS */ -}} -
+
@@ -321,26 +273,18 @@
-
- - ├─ - Swap Fee Reserves - () - - -
- └─ - Booking Fees + ├─ + DEX - +
~ - - USD + + USD
@@ -426,172 +370,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 -
-
@@ -624,6 +402,11 @@
+ +
+ {{template "allocationForm"}} +
+ {{- /* END FORMS */ -}} {{template "bottom"}} diff --git a/client/webserver/site/src/html/mmsettings.tmpl b/client/webserver/site/src/html/mmsettings.tmpl index 2402fef851..506d854f18 100644 --- a/client/webserver/site/src/html/mmsettings.tmpl +++ b/client/webserver/site/src/html/mmsettings.tmpl @@ -22,7 +22,6 @@
- {{- /* PLACEMENTS */ -}}
@@ -54,12 +53,13 @@
+ +
+ [[[bot_running]]] +
+ {{- /* MANUAL CONFIG */ -}}
- {{- /* VIEW-ONLY MODE */ -}} -
- [[[bots_running_view_only]]] -
{{- /* STRATEGY SELECTION */ -}}
@@ -182,7 +182,7 @@ {{- /* QUICK CONFIG */ -}}
- [[[Quick Placements]]] + [[[Quick Placements]]]
{{- /* ASSET SETTINGS */ -}} -
-
-
-
- - {{- /* LOGO AND TOTAL ALLOCATION */ -}} -
-
- - -
-
-
- - -
-
- ~ USD -
-
-
- - {{- /* 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 - - -
-
- = - - -
-
-
- - {{- /* FEE ALLOCATIONS */ -}} -
- - {{- /* TOKEN FEE RESERVES */ -}} -
-
- - Fee Reserves -
-
-
- - -
-
- ~ USD -
-
+
+ + {{- /* WALLET SETTINGS */ -}} +
+ {{- /* WALLET SETTINGS */ -}} +
+
+ + [[[Wallet Options]]] +
+
+
+ + + + +
no settings available
+
+
+ + +
- - {{- /* BOOKING FEES */ -}} -
+
- Booking Fees - - - lots - -
-
- x - - - per lot - -
-
- x - - reserves -
-
-
- + - - redeems -
-
- x - - - per redeem - -
-
- x - - reserves -
-
-
- = - - -
-
- - {{- /* SWAP RESERVES */ -}} -
-
- Swap Fee Reserves -
-
-
-
- -
- - -
-
- swaps -
-
- x - - - per swap - -
-
- = - - -
-
-
- -
- - {{- /* ASSET SETTINGS */ -}} -
- {{- /* WALLET SETTINGS */ -}} - -
-
- - [[[Wallet Options]]] -
-
no settings available
-
-
-
-
-
- - -
-
-
- -
-
-
-
-
- -
- {{- /* BALANCES */ -}} -
-
- - [[[Available]]] -
-
- - -
-
-
-
-
-
- ├─ - - -
-
- └─ - - -
-
-
-
-
-
-
- - [[[Available]]] -
-
- - -
-
- - {{- /* REBALANCE SETTINGS */ -}} -
- - [[[cex_rebalance]]] -
-
- [[[Minimum Transfer]]] - -
-
-
- - +
+ +
-
+
-
- {{- /* GENERAL SETTINGS */ -}} -
-
-
- - Knobs +
+ + {{- /* KNOBS */ -}} +
+
+ + Knobs +
+ + {{- /* DRIFT TOLERANCE */ -}} +
+
+ + [[[Drift tolerance]]] + +
- {{- /* CEX REBALANCE CHECKBOX */ -}} - - - {{- /* DRIFT TOLERANCE */ -}} -
-
- - [[[Drift tolerance]]] - - -
-
-
- - % -
+
+
+ + %
+
- {{- /* ORDER PERSISTENCE */ -}} -
-
- [[[Order persistence]]] - -
-
-
- - epochs -
+ {{- /* ORDER PERSISTENCE */ -}} +
+
+ [[[Order persistence]]] +
-
-
+
+
+ + epochs +
+
+
+ + {{- /* MINIMUM ALLOCATIONS */ -}} +
+
Minimum Allocations
+ Sufficient Funds +
+ Insufficient Funds +
+ Sufficient With Rebalance + +
+ {{ template "balancesTable" }} +
+
diff --git a/client/webserver/site/src/js/forms.ts b/client/webserver/site/src/js/forms.ts index d5b865a859..b0b97353eb 100644 --- a/client/webserver/site/src/js/forms.ts +++ b/client/webserver/site/src/js/forms.ts @@ -1,4 +1,4 @@ -import Doc, { Animation } from './doc' +import Doc, { Animation, MiniSlider, NumberInput } from './doc' import { postJSON } from './http' import State from './state' import * as intl from './locales' @@ -28,11 +28,26 @@ import { Token, WalletCreationNote, CoreNote, - PrepaidBondID + PrepaidBondID, + MarketReport, + UnitInfo, + RunStats, + AutoRebalanceConfig } from './registry' import { XYRangeHandler } from './opts' import { CoinExplorers } from './coinexplorers' -import { MM, setCexElements } from './mmutil' +import { + MM, + PerLot, + perLotRequirements, + setCexElements, + setMarketElements, + toAllocate, + toAllocateRunning, + AvailableFunds, + AllocationStatus, + AllocationResult +} from './mmutil' interface ConfigOptionInput extends HTMLInputElement { configOpt: ConfigOption @@ -1031,7 +1046,7 @@ export class FeeAssetSelectionForm { const privilegedLimit = conventionalLotSize * parcelSize * perTierBaseParcelLimit * parcelLimitScoreMultiplier * tier tmpl.tradeLimitLow.textContent = Doc.formatFourSigFigs(startingLimit) tmpl.tradeLimitHigh.textContent = Doc.formatFourSigFigs(privilegedLimit) - const baseFiatRate = app().fiatRatesMap[baseID] + const baseFiatRate = 0 // app().fiatRatesMap[baseID] if (baseFiatRate) { tmpl.fiatTradeLimitLow.textContent = Doc.formatFourSigFigs(startingLimit * baseFiatRate) tmpl.fiatTradeLimitHigh.textContent = Doc.formatFourSigFigs(privilegedLimit * baseFiatRate) @@ -2213,6 +2228,481 @@ export class CEXConfigurationForm { } } +export class AllocationForm { + form: PageElement + page: Record + buyBufferSlider: MiniSlider + buyBufferInput: NumberInput + sellBufferSlider: MiniSlider + sellBufferInput: NumberInput + slippageBufferSlider: MiniSlider + slippageBufferInput: NumberInput + buyFeeReserveSlider: MiniSlider + buyFeeReserveInput: NumberInput + sellFeeReserveSlider: MiniSlider + sellFeeReserveInput: 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 + baseMinTransferInput: NumberInput + baseMinTransferSlider: MiniSlider + quoteMinTransferInput: NumberInput + quoteMinTransferSlider: MiniSlider + numSellLots: number + numBuyLots: number + perSellLot: PerLot + perBuyLot: PerLot + marketReport: MarketReport + availableFunds: AvailableFunds + canRebalance: boolean + baseID: number + quoteID: number + baseFeeID: number + quoteFeeID: number + host: string + baseIsAccountLocker: boolean + quoteIsAccountLocker: boolean + baseUI: UnitInfo + quoteUI: UnitInfo + baseFeeUI: UnitInfo + quoteFeeUI: UnitInfo + allocation: AllocationResult + botID: string + buyFundingFees: number + sellFundingFees: number + isRunning: boolean + cexName: string + runStats: RunStats | undefined + baseMinTransfer: number + quoteMinTransfer: number + maxBaseTransfer: number + maxQuoteTransfer: number + + constructor (form: PageElement, submit: (allocation: AllocationResult, running: boolean, autoRebalance: AutoRebalanceConfig | undefined) => void) { + this.form = form + const page = this.page = Doc.idDescendants(form) + Doc.bind(page.startBttn, 'click', (e: PointerEvent) => { + if (!e.pointerType) return // TODO: I don't know how to prevent enter on an input from triggering this + let autoRebalance: AutoRebalanceConfig | undefined + if (page.enableRebalance.checked) { + const baseMinTransfer = this.baseMinTransferInput.value() * this.baseUI.conventional.conversionFactor + const quoteMinTransfer = this.quoteMinTransferInput.value() * this.quoteUI.conventional.conversionFactor + autoRebalance = { + minBaseTransfer: baseMinTransfer, + minQuoteTransfer: quoteMinTransfer + } + } + submit(this.allocation, this.isRunning, autoRebalance) + }) + Doc.bind(page.adjustManuallyBtn, 'click', () => this.adjustManually()) + Doc.bind(page.quickConfigBtn, 'click', () => this.quickConfig()) + Doc.bind(page.enableRebalance, 'change', () => this.enableRebalanceChanged()) + this.buyBufferSlider = new MiniSlider(page.buyBufferSlider, (amt: number) => this.sliderChanged(amt, 'buyBuffer')) + this.buyBufferInput = new NumberInput(page.buyBuffer, { prec: 0, min: 0, changed: (amt: number) => this.inputChanged(amt, 'buyBuffer') }) + this.sellBufferSlider = new MiniSlider(page.sellBufferSlider, (amt: number) => this.sliderChanged(amt, 'sellBuffer')) + this.sellBufferInput = new NumberInput(page.sellBuffer, { prec: 0, min: 0, changed: (amt: number) => this.inputChanged(amt, 'sellBuffer') }) + this.slippageBufferSlider = new MiniSlider(page.slippageBufferSlider, (amt: number) => this.sliderChanged(amt, 'slippageBuffer')) + this.slippageBufferInput = new NumberInput(page.slippageBuffer, { prec: 3, min: 0, changed: (amt: number) => this.inputChanged(amt, 'slippageBuffer') }) + this.buyFeeReserveSlider = new MiniSlider(page.buyFeeReserveSlider, (amt: number) => this.sliderChanged(amt, 'buyFeeReserve')) + this.buyFeeReserveInput = new NumberInput(page.buyFeeReserve, { prec: 0, min: 0, changed: (amt: number) => this.inputChanged(amt, 'buyFeeReserve') }) + this.sellFeeReserveSlider = new MiniSlider(page.sellFeeReserveSlider, (amt: number) => this.sliderChanged(amt, 'sellFeeReserve')) + this.sellFeeReserveInput = new NumberInput(page.sellFeeReserve, { prec: 0, min: 0, changed: (amt: number) => this.inputChanged(amt, 'sellFeeReserve') }) + this.baseDexBalanceSlider = new MiniSlider(page.dexBaseBalanceSlider, (amt: number) => this.balanceSliderChanged(amt, 'base', 'dex')) + this.baseDexBalanceInput = new NumberInput(page.dexBaseBalance, { prec: 0, min: 0, changed: (amt: number) => this.balanceInputChanged(amt, 'base', 'dex') }) + this.quoteDexBalanceSlider = new MiniSlider(page.dexQuoteBalanceSlider, (amt: number) => this.balanceSliderChanged(amt, 'quote', 'dex')) + this.quoteDexBalanceInput = new NumberInput(page.dexQuoteBalance, { prec: 0, min: 0, changed: (amt: number) => this.balanceInputChanged(amt, 'quote', 'dex') }) + this.baseFeeBalanceSlider = new MiniSlider(page.baseFeeBalanceSlider, (amt: number) => this.balanceSliderChanged(amt, 'base', 'dex')) + this.baseFeeBalanceInput = new NumberInput(page.baseFeeBalance, { prec: 0, min: 0, changed: (amt: number) => this.balanceInputChanged(amt, 'base', 'dex') }) + this.quoteFeeBalanceSlider = new MiniSlider(page.quoteFeeBalanceSlider, (amt: number) => this.balanceSliderChanged(amt, 'quote', 'dex')) + this.quoteFeeBalanceInput = new NumberInput(page.quoteFeeBalance, { prec: 0, min: 0, changed: (amt: number) => this.balanceInputChanged(amt, 'quote', 'dex') }) + this.baseCexBalanceSlider = new MiniSlider(page.cexBaseBalanceSlider, (amt: number) => this.balanceSliderChanged(amt, 'base', 'cex')) + this.baseCexBalanceInput = new NumberInput(page.cexBaseBalance, { prec: 0, min: 0, changed: (amt: number) => this.balanceInputChanged(amt, 'base', 'cex') }) + this.quoteCexBalanceSlider = new MiniSlider(page.cexQuoteBalanceSlider, (amt: number) => this.balanceSliderChanged(amt, 'quote', 'cex')) + this.quoteCexBalanceInput = new NumberInput(page.cexQuoteBalance, { prec: 0, min: 0, changed: (amt: number) => this.balanceInputChanged(amt, 'quote', 'cex') }) + 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') }) + } + + enableRebalanceChanged () { + const { page } = this + const checked = page.enableRebalance.checked + Doc.setVis(checked, page.rebalanceSettings) + } + + adjustManually () { + const { page } = this + Doc.hide(page.quickConfigSection, page.toAllocateSection) + Doc.show(page.adjustManuallySection) + } + + quickConfig () { + const { page } = this + Doc.hide(page.adjustManuallySection) + Doc.show(page.quickConfigSection, page.toAllocateSection) + this.updateAllocation() + } + + init ( + baseID: number, quoteID: number, host: string, baseFeeID: number, quoteFeeID: number, + cexName: string, bui: UnitInfo, qui: UnitInfo, baseFeeUI: UnitInfo, + quoteFeeUI: UnitInfo, marketReport: MarketReport, lotSize: number, + quoteLot: number, baseIsAccountLocker: boolean, quoteIsAccountLocker: boolean, + availableFunds: AvailableFunds, canRebalance: boolean, botID: string, + numBuyLots: number, numSellLots: number, buyFundingFees: number, sellFundingFees: number, + runStats: RunStats | undefined) { + const { page } = this + + const { perSellLot, perBuyLot } = perLotRequirements(baseID, quoteID, baseFeeID, quoteFeeID, lotSize, + quoteLot, marketReport, baseIsAccountLocker, quoteIsAccountLocker) + this.perSellLot = perSellLot + this.perBuyLot = perBuyLot + this.marketReport = marketReport + this.availableFunds = availableFunds + this.canRebalance = canRebalance + this.botID = botID + this.baseID = baseID + this.quoteID = quoteID + this.baseFeeID = baseFeeID + this.quoteFeeID = quoteFeeID + this.baseIsAccountLocker = baseIsAccountLocker + this.quoteIsAccountLocker = quoteIsAccountLocker + this.baseUI = bui + this.quoteUI = qui + this.baseFeeUI = baseFeeUI + this.quoteFeeUI = quoteFeeUI + this.numBuyLots = numBuyLots + this.numSellLots = numSellLots + this.buyFundingFees = buyFundingFees + this.sellFundingFees = sellFundingFees + this.isRunning = !!runStats + this.runStats = runStats + this.cexName = cexName + this.host = host + const basePrec = Math.log10(bui.conventional.conversionFactor) + const quotePrec = Math.log10(qui.conventional.conversionFactor) + const baseFeePrec = Math.log10(baseFeeUI.conventional.conversionFactor) + const quoteFeePrec = Math.log10(quoteFeeUI.conventional.conversionFactor) + this.baseDexBalanceInput.prec = basePrec + this.baseDexBalanceInput.min = this.minBalance('base', 'dex') + this.quoteDexBalanceInput.prec = quotePrec + this.quoteDexBalanceInput.min = this.minBalance('quote', 'dex') + this.baseFeeBalanceInput.prec = baseFeePrec + this.baseFeeBalanceInput.min = this.minBalance('baseFee', 'dex') + this.quoteFeeBalanceInput.prec = quoteFeePrec + this.quoteFeeBalanceInput.min = this.minBalance('quoteFee', 'dex') + this.baseCexBalanceInput.prec = basePrec + this.baseCexBalanceInput.min = this.minBalance('base', 'cex') + this.quoteCexBalanceInput.prec = quotePrec + this.quoteCexBalanceInput.min = this.minBalance('quote', 'cex') + this.baseMinTransferInput.prec = basePrec + this.quoteMinTransferInput.prec = quotePrec + + page.allocationFormTitle.textContent = intl.prep(intl.ID_ALLOCATION_FORM_TITLE, { + baseSymbol: bui.conventional.unit, + quoteSymbol: qui.conventional.unit, + host: host + }) + + populateBalancesTable(page.sellPerLot, baseID, quoteID, baseFeeID, quoteFeeID, cexName, + perSellLot.dex, perSellLot.cex, bui, qui, baseFeeUI, quoteFeeUI, host) + populateBalancesTable(page.buyPerLot, baseID, quoteID, baseFeeID, quoteFeeID, cexName, + perBuyLot.dex, perBuyLot.cex, bui, qui, baseFeeUI, quoteFeeUI, host) + populateBalancesTable(page.availableBalances, baseID, quoteID, baseFeeID, quoteFeeID, cexName, + availableFunds.dex, availableFunds.cex, bui, qui, baseFeeUI, quoteFeeUI, host) + + const baseFeeNotTraded = baseFeeID !== baseID && baseFeeID !== quoteID + const quoteFeeNotTraded = quoteFeeID !== baseID && quoteFeeID !== quoteID + Doc.setVis(baseFeeNotTraded, page.dexBaseFeeBalanceSection) + Doc.setVis(quoteFeeNotTraded, page.dexQuoteFeeBalanceSection) + Doc.setVis(baseFeeNotTraded || quoteFeeNotTraded, page.buyFeeReserveSection, page.sellFeeReserveSection) + + // Running bot balances + Doc.setVis(this.isRunning, page.runningBotBalances) + if (runStats) { + const availableDEX: Record = {} + const availableCEX: Record = {} + for (const assetID of Object.keys(runStats.dexBalances)) { + availableDEX[Number(assetID)] = runStats.dexBalances[Number(assetID)].available + } + for (const assetID of Object.keys(runStats.cexBalances)) { + availableCEX[Number(assetID)] = runStats.cexBalances[Number(assetID)].available + } + populateBalancesTable(page.runningBotBalancesTable, baseID, quoteID, baseFeeID, quoteFeeID, cexName, + availableDEX, availableCEX, bui, qui, baseFeeUI, quoteFeeUI, host) + } + + this.buyBufferInput.setValue(0) + this.buyBufferSlider.setValue(0) + this.sellBufferInput.setValue(0) + this.sellBufferSlider.setValue(0) + this.slippageBufferInput.setValue(0) + this.slippageBufferSlider.setValue(0) + this.buyFeeReserveInput.setValue(0) + this.buyFeeReserveSlider.setValue(0) + this.sellFeeReserveInput.setValue(0) + this.sellFeeReserveSlider.setValue(0) + + setMarketElements(this.form, baseID, quoteID, host) + Doc.setVis(cexName, page.rebalanceSection, page.adjustManuallyCexBalances) + if (cexName) { + const mktID = `${app().assets[baseID].symbol}_${app().assets[quoteID].symbol}` + const cexMkt = app().mmStatus.cexes[cexName].markets[mktID] + this.baseMinTransfer = cexMkt.baseMinWithdraw + this.quoteMinTransfer = cexMkt.quoteMinWithdraw + setCexElements(this.form, cexName) + page.enableRebalance.checked = true + this.enableRebalanceChanged() + } else { + page.enableRebalance.checked = false + } + + this.quickConfig() + } + + sliderMax (slider: 'buyBuffer' | 'sellBuffer' | 'slippageBuffer' | 'buyFeeReserve' | 'sellFeeReserve') : number { + switch (slider) { + case 'buyBuffer': return 3 * this.numBuyLots + case 'sellBuffer': return 3 * this.numSellLots + case 'slippageBuffer': return 100 + case 'buyFeeReserve': return 1000 + case 'sellFeeReserve': return 1000 + } + } + + sliderInput (slider: 'buyBuffer' | 'sellBuffer' | 'slippageBuffer' | 'buyFeeReserve' | 'sellFeeReserve') : NumberInput { + switch (slider) { + 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 + } + } + + slider (sliderName: 'buyBuffer' | 'sellBuffer' | 'slippageBuffer' | 'buyFeeReserve' | 'sellFeeReserve') : MiniSlider { + switch (sliderName) { + 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 + } + } + + balanceSlider (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 + } + } + + balanceInput (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 { + switch (asset) { + case 'base': return this.baseID + case 'quote': return this.quoteID + case 'baseFee': return this.baseFeeID + case 'quoteFee': return this.quoteFeeID + } + } + + maxBalance (asset: 'base' | 'quote' | 'baseFee' | 'quoteFee', location: 'dex' | 'cex') : number { + const assetID = this.assetID(asset) + if (location === 'dex') return this.availableFunds.dex[assetID] ?? 0 + return this.availableFunds.cex ? this.availableFunds.cex[assetID] ?? 0 : 0 + } + + minBalance (asset: 'base' | 'quote' | 'baseFee' | 'quoteFee', location: 'dex' | 'cex') : number { + if (!this.runStats) return 0 + const assetID = this.assetID(asset) + if (location === 'dex') { + return -this.runStats.dexBalances[assetID].available + } + return -this.runStats.cexBalances[assetID].available + } + + assetUnitInfo (asset: 'base' | 'quote' | 'baseFee' | 'quoteFee') : UnitInfo { + switch (asset) { + case 'base': return this.baseUI + case 'quote': return this.quoteUI + case 'baseFee': return this.baseFeeUI + case 'quoteFee': return this.quoteFeeUI + } + } + + minTransferSliderChanged (amt: number, asset: 'base' | 'quote') { + const max = asset === 'base' ? this.maxBaseTransfer : this.maxQuoteTransfer + const min = asset === 'base' ? this.baseMinTransfer : this.quoteMinTransfer + const input = asset === 'base' ? this.baseMinTransferInput : this.quoteMinTransferInput + const ui = asset === 'base' ? this.baseUI : this.quoteUI + const value = Math.floor((max - min) * amt + min) + console.log({ min, max, amt, value }) + input.setValue(value / ui.conventional.conversionFactor) + } + + minTransferInputChanged (amt: number, asset: 'base' | 'quote') { + const max = asset === 'base' ? this.maxBaseTransfer : this.maxQuoteTransfer + const min = asset === 'base' ? this.baseMinTransfer : this.quoteMinTransfer + const input = asset === 'base' ? this.baseMinTransferInput : this.quoteMinTransferInput + const slider = asset === 'base' ? this.baseMinTransferSlider : this.quoteMinTransferSlider + const ui = asset === 'base' ? this.baseUI : this.quoteUI + amt = amt * ui.conventional.conversionFactor + if (amt > max || amt < min) { + amt = amt > max ? max : min + input.setValue(amt / ui.conventional.conversionFactor) + } + slider.setValue((amt - min) / (max - min)) + } + + balanceSliderChanged (amt: number, asset: 'base' | 'quote' | 'baseFee' | 'quoteFee', location: 'dex' | 'cex') { + const max = this.maxBalance(asset, location) + const min = this.minBalance(asset, location) + const input = this.balanceInput(asset, location) + const ui = this.assetUnitInfo(asset) + 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.setBalanceManually(amt, asset, location) + } + + setBalanceManually (amt: number, asset: 'base' | 'quote' | 'baseFee' | 'quoteFee', location: 'dex' | 'cex') { + const assetID = this.assetID(asset) + if (location === 'dex') { + this.allocation.dex[assetID] = [amt, 'sufficient'] + } else { + this.allocation.cex[assetID] = [amt, 'sufficient'] + } + } + + balanceInputChanged (amt: number, asset: 'base' | 'quote' | 'baseFee' | 'quoteFee', location: 'dex' | 'cex') { + const max = this.maxBalance(asset, location) + const min = this.minBalance(asset, location) + amt = amt * this.assetUnitInfo(asset).conventional.conversionFactor + if (amt > max || amt < min) { + if (amt > max) amt = max + else amt = min + const input = this.balanceInput(asset, location) + const unitInfo = this.assetUnitInfo(asset) + if (input) input.setValue(amt / unitInfo.conventional.conversionFactor) + } + const slider = this.balanceSlider(asset, location) + if (slider) slider.setValue((amt - min) / (max - min)) + this.setBalanceManually(amt, asset, location) + } + + sliderChanged (amt: number, sliderName: 'buyBuffer' | 'sellBuffer' | 'slippageBuffer' | 'buyFeeReserve' | 'sellFeeReserve') { + const max = this.sliderMax(sliderName) + const input = this.sliderInput(sliderName) + input.setValue(max * amt) + this.updateAllocation() + } + + inputChanged (amt: number, sliderName: 'buyBuffer' | 'sellBuffer' | 'slippageBuffer' | 'buyFeeReserve' | 'sellFeeReserve') { + const slider = this.slider(sliderName) + const max = this.sliderMax(sliderName) + slider.setValue(amt / max) + this.updateAllocation() + } + + updateAllocation () { + const { page } = this + const slippageBuffer = this.slippageBufferInput.value() + const buyLotsBuffer = this.buyBufferInput.value() + const totalBuyLots = buyLotsBuffer + this.numBuyLots + const sellLotsBuffer = this.sellBufferInput.value() + const totalSellLots = sellLotsBuffer + this.numSellLots + const buyFeeBuffer = this.buyFeeReserveInput.value() + const sellFeeBuffer = this.sellFeeReserveInput.value() + + let toAlloc : AllocationResult + + if (this.runStats) { + toAlloc = toAllocateRunning(totalBuyLots, totalSellLots, slippageBuffer, buyFeeBuffer, sellFeeBuffer, this.perBuyLot, this.perSellLot, + this.marketReport, this.availableFunds, this.canRebalance, this.baseID, this.quoteID, this.baseFeeID, this.quoteFeeID, + this.baseIsAccountLocker, this.quoteIsAccountLocker, this.runStats, this.buyFundingFees, this.sellFundingFees) + } else { + toAlloc = toAllocate(totalBuyLots, totalSellLots, slippageBuffer, buyFeeBuffer, sellFeeBuffer, this.perBuyLot, this.perSellLot, + this.marketReport, this.availableFunds, this.canRebalance, this.baseID, this.quoteID, this.baseFeeID, this.quoteFeeID, + this.baseIsAccountLocker, this.quoteIsAccountLocker, this.buyFundingFees, this.sellFundingFees) + } + + this.allocation = toAlloc + populateColoredBalancesTable(page.toAllocateTable, this.baseID, this.quoteID, this.baseFeeID, this.quoteFeeID, this.cexName, + toAlloc, this.baseUI, this.quoteUI, this.baseFeeUI, this.quoteFeeUI, this.host) + + const assets = Array.from(new Set([this.baseID, this.baseFeeID, this.quoteID, this.quoteFeeID])) + for (const assetID of assets) { + const dexAlloc = toAlloc.dex[assetID] ? toAlloc.dex[assetID][0] : 0 + const cexAlloc = toAlloc.cex[assetID] ? toAlloc.cex[assetID][0] : 0 + if (assetID === this.baseID) { + this.baseDexBalanceInput.setValue(dexAlloc / this.baseUI.conventional.conversionFactor) + const dexMax = this.maxBalance('base', 'dex') + const dexMin = this.minBalance('base', 'dex') + this.baseDexBalanceSlider.setValue((dexAlloc - dexMin) / (dexMax - dexMin)) + this.baseCexBalanceInput.setValue(cexAlloc / this.baseUI.conventional.conversionFactor) + const cexMax = this.maxBalance('base', 'cex') + const cexMin = this.minBalance('base', 'cex') + this.baseCexBalanceSlider.setValue((cexAlloc - cexMin) / (cexMax - cexMin)) + } + if (assetID === this.quoteID) { + this.quoteDexBalanceInput.setValue(dexAlloc / this.quoteUI.conventional.conversionFactor) + const dexMax = this.maxBalance('quote', 'dex') + const dexMin = this.minBalance('quote', 'dex') + this.quoteDexBalanceSlider.setValue((dexAlloc - dexMin) / (dexMax - dexMin)) + this.quoteCexBalanceInput.setValue(cexAlloc / this.quoteUI.conventional.conversionFactor) + const cexMax = this.maxBalance('quote', 'cex') + const cexMin = this.minBalance('quote', 'cex') + this.quoteCexBalanceSlider.setValue((cexAlloc - cexMin) / (cexMax - cexMin)) + } + if (assetID === this.baseFeeID && this.baseFeeID !== this.baseID && this.baseFeeID !== this.quoteID) { + this.baseFeeBalanceInput.setValue(dexAlloc / this.baseFeeUI.conventional.conversionFactor) + const dexMax = this.maxBalance('baseFee', 'dex') + const dexMin = this.minBalance('baseFee', 'dex') + this.baseFeeBalanceSlider.setValue((dexAlloc - dexMin) / (dexMax - dexMin)) + } + if (assetID === this.quoteFeeID && this.quoteFeeID !== this.quoteID && this.quoteFeeID !== this.baseID) { + this.quoteFeeBalanceInput.setValue(dexAlloc / this.quoteFeeUI.conventional.conversionFactor) + const dexMax = this.maxBalance('quoteFee', 'dex') + const dexMin = this.minBalance('quoteFee', 'dex') + this.quoteFeeBalanceSlider.setValue((dexAlloc - dexMin) / (dexMax - dexMin)) + } + } + + if (this.cexName) { + const totalBaseAlloc = toAlloc.cex[this.baseID] ? toAlloc.cex[this.baseID][0] : 0 + const totalQuoteAlloc = toAlloc.cex[this.quoteID] ? toAlloc.cex[this.quoteID][0] : 0 + this.maxBaseTransfer = Math.max(this.baseMinTransfer * 2, totalBaseAlloc) + this.maxQuoteTransfer = Math.max(this.quoteMinTransfer * 2, totalQuoteAlloc) + this.minTransferInputChanged(this.baseMinTransferInput.value(), 'base') + this.minTransferInputChanged(this.quoteMinTransferInput.value(), 'quote') + } + } +} + const animationLength = 300 /* Swap form1 for form2 with an animation. */ @@ -2263,6 +2753,69 @@ export function bind (form: HTMLElement, submitBttn: HTMLElement, handler: (e: E Doc.bind(form, 'submit', wrapper) } +function populateBalancesTable ( + div: PageElement, baseID: number, quoteID: number, baseFeeID: number, quoteFeeID: number, cexName: string, + dexBalances: Record, cexBalances: Record | undefined, baseUI: UnitInfo, quoteUI: UnitInfo, + baseFeeUI: UnitInfo, quoteFeeUI: UnitInfo, host: string) { + const page = Doc.parseTemplate(div) + const baseFeeNotTraded = baseFeeID !== baseID && baseFeeID !== quoteID + const quoteFeeNotTraded = quoteFeeID !== quoteID && quoteFeeID !== baseID + + Doc.setVis(baseFeeNotTraded, page.baseFeeHeader, page.dexBaseFeeAlloc) + Doc.setVis(quoteFeeNotTraded, 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) +} + +export function populateColoredBalancesTable ( + 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 + } + } + + for (const [key, value] of Object.entries(allocationResult.dex)) { + const assetID = Number(key) + if (assetID === baseID) setColor(page.dexBaseAlloc, value[1]) + if (assetID === quoteID) setColor(page.dexQuoteAlloc, value[1]) + if (assetID === baseFeeID) setColor(page.dexBaseFeeAlloc, value[1]) + if (assetID === quoteFeeID) setColor(page.dexQuoteFeeAlloc, value[1]) + dexBalances[assetID] = value[0] + } + for (const [key, value] of Object.entries(allocationResult.cex)) { + const assetID = Number(key) + if (assetID === baseID) setColor(page.cexBaseAlloc, value[1]) + if (assetID === quoteID) setColor(page.cexQuoteAlloc, value[1]) + cexBalances[assetID] = value[0] + } + populateBalancesTable(div, baseID, quoteID, baseFeeID, quoteFeeID, cexName, + dexBalances, cexBalances, baseUI, quoteUI, baseFeeUI, quoteFeeUI, host) +} + // isTruthyString will be true if the provided string is recognized as a // value representing true. function isTruthyString (s: string) { diff --git a/client/webserver/site/src/js/locales.ts b/client/webserver/site/src/js/locales.ts index ff7b927234..21ee493ce4 100644 --- a/client/webserver/site/src/js/locales.ts +++ b/client/webserver/site/src/js/locales.ts @@ -219,6 +219,7 @@ export const ID_CEX_BALANCES = 'CEX_BALANCES' export const ID_CAUSES_SELF_MATCH = 'CAUSES_SELF_MATCH' export const ID_CEX_NOT_CONNECTED = 'CEX_NOT_CONNECTED' export const ID_DELETE_BOT = 'DELETE_BOT' +export const ID_ALLOCATION_FORM_TITLE = 'ALLOCATION_FORM_TITLE' let locale: Locale diff --git a/client/webserver/site/src/js/markets.ts b/client/webserver/site/src/js/markets.ts index 48d8b74e89..865622fa0a 100644 --- a/client/webserver/site/src/js/markets.ts +++ b/client/webserver/site/src/js/markets.ts @@ -286,7 +286,7 @@ export default class MarketsPage extends BasePage { placementAmtRowTmpl: page.placementAmtRowTmpl } Doc.cleanTemplates(page.dexBalancesRowTmpl, page.placementRowTmpl, page.placementAmtRowTmpl) - this.mm = new RunningMarketMakerDisplay(page.mmRunning, this.forms, runningMMDisplayElements, 'markets') + this.mm = new RunningMarketMakerDisplay(page.mmRunning, this.forms, runningMMDisplayElements, 'markets', () => { console.log('allocate') }) this.reputationMeter = new ReputationMeter(page.reputationMeter) diff --git a/client/webserver/site/src/js/mm.ts b/client/webserver/site/src/js/mm.ts index 08b2c76a48..de87303720 100644 --- a/client/webserver/site/src/js/mm.ts +++ b/client/webserver/site/src/js/mm.ts @@ -10,7 +10,8 @@ import { CEXNotification, EpochReportNote, CEXProblemsNote, - MarketWithHost + MarketWithHost, + BotBalanceAllocation } from './registry' import { MM, @@ -25,148 +26,19 @@ import { BotMarket, hostedMarketID, RunningMarketMakerDisplay, - RunningMMDisplayElements + RunningMMDisplayElements, + PerLot, + AvailableFunds, + AllocationResult, + requiredFunds } 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 { Forms, CEXConfigurationForm, AllocationForm } 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 @@ -186,6 +58,7 @@ export default class MarketMakerPage extends BasePage { twoColumn: boolean runningMMDisplayElements: RunningMMDisplayElements removingCfg: MarketWithHost | undefined + allocationForm: AllocationForm constructor (main: HTMLElement) { super() @@ -200,6 +73,8 @@ export default class MarketMakerPage extends BasePage { this.forms = new Forms(page.forms) this.cexConfigForm = new CEXConfigurationForm(page.cexConfigForm, (cexName: string, success: boolean) => this.cexConfigured(cexName, success)) + this.allocationForm = new AllocationForm(page.allocationForm, + (allocation: AllocationResult, running: boolean, autoRebalance: AutoRebalanceConfig | undefined) => this.allocationSubmit(allocation, running, autoRebalance)) this.runningMMDisplayElements = { orderReportForm: page.orderReportForm, dexBalancesRowTmpl: page.dexBalancesRowTmpl, @@ -211,6 +86,7 @@ export default class MarketMakerPage extends BasePage { Doc.bind(page.newBot, 'click', () => { this.newBot() }) Doc.bind(page.archivedLogsBtn, 'click', () => { app().loadPage('mmarchives') }) Doc.bind(page.confirmRemoveConfigBttn, 'click', () => { this.removeCfg() }) + Doc.bind(page.allocationForm, 'submit', (e: Event) => { e.preventDefault() }) this.twoColumn = window.innerWidth >= mediumBreakpoint const ro = new ResizeObserver(() => { this.resized() }) @@ -381,6 +257,21 @@ export default class MarketMakerPage extends BasePage { if (success) this.forms.close() } + async allocationSubmit (allocation: AllocationResult, running: boolean, autoRebalance: AutoRebalanceConfig | undefined) { + const bot = this.bots[this.allocationForm.botID] + if (!bot) return + if (running) { + try { + await bot.updateInventory(allocation) + } catch (e) { + this.page.allocationErr.textContent = intl.prep(intl.ID_API_ERROR, { msg: e.msg }) + Doc.show(this.page.allocationErr) + return + } + this.forms.close() + } else bot.allocationSubmit(allocation, autoRebalance) + } + updateCexRow (row: CEXRow) { const { tmpl, dinfo, cexName } = row tmpl.logo.src = dinfo.logo @@ -437,8 +328,6 @@ class Bot extends BotMarket { div: PageElement page: Record placementsChart: PlacementsChart - baseAllocSlider: MiniSlider - quoteAllocSlider: MiniSlider row: BotRow runDisplay: RunningMarketMakerDisplay @@ -451,7 +340,7 @@ class Bot extends BotMarket { const div = this.div = pg.page.botTmpl.cloneNode(true) as PageElement const page = this.page = Doc.parseTemplate(div) - this.runDisplay = new RunningMarketMakerDisplay(page.onBox, pg.forms, runningMMElements, 'mm') + this.runDisplay = new RunningMarketMakerDisplay(page.onBox, pg.forms, runningMMElements, 'mm', () => { this.allocate() }) setMarketElements(div, baseID, quoteID, host) if (cexName) setCexElements(div, cexName) @@ -464,23 +353,18 @@ class Bot extends BotMarket { page.botTypeDisplay.textContent = intl.prep(intl.ID_BOTTYPE_BASIC_MM) } - Doc.setVis(botType !== botTypeBasicArb, page.placementsChartBox, page.baseTokenSwapFeesBox) + Doc.setVis(botType !== botTypeBasicArb, page.placementsChartBox) 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) @@ -589,76 +473,86 @@ class Bot extends BotMarket { 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 + page, baseID, quoteID, bui, qui, baseFactor, quoteFactor, baseFeeID, quoteFeeID, cexName, + marketReport: { baseFiatRate, quoteFiatRate }, baseFeeFiatRate, quoteFeeFiatRate } = this - page.baseAlloc.textContent = Doc.formatFullPrecision(alloc[baseID], bui) - const baseUSD = alloc[baseID] / baseFactor * baseFiatRate + const { perBuyLot, perSellLot } = this.perLotRequirements() + + const minAllocation = requiredFunds(this.buyLots, this.sellLots, 0, 0, 0, perBuyLot, perSellLot, this.marketReport, + this.baseIsAccountLocker, this.quoteIsAccountLocker, this.baseID, this.quoteID, this.baseFeeID, this.quoteFeeID, + 0, 0) // TODO: update funding fees + + const totalBase = minAllocation.dex[baseID][0] + minAllocation.cex[baseID][0] + page.baseAlloc.textContent = Doc.formatFullPrecision(totalBase, bui) + const baseUSD = totalBase / 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) + page.baseDexAlloc.textContent = Doc.formatFullPrecision(minAllocation.dex[baseID][0], bui) + Doc.setVis(this.cexName, page.baseCexAllocBox) + if (cexName) { + page.baseCexAlloc.textContent = Doc.formatFullPrecision(minAllocation.cex[baseID][0], bui) } - page.quoteAlloc.textContent = Doc.formatFullPrecision(alloc[quoteID], qui) - const quoteUSD = alloc[quoteID] / quoteFactor * quoteFiatRate + const totalQuote = minAllocation.dex[quoteID][0] + minAllocation.cex[quoteID][0] + page.quoteAlloc.textContent = Doc.formatFullPrecision(totalQuote, qui) + const quoteUSD = totalQuote / 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)) + page.quoteDexAlloc.textContent = Doc.formatFullPrecision(minAllocation.dex[quoteID][0], qui) 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) + if (cexName) { + page.quoteCexAlloc.textContent = Doc.formatFullPrecision(minAllocation.cex[quoteID][0], qui) } + + Doc.setVis(baseFeeID !== baseID && baseFeeID !== quoteID, page.baseFeeDexAllocBox) + if (baseFeeID !== baseID && baseFeeID !== quoteID) { + const baseFeeDexAlloc = minAllocation.dex[baseFeeID][0] + page.baseFeeDexAlloc.textContent = Doc.formatFullPrecision(baseFeeDexAlloc, this.baseFeeUI) + page.baseTokenFeeAlloc.textContent = Doc.formatFullPrecision(baseFeeDexAlloc, this.baseFeeUI) + totalUSD += baseFeeDexAlloc / this.baseFeeFactor * baseFeeFiatRate + page.baseTokenAllocUSD.textContent = Doc.formatFourSigFigs(baseFeeDexAlloc / this.baseFeeFactor * baseFeeFiatRate) + } + + Doc.setVis(quoteFeeID !== baseID && quoteFeeID !== quoteID, page.quoteFeeDexAllocBox) + if (quoteFeeID !== baseID && quoteFeeID !== quoteID) { + const quoteFeeDexAlloc = minAllocation.dex[quoteFeeID][0] + page.quoteFeeDexAlloc.textContent = Doc.formatFullPrecision(quoteFeeDexAlloc, this.quoteFeeUI) + page.quoteTokenFeeAlloc.textContent = Doc.formatFullPrecision(quoteFeeDexAlloc, this.quoteFeeUI) + totalUSD += quoteFeeDexAlloc / this.quoteFeeFactor * quoteFeeFiatRate + page.quoteTokenAllocUSD.textContent = Doc.formatFourSigFigs(quoteFeeDexAlloc / this.quoteFeeFactor * quoteFeeFiatRate) + } + page.totalAllocUSD.textContent = Doc.formatFourSigFigs(totalUSD) } + perLotRequirements (): { perSellLot: PerLot, perBuyLot: PerLot } { + const { baseID, quoteID, baseFeeID, quoteFeeID, lotSize, quoteLot, marketReport, baseIsAccountLocker, quoteIsAccountLocker } = this + + const perSellLot: PerLot = { cex: {}, dex: {} } + perSellLot.dex[baseID] = lotSize + perSellLot.dex[baseFeeID] = (perSellLot.dex[baseFeeID] ?? 0) + marketReport.baseFees.max.swap + perSellLot.cex[quoteID] = quoteLot + if (baseIsAccountLocker) perSellLot.dex[baseFeeID] = (perSellLot.dex[baseFeeID] ?? 0) + marketReport.baseFees.max.refund + if (quoteIsAccountLocker) perSellLot.dex[quoteFeeID] = (perSellLot.dex[quoteFeeID] ?? 0) + marketReport.quoteFees.max.redeem + + const perBuyLot: PerLot = { cex: {}, dex: {} } + perBuyLot.dex[quoteID] = quoteLot + perBuyLot.dex[quoteFeeID] = (perBuyLot.dex[quoteFeeID] ?? 0) + marketReport.quoteFees.max.swap + perBuyLot.cex[baseID] = lotSize + if (baseIsAccountLocker) perBuyLot.dex[baseFeeID] = (perBuyLot.dex[baseFeeID] ?? 0) + marketReport.baseFees.max.redeem + if (quoteIsAccountLocker) perBuyLot.dex[quoteFeeID] = (perBuyLot.dex[quoteFeeID] ?? 0) + marketReport.quoteFees.max.refund + + return { perSellLot, perBuyLot } + } + /* * allocate opens a dialog to choose funding sources (if applicable) and * confirm allocations and start the bot. */ - allocate () { + async allocate () { const { - page, marketReport: { baseFiatRate, quoteFiatRate }, baseID, quoteID, - baseFeeID, quoteFeeID, baseFeeFiatRate, quoteFeeFiatRate, cexName, - baseFactor, quoteFactor, baseFeeFactor, quoteFeeFactor, host, mktID + baseID, quoteID, host, cexName, bui, qui, baseFeeID, quoteFeeID, marketReport, page, + quoteFeeUI, baseFeeUI, lotSize, quoteLot, baseIsAccountLocker, quoteIsAccountLocker } = this if (cexName) { @@ -670,135 +564,59 @@ class Bot extends BotMarket { } } - 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 - } + const assetIDs = Array.from(new Set([baseID, quoteID, baseFeeID, quoteFeeID])) + const availableFundsRes = await MM.availableBalances({ host, baseID, quoteID }, cexName) + const availableFunds: AvailableFunds = { dex: {}, cex: {} } + for (const assetID of assetIDs) { + const dexBal = availableFundsRes.dexBalances[assetID] ?? 0 + const cexBal = availableFundsRes.cexBalances[assetID] ?? 0 + availableFunds.dex[assetID] = dexBal + if (availableFunds.cex) availableFunds.cex[assetID] = cexBal } - 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 maxFundingFeesRes = await MM.maxFundingFees({ host, baseID, quoteID }) + const { buyFees: buyFundingFees, sellFees: sellFundingFees } = maxFundingFeesRes - 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) - } - } + const canRebalance = Boolean(cexName) - 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 { runStats } = this.status() - 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) - } + this.pg.allocationForm.init(baseID, quoteID, host, baseFeeID, quoteFeeID, + cexName, bui, qui, baseFeeUI, quoteFeeUI, marketReport, lotSize, quoteLot, + baseIsAccountLocker, quoteIsAccountLocker, availableFunds, canRebalance, this.id, this.sellLots, this.buyLots, + buyFundingFees, sellFundingFees, runStats) - 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 - } - } - } - Doc.setVis(existingOrders, page.existingOrdersBox) + this.pg.forms.show(this.pg.page.allocationForm) + } - Doc.show(page.allocationDialog) - const closeDialog = (e: MouseEvent) => { - if (Doc.mouseInElement(e, page.allocationDialog)) return - this.hideAllocationDialog() - Doc.unbind(document, 'click', closeDialog) + allocationSubmit (allocation: AllocationResult, autoRebalance: AutoRebalanceConfig | undefined) { + const botBalanceAllocation: BotBalanceAllocation = { dex: {}, cex: {} } + const assetIDs = Array.from(new Set([this.baseID, this.quoteID, this.baseFeeID, this.quoteFeeID])) + for (const assetID of assetIDs) { + botBalanceAllocation.dex[assetID] = allocation.dex[assetID] ? allocation.dex[assetID][0] : 0 + botBalanceAllocation.cex[assetID] = allocation.cex[assetID] ? allocation.cex[assetID][0] : 0 } - Doc.bind(document, 'click', closeDialog) + this.start(botBalanceAllocation, autoRebalance) } - hideAllocationDialog () { - Doc.hide(this.page.allocationDialog) + async updateInventory (allocation: AllocationResult) { + const botBalanceAllocation: BotBalanceAllocation = { dex: {}, cex: {} } + const assetIDs = Array.from(new Set([this.baseID, this.quoteID, this.baseFeeID, this.quoteFeeID])) + for (const assetID of assetIDs) { + botBalanceAllocation.dex[assetID] = allocation.dex[assetID] ? allocation.dex[assetID][0] : 0 + botBalanceAllocation.cex[assetID] = allocation.cex[assetID] ? allocation.cex[assetID][0] : 0 + } + const res = await MM.updateBotInventory({ host: this.host, baseID: this.baseID, quoteID: this.quoteID }, botBalanceAllocation) + if (!app().checkResponse(res)) throw res } - async start () { - const { page, alloc, baseID, quoteID, host, cexName, cfg: { uiConfig: { cexRebalance } } } = this + async start (alloc: BotBalanceAllocation, autoRebalance: AutoRebalanceConfig | undefined) { + const { baseID, quoteID, host, cexName } = this - Doc.hide(page.errMsg) + Doc.hide(this.pg.page.allocationErrMsg) if (cexName && !app().mmStatus.cexes[cexName]?.connected) { - page.errMsg.textContent = `${cexName} not connected` - Doc.show(page.errMsg) + this.pg.page.allocationErrMsg.textContent = `${cexName} not connected` + Doc.show(this.pg.page.allocationErrMsg) return } @@ -813,22 +631,23 @@ class Bot extends BotMarket { host: host, alloc: alloc } - if (cexName && cexRebalance) startConfig.autoRebalance = this.autoRebalanceSettings() + if (cexName) startConfig.autoRebalance = autoRebalance 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) + this.pg.page.allocationErrMsg.textContent = intl.prep(intl.ID_API_ERROR, e) + Doc.show(this.pg.page.allocationErrMsg) return } - this.hideAllocationDialog() + + this.pg.forms.close() } autoRebalanceSettings (): AutoRebalanceConfig { - const { + /* const { proj: { bProj, qProj, alloc }, baseFeeID, quoteFeeID, cfg: { uiConfig: { baseConfig, quoteConfig } }, baseID, quoteID, cexName, mktID } = this @@ -851,8 +670,8 @@ class Bot extends BotMarket { 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 } + const minQuoteTransfer = Math.round(minQ + quoteConfig.transferFactor * (maxQ - minQ)) */ + return { minBaseTransfer: 0, minQuoteTransfer: 0 } } reconfigure () { diff --git a/client/webserver/site/src/js/mmsettings.ts b/client/webserver/site/src/js/mmsettings.ts index 0a5e0f44e5..78e6e297dc 100644 --- a/client/webserver/site/src/js/mmsettings.ts +++ b/client/webserver/site/src/js/mmsettings.ts @@ -15,13 +15,9 @@ import { MarketMakingStatus, MMCEXStatus, BalanceNote, - BotAssetConfig, ApprovalStatus, SupportedAsset, - WalletState, - UnitInfo, - ProjectedAlloc, - AssetBookingFees + WalletState } from './registry' import Doc, { NumberInput, @@ -51,9 +47,10 @@ import { GapStrategyAbsolutePlus, GapStrategyPercent, GapStrategyPercentPlus, - feesAndCommit + toAllocate, + perLotRequirements } from './mmutil' -import { Forms, bind as bindForm, NewWalletForm, TokenApprovalForm, DepositAddress, CEXConfigurationForm } from './forms' +import { Forms, bind as bindForm, NewWalletForm, TokenApprovalForm, DepositAddress, CEXConfigurationForm, populateColoredBalancesTable } from './forms' import * as intl from './locales' import * as OrderUtil from './orderutil' @@ -62,34 +59,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, @@ -147,18 +116,9 @@ const defaultMarketMakingConfig: ConfigState = { 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,15 +138,10 @@ interface ConfigState { profit: number driftTolerance: number orderPersistence: number // epochs - cexRebalance: boolean - disabled: boolean buyPlacements: OrderPlacement[] sellPlacements: OrderPlacement[] baseOptions: Record quoteOptions: Record - baseConfig: BotAssetConfig - quoteConfig: BotAssetConfig - simpleArbLots: number } interface BotSpecs { @@ -216,6 +171,7 @@ export default class MarketMakerSettingsPage extends BasePage { page: Record forms: Forms opts: UIOpts + runningBot: boolean newWalletForm: NewWalletForm approveTokenForm: TokenApprovalForm walletAddrForm: DepositAddress @@ -245,12 +201,14 @@ 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 constructor (main: HTMLElement, specs: BotSpecs) { super() @@ -270,19 +228,18 @@ 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.quoteSettings = new WalletSettings(this, page.quoteSettings) 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() }) @@ -294,7 +251,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.switchToAdvanced, 'click', () => { this.showAdvancedConfig() }) Doc.bind(page.switchToQuickConfig, 'click', () => { this.switchToQuickConfig() }) Doc.bind(page.qcMatchBuffer, 'change', () => { this.matchBufferChanged() }) @@ -571,41 +527,45 @@ export default class MarketMakerSettingsPage extends BasePage { return } + const availableBalances = await MM.availableBalances({ host: specs.host, baseID: specs.baseID, quoteID: specs.quoteID }, specs.cexName) + this.availableDEXBalances = availableBalances.dexBalances + this.availableCEXBalances = availableBalances.cexBalances + Doc.show(page.marketLoading) State.storeLocal(specLK, specs) const mmStatus = app().mmStatus - const viewOnly = isViewOnly(specs, mmStatus) + const viewOnly = this.runningBot = isViewOnly(specs, mmStatus) 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') + } 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) + sellPlacements: [] }) as ConfigState if (botCfg) { - const { basicMarketMakingConfig: mmCfg, arbMarketMakingConfig: arbMMCfg, simpleArbConfig: arbCfg, uiConfig: { cexRebalance } } = 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 = cexRebalance if (mmCfg) { oldCfg.buyPlacements = mmCfg.buyPlacements @@ -623,12 +583,11 @@ 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.updateButton, page.resetButton) } else { this.creatingNewBot = true - Doc.setVis(!viewOnly, page.createButton) + Doc.show(page.createButton) } // Now that we've updated the originalConfig, we'll copy it. @@ -646,11 +605,10 @@ 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.driftToleranceBox, page.switchToAdvanced, page.qcTitle) Doc.setVis(Boolean(cexName), ...Doc.applySelector(document.body, '[data-cex-show]')) Doc.setVis(viewOnly, page.viewOnlyRunning) - Doc.setVis(cexName, page.cexRebalanceSettings) if (cexName) setCexElements(document.body, cexName) await this.fetchMarketReport() @@ -661,8 +619,8 @@ 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) + // 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 }) @@ -675,6 +633,9 @@ export default class MarketMakerSettingsPage extends BasePage { this.setOriginalValues() + this.baseSettings.init(this.updatedConfig.baseOptions, this.specs.baseID, true) + this.quoteSettings.init(this.updatedConfig.quoteOptions, this.specs.quoteID, false) + Doc.hide(page.marketLoading) Doc.show(page.botSettingsContainer, page.marketBox) } @@ -762,8 +723,8 @@ export default class MarketMakerSettingsPage extends BasePage { */ 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] @@ -780,25 +741,26 @@ export default class MarketMakerSettingsPage extends BasePage { spot } - let [dexBaseLots, dexQuoteLots] = [cfg.simpleArbLots, cfg.simpleArbLots] + let [sellLots, buyLots] = [1, 1] 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) } 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 - ) + const { perBuyLot, perSellLot } = perLotRequirements(baseID, quoteID, baseFeeAssetID, quoteFeeAssetID, lotSize, quoteLot, + this.marketReport, baseIsAccountLocker, quoteIsAccountLocker) + + const availableFunds = { dex: this.availableDEXBalances, cex: this.availableCEXBalances } + const minAllocation = toAllocate(buyLots, sellLots, 0, 0, 0, perBuyLot, perSellLot, this.marketReport, availableFunds, false, baseID, quoteID, + baseFeeAssetID, quoteFeeAssetID, baseIsAccountLocker, quoteIsAccountLocker, 0, 0) 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 + quoteLot, minAllocation, ...walletStuff } } @@ -867,7 +829,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() @@ -900,25 +862,28 @@ 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) + // Doc.show(page.arbLotsLabel) } } @@ -960,7 +925,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) @@ -981,22 +945,10 @@ export default class MarketMakerSettingsPage extends BasePage { } 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() + const { page } = this + const { minAllocation, baseID, quoteID, baseFeeAssetID, quoteFeeAssetID, cexName, bui, qui, baseFeeUI, quoteFeeUI, host } = this.marketStuff() + populateColoredBalancesTable(page.minAllocationTable, baseID, quoteID, baseFeeAssetID, quoteFeeAssetID, cexName || '', + minAllocation, bui, qui, baseFeeUI, quoteFeeUI, host) } matchBufferChanged () { @@ -1100,7 +1052,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) } @@ -1141,6 +1093,7 @@ export default class MarketMakerSettingsPage extends BasePage { } showMarketSelectForm () { + if (this.runningBot) return this.page.marketFilterInput.value = '' this.sortMarketRows() this.forms.show(this.page.marketSelectForm) @@ -1157,21 +1110,15 @@ export default class MarketMakerSettingsPage extends BasePage { } } - handleBalanceNote (n: BalanceNote) { - this.approveTokenForm.handleBalanceNote(n) + handleBalanceNote (_: BalanceNote) { + /* this.approveTokenForm.handleBalanceNote(n) 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() - } - } - - autoRebalanceChanged () { - const { page, updatedConfig: cfg } = this - cfg.cexRebalance = page.cexRebalanceCheckbox?.checked ?? false - this.updateAllocations() + } */ } async submitBotType () { @@ -1619,11 +1566,6 @@ export default class MarketMakerSettingsPage extends BasePage { this.qcProfit.setValue(profit * 100) this.qcProfitSlider.setValue((profit - defaultProfit.minV) / defaultProfit.range) - if (cexName) { - page.cexRebalanceCheckbox.checked = cfg.cexRebalance - this.autoRebalanceChanged() - } - // Gap strategy if (!page.gapStrategySelect.options) return Array.from(page.gapStrategySelect.options).forEach((opt: HTMLOptionElement) => { opt.selected = opt.value === cfg.gapStrategy }) @@ -1643,8 +1585,8 @@ 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() + // this.basePane.setupWalletSettings() + // this.quotePane.setupWalletSettings() this.updateModifiedMarkers() if (Doc.isDisplayed(page.quickConfig)) this.switchToQuickConfig() @@ -1697,12 +1639,6 @@ 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 - }, baseWalletOptions: cfg.baseOptions, quoteWalletOptions: cfg.quoteOptions } @@ -1718,7 +1654,10 @@ export default class MarketMakerSettingsPage extends BasePage { } app().log('mm', 'saving bot config', botCfg) - await MM.updateBotConfig(botCfg) + + if (this.runningBot) await MM.updateRunningBotConfig(botCfg) + else await MM.updateBotConfig(botCfg) + await app().fetchMMStatus() this.originalConfig = JSON.parse(JSON.stringify(cfg)) this.updateModifiedMarkers() @@ -1988,7 +1927,99 @@ function tokenAssetApprovalStatuses (host: string, b: SupportedAsset, q: Support ] } -class AssetPane { +class WalletSettings { + pg: MarketMakerSettingsPage + div: PageElement + page: Record + + constructor (pg: MarketMakerSettingsPage, div: PageElement) { + this.pg = pg + this.div = div + this.page = Doc.parseTemplate(div) + } + + 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 = {} + const addDependentOpt = (optKey: string, optSetting: PageElement, dependentOn: string) => { + if (!dependentOpts[dependentOn]) dependentOpts[dependentOn] = [] + dependentOpts[dependentOn].push(optKey) + optToDiv[optKey] = optSetting + } + const setDependentOptsVis = (parentOptKey: string, vis: boolean) => { + const optKeys = dependentOpts[parentOptKey] + if (!optKeys) return + for (const optKey of optKeys) Doc.setVis(vis, optToDiv[optKey]) + } + const addOpt = (opt: OrderOption) => { + if (opt.quoteAssetOnly && !isQuote) return + const currVal = walletConfig[opt.key] + let div: PageElement | undefined + if (opt.isboolean) { + div = page.boolSettingTmpl.cloneNode(true) as PageElement + const tmpl = Doc.parseTemplate(div) + tmpl.name.textContent = opt.displayname + tmpl.input.checked = currVal === 'true' + Doc.bind(tmpl.input, 'change', () => { + walletConfig[opt.key] = tmpl.input.checked ? 'true' : 'false' + setDependentOptsVis(opt.key, Boolean(tmpl.input.checked)) + }) + if (opt.description) tmpl.tooltip.dataset.tooltip = opt.description + } else if (opt.xyRange) { + const { start, end, xUnit } = opt.xyRange + const range = end.x - start.x + div = page.rangeSettingTmpl.cloneNode(true) as PageElement + const tmpl = Doc.parseTemplate(div) + tmpl.name.textContent = opt.displayname + if (opt.description) tmpl.tooltip.dataset.tooltip = opt.description + if (xUnit) tmpl.unit.textContent = xUnit + else Doc.hide(tmpl.unit) + + const input = new NumberInput(tmpl.value, { + prec: 1, + changed: (rawV: number) => { + const [v, s] = toFourSigFigs(rawV, 1) + walletConfig[opt.key] = s + slider.setValue((v - start.x) / range) + } + }) + const slider = new MiniSlider(tmpl.slider, (r: number) => { + const rawV = start.x + r * range + const [v, s] = toFourSigFigs(rawV, 1) + walletConfig[opt.key] = s + input.setValue(v) + }) + // TODO: default value should be smaller or none for base asset. + const [v, s] = toFourSigFigs(parseFloatDefault(currVal, start.x), 3) + walletConfig[opt.key] = s + slider.setValue((v - start.x) / range) + input.setValue(v) + tmpl.value.textContent = s + } + if (!div) return console.error("don't know how to handle opt", opt) + page.walletSettings.appendChild(div) + if (opt.dependsOn) { + addDependentOpt(opt.key, div, opt.dependsOn) + const parentOptVal = walletConfig[opt.dependsOn] + Doc.setVis(parentOptVal === 'true', div) + } + } + if (walletSettings.multifundingopts && walletSettings.multifundingopts.length > 0) { + for (const opt of walletSettings.multifundingopts) addOpt(opt) + } + app().bindTooltips(page.walletSettings) + } +} + +/* class AssetPane { pg: MarketMakerSettingsPage div: PageElement page: Record @@ -2027,7 +2058,6 @@ class AssetPane { this.pg.updateAllocations() } }) - this.nSwapFeesSlider = new MiniSlider(page.nSwapFeesSlider, (r: number) => { const { minR, range, prec } = defaultSwapReserves const [v] = toPrecision(minR + r * range, prec) @@ -2320,4 +2350,4 @@ class AssetPane { const { balance: { available: feeAvail } } = app().walletMap[feeAssetID] page.feeAvail.textContent = Doc.formatFourSigFigs(feeAvail / feeUI.conventional.conversionFactor) } -} +} */ diff --git a/client/webserver/site/src/js/mmutil.ts b/client/webserver/site/src/js/mmutil.ts index f3d951bf26..a258e6dfcf 100644 --- a/client/webserver/site/src/js/mmutil.ts +++ b/client/webserver/site/src/js/mmutil.ts @@ -64,6 +64,11 @@ export const CEXDisplayInfos: Record = { } } +export interface PerLot { + cex: Record + dex: Record +} + /* * MarketMakerBot is the front end representation of the server's * mm.MarketMaker. MarketMakerBot is a singleton assigned to MM below. @@ -78,6 +83,20 @@ class MarketMakerBot { return postJSON('/api/updatebotconfig', cfg) } + /* + * updateBotInventory updates the inventory of a running bot. + */ + async updateBotInventory (market: MarketWithHost, diffs: BotBalanceAllocation) { + return postJSON('/api/updatebotinventory', { market, diffs }) + } + + /* + * updateRunningBotConfig updates the BotConfig for a running bot. + */ + async updateRunningBotConfig (cfg: BotConfig) { + return postJSON('/api/updaterunningbotconfig', cfg) + } + /* * updateCEXConfig appends or updates the specified CEXConfig. */ @@ -93,6 +112,10 @@ class MarketMakerBot { return postJSON('/api/marketreport', { host, baseID, quoteID }) } + async maxFundingFees (market: MarketWithHost) { + return postJSON('/api/maxfundingfees', { market }) + } + async startBot (config: StartConfig) { return await postJSON('/api/startmarketmakingbot', { config }) } @@ -127,6 +150,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. @@ -373,10 +400,6 @@ interface AllocationProjection { 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 @@ -420,11 +443,13 @@ export class BotMarket { rateStep: number baseFeeFiatRate: number quoteFeeFiatRate: number - baseLots: number - quoteLots: number + sellLots: number + buyLots: number marketReport: MarketReport nBuyPlacements: number nSellPlacements: number + nBuyLots: number + nSellLots: number constructor (cfg: BotConfig) { const host = this.host = cfg.host @@ -480,18 +505,18 @@ 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.sellLots = cfg.arbMarketMakingConfig.sellPlacements.reduce(sumLots, 0) + this.buyLots = 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 + this.sellLots = 1 + this.buyLots = 1 } else if (cfg.basicMarketMakingConfig) { // basicmm this.botType = botTypeBasicMM - this.baseLots = cfg.basicMarketMakingConfig.sellPlacements.reduce(sumLots, 0) - this.quoteLots = cfg.basicMarketMakingConfig.buyPlacements.reduce(sumLots, 0) + this.sellLots = cfg.basicMarketMakingConfig.sellPlacements.reduce(sumLots, 0) + this.buyLots = cfg.basicMarketMakingConfig.buyPlacements.reduce(sumLots, 0) this.nBuyPlacements = cfg.basicMarketMakingConfig.buyPlacements.length this.nSellPlacements = cfg.basicMarketMakingConfig.sellPlacements.length } @@ -503,7 +528,7 @@ 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() + // this.proj = this.projectedAllocations() } status () { @@ -583,11 +608,10 @@ export class BotMarket { * values do not include booking fees, order reserves, etc. just the order * quantity. */ - feesAndCommit () { + /* feesAndCommit () { const { baseID, quoteID, marketReport: { baseFees, quoteFees }, lotSize, - baseLots, quoteLots, baseFeeID, quoteFeeID, baseIsAccountLocker, quoteIsAccountLocker, - cfg: { uiConfig: { baseConfig, quoteConfig } } + baseLots, quoteLots, baseFeeID, quoteFeeID, baseIsAccountLocker, quoteIsAccountLocker } = this return feesAndCommit( @@ -595,13 +619,13 @@ export class BotMarket { 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 () { + /* projectedAllocations () { const { cfg: { uiConfig: { quoteConfig, baseConfig } }, baseFactor, quoteFactor, baseID, quoteID, lotSizeConv, quoteLotConv, @@ -639,14 +663,14 @@ export class BotMarket { 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 () { + /* fundingState () { const { proj: { bProj, qProj }, baseID, quoteID, baseFeeID, quoteFeeID, cfg: { uiConfig: { cexRebalance } }, cexName @@ -766,7 +790,7 @@ export class BotMarket { fundedAndNotBalanced, starved: !fundedAndBalanced && !fundedAndNotBalanced } - } + } */ } export type RunningMMDisplayElements = { @@ -793,7 +817,7 @@ export class RunningMarketMakerDisplay { placementRowTmpl: PageElement placementAmtRowTmpl: PageElement - constructor (div: PageElement, forms: Forms, elements: RunningMMDisplayElements, page: string) { + constructor (div: PageElement, forms: Forms, elements: RunningMMDisplayElements, page: string, adjustBalances: () => void) { this.div = div this.page = Doc.parseTemplate(div) this.orderReportFormEl = elements.orderReportForm @@ -808,6 +832,11 @@ 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.adjustBalanceBttn, 'click', () => adjustBalances()) Doc.bind(this.page.buyOrdersBttn, 'click', () => this.showOrderReport('buys')) Doc.bind(this.page.sellOrdersBttn, 'click', () => this.showOrderReport('sells')) } @@ -1312,6 +1341,234 @@ export function feesAndCommit ( return { commit, fees } } +export type AllocationStatus = 'sufficient' | 'insufficient' | 'sufficient-with-rebalance' +export type AllocationResult = { + dex: Record + cex: Record +} +export type AvailableFunds = { + dex: Record + cex?: Record +} + +export function requiredFunds ( + numBuyLots: number, numSellLots: number, slippageBuffer: number, buyFeeBuffer: number, + sellFeeBuffer: number, perBuyLot: PerLot, perSellLot: PerLot, marketReport: MarketReport, + baseIsAccountLocker: boolean, quoteIsAccountLocker: boolean, baseID: number, + quoteID: number, baseFeeID: number, quoteFeeID: number, buyFundingFees: number, sellFundingFees: 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] = [0, 'sufficient'] + toAllocate.cex[assetID] = [0, 'sufficient'] + } + + // For each asset, calculate total allocation needed + for (const assetID of assetIDs) { + // Buy lots + if (perBuyLot.cex[assetID]) { + toAllocate.cex[assetID][0] += perBuyLot.cex[assetID] * numBuyLots + } + if (perBuyLot.dex[assetID]) { + toAllocate.dex[assetID][0] += perBuyLot.dex[assetID] * numBuyLots + } + + // Sell lots + if (perSellLot.cex[assetID]) { + toAllocate.cex[assetID][0] += perSellLot.cex[assetID] * numSellLots + } + if (perSellLot.dex[assetID]) { + toAllocate.dex[assetID][0] += perSellLot.dex[assetID] * numSellLots + } + + // Apply slippage buffer to quote asset allocations + if (assetID === quoteID) { + toAllocate.cex[assetID][0] *= (1 + (slippageBuffer / 100)) + toAllocate.dex[assetID][0] *= (1 + (slippageBuffer / 100)) + } + + if (assetID === baseFeeID) { + toAllocate.dex[baseFeeID][0] += sellFeeBuffer * marketReport.baseFees.max.swap + toAllocate.dex[baseFeeID][0] += sellFundingFees + if (baseIsAccountLocker) { + toAllocate.dex[baseFeeID][0] += buyFeeBuffer * marketReport.baseFees.max.redeem + toAllocate.dex[baseFeeID][0] += sellFeeBuffer * marketReport.baseFees.max.refund + } + } + + if (assetID === quoteFeeID) { + toAllocate.dex[quoteFeeID][0] += buyFeeBuffer * marketReport.quoteFees.max.swap + toAllocate.dex[quoteFeeID][0] += buyFundingFees + if (quoteIsAccountLocker) { + toAllocate.dex[quoteFeeID][0] += sellFeeBuffer * marketReport.quoteFees.max.redeem + toAllocate.dex[quoteFeeID][0] += buyFeeBuffer * marketReport.quoteFees.max.refund + } + } + } + + return toAllocate +} + +export function toAllocate ( + numBuyLots: number, numSellLots: number, slippageBuffer: number, buyFeeBuffer: number, + sellFeeBuffer: number, perBuyLot: PerLot, perSellLot: PerLot, marketReport: MarketReport, + availableFunds: AvailableFunds, canRebalance: boolean, baseID: number, quoteID: number, + baseFeeID: number, quoteFeeID: number, baseIsAccountLocker: boolean, quoteIsAccountLocker: boolean, + buyFundingFees: number, sellFundingFees: number +) : AllocationResult { + const toAllocate = requiredFunds(numBuyLots, numSellLots, slippageBuffer, buyFeeBuffer, sellFeeBuffer, perBuyLot, + perSellLot, marketReport, baseIsAccountLocker, quoteIsAccountLocker, baseID, quoteID, baseFeeID, quoteFeeID, + buyFundingFees, sellFundingFees) + const assetIDs = new Set([baseID, quoteID, baseFeeID, quoteFeeID]) + + // For each asset, check if allocation is sufficient and set status + for (const assetID of assetIDs) { + const cexRequired = toAllocate.cex[assetID][0] + const dexRequired = toAllocate.dex[assetID][0] + + const cexAvail = availableFunds.cex?.[assetID] ?? 0 + const dexAvail = availableFunds.dex?.[assetID] ?? 0 + + const cexSurplusDeficit = cexAvail - cexRequired + const dexSurplusDeficit = dexAvail - dexRequired + + // Check CEX allocation + if (cexSurplusDeficit >= 0) { + // already correct + } else { + toAllocate.cex[assetID][1] = 'insufficient' + toAllocate.cex[assetID][0] = cexAvail + } + + // Check DEX allocation + if (dexSurplusDeficit >= 0) { + // already correct + } else { + toAllocate.dex[assetID][1] = 'insufficient' + toAllocate.dex[assetID][0] = dexAvail + } + + // If dex is insufficient, increase cex allocation + if (canRebalance && toAllocate.cex[assetID][1] === 'sufficient' && toAllocate.dex[assetID][1] === 'insufficient') { + const additionalDEXRequired = dexRequired - toAllocate.dex[assetID][0] + const additionalCEX = Math.min(additionalDEXRequired, cexAvail - toAllocate.cex[assetID][0]) + toAllocate.cex[assetID][0] += additionalCEX + if (additionalCEX === additionalDEXRequired) toAllocate.dex[assetID][1] = 'sufficient-with-rebalance' + } + + // If cex is insufficient, increase dex allocation + if (canRebalance && toAllocate.cex[assetID][1] === 'insufficient' && toAllocate.dex[assetID][1] === 'sufficient') { + const additionalCEXRequired = cexRequired - toAllocate.cex[assetID][0] + const additionalDEX = Math.min(additionalCEXRequired, dexAvail - toAllocate.dex[assetID][0]) + toAllocate.dex[assetID][0] += additionalDEX + if (additionalDEX === additionalCEXRequired) toAllocate.cex[assetID][1] = 'sufficient-with-rebalance' + } + } + + return toAllocate +} + +export function toAllocateRunning ( + numBuyLots: number, numSellLots: number, slippageBuffer: number, buyFeeBuffer: number, + sellFeeBuffer: number, perBuyLot: PerLot, perSellLot: PerLot, marketReport: MarketReport, + availableFunds: AvailableFunds, canRebalance: boolean, baseID: number, quoteID: number, + baseFeeID: number, quoteFeeID: number, baseIsAccountLocker: boolean, quoteIsAccountLocker: boolean, + runStats: RunStats, buyFundingFees: number, sellFundingFees: number) : AllocationResult { + const toAllocate = requiredFunds(numBuyLots, numSellLots, slippageBuffer, buyFeeBuffer, sellFeeBuffer, + perBuyLot, perSellLot, marketReport, baseIsAccountLocker, quoteIsAccountLocker, baseID, quoteID, baseFeeID, quoteFeeID, + buyFundingFees, sellFundingFees) + const assetIDs = new Set([baseID, quoteID, baseFeeID, quoteFeeID]) + + const totalBotBalance = (source: 'cex' | 'dex', assetID: number) => { + let bals + if (source === 'dex') { + bals = runStats.dexBalances[assetID] + } else { + bals = runStats.cexBalances[assetID] + } + return bals.available + bals.locked + bals.pending + bals.reserved + } + + for (const assetID of assetIDs) { + const botDEXTotal = totalBotBalance('dex', assetID) + const botCEXTotal = totalBotBalance('cex', assetID) + const botDEXAvailable = runStats.dexBalances[assetID].available + const botCEXAvailable = runStats.cexBalances[assetID].available + const cexAvailable = availableFunds.cex ? availableFunds.cex[assetID] : 0 + const dexAvailable = availableFunds.dex[assetID] ?? 0 + + const cexRequired = toAllocate.cex[assetID] ? toAllocate.cex[assetID][0] : 0 + const dexRequired = toAllocate.dex[assetID] ? toAllocate.dex[assetID][0] : 0 + + const additionalCEXRequired = cexRequired - botCEXTotal // may be negative + const additionalDEXRequired = dexRequired - botDEXTotal // may be negative + + // CEX allocation + if (additionalCEXRequired <= 0) { + toAllocate.cex[assetID][0] = -Math.min(-additionalCEXRequired, botCEXAvailable) + } else { + toAllocate.cex[assetID][0] = Math.min(additionalCEXRequired, cexAvailable) + if (toAllocate.cex[assetID][0] < additionalCEXRequired) toAllocate.cex[assetID][1] = 'insufficient' + } + + // DEX allocation + if (additionalDEXRequired <= 0) { + toAllocate.dex[assetID][0] = -Math.min(-additionalDEXRequired, botDEXAvailable) + } else { + toAllocate.dex[assetID][0] = Math.min(additionalDEXRequired, dexAvailable) + if (toAllocate.dex[assetID][0] < additionalDEXRequired) toAllocate.dex[assetID][1] = 'insufficient' + } + + // If dex is insufficient, increase cex allocation + if (canRebalance && toAllocate.cex[assetID][1] === 'sufficient' && toAllocate.dex[assetID][1] === 'insufficient') { + const dexAdditionalRequired = additionalDEXRequired - toAllocate.dex[assetID][0] + const additionalCEX = Math.min(dexAdditionalRequired, cexAvailable - toAllocate.cex[assetID][0]) + toAllocate.cex[assetID][0] += additionalCEX + if (additionalCEX === dexAdditionalRequired) toAllocate.dex[assetID][1] = 'sufficient-with-rebalance' + } + + // If cex is insufficient, increase dex allocation + if (canRebalance && toAllocate.cex[assetID][1] === 'insufficient' && toAllocate.dex[assetID][1] === 'sufficient') { + const cexAdditionalRequired = additionalCEXRequired - toAllocate.cex[assetID][0] + const additionalDEX = Math.min(cexAdditionalRequired, dexAvailable - toAllocate.dex[assetID][0]) + toAllocate.dex[assetID][0] += additionalDEX + if (additionalDEX === cexAdditionalRequired) toAllocate.cex[assetID][1] = 'sufficient-with-rebalance' + } + } + + return toAllocate +} + +export function perLotRequirements ( + baseID: number, quoteID: number, baseFeeID: number, quoteFeeID: number, + lotSize: number, quoteLot: number, marketReport: MarketReport, + baseIsAccountLocker: boolean, quoteIsAccountLocker: boolean): { perSellLot: PerLot, perBuyLot: PerLot } { + const perSellLot: PerLot = { cex: {}, dex: {} } + perSellLot.dex[baseID] = lotSize + perSellLot.dex[baseFeeID] = (perSellLot.dex[baseFeeID] ?? 0) + marketReport.baseFees.max.swap // TODO: booking fees + perSellLot.cex[quoteID] = quoteLot + if (baseIsAccountLocker) perSellLot.dex[baseFeeID] = (perSellLot.dex[baseFeeID] ?? 0) + marketReport.baseFees.max.refund + if (quoteIsAccountLocker) perSellLot.dex[quoteFeeID] = (perSellLot.dex[quoteFeeID] ?? 0) + marketReport.quoteFees.max.redeem + + const perBuyLot: PerLot = { cex: {}, dex: {} } + perBuyLot.dex[quoteID] = quoteLot + perBuyLot.dex[quoteFeeID] = (perBuyLot.dex[quoteFeeID] ?? 0) + marketReport.quoteFees.max.swap // TODO: booking fees + perBuyLot.cex[baseID] = lotSize + if (baseIsAccountLocker) perBuyLot.dex[baseFeeID] = (perBuyLot.dex[baseFeeID] ?? 0) + marketReport.baseFees.max.redeem + if (quoteIsAccountLocker) perBuyLot.dex[quoteFeeID] = (perBuyLot.dex[quoteFeeID] ?? 0) + marketReport.quoteFees.max.refund + + for (const assetID of Array.from(new Set([baseID, quoteID, baseFeeID, quoteFeeID]))) { + perSellLot.dex[assetID] = Math.floor(perSellLot.dex[assetID] ?? 0) + perBuyLot.dex[assetID] = Math.floor(perBuyLot.dex[assetID] ?? 0) + perSellLot.cex[assetID] = Math.floor(perSellLot.cex[assetID] ?? 0) + perBuyLot.cex[assetID] = Math.floor(perBuyLot.cex[assetID] ?? 0) + } + + return { perSellLot, perBuyLot } +} + 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 d4832abee4..363ece7b8e 100644 --- a/client/webserver/site/src/js/registry.ts +++ b/client/webserver/site/src/js/registry.ts @@ -797,20 +797,6 @@ export interface BotBalanceAllocation { cex: Record } -export interface BotAssetConfig { - swapFeeN: number - orderReservesFactor: number - slippageBufferFactor: number - transferFactor: number -} - -export interface UIConfig { - baseConfig: BotAssetConfig - quoteConfig: BotAssetConfig - simpleArbLots?: number - cexRebalance: boolean -} - export interface StartConfig extends MarketWithHost { autoRebalance?: AutoRebalanceConfig alloc: BotBalanceAllocation @@ -823,7 +809,6 @@ export interface BotConfig { baseWalletOptions?: Record quoteWalletOptions?: Record cexName: string - uiConfig: UIConfig basicMarketMakingConfig?: BasicMarketMakingConfig arbMarketMakingConfig?: ArbMarketMakingConfig simpleArbConfig?: SimpleArbConfig diff --git a/client/webserver/webserver.go b/client/webserver/webserver.go index 1ea09fa6fe..9622db05d8 100644 --- a/client/webserver/webserver.go +++ b/client/webserver/webserver.go @@ -186,6 +186,10 @@ 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, saveUpdate bool) error + AvailableBalances(mkt *mm.MarketWithHost, cexName *string) (dexBalances, cexBalances map[uint32]uint64, _ error) + MaxFundingFees(mkt *mm.MarketWithHost) (buyFees, sellFees uint64, err error) + UpdateRunningBotInventory(mkt *mm.MarketWithHost, balanceDiffs *mm.BotInventoryDiffs) error } // genCertPair generates a key/cert pair to the paths provided. @@ -570,6 +574,8 @@ func New(cfg *Config) (*WebServer, error) { apiAuth.Post("/startmarketmakingbot", s.apiStartMarketMakingBot) apiAuth.Post("/stopmarketmakingbot", s.apiStopMarketMakingBot) apiAuth.Post("/updatebotconfig", s.apiUpdateBotConfig) + apiAuth.Post("/updaterunningbotconfig", s.apiUpdateRunningBotConfig) + apiAuth.Post("/updatebotinventory", s.apiUpdateBotInventory) apiAuth.Post("/updatecexconfig", s.apiUpdateCEXConfig) apiAuth.Post("/removebotconfig", s.apiRemoveBotConfig) apiAuth.Get("/marketmakingstatus", s.apiMarketMakingStatus) @@ -578,6 +584,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) + }) })