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