decred.org/dcrdex@v1.0.3/client/mm/mm.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package mm
     5  
     6  import (
     7  	"context"
     8  	"encoding/hex"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"os"
    13  	"sync"
    14  	"time"
    15  
    16  	"decred.org/dcrdex/client/asset"
    17  	"decred.org/dcrdex/client/core"
    18  	"decred.org/dcrdex/client/mm/libxc"
    19  	"decred.org/dcrdex/client/orderbook"
    20  	"decred.org/dcrdex/dex"
    21  	"decred.org/dcrdex/dex/order"
    22  )
    23  
    24  // clientCore is satisfied by core.Core.
    25  type clientCore interface {
    26  	NotificationFeed() *core.NoteFeed
    27  	ExchangeMarket(host string, baseID, quoteID uint32) (*core.Market, error)
    28  	SyncBook(host string, baseID, quoteID uint32) (*orderbook.OrderBook, core.BookFeed, error)
    29  	SupportedAssets() map[uint32]*core.SupportedAsset
    30  	SingleLotFees(form *core.SingleLotFeesForm) (uint64, uint64, uint64, error)
    31  	Cancel(oidB dex.Bytes) error
    32  	AssetBalance(assetID uint32) (*core.WalletBalance, error)
    33  	WalletTraits(assetID uint32) (asset.WalletTrait, error)
    34  	MultiTrade(pw []byte, form *core.MultiTradeForm) []*core.MultiTradeResult
    35  	MaxFundingFees(fromAsset uint32, host string, numTrades uint32, fromSettings map[string]string) (uint64, error)
    36  	Login(pw []byte) error
    37  	OpenWallet(assetID uint32, appPW []byte) error
    38  	Broadcast(core.Notification)
    39  	FiatConversionRates() map[uint32]float64
    40  	Send(pw []byte, assetID uint32, value uint64, address string, subtract bool) (asset.Coin, error)
    41  	NewDepositAddress(assetID uint32) (string, error)
    42  	Network() dex.Network
    43  	Order(oidB dex.Bytes) (*core.Order, error)
    44  	WalletTransaction(uint32, string) (*asset.WalletTransaction, error)
    45  	TradingLimits(host string) (userParcels, parcelLimit uint32, err error)
    46  	WalletState(assetID uint32) *core.WalletState
    47  	Exchange(host string) (*core.Exchange, error)
    48  }
    49  
    50  var _ clientCore = (*core.Core)(nil)
    51  
    52  // dexOrderBook is satisfied by orderbook.OrderBook.
    53  // Avoids having to mock the entire orderbook in tests.
    54  type dexOrderBook interface {
    55  	MidGap() (uint64, error)
    56  	VWAP(lots, lotSize uint64, sell bool) (avg, extrema uint64, filled bool, err error)
    57  }
    58  
    59  var _ dexOrderBook = (*orderbook.OrderBook)(nil)
    60  
    61  // MarketWithHost represents a market on a specific dex server.
    62  type MarketWithHost struct {
    63  	Host    string `json:"host"`
    64  	BaseID  uint32 `json:"baseID"`
    65  	QuoteID uint32 `json:"quoteID"`
    66  }
    67  
    68  func (m MarketWithHost) String() string {
    69  	return fmt.Sprintf("%s-%d-%d", m.Host, m.BaseID, m.QuoteID)
    70  }
    71  
    72  func (m MarketWithHost) ID() string {
    73  	n, _ := dex.MarketName(m.BaseID, m.QuoteID)
    74  	return n
    75  }
    76  
    77  // centralizedExchange is used to manage an exchange API connection.
    78  type centralizedExchange struct {
    79  	libxc.CEX
    80  	*CEXConfig
    81  
    82  	mtx        sync.RWMutex
    83  	cm         *dex.ConnectionMaster
    84  	mkts       map[string]*libxc.Market
    85  	balances   map[uint32]*libxc.ExchangeBalance
    86  	connectErr string
    87  }
    88  
    89  // mtx must be locked
    90  func (c *centralizedExchange) balancesCopy() map[uint32]*libxc.ExchangeBalance {
    91  	bs := make(map[uint32]*libxc.ExchangeBalance, len(c.balances))
    92  	for assetID, bal := range c.balances {
    93  		bs[assetID] = bal
    94  	}
    95  	return bs
    96  }
    97  
    98  // bot is an interface used by the MarketMaker to access functions in order to
    99  // check balances and update the bot configuration. An interface is created to
   100  // simplify testing.
   101  type bot interface {
   102  	dex.Connector
   103  	refreshAllPendingEvents(context.Context)
   104  	DEXBalance(assetID uint32) *BotBalance
   105  	CEXBalance(assetID uint32) *BotBalance
   106  	stats() *RunStats
   107  	latestEpoch() *EpochReport
   108  	latestCEXProblems() *CEXProblems
   109  	updateConfig(cfg *BotConfig) error
   110  	updateInventory(balanceDiffs *BotInventoryDiffs)
   111  	withPause(func() error) error
   112  	timeStart() int64
   113  	botCfg() *BotConfig
   114  	Book() (buys, sells []*core.MiniOrder, _ error)
   115  }
   116  
   117  type runningBot struct {
   118  	bot
   119  	cm     *dex.ConnectionMaster
   120  	cexCfg *CEXConfig
   121  }
   122  
   123  func (rb *runningBot) assets() map[uint32]interface{} {
   124  	assets := make(map[uint32]interface{})
   125  	cfg := rb.botCfg()
   126  	assets[cfg.BaseID] = struct{}{}
   127  	assets[cfg.QuoteID] = struct{}{}
   128  	assets[feeAssetID(cfg.BaseID)] = struct{}{}
   129  	assets[feeAssetID(cfg.QuoteID)] = struct{}{}
   130  
   131  	return assets
   132  }
   133  
   134  func (rb *runningBot) cexName() string {
   135  	return rb.botCfg().CEXName
   136  }
   137  
   138  // MarketMaker handles the market making process. It supports running different
   139  // strategies on different markets.
   140  type MarketMaker struct {
   141  	ctx            context.Context
   142  	log            dex.Logger
   143  	core           clientCore
   144  	defaultCfgPath string
   145  	eventLogDBPath string
   146  	eventLogDB     eventLogDB
   147  	oracle         *priceOracle
   148  
   149  	defaultCfgMtx sync.RWMutex
   150  	// defaultCfg is the configuration specified by the file at the path passed
   151  	// to NewMarketMaker as an argument. An alternateCfgPath can be passed to
   152  	// some functions to use a different config file (this is how the
   153  	// MarketMaker is used from the CLI).
   154  	defaultCfg *MarketMakingConfig
   155  
   156  	runningBotsMtx sync.RWMutex
   157  	runningBots    map[MarketWithHost]*runningBot
   158  
   159  	// startUpdateMtx is used to prevent starting or updating bots concurrently.
   160  	startUpdateMtx sync.Mutex
   161  
   162  	cexMtx sync.RWMutex
   163  	cexes  map[string]*centralizedExchange
   164  }
   165  
   166  // NewMarketMaker creates a new MarketMaker.
   167  func NewMarketMaker(c clientCore, eventLogDBPath, cfgPath string, log dex.Logger) (*MarketMaker, error) {
   168  	var cfg MarketMakingConfig
   169  	if b, err := os.ReadFile(cfgPath); err != nil && !os.IsNotExist(err) {
   170  		return nil, fmt.Errorf("error reading config file from %q: %w", cfgPath, err)
   171  	} else if len(b) > 0 {
   172  		if err := json.Unmarshal(b, &cfg); err != nil {
   173  			return nil, fmt.Errorf("error unmarshaling config file: %v", err)
   174  		}
   175  	}
   176  
   177  	return &MarketMaker{
   178  		core:           c,
   179  		log:            log,
   180  		defaultCfgPath: cfgPath,
   181  		defaultCfg:     &cfg,
   182  		eventLogDBPath: eventLogDBPath,
   183  		runningBots:    make(map[MarketWithHost]*runningBot),
   184  		cexes:          make(map[string]*centralizedExchange),
   185  	}, nil
   186  }
   187  
   188  // runningBotsLookup returns a lookup map for running bots.
   189  func (m *MarketMaker) runningBotsLookup() map[MarketWithHost]*runningBot {
   190  	m.runningBotsMtx.RLock()
   191  	defer m.runningBotsMtx.RUnlock()
   192  
   193  	mkts := make(map[MarketWithHost]*runningBot, len(m.runningBots))
   194  	for mkt, rb := range m.runningBots {
   195  		mkts[mkt] = rb
   196  	}
   197  
   198  	return mkts
   199  }
   200  
   201  // Status is state information about the MarketMaker.
   202  type Status struct {
   203  	Bots  []*BotStatus          `json:"bots"`
   204  	CEXes map[string]*CEXStatus `json:"cexes"`
   205  }
   206  
   207  // CEXStatus is state information about a cex.
   208  type CEXStatus struct {
   209  	Config          *CEXConfig                        `json:"config"`
   210  	Connected       bool                              `json:"connected"`
   211  	ConnectionError string                            `json:"connectErr"`
   212  	Markets         map[string]*libxc.Market          `json:"markets"`
   213  	Balances        map[uint32]*libxc.ExchangeBalance `json:"balances"`
   214  }
   215  
   216  // StampedError is an error with a timestamp.
   217  type StampedError struct {
   218  	Stamp int64  `json:"stamp"`
   219  	Error string `json:"error"`
   220  }
   221  
   222  func (se *StampedError) isEqual(se2 *StampedError) bool {
   223  	if se == nil != (se2 == nil) {
   224  		return false
   225  	}
   226  	if se == nil {
   227  		return true
   228  	}
   229  
   230  	return se.Stamp == se2.Stamp && se.Error == se2.Error
   231  }
   232  
   233  func newStampedError(err error) *StampedError {
   234  	if err == nil {
   235  		return nil
   236  	}
   237  	return &StampedError{
   238  		Stamp: time.Now().Unix(),
   239  		Error: err.Error(),
   240  	}
   241  }
   242  
   243  // BotProblems contains problems that prevent orders from being placed.
   244  type BotProblems struct {
   245  	// WalletNotSynced is true if orders were unable to be placed due to a
   246  	// wallet not being synced.
   247  	WalletNotSynced map[uint32]bool `json:"walletNotSynced"`
   248  	// NoWalletPeers is true if orders were unable to be placed due to a wallet
   249  	// not having any peers.
   250  	NoWalletPeers map[uint32]bool `json:"noWalletPeers"`
   251  	// AccountSuspended is true if orders were unable to be placed due to the
   252  	// account being suspended.
   253  	AccountSuspended bool `json:"accountSuspended"`
   254  	// UserLimitTooLow is true if the user does not have the bonding amount
   255  	// necessary to place all of their orders.
   256  	UserLimitTooLow bool `json:"userLimitTooLow"`
   257  	// NoPriceSource is true if there is no oracle or fiat rate available.
   258  	NoPriceSource bool `json:"noPriceSource"`
   259  	// OracleFiatMismatch is true if the mid-gap is outside the oracle's
   260  	// safe range as defined by the config.
   261  	OracleFiatMismatch bool `json:"oracleFiatMismatch"`
   262  	// CEXOrderbookUnsynced is true if the CEX orderbook is unsynced.
   263  	CEXOrderbookUnsynced bool `json:"cexOrderbookUnsynced"`
   264  	// CausesSelfMatch is true if the order would cause a self match.
   265  	CausesSelfMatch bool `json:"causesSelfMatch"`
   266  	// UnknownError is set if an error occurred that was not one of the above.
   267  	UnknownError string `json:"unknownError"`
   268  }
   269  
   270  // EpochReport contains a report of a bot's activity during an epoch.
   271  type EpochReport struct {
   272  	// PreOrderProblems is set if there were problems with the bot's
   273  	// configuration or state that prevents orders from being placed.
   274  	PreOrderProblems *BotProblems `json:"preOrderProblems"`
   275  	// BuysReport is the report for the buys.
   276  	BuysReport *OrderReport `json:"buysReport"`
   277  	// SellsReport is the report for the sells.
   278  	SellsReport *OrderReport `json:"sellsReport"`
   279  	// EpochNum is the number of the epoch.
   280  	EpochNum uint64 `json:"epochNum"`
   281  }
   282  
   283  func (er *EpochReport) setPreOrderProblems(err error) {
   284  	if err == nil {
   285  		er.PreOrderProblems = nil
   286  		return
   287  	}
   288  
   289  	er.PreOrderProblems = &BotProblems{}
   290  	updateBotProblemsBasedOnError(er.PreOrderProblems, err)
   291  }
   292  
   293  // CEXProblems contains a record of the last attempted CEX operations by
   294  // a bot.
   295  type CEXProblems struct {
   296  	// DepositErr is set if the last attempted deposit for an asset failed.
   297  	DepositErr map[uint32]*StampedError `json:"depositErr"`
   298  	// WithdrawErr is set if the last attempted withdrawal for an asset failed.
   299  	WithdrawErr map[uint32]*StampedError `json:"withdrawErr"`
   300  	// TradeErr is set if the last attempted CEX trade failed.
   301  	TradeErr *StampedError `json:"tradeErr"`
   302  }
   303  
   304  func (c *CEXProblems) copy() *CEXProblems {
   305  	cp := &CEXProblems{
   306  		DepositErr:  make(map[uint32]*StampedError, len(c.DepositErr)),
   307  		WithdrawErr: make(map[uint32]*StampedError, len(c.WithdrawErr)),
   308  	}
   309  	for assetID, err := range c.DepositErr {
   310  		if err == nil {
   311  			continue
   312  		}
   313  		cp.DepositErr[assetID] = &StampedError{
   314  			Stamp: err.Stamp,
   315  			Error: err.Error,
   316  		}
   317  	}
   318  	for assetID, err := range c.WithdrawErr {
   319  		if err == nil {
   320  			continue
   321  		}
   322  		cp.WithdrawErr[assetID] = &StampedError{
   323  			Stamp: err.Stamp,
   324  			Error: err.Error,
   325  		}
   326  	}
   327  	if c.TradeErr != nil {
   328  		cp.TradeErr = &StampedError{
   329  			Stamp: c.TradeErr.Stamp,
   330  			Error: c.TradeErr.Error,
   331  		}
   332  	}
   333  	return cp
   334  }
   335  
   336  func newCEXProblems() *CEXProblems {
   337  	return &CEXProblems{
   338  		DepositErr:  make(map[uint32]*StampedError),
   339  		WithdrawErr: make(map[uint32]*StampedError),
   340  	}
   341  }
   342  
   343  // BotStatus is state information about a configured bot.
   344  type BotStatus struct {
   345  	Config  *BotConfig `json:"config"`
   346  	Running bool       `json:"running"`
   347  	// RunStats being non-nil means the bot is running.
   348  	RunStats    *RunStats    `json:"runStats"`
   349  	LatestEpoch *EpochReport `json:"latestEpoch"`
   350  	CEXProblems *CEXProblems `json:"cexProblems"`
   351  }
   352  
   353  // Status generates a Status for the MarketMaker. This returns the status of
   354  // all bots specified in the default config file.
   355  func (m *MarketMaker) Status() *Status {
   356  	cfg := m.defaultConfig()
   357  	status := &Status{
   358  		CEXes: make(map[string]*CEXStatus, len(cfg.CexConfigs)),
   359  		Bots:  make([]*BotStatus, 0, len(cfg.BotConfigs)),
   360  	}
   361  	runningBots := m.runningBotsLookup()
   362  	for _, botCfg := range cfg.BotConfigs {
   363  		mkt := MarketWithHost{botCfg.Host, botCfg.BaseID, botCfg.QuoteID}
   364  		rb := runningBots[mkt]
   365  		var stats *RunStats
   366  		var epochReport *EpochReport
   367  		var cexProblems *CEXProblems
   368  		if rb != nil {
   369  			stats = rb.stats()
   370  			epochReport = rb.latestEpoch()
   371  			cexProblems = rb.latestCEXProblems()
   372  		}
   373  		status.Bots = append(status.Bots, &BotStatus{
   374  			Config:      botCfg,
   375  			Running:     rb != nil,
   376  			RunStats:    stats,
   377  			LatestEpoch: epochReport,
   378  			CEXProblems: cexProblems,
   379  		})
   380  	}
   381  	for _, cex := range m.cexList() {
   382  		s := &CEXStatus{Config: cex.CEXConfig}
   383  		if cex != nil {
   384  			cex.mtx.RLock()
   385  			s.Connected = cex.cm != nil && cex.cm.On()
   386  			s.Markets = cex.mkts
   387  			s.ConnectionError = cex.connectErr
   388  			s.Balances = cex.balancesCopy()
   389  			cex.mtx.RUnlock()
   390  		}
   391  		status.CEXes[cex.Name] = s
   392  	}
   393  	return status
   394  }
   395  
   396  // RunningBotsStatus returns the status of all currently running bots. This
   397  // should be used by the CLI which may have passed in an alternate config
   398  // file when starting bots.
   399  func (m *MarketMaker) RunningBotsStatus() *Status {
   400  	status := &Status{
   401  		CEXes: make(map[string]*CEXStatus, 0),
   402  		Bots:  make([]*BotStatus, 0),
   403  	}
   404  	runningBots := m.runningBotsLookup()
   405  	for _, rb := range runningBots {
   406  		status.Bots = append(status.Bots, &BotStatus{
   407  			Config:      rb.botCfg(),
   408  			Running:     true,
   409  			RunStats:    rb.stats(),
   410  			LatestEpoch: rb.latestEpoch(),
   411  			CEXProblems: rb.latestCEXProblems(),
   412  		})
   413  	}
   414  	return status
   415  }
   416  
   417  func (m *MarketMaker) CEXBalance(cexName string, assetID uint32) (*libxc.ExchangeBalance, error) {
   418  	cfg := m.defaultConfig()
   419  
   420  	var cexCfg *CEXConfig
   421  	for _, cfg := range cfg.CexConfigs {
   422  		if cfg.Name == cexName {
   423  			cexCfg = cfg
   424  			break
   425  		}
   426  	}
   427  	if cexCfg == nil {
   428  		return nil, fmt.Errorf("no CEX config found for %s", cexName)
   429  	}
   430  
   431  	cex, err := m.loadAndConnectCEX(m.ctx, cexCfg)
   432  	if err != nil {
   433  		return nil, fmt.Errorf("error getting connected CEX: %w", err)
   434  	}
   435  
   436  	return cex.Balance(assetID)
   437  }
   438  
   439  // MarketReport returns information about the oracle rates on a market
   440  // pair and the fiat rates of the base and quote assets.
   441  func (m *MarketMaker) MarketReport(host string, baseID, quoteID uint32) (*MarketReport, error) {
   442  	fiatRates := m.core.FiatConversionRates()
   443  	baseFiatRate := fiatRates[baseID]
   444  	quoteFiatRate := fiatRates[quoteID]
   445  
   446  	price, oracles, err := m.oracle.getOracleInfo(baseID, quoteID)
   447  	if err != nil {
   448  		return nil, err
   449  	}
   450  	if price == 0 && baseFiatRate > 0 && quoteFiatRate > 0 {
   451  		price = baseFiatRate / quoteFiatRate
   452  	}
   453  
   454  	baseFeesEst, quoteFeesEst, err := marketFees(m.core, host, baseID, quoteID, false)
   455  	if err != nil {
   456  		return nil, err
   457  	}
   458  
   459  	baseFeesMax, quoteFeesMax, err := marketFees(m.core, host, baseID, quoteID, true)
   460  	if err != nil {
   461  		return nil, err
   462  	}
   463  
   464  	return &MarketReport{
   465  		Price:         price,
   466  		Oracles:       oracles,
   467  		BaseFiatRate:  baseFiatRate,
   468  		QuoteFiatRate: quoteFiatRate,
   469  		BaseFees: &LotFeeRange{
   470  			Max:       baseFeesMax,
   471  			Estimated: baseFeesEst,
   472  		},
   473  		QuoteFees: &LotFeeRange{
   474  			Max:       quoteFeesMax,
   475  			Estimated: quoteFeesEst,
   476  		},
   477  	}, nil
   478  }
   479  
   480  func (m *MarketMaker) loginAndUnlockWallets(pw []byte, cfg *BotConfig) error {
   481  	err := m.core.Login(pw)
   482  	if err != nil {
   483  		return fmt.Errorf("failed to login: %w", err)
   484  	}
   485  
   486  	err = m.core.OpenWallet(cfg.BaseID, pw)
   487  	if err != nil {
   488  		return fmt.Errorf("failed to unlock wallet for asset %d: %w", cfg.BaseID, err)
   489  	}
   490  
   491  	err = m.core.OpenWallet(cfg.QuoteID, pw)
   492  	if err != nil {
   493  		return fmt.Errorf("failed to unlock wallet for asset %d: %w", cfg.QuoteID, err)
   494  	}
   495  
   496  	return nil
   497  }
   498  
   499  func (m *MarketMaker) connectCEX(ctx context.Context, c *centralizedExchange) error {
   500  	var cm *dex.ConnectionMaster
   501  	c.mtx.Lock()
   502  	defer c.mtx.Unlock()
   503  	if c.cm == nil || !c.cm.On() {
   504  		cm = dex.NewConnectionMaster(c)
   505  		c.cm = cm
   506  	} else {
   507  		cm = c.cm
   508  	}
   509  
   510  	if !cm.On() {
   511  		c.connectErr = ""
   512  		if err := cm.ConnectOnce(ctx); err != nil {
   513  			c.connectErr = core.UnwrapErr(err).Error()
   514  			return fmt.Errorf("failed to connect to CEX: %w", err)
   515  		}
   516  		mkts, err := c.Markets(ctx)
   517  		if err != nil {
   518  			// Probably can't get here if we didn't error on connect, but
   519  			// checking anyway.
   520  			c.connectErr = core.UnwrapErr(err).Error()
   521  			return fmt.Errorf("error refreshing markets: %w", err)
   522  		}
   523  		c.mkts = mkts
   524  		bals, err := c.Balances(ctx)
   525  		if err != nil {
   526  			c.connectErr = core.UnwrapErr(err).Error()
   527  			return fmt.Errorf("error getting balances: %w", err)
   528  		}
   529  		c.balances = bals
   530  	}
   531  
   532  	return nil
   533  }
   534  
   535  // loadAndConnectCEX initializes the centralizedExchange if required, and
   536  // connects if not already connected.
   537  func (m *MarketMaker) loadAndConnectCEX(ctx context.Context, cfg *CEXConfig) (*centralizedExchange, error) {
   538  	c, err := m.loadCEX(ctx, cfg)
   539  	if err != nil {
   540  		return nil, fmt.Errorf("error loading CEX: %w", err)
   541  	}
   542  
   543  	if err := m.connectCEX(ctx, c); err != nil {
   544  		return nil, fmt.Errorf("error connecting to CEX: %w", err)
   545  	}
   546  
   547  	return c, nil
   548  }
   549  
   550  // loadCEX initializes the cex if required and returns the centralizedExchange.
   551  func (m *MarketMaker) loadCEX(ctx context.Context, cfg *CEXConfig) (*centralizedExchange, error) {
   552  	m.cexMtx.Lock()
   553  	defer m.cexMtx.Unlock()
   554  	var success bool
   555  	if cex := m.cexes[cfg.Name]; cex != nil {
   556  		if cex.APIKey == cfg.APIKey && cex.APISecret == cfg.APISecret {
   557  			return cex, nil
   558  		}
   559  		if m.cexInUse(cfg.Name) {
   560  			return nil, fmt.Errorf("CEX %s already in use with different API key", cfg.Name)
   561  		}
   562  		// New credentials. Delete the old cex.
   563  		defer func() {
   564  			if success {
   565  				cex.mtx.Lock()
   566  				cex.cm.Disconnect()
   567  				cex.cm = nil
   568  				cex.mtx.Unlock()
   569  			}
   570  		}()
   571  	}
   572  	logger := m.log.SubLogger(fmt.Sprintf("CEX-%s", cfg.Name))
   573  	cex, err := libxc.NewCEX(cfg.Name, &libxc.CEXConfig{
   574  		APIKey:    cfg.APIKey,
   575  		SecretKey: cfg.APISecret,
   576  		Logger:    logger,
   577  		Net:       m.core.Network(),
   578  		Notify: func(n interface{}) {
   579  			m.handleCEXUpdate(cfg.Name, n)
   580  		},
   581  	})
   582  	if err != nil {
   583  		return nil, fmt.Errorf("failed to create CEX: %v", err)
   584  	}
   585  	c := &centralizedExchange{
   586  		CEX:       cex,
   587  		CEXConfig: cfg,
   588  	}
   589  	c.mkts, err = cex.Markets(ctx)
   590  	if err != nil {
   591  		m.log.Errorf("Failed to get markets for %s: %v", cfg.Name, err)
   592  		c.mkts = make(map[string]*libxc.Market)
   593  		c.connectErr = core.UnwrapErr(err).Error()
   594  	}
   595  	if c.balances, err = c.Balances(ctx); err != nil {
   596  		m.log.Errorf("Failed to get balances for %s: %v", cfg.Name, err)
   597  		c.balances = make(map[uint32]*libxc.ExchangeBalance)
   598  		c.connectErr = core.UnwrapErr(err).Error()
   599  	}
   600  	m.cexes[cfg.Name] = c
   601  	success = true
   602  	return c, nil
   603  }
   604  
   605  func (m *MarketMaker) handleCEXUpdate(cexName string, ni interface{}) {
   606  	switch n := ni.(type) {
   607  	case *libxc.BalanceUpdate:
   608  		m.cexMtx.RLock()
   609  		cex := m.cexes[cexName]
   610  		m.cexMtx.RUnlock()
   611  		if cex == nil {
   612  			m.log.Errorf("CEX update received from unknown cex %q?", cexName)
   613  			return
   614  		}
   615  		cex.mtx.Lock()
   616  		cex.balances[n.AssetID] = n.Balance
   617  		cex.mtx.Unlock()
   618  		m.core.Broadcast(newCexUpdateNote(cexName, TopicBalanceUpdate, ni))
   619  	}
   620  }
   621  
   622  // cexList generates a slice of configured centralizedExchange.
   623  func (m *MarketMaker) cexList() []*centralizedExchange {
   624  	m.cexMtx.RLock()
   625  	defer m.cexMtx.RUnlock()
   626  
   627  	cexes := make([]*centralizedExchange, 0, len(m.cexes))
   628  	for _, cex := range m.cexes {
   629  		cexes = append(cexes, cex)
   630  	}
   631  
   632  	return cexes
   633  }
   634  
   635  func (m *MarketMaker) defaultConfig() *MarketMakingConfig {
   636  	m.defaultCfgMtx.RLock()
   637  	defer m.defaultCfgMtx.RUnlock()
   638  	return m.defaultCfg.Copy()
   639  }
   640  
   641  func (m *MarketMaker) Connect(ctx context.Context) (*sync.WaitGroup, error) {
   642  	m.ctx = ctx
   643  	cfg := m.defaultConfig()
   644  	for _, cexCfg := range cfg.CexConfigs {
   645  		if c, err := m.loadCEX(ctx, cexCfg); err != nil {
   646  			m.log.Errorf("Error adding %s: %v", cexCfg.Name, err)
   647  		} else {
   648  			// Try to connect so we can update our balances and set the
   649  			// connected flag, but ignore errors.
   650  			if err := m.connectCEX(ctx, c); err != nil {
   651  				m.log.Infof("Could not connect to %q: %v", cexCfg.Name, err)
   652  			}
   653  		}
   654  	}
   655  
   656  	eventLogDB, err := newBoltEventLogDB(ctx, m.eventLogDBPath, m.log.SubLogger("eventlogdb"))
   657  	if err != nil {
   658  		return nil, fmt.Errorf("error creating event log DB: %v", err)
   659  	}
   660  	m.eventLogDB = eventLogDB
   661  
   662  	m.oracle = newPriceOracle(m.ctx, m.log.SubLogger("oracle"))
   663  
   664  	var wg sync.WaitGroup
   665  
   666  	wg.Add(1)
   667  	go func() {
   668  		defer wg.Done()
   669  		<-ctx.Done()
   670  
   671  		m.cexMtx.Lock()
   672  		defer m.cexMtx.Unlock()
   673  
   674  		for _, cex := range m.cexes {
   675  			cex.mtx.RLock()
   676  			cm := cex.cm
   677  			cex.mtx.RUnlock()
   678  			if cm != nil {
   679  				cm.Disconnect()
   680  			}
   681  
   682  			delete(m.cexes, cex.Name)
   683  		}
   684  	}()
   685  
   686  	return &wg, nil
   687  }
   688  
   689  func (m *MarketMaker) balancesSufficient(balances *BotBalanceAllocation, mkt *MarketWithHost, cexCfg *CEXConfig) error {
   690  	availableDEXBalances, availableCEXBalances, err := m.availableBalances(mkt, cexCfg)
   691  	if err != nil {
   692  		return fmt.Errorf("error getting available balances: %v", err)
   693  	}
   694  
   695  	for assetID, amount := range balances.DEX {
   696  		availableBalance := availableDEXBalances[assetID]
   697  		if amount > availableBalance {
   698  			return fmt.Errorf("insufficient DEX balance for %s: %d < %d", dex.BipIDSymbol(assetID), availableBalance, amount)
   699  		}
   700  	}
   701  
   702  	for assetID, amount := range balances.CEX {
   703  		availableBalance := availableCEXBalances[assetID]
   704  		if amount > availableBalance {
   705  			return fmt.Errorf("insufficient CEX balance for %s: %d < %d", dex.BipIDSymbol(assetID), availableBalance, amount)
   706  		}
   707  	}
   708  
   709  	return nil
   710  }
   711  
   712  // botCfgForMarket returns the configuration for a bot on a specific market.
   713  // If alternateConfigPath is not nil, the configuration will be loaded from the
   714  // file at that path.
   715  func (m *MarketMaker) configsForMarket(mkt *MarketWithHost, alternateConfigPath *string) (botConfig *BotConfig, cexConfig *CEXConfig, err error) {
   716  	fullCfg := m.defaultConfig()
   717  	if alternateConfigPath != nil {
   718  		fullCfg, err = getMarketMakingConfig(*alternateConfigPath)
   719  		if err != nil {
   720  			return nil, nil, fmt.Errorf("error loading custom market making config: %v", err)
   721  		}
   722  	}
   723  
   724  	for _, c := range fullCfg.BotConfigs {
   725  		if c.Host == mkt.Host && c.BaseID == mkt.BaseID && c.QuoteID == mkt.QuoteID {
   726  			botConfig = c
   727  		}
   728  	}
   729  	if botConfig == nil {
   730  		return nil, nil, fmt.Errorf("no bot config found for %s", mkt)
   731  	}
   732  
   733  	if botConfig.CEXName != "" {
   734  		for _, c := range fullCfg.CexConfigs {
   735  			if c.Name == botConfig.CEXName {
   736  				cexConfig = c
   737  			}
   738  		}
   739  		if cexConfig == nil {
   740  			return nil, nil, fmt.Errorf("no CEX config found for %s", botConfig.CEXName)
   741  		}
   742  	}
   743  
   744  	return
   745  }
   746  
   747  func (m *MarketMaker) botSubLogger(cfg *BotConfig) dex.Logger {
   748  	mktID := dexMarketID(cfg.Host, cfg.BaseID, cfg.QuoteID)
   749  	switch {
   750  	case cfg.BasicMMConfig != nil:
   751  		return m.log.SubLogger(fmt.Sprintf("MM-%s", mktID))
   752  	case cfg.SimpleArbConfig != nil:
   753  		return m.log.SubLogger(fmt.Sprintf("ARB-%s", mktID))
   754  	case cfg.ArbMarketMakerConfig != nil:
   755  		return m.log.SubLogger(fmt.Sprintf("AMM-%s", mktID))
   756  	}
   757  	// This will error in the caller.
   758  	return m.log.SubLogger(fmt.Sprintf("Bot-%s", mktID))
   759  }
   760  
   761  func (m *MarketMaker) cexInUse(cexName string) bool {
   762  	runningBots := m.runningBotsLookup()
   763  	for _, bot := range runningBots {
   764  		if bot.cexName() == cexName {
   765  			return true
   766  		}
   767  	}
   768  	return false
   769  }
   770  
   771  func (m *MarketMaker) newBot(cfg *BotConfig, adaptorCfg *exchangeAdaptorCfg) (bot, error) {
   772  	mktID := dexMarketID(cfg.Host, cfg.BaseID, cfg.QuoteID)
   773  	switch {
   774  	case cfg.ArbMarketMakerConfig != nil:
   775  		return newArbMarketMaker(cfg, adaptorCfg, m.log.SubLogger(fmt.Sprintf("AMM-%s", mktID)))
   776  	case cfg.BasicMMConfig != nil:
   777  		return newBasicMarketMaker(cfg, adaptorCfg, m.oracle, m.log.SubLogger(fmt.Sprintf("MM-%s", mktID)))
   778  	case cfg.SimpleArbConfig != nil:
   779  		return newSimpleArbMarketMaker(cfg, adaptorCfg, m.log.SubLogger(fmt.Sprintf("ARB-%s", mktID)))
   780  	default:
   781  		return nil, fmt.Errorf("not bot config found")
   782  	}
   783  }
   784  
   785  // StartConfig contains the data that must be submitted with a call to StartBot.
   786  type StartConfig struct {
   787  	MarketWithHost
   788  	AutoRebalance *AutoRebalanceConfig  `json:"autoRebalance"`
   789  	Alloc         *BotBalanceAllocation `json:"alloc"`
   790  }
   791  
   792  // StartBot starts a market making bot.
   793  func (m *MarketMaker) StartBot(startCfg *StartConfig, alternateConfigPath *string, appPW []byte) (err error) {
   794  	mkt := startCfg.MarketWithHost
   795  
   796  	m.startUpdateMtx.Lock()
   797  	defer m.startUpdateMtx.Unlock()
   798  
   799  	m.runningBotsMtx.RLock()
   800  	_, found := m.runningBots[startCfg.MarketWithHost]
   801  	m.runningBotsMtx.RUnlock()
   802  	if found {
   803  		return fmt.Errorf("bot for %s already running", mkt)
   804  	}
   805  
   806  	coreMkt, err := m.core.ExchangeMarket(startCfg.Host, startCfg.BaseID, startCfg.QuoteID)
   807  	if err != nil {
   808  		return fmt.Errorf("error getting market: %v", err)
   809  	}
   810  
   811  	for _, ord := range coreMkt.Orders {
   812  		if ord.Status <= order.OrderStatusBooked {
   813  			err = m.core.Cancel(ord.ID)
   814  			if err != nil {
   815  				return fmt.Errorf("error canceling order %s: %v", ord.ID, err)
   816  			}
   817  		}
   818  	}
   819  
   820  	botCfg, cexCfg, err := m.configsForMarket(&startCfg.MarketWithHost, alternateConfigPath)
   821  	if err != nil {
   822  		return err
   823  	}
   824  
   825  	if botCfg.RPCConfig != nil {
   826  		startCfg.Alloc = botCfg.RPCConfig.Alloc
   827  		startCfg.AutoRebalance = botCfg.RPCConfig.AutoRebalance
   828  	}
   829  
   830  	return m.startBot(startCfg, botCfg, cexCfg, appPW)
   831  }
   832  
   833  func (m *MarketMaker) startBot(startCfg *StartConfig, botCfg *BotConfig, cexCfg *CEXConfig, appPW []byte) (err error) {
   834  	mwh := &startCfg.MarketWithHost
   835  	if err := m.balancesSufficient(startCfg.Alloc, mwh, cexCfg); err != nil {
   836  		return err
   837  	}
   838  
   839  	if err := m.loginAndUnlockWallets(appPW, botCfg); err != nil {
   840  		return err
   841  	}
   842  
   843  	var cex *centralizedExchange
   844  	if cexCfg != nil {
   845  		cex, err = m.loadAndConnectCEX(m.ctx, cexCfg)
   846  		if err != nil {
   847  			return fmt.Errorf("error loading %s: %w", cexCfg.Name, err)
   848  		}
   849  	}
   850  
   851  	var startedBot bool
   852  
   853  	requiresOracle := botCfg.requiresPriceOracle()
   854  	if requiresOracle {
   855  		err := m.oracle.startAutoSyncingMarket(botCfg.BaseID, botCfg.QuoteID)
   856  		if err != nil {
   857  			return err
   858  		}
   859  		defer func() {
   860  			if !startedBot {
   861  				m.oracle.stopAutoSyncingMarket(botCfg.BaseID, botCfg.QuoteID)
   862  			}
   863  		}()
   864  	}
   865  
   866  	adaptorCfg := &exchangeAdaptorCfg{
   867  		botID:               dexMarketID(botCfg.Host, botCfg.BaseID, botCfg.QuoteID),
   868  		mwh:                 mwh,
   869  		baseDexBalances:     startCfg.Alloc.DEX,
   870  		baseCexBalances:     startCfg.Alloc.CEX,
   871  		autoRebalanceConfig: startCfg.AutoRebalance,
   872  		core:                m.core,
   873  		cex:                 cex,
   874  		log:                 m.botSubLogger(botCfg),
   875  		botCfg:              botCfg,
   876  		eventLogDB:          m.eventLogDB,
   877  	}
   878  
   879  	bot, err := m.newBot(botCfg, adaptorCfg)
   880  	if err != nil {
   881  		return err
   882  	}
   883  
   884  	cm := dex.NewConnectionMaster(bot)
   885  	if err := cm.ConnectOnce(m.ctx); err != nil {
   886  		return fmt.Errorf("error connecting bot: %w", err)
   887  	}
   888  
   889  	go func() {
   890  		cm.Wait()
   891  		m.runningBotsMtx.Lock()
   892  		if bot, found := m.runningBots[*mwh]; found {
   893  			if bot.botCfg().requiresPriceOracle() {
   894  				m.oracle.stopAutoSyncingMarket(mwh.BaseID, mwh.QuoteID)
   895  			}
   896  			delete(m.runningBots, *mwh)
   897  		}
   898  		m.runningBotsMtx.Unlock()
   899  		m.core.Broadcast(newRunStatsNote(mwh.Host, mwh.BaseID, mwh.QuoteID, nil))
   900  	}()
   901  
   902  	startedBot = true
   903  
   904  	rb := &runningBot{
   905  		bot:    bot,
   906  		cm:     cm,
   907  		cexCfg: cexCfg,
   908  	}
   909  
   910  	m.runningBotsMtx.Lock()
   911  	m.runningBots[*mwh] = rb
   912  	m.runningBotsMtx.Unlock()
   913  
   914  	return nil
   915  }
   916  
   917  // StopBot stops a running bot.
   918  func (m *MarketMaker) StopBot(mkt *MarketWithHost) error {
   919  	runningBots := m.runningBotsLookup()
   920  	bot, found := runningBots[*mkt]
   921  	if !found {
   922  		return fmt.Errorf("no bot running on market: %s", mkt)
   923  	}
   924  	bot.cm.Disconnect()
   925  	m.core.Broadcast(newRunStatsNote(mkt.Host, mkt.BaseID, mkt.QuoteID, nil))
   926  	return nil
   927  }
   928  
   929  func getMarketMakingConfig(path string) (*MarketMakingConfig, error) {
   930  	if path == "" {
   931  		return nil, fmt.Errorf("no config file provided")
   932  	}
   933  
   934  	data, err := os.ReadFile(path)
   935  	if err != nil {
   936  		return nil, err
   937  	}
   938  
   939  	cfg := &MarketMakingConfig{}
   940  	err = json.Unmarshal(data, cfg)
   941  	if err != nil {
   942  		return nil, err
   943  	}
   944  
   945  	return cfg, nil
   946  }
   947  
   948  func (m *MarketMaker) writeConfigFile(cfg *MarketMakingConfig) error {
   949  	data, err := json.MarshalIndent(cfg, "", "    ")
   950  	if err != nil {
   951  		return fmt.Errorf("error marshalling market making config: %v", err)
   952  	}
   953  
   954  	err = os.WriteFile(m.defaultCfgPath, data, 0644)
   955  	if err != nil {
   956  		return fmt.Errorf("error writing market making config: %v", err)
   957  	}
   958  	m.defaultCfgMtx.Lock()
   959  	m.defaultCfg = cfg
   960  	m.defaultCfgMtx.Unlock()
   961  	return nil
   962  }
   963  
   964  func (m *MarketMaker) updateDefaultBotConfig(updatedCfg *BotConfig) {
   965  	cfg := m.defaultConfig()
   966  
   967  	var updated bool
   968  	for i, c := range cfg.BotConfigs {
   969  		if c.Host == updatedCfg.Host && c.QuoteID == updatedCfg.QuoteID && c.BaseID == updatedCfg.BaseID {
   970  			cfg.BotConfigs[i] = updatedCfg
   971  			updated = true
   972  			break
   973  		}
   974  	}
   975  	if !updated {
   976  		cfg.BotConfigs = append(cfg.BotConfigs, updatedCfg)
   977  	}
   978  
   979  	if err := m.writeConfigFile(cfg); err != nil {
   980  		m.log.Errorf("Error saving configuration file: %v", err)
   981  	}
   982  }
   983  
   984  // UpdateBotConfig updates the configuration for one of the bots.
   985  func (m *MarketMaker) UpdateBotConfig(updatedCfg *BotConfig) error {
   986  	m.runningBotsMtx.RLock()
   987  	_, running := m.runningBots[MarketWithHost{updatedCfg.Host, updatedCfg.BaseID, updatedCfg.QuoteID}]
   988  	m.runningBotsMtx.RUnlock()
   989  	if running {
   990  		return fmt.Errorf("call UpdateRunningBotCfg to update the config of a running bot")
   991  	}
   992  
   993  	m.updateDefaultBotConfig(updatedCfg)
   994  	return nil
   995  }
   996  
   997  func (m *MarketMaker) UpdateCEXConfig(updatedCfg *CEXConfig) error {
   998  	_, err := m.loadAndConnectCEX(m.ctx, updatedCfg)
   999  	if err != nil {
  1000  		return fmt.Errorf("error loading %s with updated config: %w", updatedCfg.Name, err)
  1001  	}
  1002  
  1003  	var updated bool
  1004  	m.defaultCfgMtx.Lock()
  1005  	for i, c := range m.defaultCfg.CexConfigs {
  1006  		if c.Name == updatedCfg.Name {
  1007  			m.defaultCfg.CexConfigs[i] = updatedCfg
  1008  			updated = true
  1009  			break
  1010  		}
  1011  	}
  1012  	if !updated {
  1013  		m.defaultCfg.CexConfigs = append(m.defaultCfg.CexConfigs, updatedCfg)
  1014  	}
  1015  	m.defaultCfgMtx.Unlock()
  1016  
  1017  	if err := m.writeConfigFile(m.defaultConfig()); err != nil {
  1018  		m.log.Errorf("Error saving new bot configuration: %w", err)
  1019  	}
  1020  
  1021  	return nil
  1022  }
  1023  
  1024  // RemoveConfig removes a bot config from the market making config.
  1025  func (m *MarketMaker) RemoveBotConfig(host string, baseID, quoteID uint32) error {
  1026  	cfg := m.defaultConfig()
  1027  
  1028  	var updated bool
  1029  	for i, c := range cfg.BotConfigs {
  1030  		if c.Host == host && c.QuoteID == quoteID && c.BaseID == baseID {
  1031  			cfg.BotConfigs = append(cfg.BotConfigs[:i], cfg.BotConfigs[i+1:]...)
  1032  			updated = true
  1033  			break
  1034  		}
  1035  	}
  1036  	if !updated {
  1037  		return fmt.Errorf("config not found")
  1038  	}
  1039  
  1040  	if err := m.writeConfigFile(cfg); err != nil {
  1041  		m.log.Errorf("Error saving updated config file: %v", err)
  1042  	}
  1043  
  1044  	return nil
  1045  }
  1046  
  1047  func validRunningBotCfgUpdate(oldCfg, newCfg *BotConfig) error {
  1048  	if oldCfg.CEXName != "" && newCfg.CEXName == "" {
  1049  		return fmt.Errorf("cannot remove CEX config from running bot")
  1050  	}
  1051  
  1052  	if oldCfg.CEXName != "" && (oldCfg.CEXName != newCfg.CEXName) {
  1053  		return fmt.Errorf("cannot change CEX config for running bot")
  1054  	}
  1055  
  1056  	if oldCfg.BasicMMConfig == nil != (newCfg.BasicMMConfig == nil) {
  1057  		return fmt.Errorf("cannot change bot type for running bot")
  1058  	}
  1059  
  1060  	if oldCfg.SimpleArbConfig == nil != (newCfg.SimpleArbConfig == nil) {
  1061  		return fmt.Errorf("cannot change bot type for running bot")
  1062  	}
  1063  
  1064  	if oldCfg.ArbMarketMakerConfig == nil != (newCfg.ArbMarketMakerConfig == nil) {
  1065  		return fmt.Errorf("cannot change bot type for running bot")
  1066  	}
  1067  
  1068  	return nil
  1069  }
  1070  
  1071  // UpdateRunningBotInventory updates the inventory of a running bot.
  1072  func (m *MarketMaker) UpdateRunningBotInventory(mkt *MarketWithHost, balanceDiffs *BotInventoryDiffs) error {
  1073  	m.startUpdateMtx.Lock()
  1074  	defer m.startUpdateMtx.Unlock()
  1075  
  1076  	m.runningBotsMtx.RLock()
  1077  	rb := m.runningBots[*mkt]
  1078  	m.runningBotsMtx.RUnlock()
  1079  	if rb == nil {
  1080  		return fmt.Errorf("no bot running on market: %s", mkt)
  1081  	}
  1082  
  1083  	if err := m.balancesSufficient(balanceDiffsToAllocation(balanceDiffs), mkt, rb.cexCfg); err != nil {
  1084  		return err
  1085  	}
  1086  
  1087  	if err := rb.withPause(func() error {
  1088  		rb.bot.updateInventory(balanceDiffs)
  1089  		return nil
  1090  	}); err != nil {
  1091  		rb.cm.Disconnect()
  1092  		return fmt.Errorf("configuration update error. bot stopped: %w", err)
  1093  	}
  1094  	return nil
  1095  }
  1096  
  1097  // UpdateRunningBotCfg updates the configuration and balance allocation for a
  1098  // running bot. If saveUpdate is true, the update configuration will be saved
  1099  // to the default config file.
  1100  func (m *MarketMaker) UpdateRunningBotCfg(cfg *BotConfig, balanceDiffs *BotInventoryDiffs, saveUpdate bool) error {
  1101  	m.startUpdateMtx.Lock()
  1102  	defer m.startUpdateMtx.Unlock()
  1103  
  1104  	if cfg == nil {
  1105  		return fmt.Errorf("nil config")
  1106  	}
  1107  
  1108  	mkt := MarketWithHost{cfg.Host, cfg.BaseID, cfg.QuoteID}
  1109  	m.runningBotsMtx.RLock()
  1110  	rb := m.runningBots[mkt]
  1111  	m.runningBotsMtx.RUnlock()
  1112  	if rb == nil {
  1113  		return fmt.Errorf("no bot running on market: %s", mkt)
  1114  	}
  1115  
  1116  	oldCfg := rb.botCfg()
  1117  	if err := validRunningBotCfgUpdate(oldCfg, cfg); err != nil {
  1118  		return err
  1119  	}
  1120  
  1121  	if balanceDiffs != nil {
  1122  		if err := m.balancesSufficient(balanceDiffsToAllocation(balanceDiffs), &mkt, rb.cexCfg); err != nil {
  1123  			return err
  1124  		}
  1125  	}
  1126  
  1127  	var stoppedOracle, startedOracle, updateSuccess bool
  1128  	defer func() {
  1129  		if updateSuccess {
  1130  			return
  1131  		}
  1132  		if startedOracle {
  1133  			m.oracle.stopAutoSyncingMarket(cfg.BaseID, cfg.QuoteID)
  1134  		} else if stoppedOracle {
  1135  			err := m.oracle.startAutoSyncingMarket(oldCfg.BaseID, oldCfg.QuoteID)
  1136  			if err != nil {
  1137  				m.log.Errorf("Error restarting oracle for %s: %v", mkt, err)
  1138  			}
  1139  		}
  1140  	}()
  1141  
  1142  	if !oldCfg.requiresPriceOracle() && cfg.requiresPriceOracle() {
  1143  		err := m.oracle.startAutoSyncingMarket(cfg.BaseID, cfg.QuoteID)
  1144  		if err != nil {
  1145  			return err
  1146  		}
  1147  		startedOracle = true
  1148  	} else if oldCfg.requiresPriceOracle() && !cfg.requiresPriceOracle() {
  1149  		m.oracle.stopAutoSyncingMarket(cfg.BaseID, cfg.QuoteID)
  1150  		stoppedOracle = true
  1151  	}
  1152  
  1153  	if err := rb.withPause(func() error {
  1154  		if err := rb.updateConfig(cfg); err != nil {
  1155  			return err
  1156  		}
  1157  		if balanceDiffs != nil {
  1158  			rb.updateInventory(balanceDiffs)
  1159  		}
  1160  		return nil
  1161  	}); err != nil {
  1162  		rb.cm.Disconnect()
  1163  		return fmt.Errorf("running bot reconfiguration unsuccessful. bot stopped: %w", err)
  1164  	}
  1165  
  1166  	updateSuccess = true
  1167  
  1168  	return nil
  1169  }
  1170  
  1171  // ArchivedRuns returns all archived market making runs.
  1172  func (m *MarketMaker) ArchivedRuns() ([]*MarketMakingRun, error) {
  1173  	allRuns, err := m.eventLogDB.runs(0, nil, nil)
  1174  	if err != nil {
  1175  		return nil, err
  1176  	}
  1177  
  1178  	runningBots := m.runningBotsLookup()
  1179  	archivedRuns := make([]*MarketMakingRun, 0, len(allRuns))
  1180  	for _, run := range allRuns {
  1181  		runningBot := runningBots[*run.Market]
  1182  		if runningBot == nil || runningBot.bot.timeStart() != run.StartTime {
  1183  			archivedRuns = append(archivedRuns, run)
  1184  		}
  1185  	}
  1186  
  1187  	return archivedRuns, nil
  1188  }
  1189  
  1190  // RunOverview returns the overview of a market making run.
  1191  func (m *MarketMaker) RunOverview(startTime int64, mkt *MarketWithHost) (*MarketMakingRunOverview, error) {
  1192  	return m.eventLogDB.runOverview(startTime, mkt)
  1193  }
  1194  
  1195  func (m *MarketMaker) updateDEXOrderEvent(mkt *MarketWithHost, event *MarketMakingEvent) (*MarketMakingEvent, error) {
  1196  	orderEvent := event.DEXOrderEvent
  1197  
  1198  	findEventTx := func(txid string) *asset.WalletTransaction {
  1199  		for _, tx := range orderEvent.Transactions {
  1200  			if tx.ID == txid {
  1201  				return tx
  1202  			}
  1203  		}
  1204  		return nil
  1205  	}
  1206  
  1207  	oidB, err := hex.DecodeString(orderEvent.ID)
  1208  	if err != nil {
  1209  		return nil, fmt.Errorf("error decoding order ID: %v", err)
  1210  	}
  1211  	o, err := m.core.Order(oidB)
  1212  	if err != nil {
  1213  		return nil, fmt.Errorf("error fetching order: %v", err)
  1214  	}
  1215  
  1216  	swapIDs, redeemIDs, refundIDs := orderCoinIDs(o)
  1217  	fromAsset, _, toAsset, _ := orderAssets(mkt.BaseID, mkt.QuoteID, o.Sell)
  1218  	swaps := make(map[string]*asset.WalletTransaction, len(swapIDs))
  1219  	redeems := make(map[string]*asset.WalletTransaction, len(redeemIDs))
  1220  	refunds := make(map[string]*asset.WalletTransaction, len(refundIDs))
  1221  	allTxs := make([]*asset.WalletTransaction, 0, len(orderEvent.Transactions))
  1222  	pendingTx := false
  1223  
  1224  	processTxs := func(assetID uint32, coinIDs map[string]bool, txs map[string]*asset.WalletTransaction) {
  1225  		for coinID := range coinIDs {
  1226  			tx := findEventTx(coinID)
  1227  
  1228  			if tx == nil || !tx.Confirmed {
  1229  				var err error
  1230  				tx, err = m.core.WalletTransaction(assetID, coinID)
  1231  				if err != nil {
  1232  					m.log.Errorf("Error fetching transaction %s for %s: %v", coinID, mkt, err)
  1233  					pendingTx = true
  1234  					continue
  1235  				}
  1236  			}
  1237  
  1238  			txs[tx.ID] = tx
  1239  			allTxs = append(allTxs, tx)
  1240  			pendingTx = pendingTx || !tx.Confirmed
  1241  		}
  1242  	}
  1243  
  1244  	processTxs(fromAsset, swapIDs, swaps)
  1245  	processTxs(toAsset, redeemIDs, redeems)
  1246  	processTxs(fromAsset, refundIDs, refunds)
  1247  
  1248  	var activeMatches bool
  1249  	for _, match := range o.Matches {
  1250  		if match.Active {
  1251  			activeMatches = true
  1252  			break
  1253  		}
  1254  	}
  1255  
  1256  	baseTraits, err := m.core.WalletTraits(mkt.BaseID)
  1257  	if err != nil {
  1258  		return nil, fmt.Errorf("error getting base asset traits: %v", err)
  1259  	}
  1260  
  1261  	quoteTraits, err := m.core.WalletTraits(mkt.QuoteID)
  1262  	if err != nil {
  1263  		return nil, fmt.Errorf("error getting quote asset traits: %v", err)
  1264  	}
  1265  
  1266  	return &MarketMakingEvent{
  1267  		ID:             event.ID,
  1268  		TimeStamp:      event.TimeStamp,
  1269  		Pending:        pendingTx || o.Status <= order.OrderStatusBooked || activeMatches,
  1270  		BalanceEffects: combineBalanceEffects(dexOrderEffects(o, swaps, redeems, refunds, 0, baseTraits, quoteTraits)),
  1271  		DEXOrderEvent: &DEXOrderEvent{
  1272  			ID:           orderEvent.ID,
  1273  			Sell:         o.Sell,
  1274  			Rate:         o.Rate,
  1275  			Qty:          o.Qty,
  1276  			Transactions: allTxs,
  1277  		},
  1278  	}, nil
  1279  }
  1280  
  1281  func (m *MarketMaker) updateCEXOrderEvent(mkt *MarketWithHost, event *MarketMakingEvent, cexName string) (*MarketMakingEvent, error) {
  1282  	cex, err := m.connectedCEX(cexName)
  1283  	if err != nil {
  1284  		return nil, fmt.Errorf("error connecting to CEX: %v", err)
  1285  	}
  1286  
  1287  	orderEvent := event.CEXOrderEvent
  1288  
  1289  	trade, err := cex.TradeStatus(m.ctx, orderEvent.ID, mkt.BaseID, mkt.QuoteID)
  1290  	if err != nil {
  1291  		return nil, fmt.Errorf("error fetching trade status: %v", err)
  1292  	}
  1293  
  1294  	return cexOrderEvent(trade, event.ID, event.TimeStamp), nil
  1295  }
  1296  
  1297  func (m *MarketMaker) updateDepositEvent(event *MarketMakingEvent, cexName string) (*MarketMakingEvent, error) {
  1298  	wt := event.DepositEvent.Transaction
  1299  	if wt == nil {
  1300  		return nil, fmt.Errorf("nil transaction")
  1301  	}
  1302  
  1303  	if !wt.Confirmed {
  1304  		tx, err := m.core.WalletTransaction(event.DepositEvent.AssetID, wt.ID)
  1305  		if err != nil {
  1306  			return nil, fmt.Errorf("error fetching transaction: %v", err)
  1307  		}
  1308  		wt = tx
  1309  	}
  1310  
  1311  	cex, err := m.connectedCEX(cexName)
  1312  	if err != nil {
  1313  		return nil, fmt.Errorf("error connecting to CEX: %v", err)
  1314  	}
  1315  
  1316  	unitInfo, err := asset.UnitInfo(event.DepositEvent.AssetID)
  1317  	if err != nil {
  1318  		return nil, fmt.Errorf("error getting unit info: %v", err)
  1319  	}
  1320  
  1321  	convAmount := float64(wt.Amount) / float64(unitInfo.Conventional.ConversionFactor)
  1322  	confirmed, cexCredit := cex.ConfirmDeposit(m.ctx, &libxc.DepositData{
  1323  		AssetID:            event.DepositEvent.AssetID,
  1324  		AmountConventional: convAmount,
  1325  		TxID:               wt.ID,
  1326  	})
  1327  
  1328  	return &MarketMakingEvent{
  1329  		ID:             event.ID,
  1330  		TimeStamp:      event.TimeStamp,
  1331  		Pending:        !confirmed,
  1332  		BalanceEffects: combineBalanceEffects(depositBalanceEffects(event.DepositEvent.AssetID, wt, confirmed)),
  1333  		DepositEvent: &DepositEvent{
  1334  			Transaction: wt,
  1335  			AssetID:     event.DepositEvent.AssetID,
  1336  			CEXCredit:   cexCredit,
  1337  		},
  1338  	}, nil
  1339  }
  1340  
  1341  func (m *MarketMaker) updateWithdrawalEvent(mkt *MarketWithHost, event *MarketMakingEvent, cexName string) (*MarketMakingEvent, error) {
  1342  	tx := event.WithdrawalEvent.Transaction
  1343  	withdrawalID := event.WithdrawalEvent.ID
  1344  	assetID := event.WithdrawalEvent.AssetID
  1345  	var cexDebit uint64
  1346  	if tx == nil {
  1347  		cex, err := m.connectedCEX(cexName)
  1348  		if err != nil {
  1349  			return nil, fmt.Errorf("error connecting to CEX: %v", err)
  1350  		}
  1351  
  1352  		var txID string
  1353  		cexDebit, txID, err = cex.ConfirmWithdrawal(m.ctx, withdrawalID, assetID)
  1354  		if errors.Is(err, libxc.ErrWithdrawalPending) {
  1355  			return event, nil
  1356  		}
  1357  		if err != nil {
  1358  			return nil, fmt.Errorf("error confirming withdrawal: %v", err)
  1359  		}
  1360  
  1361  		tx, err = m.core.WalletTransaction(assetID, txID)
  1362  		if err != nil {
  1363  			return nil, fmt.Errorf("error fetching transaction: %v", err)
  1364  		}
  1365  	} else {
  1366  		cexDebit = event.WithdrawalEvent.CEXDebit
  1367  	}
  1368  
  1369  	return &MarketMakingEvent{
  1370  		ID:             event.ID,
  1371  		TimeStamp:      event.TimeStamp,
  1372  		BalanceEffects: combineBalanceEffects(withdrawalBalanceEffects(tx, cexDebit, event.WithdrawalEvent.AssetID)),
  1373  		Pending:        tx == nil || !tx.Confirmed,
  1374  		WithdrawalEvent: &WithdrawalEvent{
  1375  			AssetID:     assetID,
  1376  			ID:          withdrawalID,
  1377  			Transaction: tx,
  1378  			CEXDebit:    cexDebit,
  1379  		},
  1380  	}, nil
  1381  }
  1382  
  1383  func (m *MarketMaker) connectedCEX(cexName string) (*centralizedExchange, error) {
  1384  	m.cexMtx.RLock()
  1385  	cex := m.cexes[cexName]
  1386  	m.cexMtx.RUnlock()
  1387  	if cex == nil {
  1388  		return nil, fmt.Errorf("CEX %s not found", cexName)
  1389  	}
  1390  
  1391  	err := m.connectCEX(m.ctx, cex)
  1392  	if err != nil {
  1393  		return nil, fmt.Errorf("error connecting to CEX: %w", err)
  1394  	}
  1395  
  1396  	return cex, nil
  1397  }
  1398  
  1399  // updatePendingEvent looks up the latest state related to a pending
  1400  // MarketMakingEvent returns an updated MarketMakingEvent.
  1401  func (m *MarketMaker) updatePendingEvent(mkt *MarketWithHost, event *MarketMakingEvent, overview *MarketMakingRunOverview) (*MarketMakingEvent, error) {
  1402  	if len(overview.Cfgs) == 0 {
  1403  		return nil, fmt.Errorf("no bot config found for %s", mkt)
  1404  	}
  1405  	cexName := overview.Cfgs[0].Cfg.CEXName // may be empty string, but that's OK
  1406  
  1407  	switch {
  1408  	case event.DEXOrderEvent != nil:
  1409  		return m.updateDEXOrderEvent(mkt, event)
  1410  	case event.CEXOrderEvent != nil:
  1411  		return m.updateCEXOrderEvent(mkt, event, cexName)
  1412  	case event.DepositEvent != nil:
  1413  		return m.updateDepositEvent(event, cexName)
  1414  	case event.WithdrawalEvent != nil:
  1415  		return m.updateWithdrawalEvent(mkt, event, cexName)
  1416  	default:
  1417  		return event, nil
  1418  	}
  1419  }
  1420  
  1421  type RunLogFilters struct {
  1422  	DexBuys     bool `json:"dexBuys"`
  1423  	DexSells    bool `json:"dexSells"`
  1424  	CexBuys     bool `json:"cexBuys"`
  1425  	CexSells    bool `json:"cexSells"`
  1426  	Deposits    bool `json:"deposits"`
  1427  	Withdrawals bool `json:"withdrawals"`
  1428  }
  1429  
  1430  func (f *RunLogFilters) filter(event *MarketMakingEvent) bool {
  1431  	switch {
  1432  	case event.DEXOrderEvent != nil:
  1433  		if event.DEXOrderEvent.Sell {
  1434  			return f.DexSells
  1435  		}
  1436  		return f.DexBuys
  1437  	case event.CEXOrderEvent != nil:
  1438  		if event.CEXOrderEvent.Sell {
  1439  			return f.CexSells
  1440  		}
  1441  		return f.CexBuys
  1442  	case event.DepositEvent != nil:
  1443  		return f.Deposits
  1444  	case event.WithdrawalEvent != nil:
  1445  		return f.Withdrawals
  1446  	default:
  1447  		return false
  1448  	}
  1449  }
  1450  
  1451  var noFilters = &RunLogFilters{
  1452  	DexBuys:     true,
  1453  	DexSells:    true,
  1454  	CexBuys:     true,
  1455  	CexSells:    true,
  1456  	Deposits:    true,
  1457  	Withdrawals: true,
  1458  }
  1459  
  1460  // RunLogs returns the event logs of a market making run. At most n events are
  1461  // returned, if n == 0 then all events are returned. If refID is not nil, then
  1462  // the events including and after refID are returned.
  1463  // Updated events are events that were updated from pending to confirmed during
  1464  // this call. For completed runs, on each call to RunLogs, all pending events are
  1465  // checked for updates, and anything that was updated is returned.
  1466  func (m *MarketMaker) RunLogs(startTime int64, mkt *MarketWithHost, n uint64, refID *uint64, filters *RunLogFilters) (events, updatedEvents []*MarketMakingEvent, overview *MarketMakingRunOverview, err error) {
  1467  	var running bool
  1468  	runningBotsLookup := m.runningBotsLookup()
  1469  	if bot, found := runningBotsLookup[*mkt]; found {
  1470  		running = bot.timeStart() == startTime
  1471  	}
  1472  
  1473  	if filters == nil {
  1474  		filters = noFilters
  1475  	}
  1476  
  1477  	if !running {
  1478  		pendingEvents, err := m.eventLogDB.runEvents(startTime, mkt, 0, nil, true, noFilters)
  1479  		if err != nil {
  1480  			return nil, nil, nil, err
  1481  		}
  1482  		if len(pendingEvents) > 0 {
  1483  			updatedEvents = make([]*MarketMakingEvent, 0, len(pendingEvents))
  1484  			overview, err := m.eventLogDB.runOverview(startTime, mkt)
  1485  			if err != nil {
  1486  				return nil, nil, nil, err
  1487  			}
  1488  			for _, event := range pendingEvents {
  1489  				if event.Pending {
  1490  					updatedEvent, err := m.updatePendingEvent(mkt, event, overview)
  1491  					if err != nil {
  1492  						m.log.Errorf("Error updating pending event: %v", err)
  1493  						continue
  1494  					}
  1495  					updatedEvents = append(updatedEvents, updatedEvent)
  1496  					m.eventLogDB.storeEvent(startTime, mkt, updatedEvent, nil)
  1497  				}
  1498  			}
  1499  		}
  1500  	}
  1501  
  1502  	events, err = m.eventLogDB.runEvents(startTime, mkt, n, refID, false, filters)
  1503  	if err != nil {
  1504  		return nil, nil, nil, err
  1505  	}
  1506  
  1507  	overview, err = m.eventLogDB.runOverview(startTime, mkt)
  1508  	if err != nil {
  1509  		return nil, nil, nil, err
  1510  	}
  1511  
  1512  	return events, updatedEvents, overview, nil
  1513  }
  1514  
  1515  // CEXBook generates a snapshot of the specified CEX order book.
  1516  func (m *MarketMaker) CEXBook(host string, baseID, quoteID uint32) (buys, sells []*core.MiniOrder, _ error) {
  1517  	mwh := MarketWithHost{Host: host, BaseID: baseID, QuoteID: quoteID}
  1518  	m.runningBotsMtx.RLock()
  1519  	bot, found := m.runningBots[mwh]
  1520  	m.runningBotsMtx.RUnlock()
  1521  	if !found {
  1522  		return nil, nil, fmt.Errorf("no running bot found for market %s", mwh)
  1523  	}
  1524  	return bot.Book()
  1525  }
  1526  
  1527  // LotFees are the fees for trading one lot.
  1528  type LotFees struct {
  1529  	Swap   uint64 `json:"swap"`
  1530  	Redeem uint64 `json:"redeem"`
  1531  	Refund uint64 `json:"refund"`
  1532  }
  1533  
  1534  // LotFeeRange combine the estimated and maximum LotFees.
  1535  type LotFeeRange struct {
  1536  	Max       *LotFees `json:"max"`
  1537  	Estimated *LotFees `json:"estimated"`
  1538  }
  1539  
  1540  // marketFees calculates the LotFees for the base and quote assets.
  1541  func marketFees(c clientCore, host string, baseID, quoteID uint32, useMaxFeeRate bool) (baseFees, quoteFees *LotFees, _ error) {
  1542  	buySwapFees, buyRedeemFees, buyRefundFees, err := c.SingleLotFees(&core.SingleLotFeesForm{
  1543  		Host:          host,
  1544  		Base:          baseID,
  1545  		Quote:         quoteID,
  1546  		UseMaxFeeRate: useMaxFeeRate,
  1547  		UseSafeTxSize: useMaxFeeRate,
  1548  	})
  1549  	if err != nil {
  1550  		return nil, nil, fmt.Errorf("failed to get buy single lot fees: %v", err)
  1551  	}
  1552  
  1553  	sellSwapFees, sellRedeemFees, sellRefundFees, err := c.SingleLotFees(&core.SingleLotFeesForm{
  1554  		Host:          host,
  1555  		Base:          baseID,
  1556  		Quote:         quoteID,
  1557  		UseMaxFeeRate: useMaxFeeRate,
  1558  		UseSafeTxSize: useMaxFeeRate,
  1559  		Sell:          true,
  1560  	})
  1561  	if err != nil {
  1562  		return nil, nil, fmt.Errorf("failed to get sell single lot fees: %v", err)
  1563  	}
  1564  
  1565  	return &LotFees{
  1566  			Swap:   sellSwapFees,
  1567  			Redeem: buyRedeemFees,
  1568  			Refund: sellRefundFees,
  1569  		}, &LotFees{
  1570  			Swap:   buySwapFees,
  1571  			Redeem: sellRedeemFees,
  1572  			Refund: buyRefundFees,
  1573  		}, nil
  1574  }
  1575  
  1576  func (m *MarketMaker) availableBalances(mkt *MarketWithHost, cexCfg *CEXConfig) (dexBalances, cexBalances map[uint32]uint64, _ error) {
  1577  	dexAssets := make(map[uint32]interface{})
  1578  	cexAssets := make(map[uint32]interface{})
  1579  
  1580  	dexAssets[mkt.BaseID] = struct{}{}
  1581  	dexAssets[mkt.QuoteID] = struct{}{}
  1582  	dexAssets[feeAssetID(mkt.BaseID)] = struct{}{}
  1583  	dexAssets[feeAssetID(mkt.QuoteID)] = struct{}{}
  1584  
  1585  	if cexCfg != nil {
  1586  		cexAssets[mkt.BaseID] = struct{}{}
  1587  		cexAssets[mkt.QuoteID] = struct{}{}
  1588  	}
  1589  
  1590  	checkTotalBalances := func() (dexBals, cexBals map[uint32]uint64, err error) {
  1591  		dexBals = make(map[uint32]uint64, len(dexAssets))
  1592  		cexBals = make(map[uint32]uint64, len(cexAssets))
  1593  
  1594  		for assetID := range dexAssets {
  1595  			bal, err := m.core.AssetBalance(assetID)
  1596  			if err != nil {
  1597  				return nil, nil, err
  1598  			}
  1599  			dexBals[assetID] = bal.Available
  1600  		}
  1601  
  1602  		if cexCfg != nil {
  1603  			cex, err := m.loadAndConnectCEX(m.ctx, cexCfg)
  1604  			if err != nil {
  1605  				return nil, nil, err
  1606  			}
  1607  
  1608  			for assetID := range cexAssets {
  1609  				balance, err := cex.Balance(assetID)
  1610  				if err != nil {
  1611  					return nil, nil, err
  1612  				}
  1613  
  1614  				cexBals[assetID] = balance.Available
  1615  			}
  1616  		}
  1617  
  1618  		return dexBals, cexBals, nil
  1619  	}
  1620  
  1621  	checkBot := func(bot *runningBot) bool {
  1622  		botAssets := bot.assets()
  1623  		for assetID := range dexAssets {
  1624  			if _, found := botAssets[assetID]; found {
  1625  				return true
  1626  			}
  1627  		}
  1628  		return false
  1629  	}
  1630  
  1631  	balancesEqual := func(bal1, bal2 map[uint32]uint64) bool {
  1632  		if len(bal1) != len(bal2) {
  1633  			return false
  1634  		}
  1635  		for assetID, bal := range bal1 {
  1636  			if bal2[assetID] != bal {
  1637  				return false
  1638  			}
  1639  		}
  1640  		return true
  1641  	}
  1642  
  1643  	// We first check the available balances in the DEX wallets and on
  1644  	// the CEX, then check the amounts reserved by the running bots,
  1645  	// and then recheck the amounts available on the DEX and CEX. If
  1646  	// the available balances in the first and last checks are equal,
  1647  	// then we know that nothing has changed. If not, we try again.
  1648  	totalDEXBalances, totalCEXBalances, err := checkTotalBalances()
  1649  	if err != nil {
  1650  		return nil, nil, err
  1651  	}
  1652  
  1653  	const maxTries = 5
  1654  	for i := 0; i < maxTries; i++ {
  1655  		reservedDEXBalances := make(map[uint32]uint64, len(dexAssets))
  1656  		reservedCEXBalances := make(map[uint32]uint64, len(cexAssets))
  1657  
  1658  		runningBots := m.runningBotsLookup()
  1659  		for _, rb := range runningBots {
  1660  			if !checkBot(rb) {
  1661  				continue
  1662  			}
  1663  
  1664  			rb.refreshAllPendingEvents(m.ctx)
  1665  
  1666  			for assetID := range dexAssets {
  1667  				botBalance := rb.DEXBalance(assetID)
  1668  				reservedDEXBalances[assetID] += botBalance.Available
  1669  			}
  1670  
  1671  			if cexCfg != nil && rb.cexName() == cexCfg.Name {
  1672  				for assetID := range cexAssets {
  1673  					botBalance := rb.CEXBalance(assetID)
  1674  					reservedCEXBalances[assetID] += botBalance.Available + botBalance.Reserved
  1675  				}
  1676  			}
  1677  		}
  1678  
  1679  		updatedDEXBalances, updatedCEXBalances, err := checkTotalBalances()
  1680  		if err != nil {
  1681  			return nil, nil, err
  1682  		}
  1683  
  1684  		if balancesEqual(updatedDEXBalances, totalDEXBalances) && balancesEqual(updatedCEXBalances, totalCEXBalances) {
  1685  			for assetID, bal := range reservedDEXBalances {
  1686  				if bal > totalDEXBalances[assetID] {
  1687  					m.log.Warnf("reserved DEX balance for %s exceeds available balance: %d > %d", dex.BipIDSymbol(assetID), bal, totalDEXBalances[assetID])
  1688  					totalDEXBalances[assetID] = 0
  1689  				} else {
  1690  					totalDEXBalances[assetID] -= bal
  1691  				}
  1692  			}
  1693  			for assetID, bal := range reservedCEXBalances {
  1694  				if bal > totalCEXBalances[assetID] {
  1695  					m.log.Warnf("reserved CEX balance for %s exceeds available balance: %d > %d", dex.BipIDSymbol(assetID), bal, totalCEXBalances[assetID])
  1696  					totalCEXBalances[assetID] = 0
  1697  				} else {
  1698  					totalCEXBalances[assetID] -= bal
  1699  				}
  1700  			}
  1701  			return totalDEXBalances, totalCEXBalances, nil
  1702  		}
  1703  
  1704  		totalDEXBalances = updatedDEXBalances
  1705  		totalCEXBalances = updatedCEXBalances
  1706  	}
  1707  
  1708  	return nil, nil, fmt.Errorf("failed to get available balances after %d tries", maxTries)
  1709  }
  1710  
  1711  // AvailableBalances returns the available balances of assets relevant to
  1712  // market making on the specified market on the DEX (including fee assets),
  1713  // and optionally a CEX depending on the configured strategy.
  1714  func (m *MarketMaker) AvailableBalances(mkt *MarketWithHost, alternateConfigPath *string) (dexBalances, cexBalances map[uint32]uint64, _ error) {
  1715  	_, cexCfg, err := m.configsForMarket(mkt, alternateConfigPath)
  1716  	if err != nil {
  1717  		return nil, nil, err
  1718  	}
  1719  
  1720  	return m.availableBalances(mkt, cexCfg)
  1721  }
  1722  
  1723  func sellStr(sell bool) string {
  1724  	if sell {
  1725  		return "sell"
  1726  	}
  1727  	return "buy"
  1728  }