Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] mm: Live config / balance updates #3081

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/mm/exchange_adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3617,6 +3617,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) {
Expand Down
41 changes: 32 additions & 9 deletions client/mm/mm.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,21 @@ func (m *MarketMaker) MarketReport(host string, baseID, quoteID uint32) (*Market
}, nil
}

// MaxFundingFees returns the maximum funding fees for a bot on a market.
func (m *MarketMaker) MaxFundingFees(mwh *MarketWithHost, maxBuyPlacements, maxSellPlacements uint32, baseOptions, quoteOptions map[string]string) (buyFees, sellFees uint64, err error) {
buyFundingFees, err := m.core.MaxFundingFees(mwh.QuoteID, mwh.Host, maxBuyPlacements, quoteOptions)
if err != nil {
return 0, 0, fmt.Errorf("failed to get buy funding fees: %w", err)
}

sellFundingFees, err := m.core.MaxFundingFees(mwh.BaseID, mwh.Host, maxSellPlacements, baseOptions)
if err != nil {
return 0, 0, fmt.Errorf("failed to get sell funding fees: %w", err)
}

return buyFundingFees, sellFundingFees, nil
}

func (m *MarketMaker) loginAndUnlockWallets(pw []byte, cfg *BotConfig) error {
err := m.core.Login(pw)
if err != nil {
Expand Down Expand Up @@ -1085,12 +1100,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
}

Expand Down Expand Up @@ -1126,12 +1142,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)
Expand Down Expand Up @@ -1711,10 +1730,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)
Expand Down
31 changes: 21 additions & 10 deletions client/mm/mm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,14 @@ func (c *tCEX) Markets(ctx context.Context) (map[string]*libxc.Market, error) {
return nil, nil
}
func (c *tCEX) Balance(assetID uint32) (*libxc.ExchangeBalance, error) {
if c.balanceErr != nil {
return nil, c.balanceErr
}

if c.balances[assetID] == nil {
return &libxc.ExchangeBalance{}, nil
}

return c.balances[assetID], c.balanceErr
}
func (c *tCEX) Trade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64, updaterID int) (*libxc.Trade, error) {
Expand Down Expand Up @@ -771,9 +779,9 @@ func TestAvailableBalances(t *testing.T) {
60001: 6e5,
})

checkAvailableBalances := func(mkt *MarketWithHost, expDex, expCex map[uint32]uint64) {
checkAvailableBalances := func(mkt *MarketWithHost, cexName *string, expDex, expCex map[uint32]uint64) {
t.Helper()
dexBalances, cexBalances, err := mm.AvailableBalances(mkt, nil)
dexBalances, cexBalances, err := mm.AvailableBalances(mkt, cexName)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All @@ -785,11 +793,14 @@ func TestAvailableBalances(t *testing.T) {
}
}

binanceName := libxc.Binance
binanceUSName := libxc.BinanceUS

// No running bots
checkAvailableBalances(dcrBtc, map[uint32]uint64{42: 9e5, 0: 7e5}, map[uint32]uint64{})
checkAvailableBalances(ethBtc, map[uint32]uint64{60: 8e5, 0: 7e5}, map[uint32]uint64{60: 9e5, 0: 8e5})
checkAvailableBalances(btcUsdc, map[uint32]uint64{0: 7e5, 60: 8e5, 60001: 6e5}, map[uint32]uint64{0: 8e5, 60001: 6e5})
checkAvailableBalances(dcrUsdc, map[uint32]uint64{42: 9e5, 60: 8e5, 60001: 6e5}, map[uint32]uint64{42: 7e5, 60001: 6e5})
checkAvailableBalances(dcrBtc, nil, map[uint32]uint64{42: 9e5, 0: 7e5}, map[uint32]uint64{})
checkAvailableBalances(ethBtc, &binanceName, map[uint32]uint64{60: 8e5, 0: 7e5}, map[uint32]uint64{60: 9e5, 0: 8e5})
checkAvailableBalances(btcUsdc, &binanceName, map[uint32]uint64{0: 7e5, 60: 8e5, 60001: 6e5}, map[uint32]uint64{0: 8e5, 60001: 6e5})
checkAvailableBalances(dcrUsdc, &binanceUSName, map[uint32]uint64{42: 9e5, 60: 8e5, 60001: 6e5}, map[uint32]uint64{42: 7e5, 60001: 6e5})

rb := &runningBot{
bot: &tExchangeAdaptor{
Expand All @@ -807,8 +818,8 @@ func TestAvailableBalances(t *testing.T) {
}
mm.runningBots[*btcUsdc] = rb

checkAvailableBalances(dcrBtc, map[uint32]uint64{42: 9e5, 0: 3e5}, map[uint32]uint64{})
checkAvailableBalances(ethBtc, map[uint32]uint64{60: 7e5, 0: 3e5}, map[uint32]uint64{60: 9e5, 0: 5e5})
checkAvailableBalances(btcUsdc, map[uint32]uint64{0: 3e5, 60: 7e5, 60001: 4e5}, map[uint32]uint64{0: 5e5, 60001: 4e5})
checkAvailableBalances(dcrUsdc, map[uint32]uint64{42: 9e5, 60: 7e5, 60001: 4e5}, map[uint32]uint64{42: 7e5, 60001: 6e5})
checkAvailableBalances(dcrBtc, nil, map[uint32]uint64{42: 9e5, 0: 3e5}, map[uint32]uint64{})
checkAvailableBalances(ethBtc, &binanceName, map[uint32]uint64{60: 7e5, 0: 3e5}, map[uint32]uint64{60: 9e5, 0: 5e5})
checkAvailableBalances(btcUsdc, &binanceName, map[uint32]uint64{0: 3e5, 60: 7e5, 60001: 4e5}, map[uint32]uint64{0: 5e5, 60001: 4e5})
checkAvailableBalances(dcrUsdc, &binanceUSName, map[uint32]uint64{42: 9e5, 60: 7e5, 60001: 4e5}, map[uint32]uint64{42: 7e5, 60001: 6e5})
}
2 changes: 1 addition & 1 deletion client/rpcserver/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 7 additions & 5 deletions client/rpcserver/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ type addRemovePeerForm struct {
}

type mmAvailableBalancesForm struct {
cfgFilePath string
mkt *mm.MarketWithHost
mkt *mm.MarketWithHost
cexName *string
}

type startBotForm struct {
Expand Down Expand Up @@ -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 = &params.Args[3]
}
return form, nil
}

Expand Down
70 changes: 70 additions & 0 deletions client/webserver/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1751,6 +1751,58 @@ func (s *WebServer) apiStakeStatus(w http.ResponseWriter, r *http.Request) {
})
}

