decred.org/dcrdex@v1.0.5/client/mm/exchange_adaptor.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/json"
     9  	"errors"
    10  	"fmt"
    11  	"math"
    12  	"sort"
    13  	"strconv"
    14  	"strings"
    15  	"sync"
    16  	"sync/atomic"
    17  	"time"
    18  
    19  	"decred.org/dcrdex/client/asset"
    20  	"decred.org/dcrdex/client/core"
    21  	"decred.org/dcrdex/client/mm/libxc"
    22  	"decred.org/dcrdex/client/orderbook"
    23  	"decred.org/dcrdex/dex"
    24  	"decred.org/dcrdex/dex/calc"
    25  	"decred.org/dcrdex/dex/order"
    26  	"decred.org/dcrdex/dex/utils"
    27  )
    28  
    29  // BotBalance keeps track of the amount of funds available for a
    30  // bot's use, locked to fund orders, and pending.
    31  type BotBalance struct {
    32  	Available uint64 `json:"available"`
    33  	Locked    uint64 `json:"locked"`
    34  	Pending   uint64 `json:"pending"`
    35  	Reserved  uint64 `json:"reserved"`
    36  }
    37  
    38  func (b *BotBalance) copy() *BotBalance {
    39  	return &BotBalance{
    40  		Available: b.Available,
    41  		Locked:    b.Locked,
    42  		Pending:   b.Pending,
    43  		Reserved:  b.Reserved,
    44  	}
    45  }
    46  
    47  // OrderFees represents the fees that will be required for a single lot of a
    48  // dex order.
    49  type OrderFees struct {
    50  	*LotFeeRange
    51  	Funding uint64 `json:"funding"`
    52  	// bookingFeesPerLot is the amount of fee asset that needs to be reserved
    53  	// for fees, per ordered lot. For all assets, this will include
    54  	// LotFeeRange.Max.Swap. For non-token EVM assets (eth, matic) Max.Refund
    55  	// will be added. If the asset is the parent chain of a token counter-asset,
    56  	// Max.Redeem is added. This is a commonly needed sum in various validation
    57  	// and optimization functions.
    58  	BookingFeesPerLot uint64 `json:"bookingFeesPerLot"`
    59  }
    60  
    61  // botCoreAdaptor is an interface used by bots to access DEX related
    62  // functions. Common functionality used by multiple market making
    63  // strategies is implemented here. The functions in this interface
    64  // do not need to take assetID parameters, as the bot will only be
    65  // trading on a single DEX market.
    66  type botCoreAdaptor interface {
    67  	SyncBook(host string, base, quote uint32) (*orderbook.OrderBook, core.BookFeed, error)
    68  	Cancel(oidB dex.Bytes) error
    69  	DEXTrade(rate, qty uint64, sell bool) (*core.Order, error)
    70  	ExchangeMarket(host string, baseID, quoteID uint32) (*core.Market, error)
    71  	ExchangeRateFromFiatSources() uint64
    72  	OrderFeesInUnits(sell, base bool, rate uint64) (uint64, error) // estimated fees, not max
    73  	SubscribeOrderUpdates() (updates <-chan *core.Order)
    74  	SufficientBalanceForDEXTrade(rate, qty uint64, sell bool) (bool, error)
    75  }
    76  
    77  // botCexAdaptor is an interface used by bots to access CEX related
    78  // functions. Common functionality used by multiple market making
    79  // strategies is implemented here. The functions in this interface
    80  // take assetID parameters, unlike botCoreAdaptor, to support a
    81  // multi-hop strategy.
    82  type botCexAdaptor interface {
    83  	CancelTrade(ctx context.Context, baseID, quoteID uint32, tradeID string) error
    84  	SubscribeMarket(ctx context.Context, baseID, quoteID uint32) error
    85  	SubscribeTradeUpdates() <-chan *libxc.Trade
    86  	CEXTrade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64) (*libxc.Trade, error)
    87  	SufficientBalanceForCEXTrade(baseID, quoteID uint32, sell bool, rate, qty uint64) bool
    88  	MidGap(baseID, quoteID uint32) uint64
    89  	Book() (buys, sells []*core.MiniOrder, _ error)
    90  }
    91  
    92  // BalanceEffects represents the effects that a market making event has on
    93  // the bot's balances.
    94  type BalanceEffects struct {
    95  	Settled  map[uint32]int64  `json:"settled"`
    96  	Locked   map[uint32]uint64 `json:"locked"`
    97  	Pending  map[uint32]uint64 `json:"pending"`
    98  	Reserved map[uint32]uint64 `json:"reserved"`
    99  }
   100  
   101  func newBalanceEffects() *BalanceEffects {
   102  	return &BalanceEffects{
   103  		Settled:  make(map[uint32]int64),
   104  		Locked:   make(map[uint32]uint64),
   105  		Pending:  make(map[uint32]uint64),
   106  		Reserved: make(map[uint32]uint64),
   107  	}
   108  }
   109  
   110  type balanceEffectsDiff struct {
   111  	settled  map[uint32]int64
   112  	locked   map[uint32]int64
   113  	pending  map[uint32]int64
   114  	reserved map[uint32]int64
   115  }
   116  
   117  func newBalanceEffectsDiff() *balanceEffectsDiff {
   118  	return &balanceEffectsDiff{
   119  		settled:  make(map[uint32]int64),
   120  		locked:   make(map[uint32]int64),
   121  		pending:  make(map[uint32]int64),
   122  		reserved: make(map[uint32]int64),
   123  	}
   124  }
   125  
   126  func (b *BalanceEffects) sub(other *BalanceEffects) *balanceEffectsDiff {
   127  	res := newBalanceEffectsDiff()
   128  
   129  	for assetID, v := range b.Settled {
   130  		res.settled[assetID] = v
   131  	}
   132  	for assetID, v := range b.Locked {
   133  		res.locked[assetID] = int64(v)
   134  	}
   135  	for assetID, v := range b.Pending {
   136  		res.pending[assetID] = int64(v)
   137  	}
   138  	for assetID, v := range b.Reserved {
   139  		res.reserved[assetID] = int64(v)
   140  	}
   141  
   142  	for assetID, v := range other.Settled {
   143  		res.settled[assetID] -= v
   144  	}
   145  	for assetID, v := range other.Locked {
   146  		res.locked[assetID] -= int64(v)
   147  	}
   148  	for assetID, v := range other.Pending {
   149  		res.pending[assetID] -= int64(v)
   150  	}
   151  	for assetID, v := range other.Reserved {
   152  		res.reserved[assetID] -= int64(v)
   153  	}
   154  
   155  	return res
   156  }
   157  
   158  // pendingWithdrawal represents a withdrawal from a CEX that has been
   159  // initiated, but the DEX has not yet received.
   160  type pendingWithdrawal struct {
   161  	eventLogID   uint64
   162  	timestamp    int64
   163  	withdrawalID string
   164  	assetID      uint32
   165  	// amtWithdrawn is the amount the CEX balance is decreased by.
   166  	// It will not be the same as the amount received in the dex wallet.
   167  	amtWithdrawn uint64
   168  
   169  	txMtx sync.RWMutex
   170  	txID  string
   171  	tx    *asset.WalletTransaction
   172  }
   173  
   174  func withdrawalBalanceEffects(tx *asset.WalletTransaction, cexDebit uint64, assetID uint32) (dex, cex *BalanceEffects) {
   175  	dex = newBalanceEffects()
   176  	cex = newBalanceEffects()
   177  
   178  	cex.Settled[assetID] = -int64(cexDebit)
   179  
   180  	if tx != nil {
   181  		if tx.Confirmed {
   182  			dex.Settled[assetID] += int64(tx.Amount)
   183  		} else {
   184  			dex.Pending[assetID] += tx.Amount
   185  		}
   186  	} else {
   187  		dex.Pending[assetID] += cexDebit
   188  	}
   189  
   190  	return
   191  }
   192  
   193  func (w *pendingWithdrawal) balanceEffects() (dex, cex *BalanceEffects) {
   194  	w.txMtx.RLock()
   195  	defer w.txMtx.RUnlock()
   196  
   197  	return withdrawalBalanceEffects(w.tx, w.amtWithdrawn, w.assetID)
   198  }
   199  
   200  // pendingDeposit represents a deposit to a CEX that has not yet been
   201  // confirmed.
   202  type pendingDeposit struct {
   203  	eventLogID      uint64
   204  	timestamp       int64
   205  	assetID         uint32
   206  	amtConventional float64
   207  
   208  	mtx          sync.RWMutex
   209  	tx           *asset.WalletTransaction
   210  	feeConfirmed bool
   211  	cexConfirmed bool
   212  	amtCredited  uint64
   213  }
   214  
   215  func depositBalanceEffects(assetID uint32, tx *asset.WalletTransaction, cexConfirmed bool) (dex, cex *BalanceEffects) {
   216  	feeAsset := assetID
   217  	token := asset.TokenInfo(assetID)
   218  	if token != nil {
   219  		feeAsset = token.ParentID
   220  	}
   221  
   222  	dex, cex = newBalanceEffects(), newBalanceEffects()
   223  
   224  	dex.Settled[assetID] -= int64(tx.Amount)
   225  	dex.Settled[feeAsset] -= int64(tx.Fees)
   226  
   227  	if cexConfirmed {
   228  		cex.Settled[assetID] += int64(tx.Amount)
   229  	} else {
   230  		cex.Pending[assetID] += tx.Amount
   231  	}
   232  
   233  	return dex, cex
   234  }
   235  
   236  func (d *pendingDeposit) balanceEffects() (dex, cex *BalanceEffects) {
   237  	d.mtx.RLock()
   238  	defer d.mtx.RUnlock()
   239  
   240  	return depositBalanceEffects(d.assetID, d.tx, d.cexConfirmed)
   241  }
   242  
   243  type dexOrderState struct {
   244  	dexBalanceEffects *BalanceEffects
   245  	cexBalanceEffects *BalanceEffects
   246  	order             *core.Order
   247  	counterTradeRate  uint64
   248  }
   249  
   250  // pendingDEXOrder keeps track of the balance effects of a pending DEX order.
   251  // The actual order is not stored here, only its effects on the balance.
   252  type pendingDEXOrder struct {
   253  	eventLogID uint64
   254  	timestamp  int64
   255  
   256  	// swaps, redeems, and refunds are caches of transactions. This avoids
   257  	// having to query the wallet for transactions that are already confirmed.
   258  	txsMtx             sync.RWMutex
   259  	swaps              map[string]*asset.WalletTransaction
   260  	swapCoinIDToTxID   map[string]string
   261  	redeems            map[string]*asset.WalletTransaction
   262  	redeemCoinIDToTxID map[string]string
   263  	refunds            map[string]*asset.WalletTransaction
   264  	refundCoinIDToTxID map[string]string
   265  	// txsMtx is required to be locked for writes to state
   266  	state atomic.Value // *dexOrderState
   267  
   268  	// placementIndex/counterTradeRate are used by MultiTrade to know
   269  	// which orders to place/cancel.
   270  	placementIndex   uint64
   271  	counterTradeRate uint64
   272  }
   273  
   274  func (p *pendingDEXOrder) cexBalanceEffects() *BalanceEffects {
   275  	return p.currentState().cexBalanceEffects
   276  }
   277  
   278  // currentState can be called without locking, but to get a consistent view of
   279  // the transactions and the state, txsMtx should be read locked.
   280  func (p *pendingDEXOrder) currentState() *dexOrderState {
   281  	return p.state.Load().(*dexOrderState)
   282  }
   283  
   284  // counterTradeAsset is the asset that the bot will need to trade on the CEX
   285  // to arbitrage matches on the DEX.
   286  func (p *pendingDEXOrder) counterTradeAsset() uint32 {
   287  	o := p.currentState().order
   288  	if o.Sell {
   289  		return o.QuoteID
   290  	}
   291  	return o.BaseID
   292  }
   293  
   294  type pendingCEXOrder struct {
   295  	eventLogID uint64
   296  	timestamp  int64
   297  
   298  	tradeMtx sync.RWMutex
   299  	trade    *libxc.Trade
   300  }
   301  
   302  // market is the market-related data for the unifiedExchangeAdaptor and the
   303  // calculators. market provides a number of methods for conversions and
   304  // formatting.
   305  type market struct {
   306  	host        string
   307  	name        string
   308  	rateStep    atomic.Uint64
   309  	lotSize     atomic.Uint64
   310  	baseID      uint32
   311  	baseTicker  string
   312  	bui         dex.UnitInfo
   313  	baseFeeID   uint32
   314  	baseFeeUI   dex.UnitInfo
   315  	quoteID     uint32
   316  	quoteTicker string
   317  	qui         dex.UnitInfo
   318  	quoteFeeID  uint32
   319  	quoteFeeUI  dex.UnitInfo
   320  }
   321  
   322  func parseMarket(host string, mkt *core.Market) (*market, error) {
   323  	bui, err := asset.UnitInfo(mkt.BaseID)
   324  	if err != nil {
   325  		return nil, err
   326  	}
   327  	baseFeeID := mkt.BaseID
   328  	baseFeeUI := bui
   329  	if tkn := asset.TokenInfo(mkt.BaseID); tkn != nil {
   330  		baseFeeID = tkn.ParentID
   331  		baseFeeUI, err = asset.UnitInfo(tkn.ParentID)
   332  		if err != nil {
   333  			return nil, err
   334  		}
   335  	}
   336  	qui, err := asset.UnitInfo(mkt.QuoteID)
   337  	if err != nil {
   338  		return nil, err
   339  	}
   340  	quoteFeeID := mkt.QuoteID
   341  	quoteFeeUI := qui
   342  	if tkn := asset.TokenInfo(mkt.QuoteID); tkn != nil {
   343  		quoteFeeID = tkn.ParentID
   344  		quoteFeeUI, err = asset.UnitInfo(tkn.ParentID)
   345  		if err != nil {
   346  			return nil, err
   347  		}
   348  	}
   349  
   350  	m := &market{
   351  		host:        host,
   352  		name:        mkt.Name,
   353  		baseID:      mkt.BaseID,
   354  		baseTicker:  bui.Conventional.Unit,
   355  		bui:         bui,
   356  		baseFeeID:   baseFeeID,
   357  		baseFeeUI:   baseFeeUI,
   358  		quoteID:     mkt.QuoteID,
   359  		quoteTicker: qui.Conventional.Unit,
   360  		qui:         qui,
   361  		quoteFeeID:  quoteFeeID,
   362  		quoteFeeUI:  quoteFeeUI,
   363  	}
   364  	m.lotSize.Store(mkt.LotSize)
   365  	m.rateStep.Store(mkt.RateStep)
   366  	return m, nil
   367  }
   368  
   369  func (m *market) fmtRate(msgRate uint64) string {
   370  	r := calc.ConventionalRate(msgRate, m.bui, m.qui)
   371  	s := strconv.FormatFloat(r, 'f', 8, 64)
   372  	if strings.Contains(s, ".") {
   373  		s = strings.TrimRight(strings.TrimRight(s, "0"), ".")
   374  	}
   375  	return s
   376  }
   377  func (m *market) fmtBase(atoms uint64) string {
   378  	return m.bui.FormatAtoms(atoms)
   379  }
   380  func (m *market) fmtQuote(atoms uint64) string {
   381  	return m.qui.FormatAtoms(atoms)
   382  }
   383  func (m *market) fmtQty(assetID uint32, atoms uint64) string {
   384  	if assetID == m.baseID {
   385  		return m.fmtBase(atoms)
   386  	}
   387  	return m.fmtQuote(atoms)
   388  }
   389  
   390  func (m *market) fmtBaseFees(atoms uint64) string {
   391  	return m.baseFeeUI.FormatAtoms(atoms)
   392  }
   393  
   394  func (m *market) fmtQuoteFees(atoms uint64) string {
   395  	return m.quoteFeeUI.FormatAtoms(atoms)
   396  }
   397  func (m *market) fmtFees(assetID uint32, atoms uint64) string {
   398  	if assetID == m.baseID {
   399  		return m.fmtBaseFees(atoms)
   400  	}
   401  	return m.fmtQuoteFees(atoms)
   402  }
   403  
   404  func (m *market) msgRate(convRate float64) uint64 {
   405  	return calc.MessageRate(convRate, m.bui, m.qui)
   406  }
   407  
   408  // unifiedExchangeAdaptor implements both botCoreAdaptor and botCexAdaptor.
   409  type unifiedExchangeAdaptor struct {
   410  	*market
   411  	clientCore
   412  	libxc.CEX
   413  
   414  	ctx             context.Context
   415  	kill            context.CancelFunc
   416  	wg              sync.WaitGroup
   417  	botID           string
   418  	log             dex.Logger
   419  	fiatRates       atomic.Value // map[uint32]float64
   420  	orderUpdates    atomic.Value // chan *core.Order
   421  	mwh             *MarketWithHost
   422  	eventLogDB      eventLogDB
   423  	botCfgV         atomic.Value // *BotConfig
   424  	initialBalances map[uint32]uint64
   425  	baseTraits      asset.WalletTrait
   426  	quoteTraits     asset.WalletTrait
   427  
   428  	botLooper dex.Connector
   429  	botLoop   *dex.ConnectionMaster
   430  	paused    atomic.Bool
   431  
   432  	autoRebalanceCfg *AutoRebalanceConfig
   433  
   434  	subscriptionIDMtx sync.RWMutex
   435  	subscriptionID    *int
   436  
   437  	feesMtx  sync.RWMutex
   438  	buyFees  *OrderFees
   439  	sellFees *OrderFees
   440  
   441  	startTime  atomic.Int64
   442  	eventLogID atomic.Uint64
   443  
   444  	balancesMtx sync.RWMutex
   445  	// baseDEXBalance/baseCEXBalance are the balances the bots have before
   446  	// taking into account any pending actions. These are updated whenever
   447  	// a pending action is completed. They may become negative if a balance
   448  	// is decreased during an update while there are pending actions that
   449  	// positively affect the available balance.
   450  	baseDexBalances    map[uint32]int64
   451  	baseCexBalances    map[uint32]int64
   452  	pendingDEXOrders   map[order.OrderID]*pendingDEXOrder
   453  	pendingCEXOrders   map[string]*pendingCEXOrder
   454  	pendingWithdrawals map[string]*pendingWithdrawal
   455  	pendingDeposits    map[string]*pendingDeposit
   456  	inventoryMods      map[uint32]int64
   457  
   458  	// If pendingBaseRebalance/pendingQuoteRebalance are true, it means
   459  	// there is a pending deposit/withdrawal of the base/quote asset,
   460  	// and no other deposits/withdrawals of that asset should happen
   461  	// until it is complete.
   462  	pendingBaseRebalance  atomic.Bool
   463  	pendingQuoteRebalance atomic.Bool
   464  
   465  	// The following are updated whenever a pending action is complete.
   466  	// For accurate run stats, the pending actions must be taken into
   467  	// account.
   468  	runStats struct {
   469  		completedMatches atomic.Uint32
   470  		tradedUSD        struct {
   471  			sync.Mutex
   472  			v float64
   473  		}
   474  		feeGapStats atomic.Value
   475  	}
   476  
   477  	epochReport atomic.Value // *EpochReport
   478  
   479  	cexProblemsMtx sync.RWMutex
   480  	cexProblems    *CEXProblems
   481  }
   482  
   483  var _ botCoreAdaptor = (*unifiedExchangeAdaptor)(nil)
   484  var _ botCexAdaptor = (*unifiedExchangeAdaptor)(nil)
   485  
   486  func (u *unifiedExchangeAdaptor) botCfg() *BotConfig {
   487  	return u.botCfgV.Load().(*BotConfig)
   488  }
   489  
   490  // botLooper is just a dex.Connector for a function.
   491  type botLooper func(context.Context) (*sync.WaitGroup, error)
   492  
   493  func (f botLooper) Connect(ctx context.Context) (*sync.WaitGroup, error) {
   494  	return f(ctx)
   495  }
   496  
   497  // setBotLoop sets the loop that must be shut down for configuration updates.
   498  // Every bot should call setBotLoop during construction.
   499  func (u *unifiedExchangeAdaptor) setBotLoop(f botLooper) {
   500  	u.botLooper = f
   501  }
   502  
   503  func (u *unifiedExchangeAdaptor) runBotLoop(ctx context.Context) error {
   504  	if u.botLooper == nil {
   505  		return errors.New("no bot looper set")
   506  	}
   507  	u.botLoop = dex.NewConnectionMaster(u.botLooper)
   508  	return u.botLoop.ConnectOnce(ctx)
   509  }
   510  
   511  // withPause runs a function with the bot loop paused.
   512  func (u *unifiedExchangeAdaptor) withPause(f func() error) error {
   513  	if !u.paused.CompareAndSwap(false, true) {
   514  		return errors.New("already paused")
   515  	}
   516  	defer u.paused.Store(false)
   517  
   518  	u.botLoop.Disconnect()
   519  
   520  	if err := f(); err != nil {
   521  		return err
   522  	}
   523  	if u.ctx.Err() != nil { // Make sure we weren't shut down during pause.
   524  		return u.ctx.Err()
   525  	}
   526  
   527  	return u.botLoop.ConnectOnce(u.ctx)
   528  }
   529  
   530  // logBalanceAdjustments logs a trace log of balance adjustments and updated
   531  // settled balances.
   532  //
   533  // balancesMtx must be read locked when calling this function.
   534  func (u *unifiedExchangeAdaptor) logBalanceAdjustments(dexDiffs, cexDiffs map[uint32]int64, reason string) {
   535  	if u.log.Level() > dex.LevelTrace {
   536  		return
   537  	}
   538  
   539  	var msg strings.Builder
   540  	writeLine := func(s string, a ...interface{}) {
   541  		msg.WriteString("\n" + fmt.Sprintf(s, a...))
   542  	}
   543  	writeLine("")
   544  	writeLine("Balance adjustments(%s):", reason)
   545  
   546  	format := func(assetID uint32, v int64, plusSign bool) string {
   547  		ui, err := asset.UnitInfo(assetID)
   548  		if err != nil {
   549  			return "<what the asset?>"
   550  		}
   551  		return ui.FormatSignedAtoms(v, plusSign)
   552  	}
   553  
   554  	if len(dexDiffs) > 0 {
   555  		writeLine("  DEX:")
   556  		for assetID, dexDiff := range dexDiffs {
   557  			writeLine("    %s", format(assetID, dexDiff, true))
   558  		}
   559  	}
   560  
   561  	if len(cexDiffs) > 0 {
   562  		writeLine("  CEX:")
   563  		for assetID, cexDiff := range cexDiffs {
   564  			writeLine("    %s", format(assetID, cexDiff, true))
   565  		}
   566  	}
   567  
   568  	writeLine("Updated settled balances:")
   569  	writeLine("  DEX:")
   570  
   571  	for assetID, bal := range u.baseDexBalances {
   572  		writeLine("    %s", format(assetID, bal, false))
   573  	}
   574  	if len(u.baseCexBalances) > 0 {
   575  		writeLine("  CEX:")
   576  		for assetID, bal := range u.baseCexBalances {
   577  			writeLine("    %s", format(assetID, bal, false))
   578  		}
   579  	}
   580  
   581  	dexPending := make(map[uint32]uint64)
   582  	addDexPending := func(assetID uint32) {
   583  		if v := u.dexBalance(assetID).Pending; v > 0 {
   584  			dexPending[assetID] = v
   585  		}
   586  	}
   587  	cexPending := make(map[uint32]uint64)
   588  	addCexPending := func(assetID uint32) {
   589  		if v := u.cexBalance(assetID).Pending; v > 0 {
   590  			cexPending[assetID] = v
   591  		}
   592  	}
   593  	addDexPending(u.baseID)
   594  	addCexPending(u.baseID)
   595  	addDexPending(u.quoteID)
   596  	addCexPending(u.quoteID)
   597  	if u.baseFeeID != u.baseID {
   598  		addCexPending(u.baseFeeID)
   599  		addCexPending(u.baseFeeID)
   600  	}
   601  	if u.quoteFeeID != u.quoteID && u.quoteFeeID != u.baseID {
   602  		addCexPending(u.quoteFeeID)
   603  		addCexPending(u.quoteFeeID)
   604  	}
   605  	if len(dexPending) > 0 {
   606  		writeLine("  DEX pending:")
   607  		for assetID, v := range dexPending {
   608  			writeLine("    %s", format(assetID, int64(v), true))
   609  		}
   610  	}
   611  	if len(cexPending) > 0 {
   612  		writeLine("  CEX pending:")
   613  		for assetID, v := range cexPending {
   614  			writeLine("    %s", format(assetID, int64(v), true))
   615  		}
   616  	}
   617  
   618  	writeLine("")
   619  	u.log.Tracef(msg.String())
   620  }
   621  
   622  // SufficientBalanceForDEXTrade returns whether the bot has sufficient balance
   623  // to place a DEX trade.
   624  func (u *unifiedExchangeAdaptor) SufficientBalanceForDEXTrade(rate, qty uint64, sell bool) (bool, error) {
   625  	fromAsset, fromFeeAsset, toAsset, toFeeAsset := orderAssets(u.baseID, u.quoteID, sell)
   626  	balances := map[uint32]uint64{}
   627  	for _, assetID := range []uint32{fromAsset, fromFeeAsset, toAsset, toFeeAsset} {
   628  		if _, found := balances[assetID]; !found {
   629  			bal := u.DEXBalance(assetID)
   630  			balances[assetID] = bal.Available
   631  		}
   632  	}
   633  
   634  	buyFees, sellFees, err := u.orderFees()
   635  	if err != nil {
   636  		return false, err
   637  	}
   638  	fees, fundingFees := buyFees.Max, buyFees.Funding
   639  	if sell {
   640  		fees, fundingFees = sellFees.Max, sellFees.Funding
   641  	}
   642  
   643  	if balances[fromFeeAsset] < fundingFees {
   644  		return false, nil
   645  	}
   646  	balances[fromFeeAsset] -= fundingFees
   647  
   648  	fromQty := qty
   649  	if !sell {
   650  		fromQty = calc.BaseToQuote(rate, qty)
   651  	}
   652  	if balances[fromAsset] < fromQty {
   653  		return false, nil
   654  	}
   655  	balances[fromAsset] -= fromQty
   656  
   657  	numLots := qty / u.lotSize.Load()
   658  	if balances[fromFeeAsset] < numLots*fees.Swap {
   659  		return false, nil
   660  	}
   661  	balances[fromFeeAsset] -= numLots * fees.Swap
   662  
   663  	if u.isAccountLocker(fromAsset) {
   664  		if balances[fromFeeAsset] < numLots*fees.Refund {
   665  			return false, nil
   666  		}
   667  		balances[fromFeeAsset] -= numLots * fees.Refund
   668  	}
   669  
   670  	if u.isAccountLocker(toAsset) {
   671  		if balances[toFeeAsset] < numLots*fees.Redeem {
   672  			return false, nil
   673  		}
   674  		balances[toFeeAsset] -= numLots * fees.Redeem
   675  	}
   676  
   677  	return true, nil
   678  }
   679  
   680  // SufficientBalanceOnCEXTrade returns whether the bot has sufficient balance
   681  // to place a CEX trade.
   682  func (u *unifiedExchangeAdaptor) SufficientBalanceForCEXTrade(baseID, quoteID uint32, sell bool, rate, qty uint64) bool {
   683  	var fromAssetID uint32
   684  	var fromAssetQty uint64
   685  	if sell {
   686  		fromAssetID = u.baseID
   687  		fromAssetQty = qty
   688  	} else {
   689  		fromAssetID = u.quoteID
   690  		fromAssetQty = calc.BaseToQuote(rate, qty)
   691  	}
   692  
   693  	fromAssetBal := u.CEXBalance(fromAssetID)
   694  	return fromAssetBal.Available >= fromAssetQty
   695  }
   696  
   697  // dexOrderInfo is used by MultiTrade to keep track of the placement index
   698  // and counter trade rate of an order.
   699  type dexOrderInfo struct {
   700  	placementIndex   uint64
   701  	counterTradeRate uint64
   702  	placement        *core.QtyRate
   703  }
   704  
   705  // updateDEXOrderEvent updates the event log with the current state of a
   706  // pending DEX order and sends an event notification.
   707  func (u *unifiedExchangeAdaptor) updateDEXOrderEvent(o *pendingDEXOrder, complete bool) {
   708  	o.txsMtx.RLock()
   709  	transactions := make([]*asset.WalletTransaction, 0, len(o.swaps)+len(o.redeems)+len(o.refunds))
   710  	txIDSeen := make(map[string]bool)
   711  	addTxs := func(txs map[string]*asset.WalletTransaction) {
   712  		for _, tx := range txs {
   713  			if txIDSeen[tx.ID] {
   714  				continue
   715  			}
   716  			txIDSeen[tx.ID] = true
   717  			transactions = append(transactions, tx)
   718  		}
   719  	}
   720  	addTxs(o.swaps)
   721  	addTxs(o.redeems)
   722  	addTxs(o.refunds)
   723  	state := o.currentState()
   724  	o.txsMtx.RUnlock()
   725  
   726  	e := &MarketMakingEvent{
   727  		ID:             o.eventLogID,
   728  		TimeStamp:      o.timestamp,
   729  		Pending:        !complete,
   730  		BalanceEffects: combineBalanceEffects(state.dexBalanceEffects, state.cexBalanceEffects),
   731  		DEXOrderEvent: &DEXOrderEvent{
   732  			ID:           state.order.ID.String(),
   733  			Rate:         state.order.Rate,
   734  			Qty:          state.order.Qty,
   735  			Sell:         state.order.Sell,
   736  			Transactions: transactions,
   737  		},
   738  	}
   739  
   740  	u.eventLogDB.storeEvent(u.startTime.Load(), u.mwh, e, u.balanceState())
   741  	u.notifyEvent(e)
   742  }
   743  
   744  func cexOrderEvent(trade *libxc.Trade, eventID uint64, timestamp int64) *MarketMakingEvent {
   745  	return &MarketMakingEvent{
   746  		ID:             eventID,
   747  		TimeStamp:      timestamp,
   748  		Pending:        !trade.Complete,
   749  		BalanceEffects: cexTradeBalanceEffects(trade),
   750  		CEXOrderEvent: &CEXOrderEvent{
   751  			ID:          trade.ID,
   752  			Rate:        trade.Rate,
   753  			Qty:         trade.Qty,
   754  			Sell:        trade.Sell,
   755  			BaseFilled:  trade.BaseFilled,
   756  			QuoteFilled: trade.QuoteFilled,
   757  		},
   758  	}
   759  }
   760  
   761  // updateCEXOrderEvent updates the event log with the current state of a
   762  // pending CEX order and sends an event notification.
   763  func (u *unifiedExchangeAdaptor) updateCEXOrderEvent(trade *libxc.Trade, eventID uint64, timestamp int64) {
   764  	event := cexOrderEvent(trade, eventID, timestamp)
   765  	u.eventLogDB.storeEvent(u.startTime.Load(), u.mwh, event, u.balanceState())
   766  	u.notifyEvent(event)
   767  }
   768  
   769  // updateDepositEvent updates the event log with the current state of a
   770  // pending deposit and sends an event notification.
   771  func (u *unifiedExchangeAdaptor) updateDepositEvent(deposit *pendingDeposit) {
   772  	deposit.mtx.RLock()
   773  	e := &MarketMakingEvent{
   774  		ID:             deposit.eventLogID,
   775  		TimeStamp:      deposit.timestamp,
   776  		BalanceEffects: combineBalanceEffects(depositBalanceEffects(deposit.assetID, deposit.tx, deposit.cexConfirmed)),
   777  		Pending:        !deposit.cexConfirmed || !deposit.feeConfirmed,
   778  		DepositEvent: &DepositEvent{
   779  			AssetID:     deposit.assetID,
   780  			Transaction: deposit.tx,
   781  			CEXCredit:   deposit.amtCredited,
   782  		},
   783  	}
   784  	deposit.mtx.RUnlock()
   785  
   786  	u.eventLogDB.storeEvent(u.startTime.Load(), u.mwh, e, u.balanceState())
   787  	u.notifyEvent(e)
   788  }
   789  
   790  func (u *unifiedExchangeAdaptor) updateConfigEvent(updatedCfg *BotConfig) {
   791  	e := &MarketMakingEvent{
   792  		ID:           u.eventLogID.Add(1),
   793  		TimeStamp:    time.Now().Unix(),
   794  		UpdateConfig: updatedCfg,
   795  	}
   796  	u.eventLogDB.storeEvent(u.startTime.Load(), u.mwh, e, u.balanceState())
   797  }
   798  
   799  func (u *unifiedExchangeAdaptor) updateInventoryEvent(inventoryMods map[uint32]int64) {
   800  	e := &MarketMakingEvent{
   801  		ID:              u.eventLogID.Add(1),
   802  		TimeStamp:       time.Now().Unix(),
   803  		UpdateInventory: &inventoryMods,
   804  	}
   805  	u.eventLogDB.storeEvent(u.startTime.Load(), u.mwh, e, u.balanceState())
   806  }
   807  
   808  func combineBalanceEffects(dex, cex *BalanceEffects) *BalanceEffects {
   809  	effects := newBalanceEffects()
   810  	for assetID, v := range dex.Settled {
   811  		effects.Settled[assetID] += v
   812  	}
   813  	for assetID, v := range dex.Locked {
   814  		effects.Locked[assetID] += v
   815  	}
   816  	for assetID, v := range dex.Pending {
   817  		effects.Pending[assetID] += v
   818  	}
   819  	for assetID, v := range dex.Reserved {
   820  		effects.Reserved[assetID] += v
   821  	}
   822  
   823  	for assetID, v := range cex.Settled {
   824  		effects.Settled[assetID] += v
   825  	}
   826  	for assetID, v := range cex.Locked {
   827  		effects.Locked[assetID] += v
   828  	}
   829  	for assetID, v := range cex.Pending {
   830  		effects.Pending[assetID] += v
   831  	}
   832  	for assetID, v := range cex.Reserved {
   833  		effects.Reserved[assetID] += v
   834  	}
   835  
   836  	return effects
   837  
   838  }
   839  
   840  // updateWithdrawalEvent updates the event log with the current state of a
   841  // pending withdrawal and sends an event notification.
   842  func (u *unifiedExchangeAdaptor) updateWithdrawalEvent(withdrawal *pendingWithdrawal, tx *asset.WalletTransaction) {
   843  	complete := tx != nil && tx.Confirmed
   844  	e := &MarketMakingEvent{
   845  		ID:             withdrawal.eventLogID,
   846  		TimeStamp:      withdrawal.timestamp,
   847  		BalanceEffects: combineBalanceEffects(withdrawal.balanceEffects()),
   848  		Pending:        !complete,
   849  		WithdrawalEvent: &WithdrawalEvent{
   850  			AssetID:     withdrawal.assetID,
   851  			ID:          withdrawal.withdrawalID,
   852  			Transaction: tx,
   853  			CEXDebit:    withdrawal.amtWithdrawn,
   854  		},
   855  	}
   856  
   857  	u.eventLogDB.storeEvent(u.startTime.Load(), u.mwh, e, u.balanceState())
   858  	u.notifyEvent(e)
   859  }
   860  
   861  // groupedBookedOrders returns pending dex orders grouped by the placement
   862  // index used to create them when they were placed with MultiTrade.
   863  func (u *unifiedExchangeAdaptor) groupedBookedOrders(sells bool) (orders map[uint64][]*pendingDEXOrder) {
   864  	orders = make(map[uint64][]*pendingDEXOrder)
   865  
   866  	groupPendingOrder := func(pendingOrder *pendingDEXOrder) {
   867  		o := pendingOrder.currentState().order
   868  		if o.Status > order.OrderStatusBooked {
   869  			return
   870  		}
   871  
   872  		pi := pendingOrder.placementIndex
   873  		if sells == o.Sell {
   874  			if orders[pi] == nil {
   875  				orders[pi] = []*pendingDEXOrder{}
   876  			}
   877  			orders[pi] = append(orders[pi], pendingOrder)
   878  		}
   879  	}
   880  
   881  	u.balancesMtx.RLock()
   882  	defer u.balancesMtx.RUnlock()
   883  
   884  	for _, pendingOrder := range u.pendingDEXOrders {
   885  		groupPendingOrder(pendingOrder)
   886  	}
   887  
   888  	return
   889  }
   890  
   891  // rateCausesSelfMatchFunc returns a function that can be called to determine
   892  // whether a rate would cause a self match. The sell parameter indicates whether
   893  // the returned function will support sell or buy orders.
   894  func (u *unifiedExchangeAdaptor) rateCausesSelfMatchFunc(sell bool) func(rate uint64) bool {
   895  	var highestExistingBuy, lowestExistingSell uint64 = 0, math.MaxUint64
   896  
   897  	for _, groupedOrders := range u.groupedBookedOrders(!sell) {
   898  		for _, o := range groupedOrders {
   899  			order := o.currentState().order
   900  			if sell && order.Rate > highestExistingBuy {
   901  				highestExistingBuy = order.Rate
   902  			}
   903  			if !sell && order.Rate < lowestExistingSell {
   904  				lowestExistingSell = order.Rate
   905  			}
   906  		}
   907  	}
   908  
   909  	return func(rate uint64) bool {
   910  		if sell {
   911  			return rate <= highestExistingBuy
   912  		}
   913  		return rate >= lowestExistingSell
   914  	}
   915  }
   916  
   917  // reservedForCounterTrade returns the amount that is required to be reserved
   918  // on the CEX in order for this order to be counter traded when matched.
   919  func reservedForCounterTrade(sellOnDEX bool, counterTradeRate, remainingQty uint64) uint64 {
   920  	if counterTradeRate == 0 {
   921  		return 0
   922  	}
   923  
   924  	if sellOnDEX {
   925  		return calc.BaseToQuote(counterTradeRate, remainingQty)
   926  	}
   927  
   928  	return remainingQty
   929  }
   930  
   931  func withinTolerance(rate, target uint64, driftTolerance float64) bool {
   932  	tolerance := uint64(float64(target) * driftTolerance)
   933  	lowerBound := target - tolerance
   934  	upperBound := target + tolerance
   935  	return rate >= lowerBound && rate <= upperBound
   936  }
   937  
   938  func (u *unifiedExchangeAdaptor) placeMultiTrade(placements []*dexOrderInfo, sell bool) []*core.MultiTradeResult {
   939  	corePlacements := make([]*core.QtyRate, 0, len(placements))
   940  	for _, p := range placements {
   941  		corePlacements = append(corePlacements, p.placement)
   942  	}
   943  
   944  	botCfg := u.botCfg()
   945  	var walletOptions map[string]string
   946  	if sell {
   947  		walletOptions = botCfg.BaseWalletOptions
   948  	} else {
   949  		walletOptions = botCfg.QuoteWalletOptions
   950  	}
   951  
   952  	fromAsset, fromFeeAsset, toAsset, toFeeAsset := orderAssets(u.baseID, u.quoteID, sell)
   953  	multiTradeForm := &core.MultiTradeForm{
   954  		Host:       u.host,
   955  		Base:       u.baseID,
   956  		Quote:      u.quoteID,
   957  		Sell:       sell,
   958  		Placements: corePlacements,
   959  		Options:    walletOptions,
   960  		MaxLock:    u.DEXBalance(fromAsset).Available,
   961  	}
   962  
   963  	newPendingDEXOrders := make([]*pendingDEXOrder, 0, len(placements))
   964  	defer func() {
   965  		// defer until u.balancesMtx is unlocked
   966  		for _, o := range newPendingDEXOrders {
   967  			u.updateDEXOrderEvent(o, false)
   968  		}
   969  		u.sendStatsUpdate()
   970  	}()
   971  
   972  	u.balancesMtx.Lock()
   973  	defer u.balancesMtx.Unlock()
   974  
   975  	results := u.clientCore.MultiTrade([]byte{}, multiTradeForm)
   976  
   977  	if len(placements) != len(results) {
   978  		u.log.Errorf("unexpected number of results. expected %d, got %d", len(placements), len(results))
   979  		return results
   980  	}
   981  
   982  	for i, res := range results {
   983  		if res.Error != nil {
   984  			continue
   985  		}
   986  
   987  		o := res.Order
   988  		var orderID order.OrderID
   989  		copy(orderID[:], o.ID)
   990  
   991  		dexEffects, cexEffects := newBalanceEffects(), newBalanceEffects()
   992  
   993  		dexEffects.Settled[fromAsset] -= int64(o.LockedAmt)
   994  		dexEffects.Settled[fromFeeAsset] -= int64(o.ParentAssetLockedAmt + o.RefundLockedAmt)
   995  		dexEffects.Settled[toFeeAsset] -= int64(o.RedeemLockedAmt)
   996  
   997  		dexEffects.Locked[fromAsset] += o.LockedAmt
   998  		dexEffects.Locked[fromFeeAsset] += o.ParentAssetLockedAmt + o.RefundLockedAmt
   999  		dexEffects.Locked[toFeeAsset] += o.RedeemLockedAmt
  1000  
  1001  		if o.FeesPaid != nil && o.FeesPaid.Funding > 0 {
  1002  			dexEffects.Settled[fromFeeAsset] -= int64(o.FeesPaid.Funding)
  1003  		}
  1004  
  1005  		reserved := reservedForCounterTrade(o.Sell, placements[i].counterTradeRate, o.Qty)
  1006  		cexEffects.Settled[toAsset] -= int64(reserved)
  1007  		cexEffects.Reserved[toAsset] = reserved
  1008  
  1009  		pendingOrder := &pendingDEXOrder{
  1010  			eventLogID:         u.eventLogID.Add(1),
  1011  			timestamp:          time.Now().Unix(),
  1012  			swaps:              make(map[string]*asset.WalletTransaction),
  1013  			redeems:            make(map[string]*asset.WalletTransaction),
  1014  			refunds:            make(map[string]*asset.WalletTransaction),
  1015  			swapCoinIDToTxID:   make(map[string]string),
  1016  			redeemCoinIDToTxID: make(map[string]string),
  1017  			refundCoinIDToTxID: make(map[string]string),
  1018  			placementIndex:     placements[i].placementIndex,
  1019  			counterTradeRate:   placements[i].counterTradeRate,
  1020  		}
  1021  
  1022  		pendingOrder.state.Store(
  1023  			&dexOrderState{
  1024  				order:             o,
  1025  				dexBalanceEffects: dexEffects,
  1026  				cexBalanceEffects: cexEffects,
  1027  				counterTradeRate:  pendingOrder.counterTradeRate,
  1028  			})
  1029  		u.pendingDEXOrders[orderID] = pendingOrder
  1030  		newPendingDEXOrders = append(newPendingDEXOrders, u.pendingDEXOrders[orderID])
  1031  	}
  1032  
  1033  	return results
  1034  }
  1035  
  1036  // TradePlacement represents a placement to be made on a DEX order book
  1037  // using the MultiTrade function. A non-zero counterTradeRate indicates that
  1038  // the bot intends to make a counter-trade on a CEX when matches are made on
  1039  // the DEX, and this must be taken into consideration in combination with the
  1040  // bot's balance on the CEX when deciding how many lots to place. This
  1041  // information is also used when considering deposits and withdrawals.
  1042  type TradePlacement struct {
  1043  	Rate             uint64            `json:"rate"`
  1044  	Lots             uint64            `json:"lots"`
  1045  	StandingLots     uint64            `json:"standingLots"`
  1046  	OrderedLots      uint64            `json:"orderedLots"`
  1047  	CounterTradeRate uint64            `json:"counterTradeRate"`
  1048  	RequiredDEX      map[uint32]uint64 `json:"requiredDex"`
  1049  	RequiredCEX      uint64            `json:"requiredCex"`
  1050  	UsedDEX          map[uint32]uint64 `json:"usedDex"`
  1051  	UsedCEX          uint64            `json:"usedCex"`
  1052  	Error            *BotProblems      `json:"error"`
  1053  }
  1054  
  1055  // setError sets the error field of the TradePlacement and updates the fields
  1056  // that indicate that the trade was placed to 0.
  1057  func (tp *TradePlacement) setError(err error) {
  1058  	if err == nil {
  1059  		tp.Error = nil
  1060  		return
  1061  	}
  1062  	tp.OrderedLots = 0
  1063  	tp.UsedDEX = make(map[uint32]uint64)
  1064  	tp.UsedCEX = 0
  1065  	problems := &BotProblems{}
  1066  	updateBotProblemsBasedOnError(problems, err)
  1067  	tp.Error = problems
  1068  }
  1069  
  1070  func (tp *TradePlacement) requiredLots() uint64 {
  1071  	if tp.Lots > tp.StandingLots {
  1072  		return tp.Lots - tp.StandingLots
  1073  	}
  1074  	return 0
  1075  }
  1076  
  1077  // OrderReport summarizes the results of a MultiTrade operation.
  1078  type OrderReport struct {
  1079  	Placements       []*TradePlacement      `json:"placements"`
  1080  	Fees             *OrderFees             `json:"buyFees"`
  1081  	AvailableDEXBals map[uint32]*BotBalance `json:"availableDexBals"`
  1082  	RequiredDEXBals  map[uint32]uint64      `json:"requiredDexBals"`
  1083  	UsedDEXBals      map[uint32]uint64      `json:"usedDexBals"`
  1084  	RemainingDEXBals map[uint32]uint64      `json:"remainingDexBals"`
  1085  	AvailableCEXBal  *BotBalance            `json:"availableCexBal"`
  1086  	RequiredCEXBal   uint64                 `json:"requiredCexBal"`
  1087  	UsedCEXBal       uint64                 `json:"usedCexBal"`
  1088  	RemainingCEXBal  uint64                 `json:"remainingCexBal"`
  1089  	Error            *BotProblems           `json:"error"`
  1090  }
  1091  
  1092  func (or *OrderReport) setError(err error) {
  1093  	if err == nil {
  1094  		or.Error = nil
  1095  		return
  1096  	}
  1097  	if or.Error == nil {
  1098  		or.Error = &BotProblems{}
  1099  	}
  1100  	updateBotProblemsBasedOnError(or.Error, err)
  1101  }
  1102  
  1103  func newOrderReport(placements []*TradePlacement) *OrderReport {
  1104  	cpPlacements := make([]*TradePlacement, len(placements))
  1105  	for i, p := range placements {
  1106  		cpPlacements[i] = &TradePlacement{
  1107  			Rate:             p.Rate,
  1108  			Lots:             p.Lots,
  1109  			CounterTradeRate: p.CounterTradeRate,
  1110  			RequiredDEX:      make(map[uint32]uint64),
  1111  			UsedDEX:          make(map[uint32]uint64),
  1112  		}
  1113  	}
  1114  
  1115  	return &OrderReport{
  1116  		AvailableDEXBals: make(map[uint32]*BotBalance),
  1117  		RequiredDEXBals:  make(map[uint32]uint64),
  1118  		RemainingDEXBals: make(map[uint32]uint64),
  1119  		UsedDEXBals:      make(map[uint32]uint64),
  1120  		Placements:       cpPlacements,
  1121  	}
  1122  }
  1123  
  1124  // MultiTrade places multiple orders on the DEX order book. The placements
  1125  // arguments does not represent the trades that should be placed at this time,
  1126  // but rather the amount of lots that the caller expects consistently have on
  1127  // the orderbook at various rates. It is expected that the caller will
  1128  // periodically (each epoch) call this function with the same number of
  1129  // placements in the same order, with the rates updated to reflect the current
  1130  // market conditions.
  1131  //
  1132  // When an order is placed, the index of the placement that initiated the order
  1133  // is tracked. On subsequent calls, as the rates change, the placements will be
  1134  // compared with prior trades with the same placement index. If the trades on
  1135  // the books differ from the rates in the placements by greater than
  1136  // driftTolerance, the orders will be cancelled. As orders get filled, and there
  1137  // are less than the number of lots specified in the placement on the books,
  1138  // new trades will be made.
  1139  //
  1140  // The caller can pass a rate of 0 for any placement to indicate that all orders
  1141  // that were made during previous calls to MultiTrade with the same placement index
  1142  // should be cancelled.
  1143  //
  1144  // dexReserves and cexReserves are the amount of funds that should not be used to
  1145  // place orders. These are used in case the bot is about to make a deposit or
  1146  // withdrawal, and does not want those funds to get locked up in a trade.
  1147  //
  1148  // The placements must be passed in decreasing priority order. If there is not
  1149  // enough balance to place all of the orders, the lower priority trades that
  1150  // were made in previous calls will be cancelled.
  1151  func (u *unifiedExchangeAdaptor) multiTrade(
  1152  	placements []*TradePlacement,
  1153  	sell bool,
  1154  	driftTolerance float64,
  1155  	currEpoch uint64,
  1156  ) (_ map[order.OrderID]*dexOrderInfo, or *OrderReport) {
  1157  	or = newOrderReport(placements)
  1158  	if len(placements) == 0 {
  1159  		return nil, or
  1160  	}
  1161  
  1162  	buyFees, sellFees, err := u.orderFees()
  1163  	if err != nil {
  1164  		or.setError(err)
  1165  		return nil, or
  1166  	}
  1167  	or.Fees = buyFees
  1168  	if sell {
  1169  		or.Fees = sellFees
  1170  	}
  1171  	lotSize := u.lotSize.Load()
  1172  
  1173  	fromID, fromFeeID, toID, toFeeID := orderAssets(u.baseID, u.quoteID, sell)
  1174  	fees, fundingFees := or.Fees.Max, or.Fees.Funding
  1175  
  1176  	// First, determine the amount of balances the bot has available to place
  1177  	// DEX trades taking into account dexReserves.
  1178  	for _, assetID := range []uint32{fromID, fromFeeID, toID, toFeeID} {
  1179  		if _, found := or.RemainingDEXBals[assetID]; !found {
  1180  			or.AvailableDEXBals[assetID] = u.DEXBalance(assetID).copy()
  1181  			or.RemainingDEXBals[assetID] = or.AvailableDEXBals[assetID].Available
  1182  		}
  1183  	}
  1184  
  1185  	// If the placements include a counterTradeRate, the CEX balance must also
  1186  	// be taken into account to determine how many trades can be placed.
  1187  	accountForCEXBal := placements[0].CounterTradeRate > 0
  1188  	if accountForCEXBal {
  1189  		or.AvailableCEXBal = u.CEXBalance(toID).copy()
  1190  		or.RemainingCEXBal = or.AvailableCEXBal.Available
  1191  	}
  1192  
  1193  	cancels := make([]dex.Bytes, 0, len(placements))
  1194  
  1195  	addCancel := func(o *core.Order) {
  1196  		if currEpoch-o.Epoch < 2 { // TODO: check epoch
  1197  			u.log.Debugf("multiTrade: skipping cancel not past free cancel threshold")
  1198  			return
  1199  		}
  1200  		cancels = append(cancels, o.ID)
  1201  	}
  1202  
  1203  	// keptOrders is a list of pending orders that are not being cancelled, in
  1204  	// decreasing order of placementIndex. If the bot doesn't have enough
  1205  	// balance to place an order with a higher priority (lower placementIndex)
  1206  	// then the lower priority orders in this list will be cancelled.
  1207  	keptOrders := make([]*pendingDEXOrder, 0, len(placements))
  1208  
  1209  	for _, groupedOrders := range u.groupedBookedOrders(sell) {
  1210  		for _, o := range groupedOrders {
  1211  			order := o.currentState().order
  1212  			if o.placementIndex >= uint64(len(or.Placements)) {
  1213  				// This will happen if there is a reconfig in which there are
  1214  				// now less placements than before.
  1215  				addCancel(order)
  1216  				continue
  1217  			}
  1218  
  1219  			mustCancel := !withinTolerance(order.Rate, placements[o.placementIndex].Rate, driftTolerance)
  1220  			or.Placements[o.placementIndex].StandingLots += (order.Qty - order.Filled) / lotSize
  1221  			if or.Placements[o.placementIndex].StandingLots > or.Placements[o.placementIndex].Lots {
  1222  				mustCancel = true
  1223  			}
  1224  
  1225  			if mustCancel {
  1226  				u.log.Tracef("%s cancel with order rate = %s, placement rate = %s, drift tolerance = %.4f%%",
  1227  					u.mwh, u.fmtRate(order.Rate), u.fmtRate(placements[o.placementIndex].Rate), driftTolerance*100,
  1228  				)
  1229  				addCancel(order)
  1230  			} else {
  1231  				keptOrders = append([]*pendingDEXOrder{o}, keptOrders...)
  1232  			}
  1233  		}
  1234  	}
  1235  
  1236  	rateCausesSelfMatch := u.rateCausesSelfMatchFunc(sell)
  1237  
  1238  	multiSplitBuffer := u.botCfg().multiSplitBuffer()
  1239  
  1240  	fundingReq := func(rate, lots, counterTradeRate uint64) (dexReq map[uint32]uint64, cexReq uint64) {
  1241  		qty := lotSize * lots
  1242  		swapFees := fees.Swap * lots
  1243  		if !sell {
  1244  			qty = calc.BaseToQuote(rate, qty)
  1245  			qty = uint64(math.Round(float64(qty) * (100 + multiSplitBuffer) / 100))
  1246  			swapFees = uint64(math.Round(float64(swapFees) * (100 + multiSplitBuffer) / 100))
  1247  		}
  1248  		dexReq = make(map[uint32]uint64)
  1249  		dexReq[fromID] += qty
  1250  		dexReq[fromFeeID] += swapFees
  1251  		if u.isAccountLocker(fromID) {
  1252  			dexReq[fromFeeID] += fees.Refund * lots
  1253  		}
  1254  		if u.isAccountLocker(toID) {
  1255  			dexReq[toFeeID] += fees.Redeem * lots
  1256  		}
  1257  		if accountForCEXBal {
  1258  			if sell {
  1259  				cexReq = calc.BaseToQuote(counterTradeRate, lotSize*lots)
  1260  			} else {
  1261  				cexReq = lotSize * lots
  1262  			}
  1263  		}
  1264  		return
  1265  	}
  1266  
  1267  	canAffordLots := func(rate, lots, counterTradeRate uint64) bool {
  1268  		dexReq, cexReq := fundingReq(rate, lots, counterTradeRate)
  1269  		for assetID, v := range dexReq {
  1270  			if or.RemainingDEXBals[assetID] < v {
  1271  				return false
  1272  			}
  1273  		}
  1274  		return or.RemainingCEXBal >= cexReq
  1275  	}
  1276  
  1277  	orderInfos := make([]*dexOrderInfo, 0, len(or.Placements))
  1278  
  1279  	// Calculate required balances for each placement and the total required.
  1280  	placementRequired := false
  1281  	for _, placement := range or.Placements {
  1282  		if placement.requiredLots() == 0 {
  1283  			continue
  1284  		}
  1285  		placementRequired = true
  1286  		dexReq, cexReq := fundingReq(placement.Rate, placement.requiredLots(), placement.CounterTradeRate)
  1287  		for assetID, v := range dexReq {
  1288  			placement.RequiredDEX[assetID] = v
  1289  			or.RequiredDEXBals[assetID] += v
  1290  		}
  1291  		placement.RequiredCEX = cexReq
  1292  		or.RequiredCEXBal += cexReq
  1293  	}
  1294  	if placementRequired {
  1295  		or.RequiredDEXBals[fromFeeID] += fundingFees
  1296  	}
  1297  
  1298  	or.RemainingDEXBals[fromFeeID] = utils.SafeSub(or.RemainingDEXBals[fromFeeID], fundingFees)
  1299  	for i, placement := range or.Placements {
  1300  		if placement.requiredLots() == 0 {
  1301  			continue
  1302  		}
  1303  
  1304  		if rateCausesSelfMatch(placement.Rate) {
  1305  			u.log.Warnf("multiTrade: rate %d causes self match. Placements should be farther from mid-gap.", placement.Rate)
  1306  			placement.Error = &BotProblems{CausesSelfMatch: true}
  1307  			continue
  1308  		}
  1309  
  1310  		searchN := int(placement.requiredLots() + 1)
  1311  		lotsPlus1 := sort.Search(searchN, func(lotsi int) bool {
  1312  			return !canAffordLots(placement.Rate, uint64(lotsi), placement.CounterTradeRate)
  1313  		})
  1314  
  1315  		var lotsToPlace uint64
  1316  		if lotsPlus1 > 1 {
  1317  			lotsToPlace = uint64(lotsPlus1) - 1
  1318  			placement.UsedDEX, placement.UsedCEX = fundingReq(placement.Rate, lotsToPlace, placement.CounterTradeRate)
  1319  			placement.OrderedLots = lotsToPlace
  1320  			for assetID, v := range placement.UsedDEX {
  1321  				or.RemainingDEXBals[assetID] -= v
  1322  				or.UsedDEXBals[assetID] += v
  1323  			}
  1324  			or.RemainingCEXBal -= placement.UsedCEX
  1325  			or.UsedCEXBal += placement.UsedCEX
  1326  
  1327  			orderInfos = append(orderInfos, &dexOrderInfo{
  1328  				placementIndex:   uint64(i),
  1329  				counterTradeRate: placement.CounterTradeRate,
  1330  				placement: &core.QtyRate{
  1331  					Qty:  lotsToPlace * lotSize,
  1332  					Rate: placement.Rate,
  1333  				},
  1334  			})
  1335  		}
  1336  
  1337  		// If there is insufficient balance to place a higher priority order,
  1338  		// cancel the lower priority orders.
  1339  		if lotsToPlace < placement.requiredLots() {
  1340  			u.log.Tracef("multiTrade(%s,%d) out of funds for more placements. %d of %d lots for rate %s",
  1341  				sellStr(sell), i, lotsToPlace, placement.requiredLots(), u.fmtRate(placement.Rate))
  1342  			for _, o := range keptOrders {
  1343  				if o.placementIndex > uint64(i) {
  1344  					order := o.currentState().order
  1345  					addCancel(order)
  1346  				}
  1347  			}
  1348  
  1349  			break
  1350  		}
  1351  	}
  1352  
  1353  	if len(orderInfos) > 0 {
  1354  		or.UsedDEXBals[fromFeeID] += fundingFees
  1355  	}
  1356  
  1357  	for _, cancel := range cancels {
  1358  		if err := u.Cancel(cancel); err != nil {
  1359  			u.log.Errorf("multiTrade: error canceling order %s: %v", cancel, err)
  1360  		}
  1361  	}
  1362  
  1363  	if len(orderInfos) > 0 {
  1364  		results := u.placeMultiTrade(orderInfos, sell)
  1365  		ordered := make(map[order.OrderID]*dexOrderInfo, len(placements))
  1366  		for i, res := range results {
  1367  			if res.Error != nil {
  1368  				or.Placements[orderInfos[i].placementIndex].setError(res.Error)
  1369  				continue
  1370  			}
  1371  			var orderID order.OrderID
  1372  			copy(orderID[:], res.Order.ID)
  1373  			ordered[orderID] = orderInfos[i]
  1374  		}
  1375  
  1376  		return ordered, or
  1377  	}
  1378  
  1379  	return nil, or
  1380  }
  1381  
  1382  // DEXTrade places a single order on the DEX order book.
  1383  func (u *unifiedExchangeAdaptor) DEXTrade(rate, qty uint64, sell bool) (*core.Order, error) {
  1384  	enough, err := u.SufficientBalanceForDEXTrade(rate, qty, sell)
  1385  	if err != nil {
  1386  		return nil, err
  1387  	}
  1388  	if !enough {
  1389  		return nil, fmt.Errorf("insufficient balance")
  1390  	}
  1391  
  1392  	placements := []*dexOrderInfo{{
  1393  		placement: &core.QtyRate{
  1394  			Qty:  qty,
  1395  			Rate: rate,
  1396  		},
  1397  	}}
  1398  
  1399  	// multiTrade is used instead of Trade because Trade does not support
  1400  	// maxLock.
  1401  	results := u.placeMultiTrade(placements, sell)
  1402  	if len(results) == 0 {
  1403  		return nil, fmt.Errorf("no orders placed")
  1404  	}
  1405  	if results[0].Error != nil {
  1406  		return nil, results[0].Error
  1407  	}
  1408  
  1409  	return results[0].Order, nil
  1410  }
  1411  
  1412  type BotBalances struct {
  1413  	DEX *BotBalance `json:"dex"`
  1414  	CEX *BotBalance `json:"cex"`
  1415  }
  1416  
  1417  // dexBalance must be called with the balancesMtx read locked.
  1418  func (u *unifiedExchangeAdaptor) dexBalance(assetID uint32) *BotBalance {
  1419  	bal, found := u.baseDexBalances[assetID]
  1420  	if !found {
  1421  		return &BotBalance{}
  1422  	}
  1423  
  1424  	totalEffects := newBalanceEffects()
  1425  	addEffects := func(effects *BalanceEffects) {
  1426  		for assetID, v := range effects.Settled {
  1427  			totalEffects.Settled[assetID] += v
  1428  		}
  1429  		for assetID, v := range effects.Locked {
  1430  			totalEffects.Locked[assetID] += v
  1431  		}
  1432  		for assetID, v := range effects.Pending {
  1433  			totalEffects.Pending[assetID] += v
  1434  		}
  1435  		for assetID, v := range effects.Reserved {
  1436  			totalEffects.Reserved[assetID] += v
  1437  		}
  1438  	}
  1439  
  1440  	for _, pendingOrder := range u.pendingDEXOrders {
  1441  		addEffects(pendingOrder.currentState().dexBalanceEffects)
  1442  	}
  1443  
  1444  	for _, pendingDeposit := range u.pendingDeposits {
  1445  		dexEffects, _ := pendingDeposit.balanceEffects()
  1446  		addEffects(dexEffects)
  1447  	}
  1448  
  1449  	for _, pendingWithdrawal := range u.pendingWithdrawals {
  1450  		dexEffects, _ := pendingWithdrawal.balanceEffects()
  1451  		addEffects(dexEffects)
  1452  	}
  1453  
  1454  	availableBalance := bal + totalEffects.Settled[assetID]
  1455  	if availableBalance < 0 {
  1456  		u.log.Errorf("negative dex balance for %s: %d", dex.BipIDSymbol(assetID), availableBalance)
  1457  		availableBalance = 0
  1458  	}
  1459  
  1460  	return &BotBalance{
  1461  		Available: uint64(availableBalance),
  1462  		Locked:    totalEffects.Locked[assetID],
  1463  		Pending:   totalEffects.Pending[assetID],
  1464  	}
  1465  }
  1466  
  1467  // DEXBalance returns the balance of the bot on the DEX.
  1468  func (u *unifiedExchangeAdaptor) DEXBalance(assetID uint32) *BotBalance {
  1469  	u.balancesMtx.RLock()
  1470  	defer u.balancesMtx.RUnlock()
  1471  
  1472  	return u.dexBalance(assetID)
  1473  }
  1474  
  1475  func (u *unifiedExchangeAdaptor) timeStart() int64 {
  1476  	return u.startTime.Load()
  1477  }
  1478  
  1479  // refreshAllPendingEvents updates the state of all pending events so that
  1480  // balances will be up to date.
  1481  func (u *unifiedExchangeAdaptor) refreshAllPendingEvents(ctx context.Context) {
  1482  	// Make copies of all maps to avoid locking balancesMtx for the entire process
  1483  	u.balancesMtx.Lock()
  1484  	pendingDEXOrders := make(map[order.OrderID]*pendingDEXOrder, len(u.pendingDEXOrders))
  1485  	for oid, pendingOrder := range u.pendingDEXOrders {
  1486  		pendingDEXOrders[oid] = pendingOrder
  1487  	}
  1488  	pendingCEXOrders := make(map[string]*pendingCEXOrder, len(u.pendingCEXOrders))
  1489  	for tradeID, pendingOrder := range u.pendingCEXOrders {
  1490  		pendingCEXOrders[tradeID] = pendingOrder
  1491  	}
  1492  	pendingWithdrawals := make(map[string]*pendingWithdrawal, len(u.pendingWithdrawals))
  1493  	for withdrawalID, pendingWithdrawal := range u.pendingWithdrawals {
  1494  		pendingWithdrawals[withdrawalID] = pendingWithdrawal
  1495  	}
  1496  	pendingDeposits := make(map[string]*pendingDeposit, len(u.pendingDeposits))
  1497  	for txID, pendingDeposit := range u.pendingDeposits {
  1498  		pendingDeposits[txID] = pendingDeposit
  1499  	}
  1500  	u.balancesMtx.Unlock()
  1501  
  1502  	for _, pendingOrder := range pendingDEXOrders {
  1503  		pendingOrder.txsMtx.Lock()
  1504  		state := pendingOrder.currentState()
  1505  		pendingOrder.updateState(state.order, u.clientCore.WalletTransaction, u.baseTraits, u.quoteTraits)
  1506  		pendingOrder.txsMtx.Unlock()
  1507  	}
  1508  
  1509  	for _, pendingDeposit := range pendingDeposits {
  1510  		pendingDeposit.mtx.RLock()
  1511  		id := pendingDeposit.tx.ID
  1512  		pendingDeposit.mtx.RUnlock()
  1513  		u.confirmDeposit(ctx, id)
  1514  	}
  1515  
  1516  	for _, pendingWithdrawal := range pendingWithdrawals {
  1517  		u.confirmWithdrawal(ctx, pendingWithdrawal.withdrawalID)
  1518  	}
  1519  
  1520  	for _, pendingOrder := range pendingCEXOrders {
  1521  		pendingOrder.tradeMtx.RLock()
  1522  		id, baseID, quoteID := pendingOrder.trade.ID, pendingOrder.trade.BaseID, pendingOrder.trade.QuoteID
  1523  		pendingOrder.tradeMtx.RUnlock()
  1524  
  1525  		trade, err := u.CEX.TradeStatus(ctx, id, baseID, quoteID)
  1526  		if err != nil {
  1527  			u.log.Errorf("error getting CEX trade status: %v", err)
  1528  			continue
  1529  		}
  1530  
  1531  		u.handleCEXTradeUpdate(trade)
  1532  	}
  1533  }
  1534  
  1535  // incompleteCexTradeBalanceEffects returns the balance effects of an
  1536  // incomplete CEX trade. As soon as a CEX trade is complete, it is removed
  1537  // from the pending map, so completed trades do not need to be calculated.
  1538  func cexTradeBalanceEffects(trade *libxc.Trade) (effects *BalanceEffects) {
  1539  	effects = newBalanceEffects()
  1540  
  1541  	if trade.Complete {
  1542  		if trade.Sell {
  1543  			effects.Settled[trade.BaseID] = -int64(trade.BaseFilled)
  1544  			effects.Settled[trade.QuoteID] = int64(trade.QuoteFilled)
  1545  		} else {
  1546  			effects.Settled[trade.QuoteID] = -int64(trade.QuoteFilled)
  1547  			effects.Settled[trade.BaseID] = int64(trade.BaseFilled)
  1548  		}
  1549  		return
  1550  	}
  1551  
  1552  	if trade.Sell {
  1553  		effects.Settled[trade.BaseID] = -int64(trade.Qty)
  1554  		effects.Locked[trade.BaseID] = trade.Qty - trade.BaseFilled
  1555  		effects.Settled[trade.QuoteID] = int64(trade.QuoteFilled)
  1556  	} else {
  1557  		effects.Settled[trade.QuoteID] = -int64(calc.BaseToQuote(trade.Rate, trade.Qty))
  1558  		effects.Locked[trade.QuoteID] = calc.BaseToQuote(trade.Rate, trade.Qty) - trade.QuoteFilled
  1559  		effects.Settled[trade.BaseID] = int64(trade.BaseFilled)
  1560  	}
  1561  
  1562  	return
  1563  }
  1564  
  1565  // cexBalance must be called with the balancesMtx read locked.
  1566  func (u *unifiedExchangeAdaptor) cexBalance(assetID uint32) *BotBalance {
  1567  	totalEffects := newBalanceEffects()
  1568  	addEffects := func(effects *BalanceEffects) {
  1569  		for assetID, v := range effects.Settled {
  1570  			totalEffects.Settled[assetID] += v
  1571  		}
  1572  		for assetID, v := range effects.Locked {
  1573  			totalEffects.Locked[assetID] += v
  1574  		}
  1575  		for assetID, v := range effects.Pending {
  1576  			totalEffects.Pending[assetID] += v
  1577  		}
  1578  		for assetID, v := range effects.Reserved {
  1579  			totalEffects.Reserved[assetID] += v
  1580  		}
  1581  	}
  1582  
  1583  	for _, pendingOrder := range u.pendingCEXOrders {
  1584  		pendingOrder.tradeMtx.RLock()
  1585  		trade := pendingOrder.trade
  1586  		pendingOrder.tradeMtx.RUnlock()
  1587  
  1588  		addEffects(cexTradeBalanceEffects(trade))
  1589  	}
  1590  
  1591  	for _, withdrawal := range u.pendingWithdrawals {
  1592  		_, cexEffects := withdrawal.balanceEffects()
  1593  		addEffects(cexEffects)
  1594  	}
  1595  
  1596  	// Credited deposits generally should already be part of the base balance,
  1597  	// but just in case the amount was credited before the wallet confirmed the
  1598  	// fee.
  1599  	for _, deposit := range u.pendingDeposits {
  1600  		_, cexEffects := deposit.balanceEffects()
  1601  		addEffects(cexEffects)
  1602  	}
  1603  
  1604  	for _, pendingDEXOrder := range u.pendingDEXOrders {
  1605  		addEffects(pendingDEXOrder.currentState().cexBalanceEffects)
  1606  	}
  1607  
  1608  	available := u.baseCexBalances[assetID] + totalEffects.Settled[assetID]
  1609  	if available < 0 {
  1610  		u.log.Errorf("negative CEX balance for %s: %d", dex.BipIDSymbol(assetID), available)
  1611  		available = 0
  1612  	}
  1613  
  1614  	return &BotBalance{
  1615  		Available: uint64(available),
  1616  		Locked:    totalEffects.Locked[assetID],
  1617  		Pending:   totalEffects.Pending[assetID],
  1618  		Reserved:  totalEffects.Reserved[assetID],
  1619  	}
  1620  }
  1621  
  1622  // CEXBalance returns the balance of the bot on the CEX.
  1623  func (u *unifiedExchangeAdaptor) CEXBalance(assetID uint32) *BotBalance {
  1624  	u.balancesMtx.RLock()
  1625  	defer u.balancesMtx.RUnlock()
  1626  
  1627  	return u.cexBalance(assetID)
  1628  }
  1629  
  1630  func (u *unifiedExchangeAdaptor) balanceState() *BalanceState {
  1631  	u.balancesMtx.RLock()
  1632  	defer u.balancesMtx.RUnlock()
  1633  
  1634  	fromAsset, toAsset, fromFeeAsset, toFeeAsset := orderAssets(u.baseID, u.quoteID, true)
  1635  
  1636  	balances := make(map[uint32]*BotBalance, 4)
  1637  	assets := []uint32{fromAsset, toAsset}
  1638  	if fromFeeAsset != fromAsset {
  1639  		assets = append(assets, fromFeeAsset)
  1640  	}
  1641  	if toFeeAsset != toAsset {
  1642  		assets = append(assets, toFeeAsset)
  1643  	}
  1644  
  1645  	for _, assetID := range assets {
  1646  		dexBal := u.dexBalance(assetID)
  1647  		cexBal := u.cexBalance(assetID)
  1648  		balances[assetID] = &BotBalance{
  1649  			Available: dexBal.Available + cexBal.Available,
  1650  			Locked:    dexBal.Locked + cexBal.Locked,
  1651  			Pending:   dexBal.Pending + cexBal.Pending,
  1652  			Reserved:  dexBal.Reserved + cexBal.Reserved,
  1653  		}
  1654  	}
  1655  
  1656  	mods := make(map[uint32]int64, len(u.inventoryMods))
  1657  	for assetID, mod := range u.inventoryMods {
  1658  		mods[assetID] = mod
  1659  	}
  1660  
  1661  	return &BalanceState{
  1662  		FiatRates:     u.fiatRates.Load().(map[uint32]float64),
  1663  		Balances:      balances,
  1664  		InventoryMods: mods,
  1665  	}
  1666  }
  1667  
  1668  func (u *unifiedExchangeAdaptor) pendingDepositComplete(deposit *pendingDeposit) {
  1669  	deposit.mtx.RLock()
  1670  	tx := deposit.tx
  1671  	assetID := deposit.assetID
  1672  	amtCredited := deposit.amtCredited
  1673  	deposit.mtx.RUnlock()
  1674  
  1675  	u.balancesMtx.Lock()
  1676  	if _, found := u.pendingDeposits[tx.ID]; !found {
  1677  		u.balancesMtx.Unlock()
  1678  		return
  1679  	}
  1680  
  1681  	delete(u.pendingDeposits, tx.ID)
  1682  	u.baseDexBalances[assetID] -= int64(tx.Amount)
  1683  	var feeAssetID uint32
  1684  	token := asset.TokenInfo(assetID)
  1685  	if token != nil {
  1686  		feeAssetID = token.ParentID
  1687  	} else {
  1688  		feeAssetID = assetID
  1689  	}
  1690  	u.baseDexBalances[feeAssetID] -= int64(tx.Fees)
  1691  	u.baseCexBalances[assetID] += int64(amtCredited)
  1692  	u.balancesMtx.Unlock()
  1693  
  1694  	if assetID == u.baseID {
  1695  		u.pendingBaseRebalance.Store(false)
  1696  	} else {
  1697  		u.pendingQuoteRebalance.Store(false)
  1698  	}
  1699  
  1700  	dexDiffs := map[uint32]int64{}
  1701  	cexDiffs := map[uint32]int64{}
  1702  	dexDiffs[assetID] -= int64(tx.Amount)
  1703  	dexDiffs[feeAssetID] -= int64(tx.Fees)
  1704  	cexDiffs[assetID] += int64(amtCredited)
  1705  
  1706  	var msg string
  1707  	if amtCredited > 0 {
  1708  		msg = fmt.Sprintf("Deposit %s complete.", tx.ID)
  1709  	} else {
  1710  		msg = fmt.Sprintf("Deposit %s complete, but not successful.", tx.ID)
  1711  	}
  1712  
  1713  	u.sendStatsUpdate()
  1714  
  1715  	u.balancesMtx.RLock()
  1716  	u.logBalanceAdjustments(dexDiffs, cexDiffs, msg)
  1717  	u.balancesMtx.RUnlock()
  1718  }
  1719  
  1720  func (u *unifiedExchangeAdaptor) confirmDeposit(ctx context.Context, txID string) bool {
  1721  	u.balancesMtx.RLock()
  1722  	deposit, found := u.pendingDeposits[txID]
  1723  	u.balancesMtx.RUnlock()
  1724  	if !found {
  1725  		return true
  1726  	}
  1727  
  1728  	var updated bool
  1729  	defer func() {
  1730  		if updated {
  1731  			u.updateDepositEvent(deposit)
  1732  		}
  1733  	}()
  1734  
  1735  	deposit.mtx.RLock()
  1736  	cexConfirmed, feeConfirmed := deposit.cexConfirmed, deposit.feeConfirmed
  1737  	deposit.mtx.RUnlock()
  1738  
  1739  	if !cexConfirmed {
  1740  		complete, amtCredited := u.CEX.ConfirmDeposit(ctx, &libxc.DepositData{
  1741  			AssetID:            deposit.assetID,
  1742  			TxID:               txID,
  1743  			AmountConventional: deposit.amtConventional,
  1744  		})
  1745  
  1746  		deposit.mtx.Lock()
  1747  		deposit.amtCredited = amtCredited
  1748  		if complete {
  1749  			updated = true
  1750  			cexConfirmed = true
  1751  			deposit.cexConfirmed = true
  1752  		}
  1753  		deposit.mtx.Unlock()
  1754  	}
  1755  
  1756  	if !feeConfirmed {
  1757  		tx, err := u.clientCore.WalletTransaction(deposit.assetID, txID)
  1758  		if err != nil {
  1759  			u.log.Errorf("Error confirming deposit: %v", err)
  1760  			return false
  1761  		}
  1762  
  1763  		deposit.mtx.Lock()
  1764  		deposit.tx = tx
  1765  		if tx.Confirmed {
  1766  			feeConfirmed = true
  1767  			deposit.feeConfirmed = true
  1768  			updated = true
  1769  		}
  1770  		deposit.mtx.Unlock()
  1771  	}
  1772  
  1773  	if feeConfirmed && cexConfirmed {
  1774  		u.pendingDepositComplete(deposit)
  1775  		return true
  1776  	}
  1777  
  1778  	return false
  1779  }
  1780  
  1781  // deposit deposits funds to the CEX. The deposited funds will be removed from
  1782  // the bot's wallet balance and allocated to the bot's CEX balance. After both
  1783  // the fees of the deposit transaction are confirmed by the wallet and the
  1784  // CEX confirms the amount they received, the onConfirm callback is called.
  1785  func (u *unifiedExchangeAdaptor) deposit(ctx context.Context, assetID uint32, amount uint64) error {
  1786  	balance := u.DEXBalance(assetID)
  1787  	// TODO: estimate fee and make sure we have enough to cover it.
  1788  	if balance.Available < amount {
  1789  		return fmt.Errorf("bot has insufficient balance to deposit %d. required: %v, have: %v", assetID, amount, balance.Available)
  1790  	}
  1791  
  1792  	addr, err := u.CEX.GetDepositAddress(ctx, assetID)
  1793  	if err != nil {
  1794  		return err
  1795  	}
  1796  	coin, err := u.clientCore.Send([]byte{}, assetID, amount, addr, u.isWithdrawer(assetID))
  1797  	if err != nil {
  1798  		return err
  1799  	}
  1800  
  1801  	if assetID == u.baseID {
  1802  		u.pendingBaseRebalance.Store(true)
  1803  	} else {
  1804  		u.pendingQuoteRebalance.Store(true)
  1805  	}
  1806  
  1807  	tx, err := u.clientCore.WalletTransaction(assetID, coin.TxID())
  1808  	if err != nil {
  1809  		// If the wallet does not know about the transaction it just sent,
  1810  		// this is a serious bug in the wallet. Should Send be updated to
  1811  		// return asset.WalletTransaction?
  1812  		return err
  1813  	}
  1814  
  1815  	u.log.Infof("Deposited %s. TxID = %s", u.fmtQty(assetID, amount), tx.ID)
  1816  
  1817  	eventID := u.eventLogID.Add(1)
  1818  	ui, _ := asset.UnitInfo(assetID)
  1819  	deposit := &pendingDeposit{
  1820  		eventLogID:      eventID,
  1821  		timestamp:       time.Now().Unix(),
  1822  		tx:              tx,
  1823  		assetID:         assetID,
  1824  		feeConfirmed:    !u.isDynamicSwapper(assetID),
  1825  		amtConventional: float64(amount) / float64(ui.Conventional.ConversionFactor),
  1826  	}
  1827  	u.updateDepositEvent(deposit)
  1828  
  1829  	u.balancesMtx.Lock()
  1830  	u.pendingDeposits[tx.ID] = deposit
  1831  	u.balancesMtx.Unlock()
  1832  
  1833  	u.wg.Add(1)
  1834  	go func() {
  1835  		defer u.wg.Done()
  1836  		timer := time.NewTimer(0)
  1837  		defer timer.Stop()
  1838  		for {
  1839  			select {
  1840  			case <-timer.C:
  1841  				if u.confirmDeposit(ctx, tx.ID) {
  1842  					return
  1843  				}
  1844  				timer = time.NewTimer(time.Minute)
  1845  			case <-ctx.Done():
  1846  				return
  1847  			}
  1848  		}
  1849  	}()
  1850  
  1851  	return nil
  1852  }
  1853  
  1854  // pendingWithdrawalComplete is called after a withdrawal has been confirmed.
  1855  // The amount received is applied to the base balance, and the withdrawal is
  1856  // removed from the pending map.
  1857  func (u *unifiedExchangeAdaptor) pendingWithdrawalComplete(id string, tx *asset.WalletTransaction) {
  1858  	u.balancesMtx.Lock()
  1859  	withdrawal, found := u.pendingWithdrawals[id]
  1860  	if !found {
  1861  		u.balancesMtx.Unlock()
  1862  		return
  1863  	}
  1864  	delete(u.pendingWithdrawals, id)
  1865  
  1866  	if withdrawal.assetID == u.baseID {
  1867  		u.pendingBaseRebalance.Store(false)
  1868  	} else {
  1869  		u.pendingQuoteRebalance.Store(false)
  1870  	}
  1871  
  1872  	dexEffects, cexEffects := withdrawal.balanceEffects()
  1873  	u.baseDexBalances[withdrawal.assetID] += dexEffects.Settled[withdrawal.assetID]
  1874  	u.baseCexBalances[withdrawal.assetID] += cexEffects.Settled[withdrawal.assetID]
  1875  	u.balancesMtx.Unlock()
  1876  
  1877  	u.updateWithdrawalEvent(withdrawal, tx)
  1878  	u.sendStatsUpdate()
  1879  
  1880  	dexDiffs := map[uint32]int64{withdrawal.assetID: dexEffects.Settled[withdrawal.assetID]}
  1881  	cexDiffs := map[uint32]int64{withdrawal.assetID: cexEffects.Settled[withdrawal.assetID]}
  1882  
  1883  	u.balancesMtx.RLock()
  1884  	u.logBalanceAdjustments(dexDiffs, cexDiffs, fmt.Sprintf("Withdrawal %s complete.", id))
  1885  	u.balancesMtx.RUnlock()
  1886  }
  1887  
  1888  func (u *unifiedExchangeAdaptor) confirmWithdrawal(ctx context.Context, id string) bool {
  1889  	u.balancesMtx.RLock()
  1890  	withdrawal, found := u.pendingWithdrawals[id]
  1891  	u.balancesMtx.RUnlock()
  1892  	if !found {
  1893  		u.log.Errorf("Withdrawal %s not found among pending withdrawals", id)
  1894  		return false
  1895  	}
  1896  
  1897  	withdrawal.txMtx.RLock()
  1898  	txID := withdrawal.txID
  1899  	withdrawal.txMtx.RUnlock()
  1900  
  1901  	if txID == "" {
  1902  		var err error
  1903  		_, txID, err = u.CEX.ConfirmWithdrawal(ctx, id, withdrawal.assetID)
  1904  		if errors.Is(err, libxc.ErrWithdrawalPending) {
  1905  			return false
  1906  		}
  1907  		if err != nil {
  1908  			u.log.Errorf("Error confirming withdrawal: %v", err)
  1909  			return false
  1910  		}
  1911  
  1912  		withdrawal.txMtx.Lock()
  1913  		withdrawal.txID = txID
  1914  		withdrawal.txMtx.Unlock()
  1915  	}
  1916  
  1917  	tx, err := u.clientCore.WalletTransaction(withdrawal.assetID, txID)
  1918  	if errors.Is(err, asset.CoinNotFoundError) {
  1919  		u.log.Warnf("%s wallet does not know about withdrawal tx: %s", dex.BipIDSymbol(withdrawal.assetID), id)
  1920  		return false
  1921  	}
  1922  	if err != nil {
  1923  		u.log.Errorf("Error getting wallet transaction: %v", err)
  1924  		return false
  1925  	}
  1926  
  1927  	withdrawal.txMtx.Lock()
  1928  	withdrawal.tx = tx
  1929  	withdrawal.txMtx.Unlock()
  1930  
  1931  	if tx.Confirmed {
  1932  		u.pendingWithdrawalComplete(id, tx)
  1933  		return true
  1934  	}
  1935  
  1936  	return false
  1937  }
  1938  
  1939  // withdraw withdraws funds from the CEX. After withdrawing, the CEX is queried
  1940  // for the transaction ID. After the transaction ID is available, the wallet is
  1941  // queried for the amount received.
  1942  func (u *unifiedExchangeAdaptor) withdraw(ctx context.Context, assetID uint32, amount uint64) error {
  1943  	symbol := dex.BipIDSymbol(assetID)
  1944  
  1945  	balance := u.CEXBalance(assetID)
  1946  	if balance.Available < amount {
  1947  		return fmt.Errorf("bot has insufficient balance to withdraw %s. required: %v, have: %v", symbol, amount, balance.Available)
  1948  	}
  1949  
  1950  	addr, err := u.clientCore.NewDepositAddress(assetID)
  1951  	if err != nil {
  1952  		return err
  1953  	}
  1954  
  1955  	// Pull transparent address out of unified address. There may be a different
  1956  	// field "exchangeAddress" once we add support for the new special encoding
  1957  	// required on binance global for zec and firo.
  1958  	if strings.HasPrefix(addr, "unified:") {
  1959  		var addrs struct {
  1960  			Transparent string `json:"transparent"`
  1961  		}
  1962  		if err := json.Unmarshal([]byte(addr[len("unified:"):]), &addrs); err != nil {
  1963  			return fmt.Errorf("error decoding unified address %q: %v", addr, err)
  1964  		}
  1965  		addr = addrs.Transparent
  1966  	}
  1967  
  1968  	u.balancesMtx.Lock()
  1969  	withdrawalID, err := u.CEX.Withdraw(ctx, assetID, amount, addr)
  1970  	if err != nil {
  1971  		u.balancesMtx.Unlock()
  1972  		return err
  1973  	}
  1974  
  1975  	u.log.Infof("Withdrew %s", u.fmtQty(assetID, amount))
  1976  	if assetID == u.baseID {
  1977  		u.pendingBaseRebalance.Store(true)
  1978  	} else {
  1979  		u.pendingQuoteRebalance.Store(true)
  1980  	}
  1981  	withdrawal := &pendingWithdrawal{
  1982  		eventLogID:   u.eventLogID.Add(1),
  1983  		timestamp:    time.Now().Unix(),
  1984  		assetID:      assetID,
  1985  		amtWithdrawn: amount,
  1986  		withdrawalID: withdrawalID,
  1987  	}
  1988  	u.pendingWithdrawals[withdrawalID] = withdrawal
  1989  	u.balancesMtx.Unlock()
  1990  
  1991  	u.updateWithdrawalEvent(withdrawal, nil)
  1992  	u.sendStatsUpdate()
  1993  
  1994  	u.wg.Add(1)
  1995  	go func() {
  1996  		defer u.wg.Done()
  1997  		timer := time.NewTimer(0)
  1998  		defer timer.Stop()
  1999  		for {
  2000  			select {
  2001  			case <-timer.C:
  2002  				if u.confirmWithdrawal(ctx, withdrawalID) {
  2003  					// TODO: Trigger a rebalance here somehow. Same with
  2004  					// confirmed deposit. Maybe confirmWithdrawal should be
  2005  					// checked as part of the rebalance sequence instead of in
  2006  					// a goroutine.
  2007  					return
  2008  				}
  2009  				timer = time.NewTimer(time.Minute)
  2010  			case <-ctx.Done():
  2011  				return
  2012  			}
  2013  		}
  2014  	}()
  2015  
  2016  	return nil
  2017  }
  2018  
  2019  func (u *unifiedExchangeAdaptor) reversePriorityOrders(sell bool) []*dexOrderState {
  2020  	orderMap := u.groupedBookedOrders(sell)
  2021  	orderGroups := utils.MapItems(orderMap)
  2022  
  2023  	sort.Slice(orderGroups, func(i, j int) bool {
  2024  		return orderGroups[i][0].placementIndex > orderGroups[j][0].placementIndex
  2025  	})
  2026  	orders := make([]*dexOrderState, 0, len(orderGroups))
  2027  	for _, g := range orderGroups {
  2028  		// Order the group by smallest order first.
  2029  		states := make([]*dexOrderState, len(g))
  2030  		for i, o := range g {
  2031  			states[i] = o.currentState()
  2032  		}
  2033  		sort.Slice(states, func(i, j int) bool {
  2034  			return (states[i].order.Qty - states[i].order.Filled) < (states[j].order.Qty - states[j].order.Filled)
  2035  		})
  2036  		orders = append(orders, states...)
  2037  	}
  2038  	return orders
  2039  }
  2040  
  2041  // freeUpFunds identifies cancelable orders to free up funds for a proposed
  2042  // transfer. Identified orders are sorted in reverse order of priority. For
  2043  // orders with the same placement index, smaller orders are first. minToFree
  2044  // specifies a minimum amount of funds to liberate. pruneMatchableTo is the
  2045  // counter-asset quantity for some amount of cex balance, and we'll continue to
  2046  // add cancel orders until we're not over-matching. freeUpFunds does not
  2047  // actually cancel any orders. It just identifies orders that can be canceled
  2048  // immediately to satisfy the conditions specified.
  2049  func (u *unifiedExchangeAdaptor) freeUpFunds(
  2050  	assetID uint32,
  2051  	minToFree uint64,
  2052  	pruneMatchableTo uint64,
  2053  	currEpoch uint64,
  2054  ) ([]*dexOrderState, bool) {
  2055  
  2056  	orders := u.reversePriorityOrders(assetID == u.baseID)
  2057  	var matchableCounterQty, freeable, persistentMatchable uint64
  2058  	for _, o := range orders {
  2059  		var matchable uint64
  2060  		if assetID == o.order.BaseID {
  2061  			matchable += calc.BaseToQuote(o.counterTradeRate, o.order.Qty)
  2062  		} else {
  2063  			matchable += o.order.Qty
  2064  		}
  2065  		matchableCounterQty += matchable
  2066  		if currEpoch-o.order.Epoch >= 2 {
  2067  			freeable += o.dexBalanceEffects.Locked[assetID]
  2068  		} else {
  2069  			persistentMatchable += matchable
  2070  		}
  2071  	}
  2072  
  2073  	if freeable < minToFree {
  2074  		return nil, false
  2075  	}
  2076  	if persistentMatchable > pruneMatchableTo {
  2077  		return nil, false
  2078  	}
  2079  
  2080  	if minToFree == 0 && matchableCounterQty <= pruneMatchableTo {
  2081  		return nil, true
  2082  	}
  2083  
  2084  	amtFreedByCancellingOrder := func(o *dexOrderState) (locked, counterQty uint64) {
  2085  		if assetID == o.order.BaseID {
  2086  			return o.dexBalanceEffects.Locked[assetID], calc.BaseToQuote(o.counterTradeRate, o.order.Qty)
  2087  		}
  2088  		return o.dexBalanceEffects.Locked[assetID], o.order.Qty
  2089  	}
  2090  
  2091  	unfreed := minToFree
  2092  	cancels := make([]*dexOrderState, 0, len(orders))
  2093  	for _, o := range orders {
  2094  		if currEpoch-o.order.Epoch < 2 {
  2095  			continue
  2096  		}
  2097  		cancels = append(cancels, o)
  2098  		freed, counterQty := amtFreedByCancellingOrder(o)
  2099  		if freed >= unfreed {
  2100  			unfreed = 0
  2101  		} else {
  2102  			unfreed -= freed
  2103  		}
  2104  		matchableCounterQty -= counterQty
  2105  		if matchableCounterQty <= pruneMatchableTo && unfreed == 0 {
  2106  			break
  2107  		}
  2108  	}
  2109  	return cancels, true
  2110  }
  2111  
  2112  // handleCEXTradeUpdate handles a trade update from the CEX. If the trade is in
  2113  // the pending map, it will be updated. If the trade is complete, the base balances
  2114  // will be updated.
  2115  func (u *unifiedExchangeAdaptor) handleCEXTradeUpdate(trade *libxc.Trade) {
  2116  	var currCEXOrder *pendingCEXOrder
  2117  	defer func() {
  2118  		if currCEXOrder != nil {
  2119  			u.updateCEXOrderEvent(trade, currCEXOrder.eventLogID, currCEXOrder.timestamp)
  2120  			u.sendStatsUpdate()
  2121  		}
  2122  	}()
  2123  
  2124  	u.balancesMtx.Lock()
  2125  	currCEXOrder, found := u.pendingCEXOrders[trade.ID]
  2126  	u.balancesMtx.Unlock()
  2127  	if !found {
  2128  		return
  2129  	}
  2130  
  2131  	if !trade.Complete {
  2132  		currCEXOrder.tradeMtx.Lock()
  2133  		currCEXOrder.trade = trade
  2134  		currCEXOrder.tradeMtx.Unlock()
  2135  		return
  2136  	}
  2137  
  2138  	u.balancesMtx.Lock()
  2139  	defer u.balancesMtx.Unlock()
  2140  
  2141  	delete(u.pendingCEXOrders, trade.ID)
  2142  
  2143  	if trade.BaseFilled == 0 && trade.QuoteFilled == 0 {
  2144  		u.log.Infof("CEX trade %s completed with zero filled amount", trade.ID)
  2145  		return
  2146  	}
  2147  
  2148  	diffs := make(map[uint32]int64)
  2149  
  2150  	balanceEffects := cexTradeBalanceEffects(trade)
  2151  	for assetID, v := range balanceEffects.Settled {
  2152  		u.baseCexBalances[assetID] += v
  2153  		diffs[assetID] = v
  2154  	}
  2155  
  2156  	u.logBalanceAdjustments(nil, diffs, fmt.Sprintf("CEX trade %s completed.", trade.ID))
  2157  }
  2158  
  2159  // SubscribeTradeUpdates subscribes to trade updates for the bot's trades on
  2160  // the CEX. This should be called before making any trades, and only once.
  2161  func (w *unifiedExchangeAdaptor) SubscribeTradeUpdates() <-chan *libxc.Trade {
  2162  	w.subscriptionIDMtx.Lock()
  2163  	defer w.subscriptionIDMtx.Unlock()
  2164  	if w.subscriptionID != nil {
  2165  		w.log.Errorf("SubscribeTradeUpdates called more than once by bot %s", w.botID)
  2166  		return nil
  2167  	}
  2168  
  2169  	updates, unsubscribe, subscriptionID := w.CEX.SubscribeTradeUpdates()
  2170  	w.subscriptionID = &subscriptionID
  2171  
  2172  	forwardUpdates := make(chan *libxc.Trade, 256)
  2173  	go func() {
  2174  		for {
  2175  			select {
  2176  			case <-w.ctx.Done():
  2177  				unsubscribe()
  2178  				return
  2179  			case note := <-updates:
  2180  				w.handleCEXTradeUpdate(note)
  2181  				select {
  2182  				case forwardUpdates <- note:
  2183  				default:
  2184  					w.log.Errorf("CEX trade update channel full")
  2185  				}
  2186  			}
  2187  		}
  2188  	}()
  2189  
  2190  	return forwardUpdates
  2191  }
  2192  
  2193  // Trade executes a trade on the CEX. The trade will be executed using the
  2194  // bot's CEX balance.
  2195  func (u *unifiedExchangeAdaptor) CEXTrade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64) (*libxc.Trade, error) {
  2196  	if !u.SufficientBalanceForCEXTrade(baseID, quoteID, sell, rate, qty) {
  2197  		return nil, fmt.Errorf("insufficient balance")
  2198  	}
  2199  
  2200  	u.subscriptionIDMtx.RLock()
  2201  	subscriptionID := u.subscriptionID
  2202  	u.subscriptionIDMtx.RUnlock()
  2203  	if subscriptionID == nil {
  2204  		return nil, fmt.Errorf("trade called before SubscribeTradeUpdates")
  2205  	}
  2206  
  2207  	var trade *libxc.Trade
  2208  	now := time.Now().Unix()
  2209  	eventID := u.eventLogID.Add(1)
  2210  	defer func() {
  2211  		if trade != nil {
  2212  			u.updateCEXOrderEvent(trade, eventID, now)
  2213  			u.sendStatsUpdate()
  2214  		}
  2215  	}()
  2216  
  2217  	u.balancesMtx.Lock()
  2218  	defer u.balancesMtx.Unlock()
  2219  
  2220  	trade, err := u.CEX.Trade(ctx, baseID, quoteID, sell, rate, qty, *subscriptionID)
  2221  	u.updateCEXProblems(cexTradeProblem, u.baseID, err)
  2222  	if err != nil {
  2223  		return nil, err
  2224  	}
  2225  
  2226  	if trade.Complete {
  2227  		diffs := make(map[uint32]int64)
  2228  		if trade.Sell {
  2229  			u.baseCexBalances[trade.BaseID] -= int64(trade.BaseFilled)
  2230  			u.baseCexBalances[trade.QuoteID] += int64(trade.QuoteFilled)
  2231  			diffs[trade.BaseID] = -int64(trade.BaseFilled)
  2232  			diffs[trade.QuoteID] = int64(trade.QuoteFilled)
  2233  		} else {
  2234  			u.baseCexBalances[trade.BaseID] += int64(trade.BaseFilled)
  2235  			u.baseCexBalances[trade.QuoteID] -= int64(trade.QuoteFilled)
  2236  			diffs[trade.BaseID] = int64(trade.BaseFilled)
  2237  			diffs[trade.QuoteID] = -int64(trade.QuoteFilled)
  2238  		}
  2239  		u.logBalanceAdjustments(nil, diffs, fmt.Sprintf("CEX trade %s completed.", trade.ID))
  2240  	} else {
  2241  		u.pendingCEXOrders[trade.ID] = &pendingCEXOrder{
  2242  			trade:      trade,
  2243  			eventLogID: eventID,
  2244  			timestamp:  now,
  2245  		}
  2246  	}
  2247  
  2248  	return trade, nil
  2249  }
  2250  
  2251  func (u *unifiedExchangeAdaptor) fiatRate(assetID uint32) float64 {
  2252  	rates := u.fiatRates.Load()
  2253  	if rates == nil {
  2254  		return 0
  2255  	}
  2256  
  2257  	return rates.(map[uint32]float64)[assetID]
  2258  }
  2259  
  2260  // ExchangeRateFromFiatSources returns market's exchange rate using fiat sources.
  2261  func (u *unifiedExchangeAdaptor) ExchangeRateFromFiatSources() uint64 {
  2262  	atomicCFactor, err := u.atomicConversionRateFromFiat(u.baseID, u.quoteID)
  2263  	if err != nil {
  2264  		u.log.Errorf("Error genrating atomic conversion rate: %v", err)
  2265  		return 0
  2266  	}
  2267  	return uint64(math.Round(atomicCFactor * calc.RateEncodingFactor))
  2268  }
  2269  
  2270  // atomicConversionRateFromFiat generates a conversion rate suitable for
  2271  // converting from atomic units of one asset to atomic units of another.
  2272  // This is the same as a message-rate, but without the RateEncodingFactor,
  2273  // hence a float.
  2274  func (u *unifiedExchangeAdaptor) atomicConversionRateFromFiat(fromID, toID uint32) (float64, error) {
  2275  	fromRate := u.fiatRate(fromID)
  2276  	toRate := u.fiatRate(toID)
  2277  	if fromRate == 0 || toRate == 0 {
  2278  		return 0, fmt.Errorf("missing fiat rate. rate for %d = %f, rate for %d = %f", fromID, fromRate, toID, toRate)
  2279  	}
  2280  
  2281  	fromUI, err := asset.UnitInfo(fromID)
  2282  	if err != nil {
  2283  		return 0, fmt.Errorf("exchangeRates from asset %d not found", fromID)
  2284  	}
  2285  	toUI, err := asset.UnitInfo(toID)
  2286  	if err != nil {
  2287  		return 0, fmt.Errorf("exchangeRates to asset %d not found", toID)
  2288  	}
  2289  
  2290  	// v_to_atomic = v_from_atomic / from_conv_factor * convConversionRate / to_conv_factor
  2291  	return 1 / float64(fromUI.Conventional.ConversionFactor) * fromRate / toRate * float64(toUI.Conventional.ConversionFactor), nil
  2292  }
  2293  
  2294  // OrderFees returns the fees for a buy and sell order. The order fees are for
  2295  // placing orders on the market specified by the exchangeAdaptorCfg used to
  2296  // create the unifiedExchangeAdaptor.
  2297  func (u *unifiedExchangeAdaptor) orderFees() (buyFees, sellFees *OrderFees, err error) {
  2298  	u.feesMtx.RLock()
  2299  	buyFees, sellFees = u.buyFees, u.sellFees
  2300  	u.feesMtx.RUnlock()
  2301  
  2302  	if u.buyFees == nil || u.sellFees == nil {
  2303  		return u.updateFeeRates()
  2304  	}
  2305  
  2306  	return buyFees, sellFees, nil
  2307  }
  2308  
  2309  // OrderFeesInUnits returns the estimated swap and redemption fees for either a
  2310  // buy or sell order in units of either the base or quote asset. If either the
  2311  // base or quote asset is a token, the fees are converted using fiat rates.
  2312  // Otherwise, the rate parameter is used for the conversion.
  2313  func (u *unifiedExchangeAdaptor) OrderFeesInUnits(sell, base bool, rate uint64) (uint64, error) {
  2314  	buyFeeRange, sellFeeRange, err := u.orderFees()
  2315  	if err != nil {
  2316  		return 0, fmt.Errorf("error getting order fees: %v", err)
  2317  	}
  2318  
  2319  	buyFees, sellFees := buyFeeRange.Estimated, sellFeeRange.Estimated
  2320  	baseFees, quoteFees := buyFees.Redeem, buyFees.Swap
  2321  	if sell {
  2322  		baseFees, quoteFees = sellFees.Swap, sellFees.Redeem
  2323  	}
  2324  
  2325  	convertViaFiat := func(fees uint64, fromID, toID uint32) (uint64, error) {
  2326  		atomicCFactor, err := u.atomicConversionRateFromFiat(fromID, toID)
  2327  		if err != nil {
  2328  			return 0, err
  2329  		}
  2330  		return uint64(math.Round(float64(fees) * atomicCFactor)), nil
  2331  	}
  2332  
  2333  	var baseFeesInUnits, quoteFeesInUnits uint64
  2334  	if tkn := asset.TokenInfo(u.baseID); tkn != nil {
  2335  		baseFees, err = convertViaFiat(baseFees, tkn.ParentID, u.baseID)
  2336  		if err != nil {
  2337  			return 0, err
  2338  		}
  2339  	}
  2340  	if tkn := asset.TokenInfo(u.quoteID); tkn != nil {
  2341  		quoteFees, err = convertViaFiat(quoteFees, tkn.ParentID, u.quoteID)
  2342  		if err != nil {
  2343  			return 0, err
  2344  		}
  2345  	}
  2346  
  2347  	if base {
  2348  		baseFeesInUnits = baseFees
  2349  	} else {
  2350  		baseFeesInUnits = calc.BaseToQuote(rate, baseFees)
  2351  	}
  2352  
  2353  	if base {
  2354  		quoteFeesInUnits = calc.QuoteToBase(rate, quoteFees)
  2355  	} else {
  2356  		quoteFeesInUnits = quoteFees
  2357  	}
  2358  
  2359  	return baseFeesInUnits + quoteFeesInUnits, nil
  2360  }
  2361  
  2362  // tryCancelOrders cancels all booked DEX orders that are past the free cancel
  2363  // threshold. If cancelCEXOrders is true, it will also cancel CEX orders. True
  2364  // is returned if all orders have been cancelled. If cancelCEXOrders is false,
  2365  // false will always be returned.
  2366  func (u *unifiedExchangeAdaptor) tryCancelOrders(ctx context.Context, epoch *uint64, cancelCEXOrders bool) bool {
  2367  	u.balancesMtx.RLock()
  2368  	defer u.balancesMtx.RUnlock()
  2369  
  2370  	done := true
  2371  
  2372  	freeCancel := func(orderEpoch uint64) bool {
  2373  		if epoch == nil {
  2374  			return true
  2375  		}
  2376  		return *epoch-orderEpoch >= 2
  2377  	}
  2378  
  2379  	cancels := make([]dex.Bytes, 0, len(u.pendingDEXOrders))
  2380  
  2381  	for _, pendingOrder := range u.pendingDEXOrders {
  2382  		o := pendingOrder.currentState().order
  2383  
  2384  		orderLatestState, err := u.clientCore.Order(o.ID)
  2385  		if err != nil {
  2386  			u.log.Errorf("Error fetching order %s: %v", o.ID, err)
  2387  			continue
  2388  		}
  2389  		if orderLatestState.Status > order.OrderStatusBooked {
  2390  			continue
  2391  		}
  2392  
  2393  		done = false
  2394  		if freeCancel(o.Epoch) {
  2395  			err := u.clientCore.Cancel(o.ID)
  2396  			if err != nil {
  2397  				u.log.Errorf("Error canceling order %s: %v", o.ID, err)
  2398  			} else {
  2399  				cancels = append(cancels, o.ID)
  2400  			}
  2401  		}
  2402  	}
  2403  
  2404  	if !cancelCEXOrders {
  2405  		return false
  2406  	}
  2407  
  2408  	for _, pendingOrder := range u.pendingCEXOrders {
  2409  		ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
  2410  		defer cancel()
  2411  
  2412  		pendingOrder.tradeMtx.RLock()
  2413  		tradeID, baseID, quoteID := pendingOrder.trade.ID, pendingOrder.trade.BaseID, pendingOrder.trade.QuoteID
  2414  		pendingOrder.tradeMtx.RUnlock()
  2415  
  2416  		tradeStatus, err := u.CEX.TradeStatus(ctx, tradeID, baseID, quoteID)
  2417  		if err != nil {
  2418  			u.log.Errorf("Error getting CEX trade status: %v", err)
  2419  			continue
  2420  		}
  2421  		if tradeStatus.Complete {
  2422  			continue
  2423  		}
  2424  
  2425  		done = false
  2426  		err = u.CEX.CancelTrade(ctx, baseID, quoteID, tradeID)
  2427  		if err != nil {
  2428  			u.log.Errorf("Error canceling CEX trade %s: %v", tradeID, err)
  2429  		}
  2430  	}
  2431  
  2432  	return done
  2433  }
  2434  
  2435  func (u *unifiedExchangeAdaptor) cancelAllOrders(ctx context.Context) {
  2436  	book, bookFeed, err := u.clientCore.SyncBook(u.host, u.baseID, u.quoteID)
  2437  	if err != nil {
  2438  		u.log.Errorf("Error syncing book for cancellations: %v", err)
  2439  		u.tryCancelOrders(ctx, nil, true)
  2440  		return
  2441  	}
  2442  	defer bookFeed.Close()
  2443  
  2444  	mktCfg, err := u.clientCore.ExchangeMarket(u.host, u.baseID, u.quoteID)
  2445  	if err != nil {
  2446  		u.log.Errorf("Error getting market configuration: %v", err)
  2447  		u.tryCancelOrders(ctx, nil, true)
  2448  		return
  2449  	}
  2450  
  2451  	currentEpoch := book.CurrentEpoch()
  2452  	if u.tryCancelOrders(ctx, &currentEpoch, true) {
  2453  		return
  2454  	}
  2455  
  2456  	timeout := time.Millisecond * time.Duration(3*mktCfg.EpochLen)
  2457  	timer := time.NewTimer(timeout)
  2458  	defer timer.Stop()
  2459  
  2460  	i := 0
  2461  	for {
  2462  		select {
  2463  		case ni, ok := <-bookFeed.Next():
  2464  			if !ok {
  2465  				u.log.Error("Stopping bot due to nil book feed.")
  2466  				u.kill()
  2467  				return
  2468  			}
  2469  			switch epoch := ni.Payload.(type) {
  2470  			case *core.ResolvedEpoch:
  2471  				if u.tryCancelOrders(ctx, &epoch.Current, true) {
  2472  					return
  2473  				}
  2474  				timer.Reset(timeout)
  2475  				i++
  2476  			}
  2477  		case <-timer.C:
  2478  			u.tryCancelOrders(ctx, nil, true)
  2479  			return
  2480  		}
  2481  
  2482  		if i >= 3 {
  2483  			return
  2484  		}
  2485  	}
  2486  }
  2487  
  2488  // SubscribeOrderUpdates returns a channel that sends updates for orders placed
  2489  // on the DEX. This function should be called only once.
  2490  func (u *unifiedExchangeAdaptor) SubscribeOrderUpdates() <-chan *core.Order {
  2491  	orderUpdates := make(chan *core.Order, 128)
  2492  	u.orderUpdates.Store(orderUpdates)
  2493  	return orderUpdates
  2494  }
  2495  
  2496  // isAccountLocker returns if the asset's wallet is an asset.AccountLocker.
  2497  func (u *unifiedExchangeAdaptor) isAccountLocker(assetID uint32) bool {
  2498  	if assetID == u.baseID {
  2499  		return u.baseTraits.IsAccountLocker()
  2500  	}
  2501  	return u.quoteTraits.IsAccountLocker()
  2502  }
  2503  
  2504  // isDynamicSwapper returns if the asset's wallet is an asset.DynamicSwapper.
  2505  func (u *unifiedExchangeAdaptor) isDynamicSwapper(assetID uint32) bool {
  2506  	if assetID == u.baseID {
  2507  		return u.baseTraits.IsDynamicSwapper()
  2508  	}
  2509  	return u.quoteTraits.IsDynamicSwapper()
  2510  }
  2511  
  2512  // isWithdrawer returns if the asset's wallet is an asset.Withdrawer.
  2513  func (u *unifiedExchangeAdaptor) isWithdrawer(assetID uint32) bool {
  2514  	if assetID == u.baseID {
  2515  		return u.baseTraits.IsWithdrawer()
  2516  	}
  2517  	return u.quoteTraits.IsWithdrawer()
  2518  }
  2519  
  2520  func orderAssets(baseID, quoteID uint32, sell bool) (fromAsset, fromFeeAsset, toAsset, toFeeAsset uint32) {
  2521  	if sell {
  2522  		fromAsset = baseID
  2523  		toAsset = quoteID
  2524  	} else {
  2525  		fromAsset = quoteID
  2526  		toAsset = baseID
  2527  	}
  2528  	if token := asset.TokenInfo(fromAsset); token != nil {
  2529  		fromFeeAsset = token.ParentID
  2530  	} else {
  2531  		fromFeeAsset = fromAsset
  2532  	}
  2533  	if token := asset.TokenInfo(toAsset); token != nil {
  2534  		toFeeAsset = token.ParentID
  2535  	} else {
  2536  		toFeeAsset = toAsset
  2537  	}
  2538  	return
  2539  }
  2540  
  2541  func feeAssetID(assetID uint32) uint32 {
  2542  	if token := asset.TokenInfo(assetID); token != nil {
  2543  		return token.ParentID
  2544  	}
  2545  	return assetID
  2546  }
  2547  
  2548  func dexOrderComplete(o *core.Order) bool {
  2549  	if o.Status.IsActive() {
  2550  		return false
  2551  	}
  2552  
  2553  	for _, match := range o.Matches {
  2554  		if match.Active {
  2555  			return false
  2556  		}
  2557  	}
  2558  
  2559  	return o.AllFeesConfirmed
  2560  }
  2561  
  2562  // orderCoinIDs returns all of the swap, redeem, and refund transactions
  2563  // involving a dex order. There may be multiple coin IDs representing the
  2564  // same transaction.
  2565  func orderCoinIDs(o *core.Order) (swaps map[string]bool, redeems map[string]bool, refunds map[string]bool) {
  2566  	swaps = make(map[string]bool)
  2567  	redeems = make(map[string]bool)
  2568  	refunds = make(map[string]bool)
  2569  
  2570  	for _, match := range o.Matches {
  2571  		if match.Swap != nil {
  2572  			swaps[match.Swap.ID.String()] = true
  2573  		}
  2574  		if match.Redeem != nil {
  2575  			redeems[match.Redeem.ID.String()] = true
  2576  		}
  2577  		if match.Refund != nil {
  2578  			refunds[match.Refund.ID.String()] = true
  2579  		}
  2580  	}
  2581  
  2582  	return
  2583  }
  2584  
  2585  func dexOrderEffects(o *core.Order, swaps, redeems, refunds map[string]*asset.WalletTransaction, counterTradeRate uint64, baseTraits, quoteTraits asset.WalletTrait) (dex, cex *BalanceEffects) {
  2586  	dex, cex = newBalanceEffects(), newBalanceEffects()
  2587  
  2588  	fromAsset, fromFeeAsset, toAsset, toFeeAsset := orderAssets(o.BaseID, o.QuoteID, o.Sell)
  2589  
  2590  	// Account for pending funds locked in swaps awaiting confirmation.
  2591  	for _, match := range o.Matches {
  2592  		if match.Swap == nil || match.Redeem != nil || match.Refund != nil {
  2593  			continue
  2594  		}
  2595  
  2596  		if match.Revoked {
  2597  			swapTx, found := swaps[match.Swap.ID.String()]
  2598  			if found {
  2599  				dex.Pending[fromAsset] += swapTx.Amount
  2600  			}
  2601  			continue
  2602  		}
  2603  
  2604  		var redeemAmt uint64
  2605  		if o.Sell {
  2606  			redeemAmt = calc.BaseToQuote(match.Rate, match.Qty)
  2607  		} else {
  2608  			redeemAmt = match.Qty
  2609  		}
  2610  		dex.Pending[toAsset] += redeemAmt
  2611  	}
  2612  
  2613  	dex.Settled[fromAsset] -= int64(o.LockedAmt)
  2614  	dex.Settled[fromFeeAsset] -= int64(o.ParentAssetLockedAmt + o.RefundLockedAmt)
  2615  	dex.Settled[toFeeAsset] -= int64(o.RedeemLockedAmt)
  2616  
  2617  	dex.Locked[fromAsset] += o.LockedAmt
  2618  	dex.Locked[fromFeeAsset] += o.ParentAssetLockedAmt + o.RefundLockedAmt
  2619  	dex.Locked[toFeeAsset] += o.RedeemLockedAmt
  2620  
  2621  	if o.FeesPaid != nil {
  2622  		dex.Settled[fromFeeAsset] -= int64(o.FeesPaid.Funding)
  2623  	}
  2624  
  2625  	for _, tx := range swaps {
  2626  		dex.Settled[fromAsset] -= int64(tx.Amount)
  2627  		dex.Settled[fromFeeAsset] -= int64(tx.Fees)
  2628  	}
  2629  
  2630  	var reedeemIsDynamicSwapper, refundIsDynamicSwapper bool
  2631  	if o.Sell {
  2632  		reedeemIsDynamicSwapper = quoteTraits.IsDynamicSwapper()
  2633  		refundIsDynamicSwapper = baseTraits.IsDynamicSwapper()
  2634  	} else {
  2635  		reedeemIsDynamicSwapper = baseTraits.IsDynamicSwapper()
  2636  		refundIsDynamicSwapper = quoteTraits.IsDynamicSwapper()
  2637  	}
  2638  
  2639  	for _, tx := range redeems {
  2640  		if tx.Confirmed {
  2641  			dex.Settled[toAsset] += int64(tx.Amount)
  2642  			dex.Settled[toFeeAsset] -= int64(tx.Fees)
  2643  			continue
  2644  		}
  2645  
  2646  		dex.Pending[toAsset] += tx.Amount
  2647  		if reedeemIsDynamicSwapper {
  2648  			dex.Settled[toFeeAsset] -= int64(tx.Fees)
  2649  		} else if dex.Pending[toFeeAsset] >= tx.Fees {
  2650  			dex.Pending[toFeeAsset] -= tx.Fees
  2651  		}
  2652  	}
  2653  
  2654  	for _, tx := range refunds {
  2655  		if tx.Confirmed {
  2656  			dex.Settled[fromAsset] += int64(tx.Amount)
  2657  			dex.Settled[fromFeeAsset] -= int64(tx.Fees)
  2658  			continue
  2659  		}
  2660  
  2661  		dex.Pending[fromAsset] += tx.Amount
  2662  		if refundIsDynamicSwapper {
  2663  			dex.Settled[fromFeeAsset] -= int64(tx.Fees)
  2664  		} else if dex.Pending[fromFeeAsset] >= tx.Fees {
  2665  			dex.Pending[fromFeeAsset] -= tx.Fees
  2666  		}
  2667  	}
  2668  
  2669  	if counterTradeRate > 0 {
  2670  		reserved := reservedForCounterTrade(o.Sell, counterTradeRate, o.Qty-o.Filled)
  2671  		cex.Settled[toAsset] -= int64(reserved)
  2672  		cex.Reserved[toAsset] += reserved
  2673  	}
  2674  
  2675  	return
  2676  }
  2677  
  2678  // updateState should only be called with txsMtx write locked. The dex order
  2679  // state is stored as an atomic.Value in order to allow reads without locking.
  2680  // The mutex only needs to be locked for reading if the caller wants a consistent
  2681  // view of the transactions and the state.
  2682  func (p *pendingDEXOrder) updateState(o *core.Order, getTx func(uint32, string) (*asset.WalletTransaction, error), baseTraits, quoteTraits asset.WalletTrait) {
  2683  	swaps, redeems, refunds := orderCoinIDs(o)
  2684  
  2685  	// Add new txs to tx cache
  2686  	fromAsset, _, toAsset, _ := orderAssets(o.BaseID, o.QuoteID, o.Sell)
  2687  	processTxs := func(assetID uint32, txs map[string]*asset.WalletTransaction, coinIDs map[string]bool, coinIDToTxID map[string]string) {
  2688  		// Query the wallet regarding all unconfirmed transactions
  2689  		for txID, oldTx := range txs {
  2690  			if oldTx.Confirmed {
  2691  				continue
  2692  			}
  2693  			tx, err := getTx(assetID, txID)
  2694  			if err != nil {
  2695  				continue
  2696  			}
  2697  			txs[tx.ID] = tx
  2698  		}
  2699  
  2700  		// Add new txs to tx cache
  2701  		for coinID := range coinIDs {
  2702  			txID, found := coinIDToTxID[coinID]
  2703  			if found {
  2704  				continue
  2705  			}
  2706  			if _, found := txs[txID]; found {
  2707  				continue
  2708  			}
  2709  			tx, err := getTx(assetID, coinID)
  2710  			if err != nil {
  2711  				continue
  2712  			}
  2713  			coinIDToTxID[coinID] = tx.ID
  2714  			txs[tx.ID] = tx
  2715  		}
  2716  	}
  2717  
  2718  	processTxs(fromAsset, p.swaps, swaps, p.swapCoinIDToTxID)
  2719  	processTxs(toAsset, p.redeems, redeems, p.redeemCoinIDToTxID)
  2720  	processTxs(fromAsset, p.refunds, refunds, p.refundCoinIDToTxID)
  2721  
  2722  	dexEffects, cexEffects := dexOrderEffects(o, p.swaps, p.redeems, p.refunds, p.counterTradeRate, baseTraits, quoteTraits)
  2723  	p.state.Store(&dexOrderState{
  2724  		order:             o,
  2725  		dexBalanceEffects: dexEffects,
  2726  		cexBalanceEffects: cexEffects,
  2727  		counterTradeRate:  p.counterTradeRate,
  2728  	})
  2729  }
  2730  
  2731  // updatePendingDEXOrder updates a pending DEX order based its current state.
  2732  // If the order is complete, its effects are applied to the base balance,
  2733  // and it is removed from the pending list.
  2734  func (u *unifiedExchangeAdaptor) handleDEXOrderUpdate(o *core.Order) {
  2735  	var orderID order.OrderID
  2736  	copy(orderID[:], o.ID)
  2737  
  2738  	u.balancesMtx.RLock()
  2739  	pendingOrder, found := u.pendingDEXOrders[orderID]
  2740  	u.balancesMtx.RUnlock()
  2741  	if !found {
  2742  		return
  2743  	}
  2744  
  2745  	pendingOrder.txsMtx.Lock()
  2746  	pendingOrder.updateState(o, u.clientCore.WalletTransaction, u.baseTraits, u.quoteTraits)
  2747  	dexEffects := pendingOrder.currentState().dexBalanceEffects
  2748  	var havePending bool
  2749  	for _, v := range dexEffects.Pending {
  2750  		if v > 0 {
  2751  			havePending = true
  2752  			break
  2753  		}
  2754  	}
  2755  	pendingOrder.txsMtx.Unlock()
  2756  
  2757  	orderUpdates := u.orderUpdates.Load()
  2758  	if orderUpdates != nil {
  2759  		orderUpdates.(chan *core.Order) <- o
  2760  	}
  2761  
  2762  	complete := !havePending && dexOrderComplete(o)
  2763  	// If complete, remove the order from the pending list, and update the
  2764  	// bot's balance.
  2765  
  2766  	if complete { // TODO: complete when all fees are confirmed
  2767  		u.balancesMtx.Lock()
  2768  		delete(u.pendingDEXOrders, orderID)
  2769  
  2770  		adjustedBals := false
  2771  		for assetID, diff := range dexEffects.Settled {
  2772  			adjustedBals = adjustedBals || diff != 0
  2773  			u.baseDexBalances[assetID] += diff
  2774  		}
  2775  
  2776  		if adjustedBals {
  2777  			u.logBalanceAdjustments(dexEffects.Settled, nil, fmt.Sprintf("DEX order %s complete.", orderID))
  2778  		}
  2779  		u.balancesMtx.Unlock()
  2780  	}
  2781  
  2782  	u.updateDEXOrderEvent(pendingOrder, complete)
  2783  }
  2784  
  2785  func (u *unifiedExchangeAdaptor) handleServerConfigUpdate() {
  2786  	coreMkt, err := u.clientCore.ExchangeMarket(u.host, u.baseID, u.quoteID)
  2787  	if err != nil {
  2788  		u.log.Errorf("Stopping bot due to error getting market params: %v", err)
  2789  		u.kill()
  2790  		return
  2791  	}
  2792  
  2793  	if coreMkt.LotSize == u.lotSize.Load() && coreMkt.RateStep == u.rateStep.Load() {
  2794  		return
  2795  	}
  2796  
  2797  	err = u.withPause(func() error {
  2798  		if coreMkt.LotSize != u.lotSize.Load() {
  2799  			cfg := u.botCfg()
  2800  			copy := cfg.copy()
  2801  			copy.updateLotSize(u.lotSize.Load(), coreMkt.LotSize)
  2802  			err := u.updateConfig(copy)
  2803  			if err != nil {
  2804  				return err
  2805  			}
  2806  			u.lotSize.Store(coreMkt.LotSize)
  2807  		}
  2808  		u.rateStep.Store(coreMkt.RateStep)
  2809  		return nil
  2810  	})
  2811  	if err != nil {
  2812  		u.log.Errorf("Error updating config due to server config update. stopping bot: %v", err)
  2813  		u.kill()
  2814  	}
  2815  }
  2816  
  2817  func (u *unifiedExchangeAdaptor) handleDEXNotification(n core.Notification) {
  2818  	switch note := n.(type) {
  2819  	case *core.OrderNote:
  2820  		u.handleDEXOrderUpdate(note.Order)
  2821  	case *core.MatchNote:
  2822  		o, err := u.clientCore.Order(note.OrderID)
  2823  		if err != nil {
  2824  			u.log.Errorf("handleDEXNotification: failed to get order %s: %v", note.OrderID, err)
  2825  			return
  2826  		}
  2827  		u.handleDEXOrderUpdate(o)
  2828  		cfg := u.botCfg()
  2829  		if cfg.Host != note.Host || u.mwh.ID() != note.MarketID {
  2830  			return
  2831  		}
  2832  		if note.Topic() == core.TopicRedemptionConfirmed {
  2833  			u.runStats.completedMatches.Add(1)
  2834  			fiatRates := u.fiatRates.Load().(map[uint32]float64)
  2835  			if r := fiatRates[cfg.BaseID]; r > 0 && note.Match != nil {
  2836  				ui, _ := asset.UnitInfo(cfg.BaseID)
  2837  				u.runStats.tradedUSD.Lock()
  2838  				u.runStats.tradedUSD.v += float64(note.Match.Qty) / float64(ui.Conventional.ConversionFactor) * r
  2839  				u.runStats.tradedUSD.Unlock()
  2840  			}
  2841  		}
  2842  	case *core.FiatRatesNote:
  2843  		u.fiatRates.Store(note.FiatRates)
  2844  	case *core.ServerConfigUpdateNote:
  2845  		if note.Host != u.host {
  2846  			return
  2847  		}
  2848  		u.handleServerConfigUpdate()
  2849  	}
  2850  }
  2851  
  2852  // Lot costs are the reserves and fees associated with current market rates. The
  2853  // per-lot reservations estimates include booking fees, and redemption fees if
  2854  // the asset is the counter-asset's parent asset. The quote estimates are based
  2855  // on vwap estimates using cexCounterRates,
  2856  type lotCosts struct {
  2857  	dexBase, dexQuote,
  2858  	cexBase, cexQuote uint64
  2859  	baseRedeem, quoteRedeem   uint64
  2860  	baseFunding, quoteFunding uint64 // per multi-order
  2861  }
  2862  
  2863  func (u *unifiedExchangeAdaptor) lotCosts(sellVWAP, buyVWAP uint64) (*lotCosts, error) {
  2864  	perLot := new(lotCosts)
  2865  	buyFees, sellFees, err := u.orderFees()
  2866  	if err != nil {
  2867  		return nil, fmt.Errorf("error getting order fees: %w", err)
  2868  	}
  2869  	lotSize := u.lotSize.Load()
  2870  	perLot.dexBase = lotSize
  2871  	if u.baseID == u.baseFeeID {
  2872  		perLot.dexBase += sellFees.BookingFeesPerLot
  2873  	}
  2874  	perLot.cexBase = lotSize
  2875  	perLot.baseRedeem = buyFees.Max.Redeem
  2876  	perLot.baseFunding = sellFees.Funding
  2877  
  2878  	dexQuoteLot := calc.BaseToQuote(sellVWAP, lotSize)
  2879  	cexQuoteLot := calc.BaseToQuote(buyVWAP, lotSize)
  2880  	perLot.dexQuote = dexQuoteLot
  2881  	if u.quoteID == u.quoteFeeID {
  2882  		perLot.dexQuote += buyFees.BookingFeesPerLot
  2883  	}
  2884  	perLot.cexQuote = cexQuoteLot
  2885  	perLot.quoteRedeem = sellFees.Max.Redeem
  2886  	perLot.quoteFunding = buyFees.Funding
  2887  	return perLot, nil
  2888  }
  2889  
  2890  // distribution is a collection of asset distributions and per-lot estimates.
  2891  type distribution struct {
  2892  	baseInv  *assetInventory
  2893  	quoteInv *assetInventory
  2894  	perLot   *lotCosts
  2895  }
  2896  
  2897  func (u *unifiedExchangeAdaptor) newDistribution(perLot *lotCosts) *distribution {
  2898  	return &distribution{
  2899  		baseInv:  u.inventory(u.baseID, perLot.dexBase, perLot.cexBase),
  2900  		quoteInv: u.inventory(u.quoteID, perLot.dexQuote, perLot.cexQuote),
  2901  		perLot:   perLot,
  2902  	}
  2903  }
  2904  
  2905  // optimizeTransfers populates the toDeposit and toWithdraw fields of the base
  2906  // and quote assetDistribution. To find the best asset distribution, a series
  2907  // of possible target configurations are tested and the distribution that
  2908  // results in the highest matchability is chosen.
  2909  func (u *unifiedExchangeAdaptor) optimizeTransfers(dist *distribution, dexSellLots, dexBuyLots, maxSellLots, maxBuyLots uint64) {
  2910  	baseInv, quoteInv := dist.baseInv, dist.quoteInv
  2911  	perLot := dist.perLot
  2912  
  2913  	if u.autoRebalanceCfg == nil {
  2914  		return
  2915  	}
  2916  	minBaseTransfer, minQuoteTransfer := u.autoRebalanceCfg.MinBaseTransfer, u.autoRebalanceCfg.MinQuoteTransfer
  2917  
  2918  	additionalBaseFees, additionalQuoteFees := perLot.baseFunding, perLot.quoteFunding
  2919  	if u.baseID == u.quoteFeeID {
  2920  		additionalBaseFees += perLot.baseRedeem * dexBuyLots
  2921  	}
  2922  	if u.quoteID == u.baseFeeID {
  2923  		additionalQuoteFees += perLot.quoteRedeem * dexSellLots
  2924  	}
  2925  	var baseAvail, quoteAvail uint64
  2926  	if baseInv.total > additionalBaseFees {
  2927  		baseAvail = baseInv.total - additionalBaseFees
  2928  	}
  2929  	if quoteInv.total > additionalQuoteFees {
  2930  		quoteAvail = quoteInv.total - additionalQuoteFees
  2931  	}
  2932  
  2933  	// matchability is the number of lots that can be matched with a specified
  2934  	// asset distribution.
  2935  	matchability := func(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots uint64) uint64 {
  2936  		sells := utils.Min(dexBaseLots, cexQuoteLots, maxSellLots)
  2937  		buys := utils.Min(dexQuoteLots, cexBaseLots, maxBuyLots)
  2938  		return buys + sells
  2939  	}
  2940  
  2941  	currentScore := matchability(baseInv.dexLots, quoteInv.dexLots, baseInv.cexLots, quoteInv.cexLots)
  2942  
  2943  	// targetedSplit finds a distribution that targets a specified ratio of
  2944  	// dex-to-cex.
  2945  	targetedSplit := func(avail, dexTarget, cexTarget, dexLot, cexLot uint64) (dexLots, cexLots, extra uint64) {
  2946  		if dexTarget+cexTarget == 0 {
  2947  			return
  2948  		}
  2949  		cexR := float64(cexTarget*cexLot) / float64(dexTarget*dexLot+cexTarget*cexLot)
  2950  		cexLots = uint64(math.Round(cexR*float64(avail))) / cexLot
  2951  		cexBal := cexLots * cexLot
  2952  		dexLots = (avail - cexBal) / dexLot
  2953  		dexBal := dexLots * dexLot
  2954  		if cexLot < dexLot {
  2955  			cexLots = (avail - dexBal) / cexLot
  2956  			cexBal = cexLots * cexLot
  2957  		}
  2958  		extra = avail - cexBal - dexBal
  2959  		return
  2960  	}
  2961  
  2962  	baseSplit := func(dexTarget, cexTarget uint64) (dexLots, cexLots, extra uint64) {
  2963  		return targetedSplit(baseAvail, utils.Min(dexTarget, maxSellLots), utils.Min(cexTarget, maxBuyLots), perLot.dexBase, perLot.cexBase)
  2964  	}
  2965  	quoteSplit := func(dexTarget, cexTarget uint64) (dexLots, cexLots, extra uint64) {
  2966  		return targetedSplit(quoteAvail, utils.Min(dexTarget, maxBuyLots), utils.Min(cexTarget, maxSellLots), perLot.dexQuote, perLot.cexQuote)
  2967  	}
  2968  
  2969  	// We'll keep track of any distributions that have a matchability score
  2970  	// better than the score for the current distribution.
  2971  	type scoredSplit struct {
  2972  		score uint64 // matchability
  2973  		// spread is just the minimum of baseDeposit, baseWithdraw, quoteDeposit
  2974  		// and quoteWithdraw. This is tertiary criteria for prioritizing splits,
  2975  		// with a higher spread being preferable.
  2976  		spread                      uint64
  2977  		fees                        uint64
  2978  		baseDeposit, baseWithdraw   uint64
  2979  		quoteDeposit, quoteWithdraw uint64
  2980  	}
  2981  	baseSplits := [][2]uint64{
  2982  		{baseInv.dex, baseInv.cex},   // current
  2983  		{dexSellLots, dexBuyLots},    // ideal
  2984  		{quoteInv.cex, quoteInv.dex}, // match the counter asset
  2985  	}
  2986  	quoteSplits := [][2]uint64{
  2987  		{quoteInv.dex, quoteInv.cex},
  2988  		{dexBuyLots, dexSellLots},
  2989  		{baseInv.cex, baseInv.dex},
  2990  	}
  2991  
  2992  	splits := make([]*scoredSplit, 0)
  2993  	// scoreSplit gets a score for the proposed asset distribution and, if the
  2994  	// score is higher than currentScore, saves the result to the splits slice.
  2995  	scoreSplit := func(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote uint64) {
  2996  		score := matchability(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots)
  2997  		if score <= currentScore {
  2998  			return
  2999  		}
  3000  
  3001  		var fees uint64
  3002  		var baseDeposit, baseWithdraw, quoteDeposit, quoteWithdraw uint64
  3003  		if dexBaseLots != baseInv.dexLots || cexBaseLots != baseInv.cexLots {
  3004  			fees++
  3005  			dexTarget := dexBaseLots*perLot.dexBase + additionalBaseFees + extraBase
  3006  			cexTarget := cexBaseLots * perLot.cexBase
  3007  
  3008  			if dexTarget > baseInv.dex {
  3009  				if withdraw := dexTarget - baseInv.dex; withdraw >= minBaseTransfer {
  3010  					baseWithdraw = withdraw
  3011  				} else {
  3012  					return
  3013  				}
  3014  			} else if cexTarget > baseInv.cex {
  3015  				if deposit := cexTarget - baseInv.cex; deposit >= minBaseTransfer {
  3016  					baseDeposit = deposit
  3017  				} else {
  3018  					return
  3019  				}
  3020  			}
  3021  			// TODO: Use actual fee estimates.
  3022  			if u.baseID == 0 || u.baseID == 42 {
  3023  				fees++
  3024  			}
  3025  		}
  3026  		if dexQuoteLots != quoteInv.dexLots || cexQuoteLots != quoteInv.cexLots {
  3027  			fees++
  3028  			dexTarget := dexQuoteLots*perLot.dexQuote + additionalQuoteFees + (extraQuote / 2)
  3029  			cexTarget := cexQuoteLots*perLot.cexQuote + (extraQuote / 2)
  3030  			if dexTarget > quoteInv.dex {
  3031  				if withdraw := dexTarget - quoteInv.dex; withdraw >= minQuoteTransfer {
  3032  					quoteWithdraw = withdraw
  3033  				} else {
  3034  					return
  3035  				}
  3036  
  3037  			} else if cexTarget > quoteInv.cex {
  3038  				if deposit := cexTarget - quoteInv.cex; deposit >= minQuoteTransfer {
  3039  					quoteDeposit = deposit
  3040  				} else {
  3041  					return
  3042  				}
  3043  			}
  3044  			if u.quoteID == 0 || u.quoteID == 60 {
  3045  				fees++
  3046  			}
  3047  		}
  3048  
  3049  		splits = append(splits, &scoredSplit{
  3050  			score:         score,
  3051  			fees:          fees,
  3052  			baseDeposit:   baseDeposit,
  3053  			baseWithdraw:  baseWithdraw,
  3054  			quoteDeposit:  quoteDeposit,
  3055  			quoteWithdraw: quoteWithdraw,
  3056  			spread:        utils.Min(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots),
  3057  		})
  3058  	}
  3059  
  3060  	// Try to hit all possible combinations.
  3061  	for _, b := range baseSplits {
  3062  		dexBaseLots, cexBaseLots, extraBase := baseSplit(b[0], b[1])
  3063  		dexQuoteLots, cexQuoteLots, extraQuote := quoteSplit(cexBaseLots, dexBaseLots)
  3064  		scoreSplit(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote)
  3065  		for _, q := range quoteSplits {
  3066  			dexQuoteLots, cexQuoteLots, extraQuote = quoteSplit(q[0], q[1])
  3067  			scoreSplit(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote)
  3068  		}
  3069  	}
  3070  	// Try in both directions.
  3071  	for _, q := range quoteSplits {
  3072  		dexQuoteLots, cexQuoteLots, extraQuote := quoteSplit(q[0], q[1])
  3073  		dexBaseLots, cexBaseLots, extraBase := baseSplit(cexQuoteLots, dexQuoteLots)
  3074  		scoreSplit(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote)
  3075  		for _, b := range baseSplits {
  3076  			dexBaseLots, cexBaseLots, extraBase := baseSplit(b[0], b[1])
  3077  			scoreSplit(dexBaseLots, dexQuoteLots, cexBaseLots, cexQuoteLots, extraBase, extraQuote)
  3078  		}
  3079  	}
  3080  
  3081  	if len(splits) == 0 {
  3082  		return
  3083  	}
  3084  
  3085  	// Sort by score, then fees, then spread.
  3086  	sort.Slice(splits, func(ii, ji int) bool {
  3087  		i, j := splits[ii], splits[ji]
  3088  		return i.score > j.score || (i.score == j.score && (i.fees < j.fees || i.spread > j.spread))
  3089  	})
  3090  	split := splits[0]
  3091  	baseInv.toDeposit = split.baseDeposit
  3092  	baseInv.toWithdraw = split.baseWithdraw
  3093  	quoteInv.toDeposit = split.quoteDeposit
  3094  	quoteInv.toWithdraw = split.quoteWithdraw
  3095  }
  3096  
  3097  // transfer attempts to perform the transers specified in the distribution.
  3098  func (u *unifiedExchangeAdaptor) transfer(dist *distribution, currEpoch uint64) (actionTaken bool, err error) {
  3099  	baseInv, quoteInv := dist.baseInv, dist.quoteInv
  3100  	if baseInv.toDeposit+baseInv.toWithdraw+quoteInv.toDeposit+quoteInv.toWithdraw == 0 {
  3101  		return false, nil
  3102  	}
  3103  
  3104  	var cancels []*dexOrderState
  3105  	if baseInv.toDeposit > 0 || quoteInv.toWithdraw > 0 {
  3106  		var toFree uint64
  3107  		if baseInv.dexAvail < baseInv.toDeposit {
  3108  			if baseInv.dexAvail+baseInv.dexPending >= baseInv.toDeposit {
  3109  				u.log.Tracef("Waiting on pending balance for base deposit")
  3110  				return false, nil
  3111  			}
  3112  			toFree = baseInv.toDeposit - baseInv.dexAvail
  3113  		}
  3114  		counterQty := quoteInv.cex - quoteInv.toWithdraw + quoteInv.toDeposit
  3115  		cs, ok := u.freeUpFunds(u.baseID, toFree, counterQty, currEpoch)
  3116  		if !ok {
  3117  			if u.log.Level() == dex.LevelTrace {
  3118  				u.log.Tracef(
  3119  					"Unable to free up funds for deposit = %s, withdraw = %s, "+
  3120  						"counter-quantity = %s, to free = %s, dex pending = %s",
  3121  					u.fmtBase(baseInv.toDeposit), u.fmtQuote(quoteInv.toWithdraw), u.fmtQuote(counterQty),
  3122  					u.fmtBase(toFree), u.fmtBase(baseInv.dexPending),
  3123  				)
  3124  			}
  3125  			return false, nil
  3126  		}
  3127  		cancels = cs
  3128  	}
  3129  	if quoteInv.toDeposit > 0 || baseInv.toWithdraw > 0 {
  3130  		var toFree uint64
  3131  		if quoteInv.dexAvail < quoteInv.toDeposit {
  3132  			if quoteInv.dexAvail+quoteInv.dexPending >= quoteInv.toDeposit {
  3133  				// waiting on pending
  3134  				u.log.Tracef("Waiting on pending balance for quote deposit")
  3135  				return false, nil
  3136  			}
  3137  			toFree = quoteInv.toDeposit - quoteInv.dexAvail
  3138  		}
  3139  		counterQty := baseInv.cex - baseInv.toWithdraw + baseInv.toDeposit
  3140  		cs, ok := u.freeUpFunds(u.quoteID, toFree, counterQty, currEpoch)
  3141  		if !ok {
  3142  			if u.log.Level() == dex.LevelTrace {
  3143  				u.log.Tracef(
  3144  					"Unable to free up funds for deposit = %s, withdraw = %s, "+
  3145  						"counter-quantity = %s, to free = %s, dex pending = %s",
  3146  					u.fmtQuote(quoteInv.toDeposit), u.fmtBase(baseInv.toWithdraw), u.fmtBase(counterQty),
  3147  					u.fmtQuote(toFree), u.fmtQuote(quoteInv.dexPending),
  3148  				)
  3149  			}
  3150  			return false, nil
  3151  		}
  3152  		cancels = append(cancels, cs...)
  3153  	}
  3154  
  3155  	if len(cancels) > 0 {
  3156  		for _, o := range cancels {
  3157  			if err := u.Cancel(o.order.ID); err != nil {
  3158  				return false, fmt.Errorf("error canceling order: %w", err)
  3159  			}
  3160  		}
  3161  		return true, nil
  3162  	}
  3163  
  3164  	if baseInv.toDeposit > 0 {
  3165  		err := u.deposit(u.ctx, u.baseID, baseInv.toDeposit)
  3166  		u.updateCEXProblems(cexDepositProblem, u.baseID, err)
  3167  		if err != nil {
  3168  			return false, fmt.Errorf("error depositing base: %w", err)
  3169  		}
  3170  	} else if baseInv.toWithdraw > 0 {
  3171  		err := u.withdraw(u.ctx, u.baseID, baseInv.toWithdraw)
  3172  		u.updateCEXProblems(cexWithdrawProblem, u.baseID, err)
  3173  		if err != nil {
  3174  			return false, fmt.Errorf("error withdrawing base: %w", err)
  3175  		}
  3176  	}
  3177  
  3178  	if quoteInv.toDeposit > 0 {
  3179  		err := u.deposit(u.ctx, u.quoteID, quoteInv.toDeposit)
  3180  		u.updateCEXProblems(cexDepositProblem, u.quoteID, err)
  3181  		if err != nil {
  3182  			return false, fmt.Errorf("error depositing quote: %w", err)
  3183  		}
  3184  	} else if quoteInv.toWithdraw > 0 {
  3185  		err := u.withdraw(u.ctx, u.quoteID, quoteInv.toWithdraw)
  3186  		u.updateCEXProblems(cexWithdrawProblem, u.quoteID, err)
  3187  		if err != nil {
  3188  			return false, fmt.Errorf("error withdrawing quote: %w", err)
  3189  		}
  3190  	}
  3191  	return true, nil
  3192  }
  3193  
  3194  // assetInventory is an accounting of the distribution of base- or quote-asset
  3195  // funding.
  3196  type assetInventory struct {
  3197  	dex        uint64
  3198  	dexAvail   uint64
  3199  	dexPending uint64
  3200  	dexLocked  uint64
  3201  	dexLots    uint64
  3202  
  3203  	cex         uint64
  3204  	cexAvail    uint64
  3205  	cexPending  uint64
  3206  	cexReserved uint64
  3207  	cexLocked   uint64
  3208  	cexLots     uint64
  3209  
  3210  	total uint64
  3211  
  3212  	toDeposit  uint64
  3213  	toWithdraw uint64
  3214  }
  3215  
  3216  // inventory generates a current view of the the bot's asset distribution.
  3217  // Use optimizeTransfers to set toDeposit and toWithdraw.
  3218  func (u *unifiedExchangeAdaptor) inventory(assetID uint32, dexLot, cexLot uint64) (b *assetInventory) {
  3219  	b = new(assetInventory)
  3220  	u.balancesMtx.RLock()
  3221  	defer u.balancesMtx.RUnlock()
  3222  
  3223  	dexBalance := u.dexBalance(assetID)
  3224  	b.dexAvail = dexBalance.Available
  3225  	b.dexPending = dexBalance.Pending
  3226  	b.dexLocked = dexBalance.Locked
  3227  	b.dex = dexBalance.Available + dexBalance.Locked + dexBalance.Pending
  3228  	b.dexLots = b.dex / dexLot
  3229  	cexBalance := u.cexBalance(assetID)
  3230  	b.cexAvail = cexBalance.Available
  3231  	b.cexPending = cexBalance.Pending
  3232  	b.cexReserved = cexBalance.Reserved
  3233  	b.cexLocked = cexBalance.Locked
  3234  	b.cex = cexBalance.Available + cexBalance.Reserved + cexBalance.Pending
  3235  	b.cexLots = b.cex / cexLot
  3236  	b.total = b.dex + b.cex
  3237  	return
  3238  }
  3239  
  3240  // cexCounterRates attempts to get vwap estimates for the cex book for a
  3241  // specified number of lots. If the book is too empty for the specified number
  3242  // of lots, a 1-lot estimate will be attempted too.
  3243  func (u *unifiedExchangeAdaptor) cexCounterRates(cexBuyLots, cexSellLots uint64) (dexBuyRate, dexSellRate uint64, err error) {
  3244  	lotSize := u.lotSize.Load()
  3245  	tryLots := func(b, s uint64) (uint64, uint64, bool, error) {
  3246  		if b == 0 {
  3247  			b = 1
  3248  		}
  3249  		if s == 0 {
  3250  			s = 1
  3251  		}
  3252  		buyRate, _, filled, err := u.CEX.VWAP(u.baseID, u.quoteID, true, lotSize*s)
  3253  		if err != nil {
  3254  			return 0, 0, false, fmt.Errorf("error calculating dex buy price for quote conversion: %w", err)
  3255  		}
  3256  		if !filled {
  3257  			return 0, 0, false, nil
  3258  		}
  3259  		sellRate, _, filled, err := u.CEX.VWAP(u.baseID, u.quoteID, false, lotSize*b)
  3260  		if err != nil {
  3261  			return 0, 0, false, fmt.Errorf("error calculating dex sell price for quote conversion: %w", err)
  3262  		}
  3263  		if !filled {
  3264  			return 0, 0, false, nil
  3265  		}
  3266  		return buyRate, sellRate, true, nil
  3267  	}
  3268  	var filled bool
  3269  	if dexBuyRate, dexSellRate, filled, err = tryLots(cexBuyLots, cexSellLots); err != nil || filled {
  3270  		return
  3271  	}
  3272  	u.log.Tracef("Failed to get cex counter-rate for requested lots. Trying 1 lot estimate")
  3273  	dexBuyRate, dexSellRate, filled, err = tryLots(1, 1)
  3274  	if err != nil {
  3275  		return
  3276  	}
  3277  	if !filled {
  3278  		err = errors.New("cex book too empty to get a counter-rate estimate")
  3279  	}
  3280  	return
  3281  }
  3282  
  3283  // bookingFees are the per-lot fees that have to be available before placing an
  3284  // order.
  3285  func (u *unifiedExchangeAdaptor) bookingFees(buyFees, sellFees *LotFees) (buyBookingFeesPerLot, sellBookingFeesPerLot uint64) {
  3286  	buyBookingFeesPerLot = buyFees.Swap
  3287  	// If we're redeeming on the same chain, add redemption fees.
  3288  	if u.quoteFeeID == u.baseFeeID {
  3289  		buyBookingFeesPerLot += buyFees.Redeem
  3290  	}
  3291  	// EVM assets need to reserve refund gas.
  3292  	if u.quoteTraits.IsAccountLocker() {
  3293  		buyBookingFeesPerLot += buyFees.Refund
  3294  	}
  3295  	sellBookingFeesPerLot = sellFees.Swap
  3296  	if u.baseFeeID == u.quoteFeeID {
  3297  		sellBookingFeesPerLot += sellFees.Redeem
  3298  	}
  3299  	if u.baseTraits.IsAccountLocker() {
  3300  		sellBookingFeesPerLot += sellFees.Refund
  3301  	}
  3302  	return
  3303  }
  3304  
  3305  // updateFeeRates updates the cached fee rates for placing orders on the market
  3306  // specified by the exchangeAdaptorCfg used to create the unifiedExchangeAdaptor.
  3307  func (u *unifiedExchangeAdaptor) updateFeeRates() (buyFees, sellFees *OrderFees, err error) {
  3308  	defer func() {
  3309  		if err == nil {
  3310  			return
  3311  		}
  3312  
  3313  		// In case of an error, clear the cached fees to avoid using stale data.
  3314  		u.feesMtx.Lock()
  3315  		defer u.feesMtx.Unlock()
  3316  		u.buyFees = nil
  3317  		u.sellFees = nil
  3318  	}()
  3319  
  3320  	maxBaseFees, maxQuoteFees, err := marketFees(u.clientCore, u.host, u.baseID, u.quoteID, true)
  3321  	if err != nil {
  3322  		return nil, nil, err
  3323  	}
  3324  
  3325  	estBaseFees, estQuoteFees, err := marketFees(u.clientCore, u.host, u.baseID, u.quoteID, false)
  3326  	if err != nil {
  3327  		return nil, nil, err
  3328  	}
  3329  
  3330  	botCfg := u.botCfg()
  3331  	maxBuyPlacements, maxSellPlacements := botCfg.maxPlacements()
  3332  
  3333  	buyFundingFees, err := u.clientCore.MaxFundingFees(u.quoteID, u.host, maxBuyPlacements, botCfg.QuoteWalletOptions)
  3334  	if err != nil {
  3335  		return nil, nil, fmt.Errorf("failed to get buy funding fees: %v", err)
  3336  	}
  3337  
  3338  	sellFundingFees, err := u.clientCore.MaxFundingFees(u.baseID, u.host, maxSellPlacements, botCfg.BaseWalletOptions)
  3339  	if err != nil {
  3340  		return nil, nil, fmt.Errorf("failed to get sell funding fees: %v", err)
  3341  	}
  3342  
  3343  	maxBuyFees := &LotFees{
  3344  		Swap:   maxQuoteFees.Swap,
  3345  		Redeem: maxBaseFees.Redeem,
  3346  		Refund: maxQuoteFees.Refund,
  3347  	}
  3348  	maxSellFees := &LotFees{
  3349  		Swap:   maxBaseFees.Swap,
  3350  		Redeem: maxQuoteFees.Redeem,
  3351  		Refund: maxBaseFees.Refund,
  3352  	}
  3353  
  3354  	buyBookingFeesPerLot, sellBookingFeesPerLot := u.bookingFees(maxBuyFees, maxSellFees)
  3355  
  3356  	u.feesMtx.Lock()
  3357  	defer u.feesMtx.Unlock()
  3358  
  3359  	u.buyFees = &OrderFees{
  3360  		LotFeeRange: &LotFeeRange{
  3361  			Max: maxBuyFees,
  3362  			Estimated: &LotFees{
  3363  				Swap:   estQuoteFees.Swap,
  3364  				Redeem: estBaseFees.Redeem,
  3365  				Refund: estQuoteFees.Refund,
  3366  			},
  3367  		},
  3368  		Funding:           buyFundingFees,
  3369  		BookingFeesPerLot: buyBookingFeesPerLot,
  3370  	}
  3371  
  3372  	u.sellFees = &OrderFees{
  3373  		LotFeeRange: &LotFeeRange{
  3374  			Max: maxSellFees,
  3375  			Estimated: &LotFees{
  3376  				Swap:   estBaseFees.Swap,
  3377  				Redeem: estQuoteFees.Redeem,
  3378  				Refund: estBaseFees.Refund,
  3379  			},
  3380  		},
  3381  		Funding:           sellFundingFees,
  3382  		BookingFeesPerLot: sellBookingFeesPerLot,
  3383  	}
  3384  
  3385  	return u.buyFees, u.sellFees, nil
  3386  }
  3387  
  3388  func (u *unifiedExchangeAdaptor) Connect(ctx context.Context) (*sync.WaitGroup, error) {
  3389  	ctx, u.kill = context.WithCancel(ctx)
  3390  	u.ctx = ctx
  3391  
  3392  	fiatRates := u.clientCore.FiatConversionRates()
  3393  	u.fiatRates.Store(fiatRates)
  3394  
  3395  	_, _, err := u.updateFeeRates()
  3396  	if err != nil {
  3397  		return nil, fmt.Errorf("failed to getting fee rates: %v", err)
  3398  	}
  3399  
  3400  	startTime := time.Now().Unix()
  3401  	u.startTime.Store(startTime)
  3402  
  3403  	err = u.eventLogDB.storeNewRun(startTime, u.mwh, u.botCfg(), u.balanceState())
  3404  	if err != nil {
  3405  		return nil, fmt.Errorf("failed to store new run in event log db: %v", err)
  3406  	}
  3407  
  3408  	u.wg.Add(1)
  3409  	go func() {
  3410  		defer u.wg.Done()
  3411  		<-ctx.Done()
  3412  		u.eventLogDB.endRun(startTime, u.mwh)
  3413  	}()
  3414  
  3415  	u.wg.Add(1)
  3416  	go func() {
  3417  		defer u.wg.Done()
  3418  		<-ctx.Done()
  3419  		u.cancelAllOrders(ctx)
  3420  	}()
  3421  
  3422  	// Listen for core notifications
  3423  	u.wg.Add(1)
  3424  	go func() {
  3425  		defer u.wg.Done()
  3426  		feed := u.clientCore.NotificationFeed()
  3427  		defer feed.ReturnFeed()
  3428  
  3429  		for {
  3430  			select {
  3431  			case <-ctx.Done():
  3432  				return
  3433  			case n := <-feed.C:
  3434  				u.handleDEXNotification(n)
  3435  			}
  3436  		}
  3437  	}()
  3438  
  3439  	u.wg.Add(1)
  3440  	go func() {
  3441  		defer u.wg.Done()
  3442  		refreshTime := time.Minute * 10
  3443  		for {
  3444  			select {
  3445  			case <-time.NewTimer(refreshTime).C:
  3446  				_, _, err := u.updateFeeRates()
  3447  				if err != nil {
  3448  					u.log.Error(err)
  3449  					refreshTime = time.Minute
  3450  				} else {
  3451  					refreshTime = time.Minute * 10
  3452  				}
  3453  			case <-ctx.Done():
  3454  				return
  3455  			}
  3456  		}
  3457  	}()
  3458  
  3459  	if err := u.runBotLoop(ctx); err != nil {
  3460  		return nil, fmt.Errorf("error starting bot loop: %w", err)
  3461  	}
  3462  
  3463  	u.sendStatsUpdate()
  3464  
  3465  	return &u.wg, nil
  3466  }
  3467  
  3468  // RunStats is a snapshot of the bot's balances and performance at a point in
  3469  // time.
  3470  type RunStats struct {
  3471  	InitialBalances    map[uint32]uint64      `json:"initialBalances"`
  3472  	DEXBalances        map[uint32]*BotBalance `json:"dexBalances"`
  3473  	CEXBalances        map[uint32]*BotBalance `json:"cexBalances"`
  3474  	ProfitLoss         *ProfitLoss            `json:"profitLoss"`
  3475  	StartTime          int64                  `json:"startTime"`
  3476  	PendingDeposits    int                    `json:"pendingDeposits"`
  3477  	PendingWithdrawals int                    `json:"pendingWithdrawals"`
  3478  	CompletedMatches   uint32                 `json:"completedMatches"`
  3479  	TradedUSD          float64                `json:"tradedUSD"`
  3480  	FeeGap             *FeeGapStats           `json:"feeGap"`
  3481  }
  3482  
  3483  // Amount contains the conversions and formatted strings associated with an
  3484  // amount of asset and a fiat exchange rate.
  3485  type Amount struct {
  3486  	Atoms        int64   `json:"atoms"`
  3487  	Conventional float64 `json:"conventional"`
  3488  	Fmt          string  `json:"fmt"`
  3489  	USD          float64 `json:"usd"`
  3490  	FmtUSD       string  `json:"fmtUSD"`
  3491  	FiatRate     float64 `json:"fiatRate"`
  3492  }
  3493  
  3494  // NewAmount generates an Amount for a known asset.
  3495  func NewAmount(assetID uint32, atoms int64, fiatRate float64) *Amount {
  3496  	ui, err := asset.UnitInfo(assetID)
  3497  	if err != nil {
  3498  		return &Amount{}
  3499  	}
  3500  	conv := float64(atoms) / float64(ui.Conventional.ConversionFactor)
  3501  	usd := conv * fiatRate
  3502  	return &Amount{
  3503  		Atoms:        atoms,
  3504  		Conventional: conv,
  3505  		USD:          usd,
  3506  		Fmt:          ui.FormatSignedAtoms(atoms),
  3507  		FmtUSD:       strconv.FormatFloat(usd, 'f', 2, 64) + " USD",
  3508  		FiatRate:     fiatRate,
  3509  	}
  3510  }
  3511  
  3512  // ProfitLoss is a breakdown of the profit calculations.
  3513  type ProfitLoss struct {
  3514  	Initial     map[uint32]*Amount `json:"initial"`
  3515  	InitialUSD  float64            `json:"initialUSD"`
  3516  	Mods        map[uint32]*Amount `json:"mods"`
  3517  	ModsUSD     float64            `json:"modsUSD"`
  3518  	Final       map[uint32]*Amount `json:"final"`
  3519  	FinalUSD    float64            `json:"finalUSD"`
  3520  	Diffs       map[uint32]*Amount `json:"diffs"`
  3521  	Profit      float64            `json:"profit"`
  3522  	ProfitRatio float64            `json:"profitRatio"`
  3523  }
  3524  
  3525  func newProfitLoss(
  3526  	initialBalances,
  3527  	finalBalances map[uint32]uint64,
  3528  	mods map[uint32]int64,
  3529  	fiatRates map[uint32]float64,
  3530  ) *ProfitLoss {
  3531  	pl := &ProfitLoss{
  3532  		Initial: make(map[uint32]*Amount, len(initialBalances)),
  3533  		Mods:    make(map[uint32]*Amount, len(mods)),
  3534  		Diffs:   make(map[uint32]*Amount, len(initialBalances)),
  3535  		Final:   make(map[uint32]*Amount, len(finalBalances)),
  3536  	}
  3537  	for assetID, v := range initialBalances {
  3538  		if v == 0 {
  3539  			continue
  3540  		}
  3541  		fiatRate := fiatRates[assetID]
  3542  		init := NewAmount(assetID, int64(v), fiatRate)
  3543  		pl.Initial[assetID] = init
  3544  		mod := NewAmount(assetID, mods[assetID], fiatRate)
  3545  		pl.InitialUSD += init.USD
  3546  		pl.ModsUSD += mod.USD
  3547  		diff := int64(finalBalances[assetID]) - int64(initialBalances[assetID]) - mods[assetID]
  3548  		pl.Diffs[assetID] = NewAmount(assetID, diff, fiatRate)
  3549  	}
  3550  	for assetID, v := range finalBalances {
  3551  		if v == 0 {
  3552  			continue
  3553  		}
  3554  		fin := NewAmount(assetID, int64(v), fiatRates[assetID])
  3555  		pl.Final[assetID] = fin
  3556  		pl.FinalUSD += fin.USD
  3557  	}
  3558  
  3559  	basis := pl.InitialUSD + pl.ModsUSD
  3560  	pl.Profit = pl.FinalUSD - basis
  3561  	pl.ProfitRatio = pl.Profit / basis
  3562  	return pl
  3563  }
  3564  
  3565  func (u *unifiedExchangeAdaptor) stats() *RunStats {
  3566  	u.balancesMtx.RLock()
  3567  	defer u.balancesMtx.RUnlock()
  3568  
  3569  	dexBalances := make(map[uint32]*BotBalance)
  3570  	cexBalances := make(map[uint32]*BotBalance)
  3571  	totalBalances := make(map[uint32]uint64)
  3572  
  3573  	for assetID := range u.baseDexBalances {
  3574  		bal := u.dexBalance(assetID)
  3575  		dexBalances[assetID] = bal
  3576  		totalBalances[assetID] = bal.Available + bal.Locked + bal.Pending + bal.Reserved
  3577  	}
  3578  
  3579  	for assetID := range u.baseCexBalances {
  3580  		bal := u.cexBalance(assetID)
  3581  		cexBalances[assetID] = bal
  3582  		totalBalances[assetID] += bal.Available + bal.Locked + bal.Pending + bal.Reserved
  3583  	}
  3584  
  3585  	fiatRates := u.fiatRates.Load().(map[uint32]float64)
  3586  
  3587  	var feeGap *FeeGapStats
  3588  	if feeGapI := u.runStats.feeGapStats.Load(); feeGapI != nil {
  3589  		feeGap = feeGapI.(*FeeGapStats)
  3590  	}
  3591  
  3592  	u.runStats.tradedUSD.Lock()
  3593  	tradedUSD := u.runStats.tradedUSD.v
  3594  	u.runStats.tradedUSD.Unlock()
  3595  
  3596  	// Effects of pendingWithdrawals are applied when the withdrawal is
  3597  	// complete.
  3598  	return &RunStats{
  3599  		InitialBalances:    u.initialBalances,
  3600  		DEXBalances:        dexBalances,
  3601  		CEXBalances:        cexBalances,
  3602  		ProfitLoss:         newProfitLoss(u.initialBalances, totalBalances, u.inventoryMods, fiatRates),
  3603  		StartTime:          u.startTime.Load(),
  3604  		PendingDeposits:    len(u.pendingDeposits),
  3605  		PendingWithdrawals: len(u.pendingWithdrawals),
  3606  		CompletedMatches:   u.runStats.completedMatches.Load(),
  3607  		TradedUSD:          tradedUSD,
  3608  		FeeGap:             feeGap,
  3609  	}
  3610  }
  3611  
  3612  func (u *unifiedExchangeAdaptor) sendStatsUpdate() {
  3613  	u.clientCore.Broadcast(newRunStatsNote(u.host, u.baseID, u.quoteID, u.stats()))
  3614  }
  3615  
  3616  func (u *unifiedExchangeAdaptor) notifyEvent(e *MarketMakingEvent) {
  3617  	u.clientCore.Broadcast(newRunEventNote(u.host, u.baseID, u.quoteID, u.startTime.Load(), e))
  3618  }
  3619  
  3620  func (u *unifiedExchangeAdaptor) registerFeeGap(feeGap *FeeGapStats) {
  3621  	u.runStats.feeGapStats.Store(feeGap)
  3622  }
  3623  
  3624  func (u *unifiedExchangeAdaptor) applyInventoryDiffs(balanceDiffs *BotInventoryDiffs) map[uint32]int64 {
  3625  	u.balancesMtx.Lock()
  3626  	defer u.balancesMtx.Unlock()
  3627  
  3628  	mods := map[uint32]int64{}
  3629  
  3630  	for assetID, diff := range balanceDiffs.DEX {
  3631  		if diff < 0 {
  3632  			balance := u.dexBalance(assetID)
  3633  			if balance.Available < uint64(-diff) {
  3634  				u.log.Errorf("attempting to decrease %s balance by more than available balance. Setting balance to 0.", dex.BipIDSymbol(assetID))
  3635  				diff = -int64(balance.Available)
  3636  			}
  3637  		}
  3638  		u.baseDexBalances[assetID] += diff
  3639  		mods[assetID] = diff
  3640  	}
  3641  
  3642  	for assetID, diff := range balanceDiffs.CEX {
  3643  		if diff < 0 {
  3644  			balance := u.cexBalance(assetID)
  3645  			if balance.Available < uint64(-diff) {
  3646  				u.log.Errorf("attempting to decrease %s balance by more than available balance. Setting balance to 0.", dex.BipIDSymbol(assetID))
  3647  				diff = -int64(balance.Available)
  3648  			}
  3649  		}
  3650  		u.baseCexBalances[assetID] += diff
  3651  		mods[assetID] += diff
  3652  	}
  3653  
  3654  	for assetID, diff := range mods {
  3655  		u.inventoryMods[assetID] += diff
  3656  	}
  3657  
  3658  	u.logBalanceAdjustments(balanceDiffs.DEX, balanceDiffs.CEX, "Inventory updated")
  3659  	u.log.Debugf("Aggregate inventory mods: %+v", u.inventoryMods)
  3660  
  3661  	return mods
  3662  }
  3663  
  3664  func (u *unifiedExchangeAdaptor) updateConfig(cfg *BotConfig) error {
  3665  	if err := validateConfigUpdate(u.botCfg(), cfg); err != nil {
  3666  		return err
  3667  	}
  3668  
  3669  	u.botCfgV.Store(cfg)
  3670  	u.updateConfigEvent(cfg)
  3671  	return nil
  3672  }
  3673  
  3674  func (u *unifiedExchangeAdaptor) updateInventory(balanceDiffs *BotInventoryDiffs) {
  3675  	u.updateInventoryEvent(u.applyInventoryDiffs(balanceDiffs))
  3676  }
  3677  
  3678  func (u *unifiedExchangeAdaptor) Book() (buys, sells []*core.MiniOrder, _ error) {
  3679  	if u.CEX == nil {
  3680  		return nil, nil, errors.New("not a cex-connected bot")
  3681  	}
  3682  	return u.CEX.Book(u.baseID, u.quoteID)
  3683  }
  3684  
  3685  func (u *unifiedExchangeAdaptor) latestCEXProblems() *CEXProblems {
  3686  	u.cexProblemsMtx.RLock()
  3687  	defer u.cexProblemsMtx.RUnlock()
  3688  	if u.cexProblems == nil {
  3689  		return nil
  3690  	}
  3691  	return u.cexProblems.copy()
  3692  }
  3693  
  3694  func (u *unifiedExchangeAdaptor) latestEpoch() *EpochReport {
  3695  	reportI := u.epochReport.Load()
  3696  	if reportI == nil {
  3697  		return nil
  3698  	}
  3699  	return reportI.(*EpochReport)
  3700  }
  3701  
  3702  func (u *unifiedExchangeAdaptor) updateEpochReport(report *EpochReport) {
  3703  	u.epochReport.Store(report)
  3704  	u.clientCore.Broadcast(newEpochReportNote(u.host, u.baseID, u.quoteID, report))
  3705  }
  3706  
  3707  // tradingLimitNotReached returns true if the user has not reached their trading
  3708  // limit. If it has, it updates the epoch report with the problems.
  3709  func (u *unifiedExchangeAdaptor) tradingLimitNotReached(epochNum uint64) bool {
  3710  	var tradingLimitReached bool
  3711  	var err error
  3712  	defer func() {
  3713  		if err == nil && !tradingLimitReached {
  3714  			return
  3715  		}
  3716  		var unknownErr string
  3717  		if err != nil {
  3718  			unknownErr = err.Error()
  3719  		}
  3720  		u.updateEpochReport(&EpochReport{
  3721  			PreOrderProblems: &BotProblems{
  3722  				UserLimitTooLow: tradingLimitReached,
  3723  				UnknownError:    unknownErr,
  3724  			},
  3725  			EpochNum: epochNum,
  3726  		})
  3727  	}()
  3728  
  3729  	userParcels, parcelLimit, err := u.clientCore.TradingLimits(u.host)
  3730  	if err != nil {
  3731  		return false
  3732  	}
  3733  
  3734  	tradingLimitReached = userParcels >= parcelLimit
  3735  	return !tradingLimitReached
  3736  }
  3737  
  3738  type cexProblemType uint16
  3739  
  3740  const (
  3741  	cexTradeProblem cexProblemType = iota
  3742  	cexDepositProblem
  3743  	cexWithdrawProblem
  3744  )
  3745  
  3746  // updateCEXProblemState updates the state of a cex problem. It returns	true
  3747  // if the problem state was updated. It is always updated if the error is
  3748  // non-nil.
  3749  func (u *unifiedExchangeAdaptor) updateCEXProblemState(typ cexProblemType, assetID uint32, err error) bool {
  3750  	if err != nil {
  3751  		switch typ {
  3752  		case cexTradeProblem:
  3753  			u.cexProblems.TradeErr = newStampedError(err)
  3754  		case cexDepositProblem:
  3755  			u.cexProblems.DepositErr[assetID] = newStampedError(err)
  3756  		case cexWithdrawProblem:
  3757  			u.cexProblems.WithdrawErr[assetID] = newStampedError(err)
  3758  		}
  3759  		return true
  3760  	}
  3761  
  3762  	var updated bool
  3763  	switch typ {
  3764  	case cexTradeProblem:
  3765  		updated = u.cexProblems.TradeErr != nil
  3766  		u.cexProblems.TradeErr = nil
  3767  	case cexDepositProblem:
  3768  		updated = u.cexProblems.DepositErr[assetID] != nil
  3769  		delete(u.cexProblems.DepositErr, assetID)
  3770  	case cexWithdrawProblem:
  3771  		updated = u.cexProblems.WithdrawErr[assetID] != nil
  3772  		delete(u.cexProblems.WithdrawErr, assetID)
  3773  	}
  3774  	return updated
  3775  }
  3776  
  3777  func (u *unifiedExchangeAdaptor) updateCEXProblems(typ cexProblemType, assetID uint32, err error) {
  3778  	u.cexProblemsMtx.Lock()
  3779  	defer u.cexProblemsMtx.Unlock()
  3780  
  3781  	if u.updateCEXProblemState(typ, assetID, err) {
  3782  		u.clientCore.Broadcast(newCexProblemsNote(u.host, u.baseID, u.quoteID, u.cexProblems))
  3783  	}
  3784  }
  3785  
  3786  // checkBotHealth returns true if the bot is healthy and can continue trading.
  3787  // If it is not healthy, it updates the epoch report with the problems.
  3788  func (u *unifiedExchangeAdaptor) checkBotHealth(epochNum uint64) (healthy bool) {
  3789  	var err error
  3790  	var baseAssetNotSynced, baseAssetNoPeers, quoteAssetNotSynced, quoteAssetNoPeers, accountSuspended bool
  3791  
  3792  	defer func() {
  3793  		if healthy {
  3794  			return
  3795  		}
  3796  		var unknownErr string
  3797  		if err != nil {
  3798  			unknownErr = err.Error()
  3799  		}
  3800  		problems := &BotProblems{
  3801  			NoWalletPeers: map[uint32]bool{
  3802  				u.baseID:  baseAssetNoPeers,
  3803  				u.quoteID: quoteAssetNoPeers,
  3804  			},
  3805  			WalletNotSynced: map[uint32]bool{
  3806  				u.baseID:  baseAssetNotSynced,
  3807  				u.quoteID: quoteAssetNotSynced,
  3808  			},
  3809  			AccountSuspended: accountSuspended,
  3810  			UnknownError:     unknownErr,
  3811  		}
  3812  		u.updateEpochReport(&EpochReport{
  3813  			PreOrderProblems: problems,
  3814  			EpochNum:         epochNum,
  3815  		})
  3816  	}()
  3817  
  3818  	baseWallet := u.clientCore.WalletState(u.baseID)
  3819  	if baseWallet == nil {
  3820  		err = fmt.Errorf("base asset %d wallet not found", u.baseID)
  3821  		return false
  3822  	}
  3823  
  3824  	baseAssetNotSynced = !baseWallet.Synced
  3825  	baseAssetNoPeers = baseWallet.PeerCount == 0
  3826  
  3827  	quoteWallet := u.clientCore.WalletState(u.quoteID)
  3828  	if quoteWallet == nil {
  3829  		err = fmt.Errorf("quote asset %d wallet not found", u.quoteID)
  3830  		return false
  3831  	}
  3832  
  3833  	quoteAssetNotSynced = !quoteWallet.Synced
  3834  	quoteAssetNoPeers = quoteWallet.PeerCount == 0
  3835  
  3836  	exchange, err := u.clientCore.Exchange(u.host)
  3837  	if err != nil {
  3838  		err = fmt.Errorf("error getting exchange: %w", err)
  3839  		return false
  3840  	}
  3841  	accountSuspended = exchange.Auth.EffectiveTier <= 0
  3842  
  3843  	return !(baseAssetNotSynced || baseAssetNoPeers || quoteAssetNotSynced || quoteAssetNoPeers || accountSuspended)
  3844  }
  3845  
  3846  type exchangeAdaptorCfg struct {
  3847  	botID               string
  3848  	mwh                 *MarketWithHost
  3849  	baseDexBalances     map[uint32]uint64
  3850  	baseCexBalances     map[uint32]uint64
  3851  	autoRebalanceConfig *AutoRebalanceConfig
  3852  	core                clientCore
  3853  	cex                 libxc.CEX
  3854  	log                 dex.Logger
  3855  	eventLogDB          eventLogDB
  3856  	botCfg              *BotConfig
  3857  }
  3858  
  3859  // newUnifiedExchangeAdaptor is the constructor for a unifiedExchangeAdaptor.
  3860  func newUnifiedExchangeAdaptor(cfg *exchangeAdaptorCfg) (*unifiedExchangeAdaptor, error) {
  3861  	initialBalances := make(map[uint32]uint64, len(cfg.baseDexBalances))
  3862  	for assetID, balance := range cfg.baseDexBalances {
  3863  		initialBalances[assetID] = balance
  3864  	}
  3865  	for assetID, balance := range cfg.baseCexBalances {
  3866  		initialBalances[assetID] += balance
  3867  	}
  3868  
  3869  	baseDEXBalances := make(map[uint32]int64, len(cfg.baseDexBalances))
  3870  	for assetID, balance := range cfg.baseDexBalances {
  3871  		baseDEXBalances[assetID] = int64(balance)
  3872  	}
  3873  	baseCEXBalances := make(map[uint32]int64, len(cfg.baseCexBalances))
  3874  	for assetID, balance := range cfg.baseCexBalances {
  3875  		baseCEXBalances[assetID] = int64(balance)
  3876  	}
  3877  
  3878  	coreMkt, err := cfg.core.ExchangeMarket(cfg.mwh.Host, cfg.mwh.BaseID, cfg.mwh.QuoteID)
  3879  	if err != nil {
  3880  		return nil, err
  3881  	}
  3882  
  3883  	mkt, err := parseMarket(cfg.mwh.Host, coreMkt)
  3884  	if err != nil {
  3885  		return nil, err
  3886  	}
  3887  
  3888  	baseTraits, err := cfg.core.WalletTraits(mkt.baseID)
  3889  	if err != nil {
  3890  		return nil, fmt.Errorf("wallet trait error for base asset %d", mkt.baseID)
  3891  	}
  3892  	quoteTraits, err := cfg.core.WalletTraits(mkt.quoteID)
  3893  	if err != nil {
  3894  		return nil, fmt.Errorf("wallet trait error for quote asset %d", mkt.quoteID)
  3895  	}
  3896  
  3897  	adaptor := &unifiedExchangeAdaptor{
  3898  		market:           mkt,
  3899  		clientCore:       cfg.core,
  3900  		CEX:              cfg.cex,
  3901  		botID:            cfg.botID,
  3902  		log:              cfg.log,
  3903  		eventLogDB:       cfg.eventLogDB,
  3904  		initialBalances:  initialBalances,
  3905  		baseTraits:       baseTraits,
  3906  		quoteTraits:      quoteTraits,
  3907  		autoRebalanceCfg: cfg.autoRebalanceConfig,
  3908  
  3909  		baseDexBalances:    baseDEXBalances,
  3910  		baseCexBalances:    baseCEXBalances,
  3911  		pendingDEXOrders:   make(map[order.OrderID]*pendingDEXOrder),
  3912  		pendingCEXOrders:   make(map[string]*pendingCEXOrder),
  3913  		pendingDeposits:    make(map[string]*pendingDeposit),
  3914  		pendingWithdrawals: make(map[string]*pendingWithdrawal),
  3915  		mwh:                cfg.mwh,
  3916  		inventoryMods:      make(map[uint32]int64),
  3917  		cexProblems:        newCEXProblems(),
  3918  	}
  3919  
  3920  	adaptor.fiatRates.Store(map[uint32]float64{})
  3921  	adaptor.botCfgV.Store(cfg.botCfg)
  3922  
  3923  	return adaptor, nil
  3924  }