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 @@
+
+
Stop
@@ -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
+
Adjust Manually
+
+ Sufficient Funds
+
+ Insufficient Funds
+
+ Sufficient With Rebalance
+
+
+ {{ template "balancesTable" }}
+
+
+
+ {{- /* BALANCE CONFIGURATION */ -}}
+
+
+
Balance Configuration
+
Quick Config
+
+
+ {{- /* 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 */ -}}
+
+
+
+
+
+
+
+
+
+
+ Slippage Buffer
+
+
+
+
+
+
+
+ Buy Fee Reserve
+
+
+
+
+
+
+
+ Sell Fee Reserve
+
+
+
+
+
+
+
+
+
+
+
+ Enable 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
-
+
├─
- 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.
-
-
-
-
-
-
-
-
- {{- /* PROPOSED ALLOCATIONS AND ADJUSTMENTS */ -}}
-
- {{- /* CEX AND DEX LOGO COLUMN HEADERS */ -}}
-
-
-
-
-
-
-
-
-
- {{- /* PROPOSED BASE ALLOCATIONS */ -}}
-
-
-
-
-
-
-
-
-
-
- {{- /* PROPOSED QUOTE ALLOCATIONS */ -}}
-
-
-
-
-
-
-
-
-
-
- {{- /* BASE TOKEN FEE ALLOCATIONS */ -}}
-
-
-
-
-
-
-
-
-
- {{- /* QUOTE TOKEN FEE ALLOCATIONS */ -}}
-
-
-
-
-
-
-
-
-
-
- ~
-
- USD
- total
-
-
@@ -624,6 +402,11 @@
Confirm
+
+
+
{{- /* 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 */ -}}
+
+
+ [[[bot_running]]]
+
+
{{- /* MANUAL CONFIG */ -}}
- {{- /* VIEW-ONLY MODE */ -}}
-
- [[[bots_running_view_only]]]
-
{{- /* STRATEGY SELECTION */ -}}
@@ -182,7 +182,7 @@
{{- /* QUICK CONFIG */ -}}
- [[[Quick Placements]]]
+ [[[Quick Placements]]]
[[[Configure manually]]]
@@ -355,348 +355,95 @@
{{- /* ASSET SETTINGS */ -}}
-
-
-
-
-
- {{- /* LOGO AND TOTAL ALLOCATION */ -}}
-
-
-
-
-
-
-
-
- {{- /* ALLOCATIONS */ -}}
-
-
- {{- /* ASSET ALLOCATIONS */ -}}
-
- {{- /* DEX BOOKING */ -}}
-
-
- Order Inventory
-
-
- lots
-
-
-
-
- x
-
-
- per lot
-
-
-
- =
-
-
-
-
-
- {{- /* CEX COUNTER INVENTORY */ -}}
-
-
CEX Inventory
-
-
-
-
-
-
- {{- /* ORDER RESERVES */ -}}
-
-
-
-
-
-
- x
-
-
-
-
- =
-
-
-
-
-
- {{- /* QUOTE SLIPPAGE BUFFER */ -}}
-
-
-
-
-
-
- x
-
-
-
-
- =
-
-
-
-
-
-
- {{- /* FEE ALLOCATIONS */ -}}
-
-
- {{- /* TOKEN FEE RESERVES */ -}}
-
-
-
-
Fee Reserves
-
-
+
+
+ {{- /* 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 */ -}}
-
-
-
-
- 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 */ -}}
-
-
-
- [[[Arbitrage Rebalance]]]
-
-
-
-
- {{- /* DRIFT TOLERANCE */ -}}
-
-
-
- [[[Drift tolerance]]]
-
-
-
-
+
+
- {{- /* ORDER PERSISTENCE */ -}}
-
-
- [[[Order persistence]]]
-
-
-
+ {{- /* ORDER PERSISTENCE */ -}}
+
+
+ [[[Order persistence]]]
+
-
-
+
+
+
+
+ {{- /* 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)
+
})
})