func (s *WebServer) apiAvailableBalances(w http.ResponseWriter, r *http.Request) {
var req struct {
Market *mm.MarketWithHost `json:"market"`
CEXName *string `json:"cexName,omitempty"`
}
if !readPost(w, r, &req) {
return
}
dexBalances, cexBalances, err := s.mm.AvailableBalances(req.Market, req.CEXName)
if err != nil {
s.writeAPIError(w, fmt.Errorf("error fetching available balances: %w", err))
return
}

writeJSON(w, &struct {
OK bool `json:"ok"`
DEXBalances map[uint32]uint64 `json:"dexBalances"`
CEXBalances map[uint32]uint64 `json:"cexBalances"`
}{
OK: true,
DEXBalances: dexBalances,
CEXBalances: cexBalances,
})
}

func (s *WebServer) apiMaxFundingFees(w http.ResponseWriter, r *http.Request) {
var req struct {
Market *mm.MarketWithHost `json:"market"`
MaxBuyPlacements uint32 `json:"maxBuyPlacements"`
MaxSellPlacements uint32 `json:"maxSellPlacements"`
BaseOptions map[string]string `json:"baseOptions"`
QuoteOptions map[string]string `json:"quoteOptions"`
}
if !readPost(w, r, &req) {
return
}
buyFees, sellFees, err := s.mm.MaxFundingFees(req.Market, req.MaxBuyPlacements, req.MaxSellPlacements, req.BaseOptions, req.QuoteOptions)
if err != nil {
s.writeAPIError(w, fmt.Errorf("error getting max funding fees: %w", err))
return
}
writeJSON(w, &struct {
OK bool `json:"ok"`
BuyFees uint64 `json:"buyFees"`
SellFees uint64 `json:"sellFees"`
}{
OK: true,
BuyFees: buyFees,
SellFees: sellFees,
})
}

func (s *WebServer) apiSetVSP(w http.ResponseWriter, r *http.Request) {
var req struct {
AssetID uint32 `json:"assetID"`
Expand Down Expand Up @@ -1967,6 +2019,24 @@ func (s *WebServer) apiUpdateBotConfig(w http.ResponseWriter, r *http.Request) {
writeJSON(w, simpleAck())
}

func (s *WebServer) apiUpdateRunningBot(w http.ResponseWriter, r *http.Request) {
var form struct {
Cfg *mm.BotConfig `json:"cfg"`
Diffs *mm.BotInventoryDiffs `json:"diffs"`
}
if !readPost(w, r, &form) {
s.writeAPIError(w, fmt.Errorf("failed to read config"))
return
}

if err := s.mm.UpdateRunningBotCfg(form.Cfg, form.Diffs, true); err != nil {
s.writeAPIError(w, err)
return
}

writeJSON(w, simpleAck())
}

func (s *WebServer) apiRemoveBotConfig(w http.ResponseWriter, r *http.Request) {
var form struct {
Host string `json:"host"`
Expand Down
12 changes: 12 additions & 0 deletions client/webserver/live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2603,6 +2603,18 @@ func (m *TMarketMaker) CEXBook(host string, baseID, quoteID uint32) (buys, sells
return book.Buys, book.Sells, nil
}

func (m *TMarketMaker) AvailableBalances(mkt *mm.MarketWithHost, cexName *string) (dexBalances, cexBalances map[uint32]uint64, _ error) {
return map[uint32]uint64{mkt.BaseID: 1e6, mkt.QuoteID: 1e6}, map[uint32]uint64{mkt.BaseID: 1e6, mkt.QuoteID: 1e6}, nil
}

func (m *TMarketMaker) MaxFundingFees(mkt *mm.MarketWithHost, maxBuyPlacements, maxSellPlacements uint32, baseOptions, quoteOptions map[string]string) (buyFees, sellFees uint64, _ error) {
return 1e4, 1e4, nil
}

func (m *TMarketMaker) UpdateRunningBotCfg(cfg *mm.BotConfig, balanceDiffs *mm.BotInventoryDiffs, saveUpdate bool) error {
return nil
}

func makeRequiredAction(assetID uint32, actionID string) *asset.ActionRequiredNote {
txID := dex.Bytes(encode.RandomBytes(32)).String()
var payload any
Expand Down
1 change: 0 additions & 1 deletion client/webserver/locales/ar.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,6 @@ var Ar = map[string]*intl.Translation{
"Immature tickets": {T: "التذاكر غير الناضجة"},
"app_pw_reg": {Version: 1, T: "أدخل كلمة مرور التطبيق لتأكيد تسجيل منصة المبادلات اللامركزية DEX وإنشاء السندات."},
"treasury spends": {T: "نفقات الخزينة"},
"bots_running_view_only": {T: "البوتات قيد التشغيل. أنت في وضع العرض فقط."},
"buy_placements_tooltip": {T: "تحديد مواضع الشراء للبوت. سيقوم البوت بوضع الطلبات حسب ترتيب الأولوية إذا لم يكن الرصيد كافيًا لتقديم جميع الطلبات."},
"no_limit_bullet": {T: "ليس هناك حد لعدد إنشاء اللوت"},
"Error": {T: "خطأ"},
Expand Down
4 changes: 3 additions & 1 deletion client/webserver/locales/en-us.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ var EnUS = map[string]*intl.Translation{
"Minimum Balance": {T: "Minimum Balance"},
"Minimum Transfer": {T: "Minimum Transfer"},
"update_settings": {Version: 1, T: "Save Settings"},
"update_running": {T: "Update Running Bot"},
"create_bot": {T: "Create Bot"},
"reset_settings": {T: "Reset Settings"},
"gap_strategy": {T: "Gap Strategy"},
Expand Down Expand Up @@ -547,7 +548,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"},
Expand Down Expand Up @@ -685,4 +685,6 @@ var EnUS = map[string]*intl.Translation{
"Wallet Balances": {T: "Wallet Balances"},
"Placements": {T: "Placements"},
"delete_bot": {T: "Delete Bot"},
"bot_running": {T: "This bot is currently running"},
"Asset Allocations": {T: "Asset Allocations"},
}
1 change: 0 additions & 1 deletion client/webserver/locales/pl-pl.go
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,6 @@ var PlPL = map[string]*intl.Translation{
"privacy_intro": {T: "Po włączeniu prywatności wszystkie środki będą wysyłane za pośrednictwem usługi maskowania historii adresów przy użyciu protokołu o nazwie StakeShuffle."},
"decred_privacy": {T: "Forma prywatności Decred jest szczególnie potężna, ponieważ portfele Decred integrują prywatność ze stakingiem, co daje ciągły dostęp do dużego zbioru anonimowości będącego kluczową cechą prywatności."},
"privacy_optional": {T: "Prywatność jest całkowicie opcjonalna i można ją wyłączyć w dowolnym momencie. Z prywatnością wiążą się zwiększone opłaty transakcyjne, ale w przeszłości były one stosunkowo niewielkie."},
"bots_running_view_only": {T: "Boty są uruchomione. Jesteś w trybie tylko do podglądu."},
"select_a_cex_prompt": {T: "Wybierz giełdę, aby uruchomić arbitraż"},
"Market not available": {T: "Rynek niedostępny"},
"bot_profit_title": {T: "Wybierz próg zysku"},
Expand Down
4 changes: 4 additions & 0 deletions client/webserver/site/src/css/mm.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ div[data-handler=mm] {
max-width: 75px;
}

.mw-500 {
max-width: 500px;
}

[data-tmpl=value].wide {
width: 3rem;
}
Expand Down
1 change: 1 addition & 0 deletions client/webserver/site/src/html/forms.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,7 @@
</div>
<div class="d-flex align-items-stretch ps-3 border-start">
<button data-tmpl="runLogsBttn" class="ico-textfile"></button>
<button data-tmpl="settingsBttn" class="ico-settings ms-1"></button>
<button data-tmpl="stopBttn" class="ms-3 position-relative">Stop</button>
</div>
</div>
Expand Down
Loading
Loading