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

     1  package mm
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"math"
     9  	"reflect"
    10  	"sort"
    11  	"sync"
    12  	"testing"
    13  	"time"
    14  
    15  	"decred.org/dcrdex/client/asset"
    16  	"decred.org/dcrdex/client/core"
    17  	"decred.org/dcrdex/client/mm/libxc"
    18  	"decred.org/dcrdex/dex"
    19  	"decred.org/dcrdex/dex/calc"
    20  	"decred.org/dcrdex/dex/encode"
    21  	"decred.org/dcrdex/dex/msgjson"
    22  	"decred.org/dcrdex/dex/order"
    23  	"decred.org/dcrdex/dex/utils"
    24  	"github.com/davecgh/go-spew/spew"
    25  )
    26  
    27  type tEventLogDB struct {
    28  	storedEventsMtx sync.Mutex
    29  	storedEvents    []*MarketMakingEvent
    30  }
    31  
    32  var _ eventLogDB = (*tEventLogDB)(nil)
    33  
    34  func newTEventLogDB() *tEventLogDB {
    35  	return &tEventLogDB{
    36  		storedEvents: make([]*MarketMakingEvent, 0),
    37  	}
    38  }
    39  
    40  func (db *tEventLogDB) storeNewRun(startTime int64, mkt *MarketWithHost, cfg *BotConfig, initialState *BalanceState) error {
    41  	return nil
    42  }
    43  func (db *tEventLogDB) endRun(startTime int64, mkt *MarketWithHost) error { return nil }
    44  func (db *tEventLogDB) storeEvent(startTime int64, mkt *MarketWithHost, e *MarketMakingEvent, fs *BalanceState) {
    45  	db.storedEventsMtx.Lock()
    46  	defer db.storedEventsMtx.Unlock()
    47  	db.storedEvents = append(db.storedEvents, e)
    48  }
    49  func (db *tEventLogDB) storedEventAtIndexEquals(e *MarketMakingEvent, idx int) bool {
    50  	db.storedEventsMtx.Lock()
    51  	defer db.storedEventsMtx.Unlock()
    52  	if idx < 0 || idx >= len(db.storedEvents) {
    53  		return false
    54  	}
    55  	db.storedEvents[idx].TimeStamp = 0 // ignore timestamp
    56  	return !reflect.DeepEqual(db.storedEvents[idx], e)
    57  }
    58  func (db *tEventLogDB) latestStoredEventEquals(e *MarketMakingEvent) bool {
    59  	db.storedEventsMtx.Lock()
    60  	if e == nil && len(db.storedEvents) == 0 {
    61  		db.storedEventsMtx.Unlock()
    62  		return true
    63  	}
    64  	if e == nil {
    65  		db.storedEventsMtx.Unlock()
    66  		return false
    67  	}
    68  	db.storedEventsMtx.Unlock()
    69  	return db.storedEventAtIndexEquals(e, len(db.storedEvents)-1)
    70  }
    71  func (db *tEventLogDB) latestStoredEvent() *MarketMakingEvent {
    72  	db.storedEventsMtx.Lock()
    73  	defer db.storedEventsMtx.Unlock()
    74  	if len(db.storedEvents) == 0 {
    75  		return nil
    76  	}
    77  	return db.storedEvents[len(db.storedEvents)-1]
    78  }
    79  func (db *tEventLogDB) runs(n uint64, refStartTime *uint64, refMkt *MarketWithHost) ([]*MarketMakingRun, error) {
    80  	return nil, nil
    81  }
    82  func (db *tEventLogDB) runOverview(startTime int64, mkt *MarketWithHost) (*MarketMakingRunOverview, error) {
    83  	return nil, nil
    84  }
    85  func (db *tEventLogDB) runEvents(startTime int64, mkt *MarketWithHost, n uint64, refID *uint64, pendingOnly bool, filters *RunLogFilters) ([]*MarketMakingEvent, error) {
    86  	return nil, nil
    87  }
    88  
    89  func tFees(swap, redeem, refund, funding uint64) *OrderFees {
    90  	lotFees := &LotFees{
    91  		Swap:   swap,
    92  		Redeem: redeem,
    93  		Refund: refund,
    94  	}
    95  	return &OrderFees{
    96  		LotFeeRange: &LotFeeRange{
    97  			Max:       lotFees,
    98  			Estimated: lotFees,
    99  		},
   100  		Funding: funding,
   101  	}
   102  }
   103  
   104  func TestSufficientBalanceForDEXTrade(t *testing.T) {
   105  	lotSize := uint64(1e8)
   106  	sellFees := tFees(1e5, 2e5, 3e5, 0)
   107  	buyFees := tFees(5e5, 6e5, 7e5, 0)
   108  
   109  	fundingFees := uint64(8e5)
   110  
   111  	type test struct {
   112  		name            string
   113  		baseID, quoteID uint32
   114  		balances        map[uint32]uint64
   115  		isAccountLocker map[uint32]bool
   116  		sell            bool
   117  		rate, qty       uint64
   118  	}
   119  
   120  	b2q := calc.BaseToQuote
   121  
   122  	tests := []*test{
   123  		{
   124  			name:    "sell, non account locker",
   125  			baseID:  42,
   126  			quoteID: 0,
   127  			sell:    true,
   128  			rate:    1e7,
   129  			qty:     3 * lotSize,
   130  			balances: map[uint32]uint64{
   131  				42: 3*lotSize + 3*sellFees.Max.Swap + fundingFees,
   132  				0:  0,
   133  			},
   134  		},
   135  		{
   136  			name:    "buy, non account locker",
   137  			baseID:  42,
   138  			quoteID: 0,
   139  			rate:    2e7,
   140  			qty:     2 * lotSize,
   141  			sell:    false,
   142  			balances: map[uint32]uint64{
   143  				42: 0,
   144  				0:  b2q(2e7, 2*lotSize) + 2*buyFees.Max.Swap + fundingFees,
   145  			},
   146  		},
   147  		{
   148  			name:    "sell, account locker/token",
   149  			baseID:  966001,
   150  			quoteID: 60,
   151  			sell:    true,
   152  			rate:    2e7,
   153  			qty:     3 * lotSize,
   154  			isAccountLocker: map[uint32]bool{
   155  				966001: true,
   156  				966:    true,
   157  				60:     true,
   158  			},
   159  			balances: map[uint32]uint64{
   160  				966001: 3 * lotSize,
   161  				966:    3*sellFees.Max.Swap + 3*sellFees.Max.Refund + fundingFees,
   162  				60:     3 * sellFees.Max.Redeem,
   163  			},
   164  		},
   165  		{
   166  			name:    "buy, account locker/token",
   167  			baseID:  966001,
   168  			quoteID: 60,
   169  			sell:    false,
   170  			rate:    2e7,
   171  			qty:     3 * lotSize,
   172  			isAccountLocker: map[uint32]bool{
   173  				966001: true,
   174  				966:    true,
   175  				60:     true,
   176  			},
   177  			balances: map[uint32]uint64{
   178  				966: 3 * buyFees.Max.Redeem,
   179  				60:  b2q(2e7, 3*lotSize) + 3*buyFees.Max.Swap + 3*buyFees.Max.Refund + fundingFees,
   180  			},
   181  		},
   182  	}
   183  
   184  	for _, test := range tests {
   185  		t.Run(test.name, func(t *testing.T) {
   186  			tCore := newTCore()
   187  			tCore.singleLotSellFees = sellFees
   188  			tCore.singleLotBuyFees = buyFees
   189  			tCore.maxFundingFees = fundingFees
   190  
   191  			tCore.market = &core.Market{
   192  				BaseID:  test.baseID,
   193  				QuoteID: test.quoteID,
   194  				LotSize: lotSize,
   195  			}
   196  			mwh := &MarketWithHost{
   197  				BaseID:  test.baseID,
   198  				QuoteID: test.quoteID,
   199  			}
   200  
   201  			tCore.isAccountLocker = test.isAccountLocker
   202  
   203  			checkBalanceSufficient := func(expSufficient bool) {
   204  				t.Helper()
   205  				adaptor := mustParseAdaptor(&exchangeAdaptorCfg{
   206  					core:            tCore,
   207  					baseDexBalances: test.balances,
   208  					mwh:             mwh,
   209  					eventLogDB:      &tEventLogDB{},
   210  				})
   211  				ctx, cancel := context.WithCancel(context.Background())
   212  				defer cancel()
   213  				_, err := adaptor.Connect(ctx)
   214  				if err != nil {
   215  					t.Fatalf("Connect error: %v", err)
   216  				}
   217  				sufficient, err := adaptor.SufficientBalanceForDEXTrade(test.rate, test.qty, test.sell)
   218  				if err != nil {
   219  					t.Fatalf("unexpected error: %v", err)
   220  				}
   221  				if sufficient != expSufficient {
   222  					t.Fatalf("expected sufficient=%v, got %v", expSufficient, sufficient)
   223  				}
   224  			}
   225  
   226  			checkBalanceSufficient(true)
   227  
   228  			for assetID, bal := range test.balances {
   229  				if bal == 0 {
   230  					continue
   231  				}
   232  				test.balances[assetID]--
   233  				checkBalanceSufficient(false)
   234  				test.balances[assetID]++
   235  			}
   236  		})
   237  	}
   238  }
   239  
   240  func TestSufficientBalanceForCEXTrade(t *testing.T) {
   241  	const baseID uint32 = 42
   242  	const quoteID uint32 = 0
   243  
   244  	type test struct {
   245  		name        string
   246  		cexBalances map[uint32]uint64
   247  		sell        bool
   248  		rate, qty   uint64
   249  	}
   250  
   251  	tests := []*test{
   252  		{
   253  			name: "sell",
   254  			sell: true,
   255  			rate: 5e7,
   256  			qty:  1e8,
   257  			cexBalances: map[uint32]uint64{
   258  				baseID: 1e8,
   259  			},
   260  		},
   261  		{
   262  			name: "buy",
   263  			sell: false,
   264  			rate: 5e7,
   265  			qty:  1e8,
   266  			cexBalances: map[uint32]uint64{
   267  				quoteID: calc.BaseToQuote(5e7, 1e8),
   268  			},
   269  		},
   270  	}
   271  
   272  	for _, test := range tests {
   273  		t.Run(test.name, func(t *testing.T) {
   274  			checkBalanceSufficient := func(expSufficient bool) {
   275  				tCore := newTCore()
   276  				adaptor := mustParseAdaptor(&exchangeAdaptorCfg{
   277  					core:            tCore,
   278  					baseCexBalances: test.cexBalances,
   279  					mwh: &MarketWithHost{
   280  						BaseID:  baseID,
   281  						QuoteID: quoteID,
   282  					},
   283  				})
   284  				sufficient := adaptor.SufficientBalanceForCEXTrade(baseID, quoteID, test.sell, test.rate, test.qty)
   285  				if sufficient != expSufficient {
   286  					t.Fatalf("expected sufficient=%v, got %v", expSufficient, sufficient)
   287  				}
   288  			}
   289  
   290  			checkBalanceSufficient(true)
   291  
   292  			for assetID := range test.cexBalances {
   293  				test.cexBalances[assetID]--
   294  				checkBalanceSufficient(false)
   295  				test.cexBalances[assetID]++
   296  			}
   297  		})
   298  	}
   299  }
   300  
   301  func TestCEXBalanceCounterTrade(t *testing.T) {
   302  	// Tests that CEX locked balance is increased and available balance is
   303  	// decreased when CEX funds are required for a counter trade.
   304  	tCore := newTCore()
   305  	tCEX := newTCEX()
   306  
   307  	orderIDs := make([]order.OrderID, 5)
   308  	for i := range orderIDs {
   309  		var id order.OrderID
   310  		copy(id[:], encode.RandomBytes(order.OrderIDSize))
   311  		orderIDs[i] = id
   312  	}
   313  
   314  	dexBalances := map[uint32]uint64{
   315  		0:  1e8,
   316  		42: 1e8,
   317  	}
   318  	cexBalances := map[uint32]uint64{
   319  		42: 1e8,
   320  		0:  1e8,
   321  	}
   322  
   323  	botID := dexMarketID("host1", 42, 0)
   324  	eventLogDB := newTEventLogDB()
   325  	adaptor := mustParseAdaptor(&exchangeAdaptorCfg{
   326  		botID:           botID,
   327  		core:            tCore,
   328  		cex:             tCEX,
   329  		baseDexBalances: dexBalances,
   330  		baseCexBalances: cexBalances,
   331  		mwh: &MarketWithHost{
   332  			Host:    "host1",
   333  			BaseID:  42,
   334  			QuoteID: 0,
   335  		},
   336  		eventLogDB: eventLogDB,
   337  	})
   338  
   339  	adaptor.pendingDEXOrders = map[order.OrderID]*pendingDEXOrder{
   340  		orderIDs[0]: {
   341  			counterTradeRate: 6e7,
   342  		},
   343  		orderIDs[1]: {
   344  			counterTradeRate: 5e7,
   345  		},
   346  	}
   347  
   348  	order0 := &core.Order{
   349  		Qty:     5e6,
   350  		Rate:    6.1e7,
   351  		Sell:    true,
   352  		BaseID:  42,
   353  		QuoteID: 0,
   354  	}
   355  	pendingOrder0 := adaptor.pendingDEXOrders[orderIDs[0]]
   356  	pendingOrder1 := adaptor.pendingDEXOrders[orderIDs[1]]
   357  
   358  	order1 := &core.Order{
   359  		Qty:     5e6,
   360  		Rate:    4.9e7,
   361  		Sell:    false,
   362  		BaseID:  42,
   363  		QuoteID: 0,
   364  	}
   365  
   366  	pendingOrder0.updateState(order0, adaptor.WalletTransaction, 0, 0)
   367  	pendingOrder1.updateState(order1, adaptor.WalletTransaction, 0, 0)
   368  
   369  	dcrBalance := adaptor.CEXBalance(42)
   370  	expDCR := &BotBalance{
   371  		Available: 1e8 - order1.Qty,
   372  		Reserved:  order1.Qty,
   373  	}
   374  	if !reflect.DeepEqual(dcrBalance, expDCR) {
   375  		t.Fatalf("unexpected DCR balance. wanted %+v, got %+v", expDCR, dcrBalance)
   376  	}
   377  
   378  	btcBalance := adaptor.CEXBalance(0)
   379  	expBTCReserved := calc.BaseToQuote(adaptor.pendingDEXOrders[orderIDs[0]].counterTradeRate, order0.Qty)
   380  	expBTC := &BotBalance{
   381  		Available: 1e8 - expBTCReserved,
   382  		Reserved:  expBTCReserved,
   383  	}
   384  	if !reflect.DeepEqual(btcBalance, expBTC) {
   385  		t.Fatalf("unexpected BTC balance. wanted %+v, got %+v", expBTC, btcBalance)
   386  	}
   387  }
   388  
   389  func TestFreeUpFunds(t *testing.T) {
   390  	const baseID, quoteID = 42, 0
   391  	const lotSize = 1e9
   392  	const rate = 1e6
   393  	quoteLot := calc.BaseToQuote(rate, lotSize)
   394  	u := mustParseAdaptorFromMarket(&core.Market{
   395  		RateStep:   1e3,
   396  		AtomToConv: 1,
   397  		LotSize:    lotSize,
   398  		BaseID:     baseID,
   399  		QuoteID:    quoteID,
   400  	})
   401  	oid := order.OrderID{1}
   402  	addOrder := func(assetID uint32, lots, epoch uint64) {
   403  		matchable := lots * lotSize
   404  		if assetID == quoteID {
   405  			matchable = calc.BaseToQuote(rate, lots*lotSize)
   406  		}
   407  		po := &pendingDEXOrder{}
   408  		po.state.Store(&dexOrderState{
   409  			dexBalanceEffects: &BalanceEffects{
   410  				Settled: map[uint32]int64{
   411  					assetID: -int64(matchable),
   412  				},
   413  				Locked: map[uint32]uint64{
   414  					assetID: matchable,
   415  				},
   416  				Pending: make(map[uint32]uint64),
   417  			},
   418  			cexBalanceEffects: &BalanceEffects{},
   419  			order: &core.Order{
   420  				ID:    oid[:],
   421  				Sell:  assetID == baseID,
   422  				Epoch: epoch,
   423  				Rate:  rate,
   424  			},
   425  		})
   426  		u.pendingDEXOrders[oid] = po
   427  	}
   428  	clearOrders := func() {
   429  		u.pendingDEXOrders = make(map[order.OrderID]*pendingDEXOrder)
   430  	}
   431  
   432  	const epoch uint64 = 5
   433  
   434  	check := func(assetID uint32, expOK bool, qty, pruneTo uint64, expOIDs ...order.OrderID) {
   435  		ords, ok := u.freeUpFunds(assetID, qty, pruneTo, epoch)
   436  		if ok != expOK {
   437  			t.Fatalf("wrong OK. wanted %t, got %t", expOK, ok)
   438  		}
   439  		if len(ords) != len(expOIDs) {
   440  			t.Fatalf("wrong number of orders freed. wanted %d, got %d", len(expOIDs), len(ords))
   441  		}
   442  		m := make(map[order.OrderID]struct{})
   443  		for _, o := range ords {
   444  			var oid order.OrderID
   445  			copy(oid[:], o.order.ID)
   446  			m[oid] = struct{}{}
   447  		}
   448  		for _, oid := range expOIDs {
   449  			if _, found := m[oid]; !found {
   450  				t.Fatalf("didn't find order %s", oid)
   451  			}
   452  		}
   453  	}
   454  
   455  	addOrder(baseID, 1, epoch-2)
   456  	check(baseID, true, lotSize, quoteLot, oid)
   457  	check(baseID, false, lotSize+1, quoteLot)
   458  	clearOrders()
   459  	// Uncancellable epoch prevents pruning.
   460  	addOrder(baseID, 1, epoch-1)
   461  	check(baseID, false, lotSize, quoteLot)
   462  
   463  	clearOrders()
   464  	addOrder(quoteID, 1, epoch-2)
   465  	check(quoteID, true, quoteLot, lotSize, oid)
   466  	check(quoteID, false, quoteLot+1, lotSize)
   467  }
   468  
   469  func TestDistribution(t *testing.T) {
   470  	// utxo/utxo
   471  	testDistribution(t, 42, 0)
   472  	// utxo/account-locker
   473  	testDistribution(t, 42, 60)
   474  	testDistribution(t, 60, 42)
   475  	// token/parent
   476  	testDistribution(t, 60001, 60)
   477  	testDistribution(t, 60, 60001)
   478  	// token/token - same chain
   479  	testDistribution(t, 966002, 966001)
   480  	testDistribution(t, 966001, 966002)
   481  	// token/token - different chains
   482  	testDistribution(t, 60001, 966003)
   483  	testDistribution(t, 966003, 60001)
   484  	// utxo/token
   485  	testDistribution(t, 42, 966003)
   486  	testDistribution(t, 966003, 42)
   487  }
   488  
   489  func testDistribution(t *testing.T, baseID, quoteID uint32) {
   490  	const lotSize = 5e7
   491  	const sellSwapFees, sellRedeemFees = 3e5, 1e5
   492  	const buySwapFees, buyRedeemFees = 2e4, 1e4
   493  	const sellRefundFees, buyRefundFees = 8e3, 9e4
   494  	const buyVWAP, sellVWAP = 1e7, 1.1e7
   495  	const extra = 80
   496  	const profit = 0.01
   497  
   498  	u := mustParseAdaptorFromMarket(&core.Market{
   499  		LotSize:  lotSize,
   500  		BaseID:   baseID,
   501  		QuoteID:  quoteID,
   502  		RateStep: 1e2,
   503  	})
   504  	cex := newTCEX()
   505  	tCore := newTCore()
   506  	u.CEX = cex
   507  	u.clientCore = tCore
   508  	u.autoRebalanceCfg = &AutoRebalanceConfig{}
   509  	a := &arbMarketMaker{unifiedExchangeAdaptor: u}
   510  	u.botCfgV.Store(&BotConfig{
   511  		ArbMarketMakerConfig: &ArbMarketMakerConfig{Profit: profit},
   512  	})
   513  	fiatRates := map[uint32]float64{baseID: 1, quoteID: 1}
   514  	u.fiatRates.Store(fiatRates)
   515  
   516  	isAccountLocker := func(assetID uint32) bool {
   517  		if tkn := asset.TokenInfo(assetID); tkn != nil {
   518  			fiatRates[tkn.ParentID] = 1
   519  			return true
   520  		}
   521  		return len(asset.Asset(assetID).Tokens) > 0
   522  	}
   523  	var sellFundingFees, buyFundingFees uint64
   524  	if !isAccountLocker(baseID) {
   525  		sellFundingFees = 5e3
   526  	}
   527  	if !isAccountLocker(quoteID) {
   528  		buyFundingFees = 6e3
   529  	}
   530  
   531  	maxBuyFees := &LotFees{
   532  		Swap:   buySwapFees,
   533  		Redeem: buyRedeemFees,
   534  		Refund: buyRefundFees,
   535  	}
   536  	maxSellFees := &LotFees{
   537  		Swap:   sellSwapFees,
   538  		Redeem: sellRedeemFees,
   539  		Refund: sellRefundFees,
   540  	}
   541  
   542  	buyBookingFees, sellBookingFees := u.bookingFees(maxBuyFees, maxSellFees)
   543  
   544  	a.buyFees = &OrderFees{
   545  		LotFeeRange: &LotFeeRange{
   546  			Max: maxBuyFees,
   547  			Estimated: &LotFees{
   548  				Swap:   buySwapFees,
   549  				Redeem: buyRedeemFees,
   550  				Refund: buyRefundFees,
   551  			},
   552  		},
   553  		Funding:           buyFundingFees,
   554  		BookingFeesPerLot: buyBookingFees,
   555  	}
   556  	a.sellFees = &OrderFees{
   557  		LotFeeRange: &LotFeeRange{
   558  			Max: maxSellFees,
   559  			Estimated: &LotFees{
   560  				Swap:   sellSwapFees,
   561  				Redeem: sellRedeemFees,
   562  			},
   563  		},
   564  		Funding:           sellFundingFees,
   565  		BookingFeesPerLot: sellBookingFees,
   566  	}
   567  
   568  	buyRate, _ := a.dexPlacementRate(buyVWAP, false)
   569  	sellRate, _ := a.dexPlacementRate(sellVWAP, true)
   570  
   571  	var buyLots, sellLots, minDexBase, minCexBase, totalBase, minDexQuote, minCexQuote, totalQuote uint64
   572  	var addBaseFees, addQuoteFees uint64
   573  	var perLot *lotCosts
   574  
   575  	setBals := func(dexBase, cexBase, dexQuote, cexQuote uint64) {
   576  		a.baseDexBalances[baseID] = int64(dexBase)
   577  		a.baseCexBalances[baseID] = int64(cexBase)
   578  		a.baseDexBalances[quoteID] = int64(dexQuote)
   579  		a.baseCexBalances[quoteID] = int64(cexQuote)
   580  	}
   581  
   582  	setLots := func(b, s uint64) {
   583  		buyLots, sellLots = b, s
   584  		u.botCfgV.Store(&BotConfig{
   585  			ArbMarketMakerConfig: &ArbMarketMakerConfig{
   586  				Profit: profit,
   587  				BuyPlacements: []*ArbMarketMakingPlacement{
   588  					{Lots: buyLots, Multiplier: 1},
   589  				},
   590  				SellPlacements: []*ArbMarketMakingPlacement{
   591  					{Lots: sellLots, Multiplier: 1},
   592  				},
   593  			},
   594  		})
   595  		addBaseFees, addQuoteFees = sellFundingFees, buyFundingFees
   596  		cex.asksVWAP[lotSize*buyLots] = vwapResult{avg: buyVWAP}
   597  		cex.bidsVWAP[lotSize*sellLots] = vwapResult{avg: sellVWAP}
   598  		minDexBase = sellLots*lotSize + sellFundingFees
   599  		if baseID == u.baseFeeID {
   600  			minDexBase += sellLots * a.sellFees.BookingFeesPerLot
   601  		}
   602  		if baseID == u.quoteFeeID {
   603  			addBaseFees += buyRedeemFees * buyLots
   604  			minDexBase += buyRedeemFees * buyLots
   605  		}
   606  		minCexBase = buyLots * lotSize
   607  
   608  		minDexQuote = calc.BaseToQuote(buyRate, buyLots*lotSize) + buyFundingFees
   609  		if quoteID == u.quoteFeeID {
   610  			minDexQuote += buyLots * a.buyFees.BookingFeesPerLot
   611  		}
   612  		if quoteID == u.baseFeeID {
   613  			addQuoteFees += sellRedeemFees * sellLots
   614  			minDexQuote += sellRedeemFees * sellLots
   615  		}
   616  		minCexQuote = calc.BaseToQuote(sellRate, sellLots*lotSize)
   617  		totalBase = minCexBase + minDexBase
   618  		totalQuote = minCexQuote + minDexQuote
   619  		var err error
   620  		perLot, err = a.lotCosts(buyRate, sellRate)
   621  		if err != nil {
   622  			t.Fatalf("Error getting lot costs: %v", err)
   623  		}
   624  		a.autoRebalanceCfg.MinBaseTransfer = lotSize
   625  		a.autoRebalanceCfg.MinQuoteTransfer = utils.Min(perLot.cexQuote, perLot.dexQuote)
   626  	}
   627  
   628  	checkDistribution := func(baseDeposit, baseWithdraw, quoteDeposit, quoteWithdraw uint64) {
   629  		t.Helper()
   630  		dist, err := a.distribution()
   631  		if err != nil {
   632  			t.Fatalf("distribution error: %v", err)
   633  		}
   634  		if dist.baseInv.toDeposit != baseDeposit {
   635  			t.Fatalf("wrong base deposit size. wanted %d, got %d", baseDeposit, dist.baseInv.toDeposit)
   636  		}
   637  		if dist.baseInv.toWithdraw != baseWithdraw {
   638  			t.Fatalf("wrong base withrawal size. wanted %d, got %d", baseWithdraw, dist.baseInv.toWithdraw)
   639  		}
   640  		if dist.quoteInv.toDeposit != quoteDeposit {
   641  			t.Fatalf("wrong quote deposit size. wanted %d, got %d", quoteDeposit, dist.quoteInv.toDeposit)
   642  		}
   643  		if dist.quoteInv.toWithdraw != quoteWithdraw {
   644  			t.Fatalf("wrong quote withrawal size. wanted %d, got %d", quoteWithdraw, dist.quoteInv.toWithdraw)
   645  		}
   646  	}
   647  
   648  	setLots(1, 1)
   649  	// Base asset - perfect distribution - no action
   650  	setBals(minDexBase, minCexBase, minDexQuote, minCexQuote)
   651  	checkDistribution(0, 0, 0, 0)
   652  
   653  	// Move all of the base balance to cex and max sure we get a withdraw.
   654  	setBals(0, totalBase, minDexQuote, minCexQuote)
   655  	checkDistribution(0, minDexBase, 0, 0)
   656  	// Raise the transfer theshold by one atom and it should zero the withdraw.
   657  	a.autoRebalanceCfg.MinBaseTransfer = minDexBase + 1
   658  	checkDistribution(0, 0, 0, 0)
   659  	a.autoRebalanceCfg.MinBaseTransfer = 0
   660  
   661  	// Same for quote
   662  	setBals(minDexBase, minCexBase, 0, totalQuote)
   663  	checkDistribution(0, 0, 0, minDexQuote)
   664  	a.autoRebalanceCfg.MinQuoteTransfer = minDexQuote + 1
   665  	checkDistribution(0, 0, 0, 0)
   666  	a.autoRebalanceCfg.MinQuoteTransfer = 0
   667  	// Base deposit
   668  	setBals(totalBase, 0, minDexQuote, minCexQuote)
   669  
   670  	checkDistribution(minCexBase, 0, 0, 0)
   671  	// Quote deposit
   672  	setBals(minDexBase, minCexBase, totalQuote, 0)
   673  	checkDistribution(0, 0, minCexQuote, 0)
   674  	// Doesn't have to be symmetric.
   675  	setLots(1, 3)
   676  	setBals(totalBase, 0, minDexQuote, minCexQuote)
   677  	checkDistribution(minCexBase, 0, 0, 0)
   678  	setBals(minDexBase, minCexBase, 0, totalQuote)
   679  	checkDistribution(0, 0, 0, minDexQuote)
   680  
   681  	// Even if there's extra, if neither side has too low of balance, nothing
   682  	// will happen. The extra will be split evenly between dex and cex.
   683  	// But if a side is one atom short, a full reblance will be done.
   684  	setLots(5, 3)
   685  	// Base OK
   686  	setBals(minDexBase, minCexBase*10, minDexQuote, minCexQuote)
   687  	checkDistribution(0, 0, 0, 0)
   688  	// Base withdraw. Extra goes to dex for base asset.
   689  	setBals(0, minDexBase+minCexBase+extra, minDexQuote, minCexQuote)
   690  	checkDistribution(0, minDexBase+extra, 0, 0)
   691  	// Base deposit.
   692  	setBals(minDexBase+minCexBase, extra, minDexQuote, minCexQuote)
   693  	checkDistribution(minCexBase-extra, 0, 0, 0)
   694  	// Quote OK
   695  	setBals(minDexBase, minCexBase, minDexQuote*100, minCexQuote*100)
   696  	checkDistribution(0, 0, 0, 0)
   697  	// Quote withdraw. Extra is split for the quote asset. Gotta lower the min
   698  	// transfer a little bit to make this one happen.
   699  	setBals(minDexBase, minCexBase, minDexQuote-perLot.dexQuote+extra, minCexQuote+perLot.dexQuote)
   700  	a.autoRebalanceCfg.MinQuoteTransfer = perLot.dexQuote - extra/2
   701  	checkDistribution(0, 0, 0, perLot.dexQuote-extra/2)
   702  	// Quote deposit
   703  	setBals(minDexBase, minCexBase, minDexQuote+perLot.cexQuote+extra, minCexQuote-perLot.cexQuote)
   704  	checkDistribution(0, 0, perLot.cexQuote+extra/2, 0)
   705  
   706  	// Deficit math.
   707  	// Since cex lot is smaller, dex can't use this extra.
   708  	setBals(addBaseFees+perLot.dexBase*3+perLot.cexBase, 0, addQuoteFees+minDexQuote, minCexQuote)
   709  	checkDistribution(2*perLot.cexBase, 0, 0, 0)
   710  	// Same thing, but with enough for fees, and there's no reason to transfer
   711  	// because it doesn't improve our matchability.
   712  	setBals(perLot.dexBase*3, extra, minDexQuote, minCexQuote)
   713  	checkDistribution(0, 0, 0, 0)
   714  	setBals(addBaseFees+minDexBase, minCexBase, addQuoteFees+perLot.dexQuote*5+perLot.cexQuote*2+extra, 0)
   715  	checkDistribution(0, 0, perLot.cexQuote*2+extra/2, 0)
   716  	setBals(addBaseFees+perLot.dexBase, 5*perLot.cexBase+2*perLot.dexBase+extra, addQuoteFees+minDexQuote, minCexQuote)
   717  	checkDistribution(0, 2*perLot.dexBase+extra, 0, 0)
   718  	setBals(addBaseFees+perLot.dexBase*2, perLot.cexBase*2, addQuoteFees+perLot.dexQuote, perLot.cexQuote*2+perLot.dexQuote+extra)
   719  	checkDistribution(0, 0, 0, perLot.dexQuote+extra/2)
   720  
   721  	var epok uint64
   722  	epoch := func() uint64 {
   723  		epok++
   724  		return epok
   725  	}
   726  
   727  	checkTransfers := func(expActionTaken bool, expBaseDeposit, expBaseWithdraw, expQuoteDeposit, expQuoteWithdraw uint64) {
   728  		t.Helper()
   729  		defer func() {
   730  			u.wg.Wait()
   731  			cex.withdrawals = nil
   732  			tCore.sends = nil
   733  			u.pendingDeposits = make(map[string]*pendingDeposit)
   734  			u.pendingWithdrawals = make(map[string]*pendingWithdrawal)
   735  			u.pendingDEXOrders = make(map[order.OrderID]*pendingDEXOrder)
   736  		}()
   737  
   738  		actionTaken, err := a.tryTransfers(epoch())
   739  		if err != nil {
   740  			t.Fatalf("Unexpected error: %v", err)
   741  		}
   742  		if actionTaken != expActionTaken {
   743  			t.Fatalf("wrong actionTaken result. wanted %t, got %t", expActionTaken, actionTaken)
   744  		}
   745  		var baseDeposit, quoteDeposit *sendArgs
   746  		for _, s := range tCore.sends {
   747  			if s.assetID == baseID {
   748  				baseDeposit = s
   749  			} else {
   750  				quoteDeposit = s
   751  			}
   752  		}
   753  		var baseWithdrawal, quoteWithdrawal *withdrawArgs
   754  		for _, w := range cex.withdrawals {
   755  			if w.assetID == baseID {
   756  				baseWithdrawal = w
   757  			} else {
   758  				quoteWithdrawal = w
   759  			}
   760  		}
   761  		if expBaseDeposit > 0 {
   762  			if baseDeposit == nil {
   763  				t.Fatalf("Missing base deposit")
   764  			}
   765  			if baseDeposit.value != expBaseDeposit {
   766  				t.Fatalf("Wrong value for base deposit. wanted %d, got %d", expBaseDeposit, baseDeposit.value)
   767  			}
   768  		} else if baseDeposit != nil {
   769  			t.Fatalf("Unexpected base deposit")
   770  		}
   771  		if expQuoteDeposit > 0 {
   772  			if quoteDeposit == nil {
   773  				t.Fatalf("Missing quote deposit")
   774  			}
   775  			if quoteDeposit.value != expQuoteDeposit {
   776  				t.Fatalf("Wrong value for quote deposit. wanted %d, got %d", expQuoteDeposit, quoteDeposit.value)
   777  			}
   778  		} else if quoteDeposit != nil {
   779  			t.Fatalf("Unexpected quote deposit")
   780  		}
   781  		if expBaseWithdraw > 0 {
   782  			if baseWithdrawal == nil {
   783  				t.Fatalf("Missing base withdrawal")
   784  			}
   785  			if baseWithdrawal.amt != expBaseWithdraw {
   786  				t.Fatalf("Wrong value for base withdrawal. wanted %d, got %d", expBaseWithdraw, baseWithdrawal.amt)
   787  			}
   788  		} else if baseWithdrawal != nil {
   789  			t.Fatalf("Unexpected base withdrawal")
   790  		}
   791  		if expQuoteWithdraw > 0 {
   792  			if quoteWithdrawal == nil {
   793  				t.Fatalf("Missing quote withdrawal")
   794  			}
   795  			if quoteWithdrawal.amt != expQuoteWithdraw {
   796  				t.Fatalf("Wrong value for quote withdrawal. wanted %d, got %d", expQuoteWithdraw, quoteWithdrawal.amt)
   797  			}
   798  		} else if quoteWithdrawal != nil {
   799  			t.Fatalf("Unexpected quote withdrawal")
   800  		}
   801  	}
   802  
   803  	setLots(1, 1)
   804  	setBals(minDexBase, minCexBase, minDexQuote, minCexQuote)
   805  	checkTransfers(false, 0, 0, 0, 0)
   806  
   807  	coinID := []byte{0xa0}
   808  	coin := &tCoin{coinID: coinID, value: 1}
   809  	txID := coin.TxID()
   810  	tCore.sendCoin = coin
   811  	tCore.walletTxs[txID] = &asset.WalletTransaction{Confirmed: true}
   812  	cex.confirmedDeposit = &coin.value
   813  
   814  	// Base deposit.
   815  	setBals(totalBase, 0, minDexQuote, minCexQuote)
   816  	checkTransfers(true, minCexBase, 0, 0, 0)
   817  
   818  	// Base withdrawal
   819  	cex.confirmWithdrawal = &withdrawArgs{txID: txID}
   820  	setBals(0, totalBase, minDexQuote, minCexQuote)
   821  	checkTransfers(true, 0, minDexBase, 0, 0)
   822  
   823  	// Quote deposit
   824  	setBals(minDexBase, minCexBase, totalQuote, 0)
   825  	checkTransfers(true, 0, 0, minCexQuote, 0)
   826  
   827  	// Quote withdrawal
   828  	setBals(minDexBase, minCexBase, 0, totalQuote)
   829  	checkTransfers(true, 0, 0, 0, minDexQuote)
   830  
   831  	// Base deposit, but we need to cancel an order to free up the funds.
   832  	setBals(totalBase, 0, minDexQuote, minCexQuote)
   833  	oid := order.OrderID{0x1b}
   834  	addLocked := func(assetID uint32, val uint64) {
   835  		po := &pendingDEXOrder{}
   836  		po.state.Store(&dexOrderState{
   837  			dexBalanceEffects: &BalanceEffects{
   838  				Settled: map[uint32]int64{
   839  					assetID: -int64(val),
   840  				},
   841  				Locked: map[uint32]uint64{
   842  					assetID: val,
   843  				},
   844  				Pending: make(map[uint32]uint64),
   845  			},
   846  			cexBalanceEffects: &BalanceEffects{},
   847  			order: &core.Order{
   848  				ID:   oid[:],
   849  				Sell: assetID == baseID,
   850  			},
   851  		})
   852  		u.pendingDEXOrders[oid] = po
   853  	}
   854  	checkCancel := func() {
   855  		if len(tCore.cancelsPlaced) != 1 || tCore.cancelsPlaced[0] != oid {
   856  			t.Fatalf("No cancels placed")
   857  		}
   858  		tCore.cancelsPlaced = nil
   859  	}
   860  	addLocked(baseID, totalBase)
   861  	checkTransfers(true, 0, 0, 0, 0)
   862  	checkCancel()
   863  
   864  	setBals(minDexBase, minCexBase, totalQuote, 0)
   865  	addLocked(quoteID, totalQuote)
   866  	checkTransfers(true, 0, 0, 0, 0)
   867  	checkCancel()
   868  
   869  	setBals(0, totalBase /* being withdrawn */, minDexQuote, minCexQuote)
   870  	u.pendingWithdrawals["a"] = &pendingWithdrawal{
   871  		assetID:      baseID,
   872  		amtWithdrawn: totalBase,
   873  	}
   874  	// Distribution should indicate a deposit.
   875  	checkDistribution(minCexBase, 0, 0, 0)
   876  	// But freeUpFunds will come up short. No action taken.
   877  	checkTransfers(false, 0, 0, 0, 0)
   878  
   879  	setBals(minDexBase, minCexBase, 0, totalQuote)
   880  	u.pendingWithdrawals["a"] = &pendingWithdrawal{
   881  		assetID:      quoteID,
   882  		amtWithdrawn: totalQuote,
   883  	}
   884  	checkDistribution(0, 0, minCexQuote, 0)
   885  	checkTransfers(false, 0, 0, 0, 0)
   886  
   887  	u.market = mustParseMarket(&core.Market{})
   888  }
   889  
   890  func TestMultiTrade(t *testing.T) {
   891  	const lotSize uint64 = 50e8
   892  	const rateStep uint64 = 1e3
   893  	const currEpoch = 100
   894  	const driftTolerance = 0.001
   895  	sellFees := tFees(1e5, 2e5, 3e5, 4e5)
   896  	buyFees := tFees(5e5, 6e5, 7e5, 8e5)
   897  	orderIDs := make([]order.OrderID, 10)
   898  	for i := range orderIDs {
   899  		var id order.OrderID
   900  		copy(id[:], encode.RandomBytes(order.OrderIDSize))
   901  		orderIDs[i] = id
   902  	}
   903  
   904  	driftToleranceEdge := func(rate uint64, within bool) uint64 {
   905  		edge := rate + uint64(float64(rate)*driftTolerance)
   906  		if within {
   907  			return edge - rateStep
   908  		}
   909  		return edge + rateStep
   910  	}
   911  
   912  	sellPlacements := []*TradePlacement{
   913  		{Lots: 1, Rate: 1e7, CounterTradeRate: 0.9e7},
   914  		{Lots: 2, Rate: 2e7, CounterTradeRate: 1.9e7},
   915  		{Lots: 3, Rate: 3e7, CounterTradeRate: 2.9e7},
   916  		{Lots: 2, Rate: 4e7, CounterTradeRate: 3.9e7},
   917  	}
   918  	sps := sellPlacements
   919  
   920  	buyPlacements := []*TradePlacement{
   921  		{Lots: 1, Rate: 4e7, CounterTradeRate: 4.1e7},
   922  		{Lots: 2, Rate: 3e7, CounterTradeRate: 3.1e7},
   923  		{Lots: 3, Rate: 2e7, CounterTradeRate: 2.1e7},
   924  		{Lots: 2, Rate: 1e7, CounterTradeRate: 1.1e7},
   925  	}
   926  	bps := buyPlacements
   927  
   928  	// cancelLastPlacement is the same as placements, but with the rate
   929  	// and lots of the last order set to zero, which should cause pending
   930  	// orders at that placementIndex to be cancelled.
   931  	cancelLastPlacement := func(sell bool) []*TradePlacement {
   932  		placements := make([]*TradePlacement, len(sellPlacements))
   933  		if sell {
   934  			copy(placements, sellPlacements)
   935  		} else {
   936  			copy(placements, buyPlacements)
   937  		}
   938  		placements[len(placements)-1] = &TradePlacement{}
   939  		return placements
   940  	}
   941  
   942  	// removeLastPlacement simulates a reconfiguration is which the
   943  	// last placement is removed.
   944  	removeLastPlacement := func(sell bool) []*TradePlacement {
   945  		placements := make([]*TradePlacement, len(sellPlacements))
   946  		if sell {
   947  			copy(placements, sellPlacements)
   948  		} else {
   949  			copy(placements, buyPlacements)
   950  		}
   951  		return placements[:len(placements)-1]
   952  	}
   953  
   954  	// reconfigToMorePlacements simulates a reconfiguration in which
   955  	// the lots allocated to the placement at index 1 is reduced by 1.
   956  	reconfigToLessPlacements := func(sell bool) []*TradePlacement {
   957  		placements := make([]*TradePlacement, len(sellPlacements))
   958  		if sell {
   959  			copy(placements, sellPlacements)
   960  		} else {
   961  			copy(placements, buyPlacements)
   962  		}
   963  		placements[1] = &TradePlacement{
   964  			Lots:             placements[1].Lots - 1,
   965  			Rate:             placements[1].Rate,
   966  			CounterTradeRate: placements[1].CounterTradeRate,
   967  		}
   968  		return placements
   969  	}
   970  
   971  	pendingOrders := func(sell bool, baseID, quoteID uint32) map[order.OrderID]*pendingDEXOrder {
   972  		var placements []*TradePlacement
   973  		if sell {
   974  			placements = sellPlacements
   975  		} else {
   976  			placements = buyPlacements
   977  		}
   978  
   979  		toAsset := baseID
   980  		if sell {
   981  			toAsset = quoteID
   982  		}
   983  
   984  		orders := map[order.OrderID]*core.Order{
   985  			orderIDs[0]: { // Should cancel, but cannot due to epoch > currEpoch - 2
   986  				Qty:     lotSize,
   987  				Sell:    sell,
   988  				ID:      orderIDs[0][:],
   989  				Rate:    driftToleranceEdge(placements[0].Rate, true),
   990  				Epoch:   currEpoch - 1,
   991  				BaseID:  baseID,
   992  				QuoteID: quoteID,
   993  			},
   994  			orderIDs[1]: { // Within tolerance, don't cancel
   995  				Qty:     2 * lotSize,
   996  				Filled:  lotSize,
   997  				Sell:    sell,
   998  				ID:      orderIDs[1][:],
   999  				Rate:    driftToleranceEdge(placements[1].Rate, true),
  1000  				Epoch:   currEpoch - 2,
  1001  				BaseID:  baseID,
  1002  				QuoteID: quoteID,
  1003  			},
  1004  			orderIDs[2]: { // Cancel
  1005  				Qty:     lotSize,
  1006  				Sell:    sell,
  1007  				ID:      orderIDs[2][:],
  1008  				Rate:    driftToleranceEdge(placements[2].Rate, false),
  1009  				Epoch:   currEpoch - 2,
  1010  				BaseID:  baseID,
  1011  				QuoteID: quoteID,
  1012  			},
  1013  			orderIDs[3]: { // Within tolerance, don't cancel
  1014  				Qty:     lotSize,
  1015  				Sell:    sell,
  1016  				ID:      orderIDs[3][:],
  1017  				Rate:    driftToleranceEdge(placements[3].Rate, true),
  1018  				Epoch:   currEpoch - 2,
  1019  				BaseID:  baseID,
  1020  				QuoteID: quoteID,
  1021  			},
  1022  		}
  1023  
  1024  		toReturn := map[order.OrderID]*pendingDEXOrder{
  1025  			orderIDs[0]: { // Should cancel, but cannot due to epoch > currEpoch - 2
  1026  				placementIndex:   0,
  1027  				counterTradeRate: placements[0].CounterTradeRate,
  1028  			},
  1029  			orderIDs[1]: {
  1030  				placementIndex:   1,
  1031  				counterTradeRate: placements[1].CounterTradeRate,
  1032  			},
  1033  			orderIDs[2]: {
  1034  				placementIndex:   2,
  1035  				counterTradeRate: placements[2].CounterTradeRate,
  1036  			},
  1037  			orderIDs[3]: {
  1038  				placementIndex:   3,
  1039  				counterTradeRate: placements[3].CounterTradeRate,
  1040  			},
  1041  		}
  1042  
  1043  		for oid, order := range orders {
  1044  			reserved := reservedForCounterTrade(sell, toReturn[oid].counterTradeRate, orders[oid].Qty-orders[oid].Filled)
  1045  			toReturn[oid].state.Store(&dexOrderState{
  1046  				order:             order,
  1047  				dexBalanceEffects: &BalanceEffects{},
  1048  				cexBalanceEffects: &BalanceEffects{
  1049  					Settled: map[uint32]int64{
  1050  						toAsset: -int64(reserved),
  1051  					},
  1052  					Reserved: map[uint32]uint64{
  1053  						toAsset: reserved,
  1054  					},
  1055  				},
  1056  				counterTradeRate: toReturn[oid].counterTradeRate,
  1057  			})
  1058  		}
  1059  		return toReturn
  1060  	}
  1061  
  1062  	// secondPendingOrderNotFilled returns the same pending orders as
  1063  	// pendingOrders, but with the second order not filled.
  1064  	secondPendingOrderNotFilled := func(sell bool, baseID, quoteID uint32) map[order.OrderID]*pendingDEXOrder {
  1065  		orders := pendingOrders(sell, baseID, quoteID)
  1066  		toAsset := baseID
  1067  		if sell {
  1068  			toAsset = quoteID
  1069  		}
  1070  		currentState := orders[orderIDs[1]].currentState()
  1071  		reserved := reservedForCounterTrade(sell, currentState.counterTradeRate, currentState.order.Qty)
  1072  		orders[orderIDs[1]].currentState().order.Filled = 0
  1073  		orders[orderIDs[1]].currentState().cexBalanceEffects = &BalanceEffects{
  1074  			Settled: map[uint32]int64{
  1075  				toAsset: -int64(reserved),
  1076  			},
  1077  			Reserved: map[uint32]uint64{
  1078  				toAsset: reserved,
  1079  			},
  1080  		}
  1081  
  1082  		return orders
  1083  	}
  1084  
  1085  	// pendingWithSelfMatch returns the same pending orders as pendingOrders,
  1086  	// but with an additional order on the other side of the market that
  1087  	// would cause a self-match.
  1088  	pendingOrdersSelfMatch := func(sell bool, baseID, quoteID uint32) map[order.OrderID]*pendingDEXOrder {
  1089  		orders := pendingOrders(sell, baseID, quoteID)
  1090  		var rate uint64
  1091  		if sell {
  1092  			rate = driftToleranceEdge(2e7, true) // 2e7 is the rate of the lowest sell placement
  1093  		} else {
  1094  			rate = 3e7 // 3e7 is the rate of the highest buy placement
  1095  		}
  1096  		pendingOrder := &pendingDEXOrder{
  1097  			placementIndex: 0,
  1098  		}
  1099  		pendingOrder.state.Store(&dexOrderState{
  1100  			order: &core.Order{ // Within tolerance, don't cancel
  1101  				Qty:   lotSize,
  1102  				Sell:  !sell,
  1103  				ID:    orderIDs[4][:],
  1104  				Rate:  rate,
  1105  				Epoch: currEpoch - 2,
  1106  			},
  1107  			dexBalanceEffects: &BalanceEffects{},
  1108  			cexBalanceEffects: &BalanceEffects{},
  1109  			counterTradeRate:  pendingOrder.counterTradeRate,
  1110  		})
  1111  
  1112  		orders[orderIDs[4]] = pendingOrder
  1113  		return orders
  1114  	}
  1115  
  1116  	b2q := calc.BaseToQuote
  1117  
  1118  	addBuffer := func(qty uint64, buffer float64) uint64 {
  1119  		return uint64(math.Round(float64(qty) * (100 + buffer) / 100))
  1120  	}
  1121  
  1122  	/*
  1123  	 * The dexBalance and cexBalances fields of this test are set so that they
  1124  	 * are at an edge. If any non-zero balance is decreased by 1, the behavior
  1125  	 * of the function should change. Each of the "WithDecrement" fields are
  1126  	 * the expected result if any of the non-zero balances are decreased by 1.
  1127  	 */
  1128  	type test struct {
  1129  		name    string
  1130  		baseID  uint32
  1131  		quoteID uint32
  1132  
  1133  		multiSplitBuffer float64
  1134  
  1135  		sellDexBalances   map[uint32]uint64
  1136  		sellCexBalances   map[uint32]uint64
  1137  		sellPlacements    []*TradePlacement
  1138  		sellPendingOrders map[order.OrderID]*pendingDEXOrder
  1139  
  1140  		buyCexBalances   map[uint32]uint64
  1141  		buyDexBalances   map[uint32]uint64
  1142  		buyPlacements    []*TradePlacement
  1143  		buyPendingOrders map[order.OrderID]*pendingDEXOrder
  1144  
  1145  		isAccountLocker               map[uint32]bool
  1146  		multiTradeResult              []*core.MultiTradeResult
  1147  		multiTradeResultWithDecrement []*core.MultiTradeResult
  1148  
  1149  		expectedOrderIDs              []order.OrderID
  1150  		expectedOrderIDsWithDecrement []order.OrderID
  1151  
  1152  		expectedSellPlacements                  []*core.QtyRate
  1153  		expectedSellPlacementsWithDecrement     []*core.QtyRate
  1154  		expectedSellOrderReport                 *OrderReport
  1155  		expectedSellOrderReportWithDEXDecrement *OrderReport
  1156  
  1157  		expectedBuyPlacements                  []*core.QtyRate
  1158  		expectedBuyPlacementsWithDecrement     []*core.QtyRate
  1159  		expectedBuyOrderReport                 *OrderReport
  1160  		expectedBuyOrderReportWithDEXDecrement *OrderReport
  1161  
  1162  		expectedCancels              []order.OrderID
  1163  		expectedCancelsWithDecrement []order.OrderID
  1164  	}
  1165  
  1166  	tests := []*test{
  1167  		{
  1168  			name:    "non account locker",
  1169  			baseID:  42,
  1170  			quoteID: 0,
  1171  
  1172  			// ---- Sell ----
  1173  			sellDexBalances: map[uint32]uint64{
  1174  				42: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
  1175  				0:  0,
  1176  			},
  1177  			sellCexBalances: map[uint32]uint64{
  1178  				42: 0,
  1179  				0: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  1180  					b2q(sellPlacements[1].CounterTradeRate, 2*lotSize) +
  1181  					b2q(sellPlacements[2].CounterTradeRate, 3*lotSize) +
  1182  					b2q(sellPlacements[3].CounterTradeRate, 2*lotSize),
  1183  			},
  1184  			sellPlacements:    sellPlacements,
  1185  			sellPendingOrders: pendingOrders(true, 42, 0),
  1186  			expectedSellPlacements: []*core.QtyRate{
  1187  				{Qty: lotSize, Rate: sellPlacements[1].Rate},
  1188  				{Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
  1189  				{Qty: lotSize, Rate: sellPlacements[3].Rate},
  1190  			},
  1191  			expectedSellOrderReport: &OrderReport{
  1192  				Placements: []*TradePlacement{
  1193  					{Lots: 1, Rate: sps[0].Rate, CounterTradeRate: sps[0].CounterTradeRate,
  1194  						StandingLots: 1,
  1195  						RequiredDEX:  map[uint32]uint64{},
  1196  						UsedDEX:      map[uint32]uint64{},
  1197  						RequiredCEX:  0,
  1198  						UsedCEX:      0,
  1199  					},
  1200  					{Lots: 2, Rate: sps[1].Rate, CounterTradeRate: sps[1].CounterTradeRate,
  1201  						StandingLots: 1,
  1202  						RequiredDEX: map[uint32]uint64{
  1203  							42: lotSize + sellFees.Max.Swap,
  1204  						},
  1205  						UsedDEX: map[uint32]uint64{
  1206  							42: lotSize + sellFees.Max.Swap,
  1207  						},
  1208  						RequiredCEX: b2q(sellPlacements[1].CounterTradeRate, lotSize),
  1209  						UsedCEX:     b2q(sellPlacements[1].CounterTradeRate, lotSize),
  1210  						OrderedLots: 1,
  1211  					},
  1212  					{Lots: 3, Rate: sps[2].Rate, CounterTradeRate: sps[2].CounterTradeRate,
  1213  						StandingLots: 1,
  1214  						RequiredDEX: map[uint32]uint64{
  1215  							42: 2 * (lotSize + sellFees.Max.Swap),
  1216  						},
  1217  						UsedDEX: map[uint32]uint64{
  1218  							42: 2 * (lotSize + sellFees.Max.Swap),
  1219  						},
  1220  						RequiredCEX: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  1221  						UsedCEX:     b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  1222  						OrderedLots: 2,
  1223  					},
  1224  					{Lots: 2, Rate: sps[3].Rate, CounterTradeRate: sps[3].CounterTradeRate,
  1225  						StandingLots: 1,
  1226  						RequiredDEX: map[uint32]uint64{
  1227  							42: lotSize + sellFees.Max.Swap,
  1228  						},
  1229  						UsedDEX: map[uint32]uint64{
  1230  							42: lotSize + sellFees.Max.Swap,
  1231  						},
  1232  						RequiredCEX: b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1233  						UsedCEX:     b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1234  						OrderedLots: 1,
  1235  					},
  1236  				},
  1237  				Fees: sellFees,
  1238  				AvailableDEXBals: map[uint32]*BotBalance{
  1239  					42: {
  1240  						Available: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
  1241  					},
  1242  					0: {},
  1243  				},
  1244  				RequiredDEXBals: map[uint32]uint64{
  1245  					42: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
  1246  				},
  1247  				RemainingDEXBals: map[uint32]uint64{
  1248  					42: 0,
  1249  					0:  0,
  1250  				},
  1251  				AvailableCEXBal: &BotBalance{
  1252  					Available: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1253  						b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  1254  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1255  					Reserved: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  1256  						b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1257  						b2q(sellPlacements[2].CounterTradeRate, lotSize) +
  1258  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1259  				},
  1260  				RequiredCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1261  					b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  1262  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1263  				RemainingCEXBal: 0,
  1264  				UsedDEXBals: map[uint32]uint64{
  1265  					42: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
  1266  				},
  1267  				UsedCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1268  					b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  1269  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1270  			},
  1271  			expectedSellPlacementsWithDecrement: []*core.QtyRate{
  1272  				{Qty: lotSize, Rate: sellPlacements[1].Rate},
  1273  				{Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
  1274  			},
  1275  			expectedSellOrderReportWithDEXDecrement: &OrderReport{
  1276  				Placements: []*TradePlacement{
  1277  					{Lots: 1, Rate: sps[0].Rate, CounterTradeRate: sps[0].CounterTradeRate,
  1278  						StandingLots: 1,
  1279  						RequiredDEX:  map[uint32]uint64{},
  1280  						UsedDEX:      map[uint32]uint64{},
  1281  						RequiredCEX:  0,
  1282  						UsedCEX:      0,
  1283  					},
  1284  					{Lots: 2, Rate: sps[1].Rate, CounterTradeRate: sps[1].CounterTradeRate,
  1285  						StandingLots: 1,
  1286  						RequiredDEX: map[uint32]uint64{
  1287  							42: lotSize + sellFees.Max.Swap,
  1288  						},
  1289  						UsedDEX: map[uint32]uint64{
  1290  							42: lotSize + sellFees.Max.Swap,
  1291  						},
  1292  						RequiredCEX: b2q(sellPlacements[1].CounterTradeRate, lotSize),
  1293  						UsedCEX:     b2q(sellPlacements[1].CounterTradeRate, lotSize),
  1294  						OrderedLots: 1,
  1295  					},
  1296  					{Lots: 3, Rate: sps[2].Rate, CounterTradeRate: sps[2].CounterTradeRate,
  1297  						StandingLots: 1,
  1298  						RequiredDEX: map[uint32]uint64{
  1299  							42: 2 * (lotSize + sellFees.Max.Swap),
  1300  						},
  1301  						UsedDEX: map[uint32]uint64{
  1302  							42: 2 * (lotSize + sellFees.Max.Swap),
  1303  						},
  1304  						RequiredCEX: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  1305  						UsedCEX:     b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  1306  						OrderedLots: 2,
  1307  					},
  1308  					{Lots: 2, Rate: sps[3].Rate, CounterTradeRate: sps[3].CounterTradeRate,
  1309  						StandingLots: 1,
  1310  						RequiredDEX: map[uint32]uint64{
  1311  							42: lotSize + sellFees.Max.Swap,
  1312  						},
  1313  						UsedDEX:     map[uint32]uint64{},
  1314  						RequiredCEX: b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1315  						UsedCEX:     0,
  1316  						OrderedLots: 0,
  1317  					},
  1318  				},
  1319  				Fees: sellFees,
  1320  				AvailableDEXBals: map[uint32]*BotBalance{
  1321  					42: {
  1322  						Available: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding - 1,
  1323  					},
  1324  					0: {},
  1325  				},
  1326  				RequiredDEXBals: map[uint32]uint64{
  1327  					42: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
  1328  				},
  1329  				RemainingDEXBals: map[uint32]uint64{
  1330  					42: lotSize + sellFees.Max.Swap - 1,
  1331  					0:  0,
  1332  				},
  1333  				AvailableCEXBal: &BotBalance{
  1334  					Available: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1335  						b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  1336  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1337  					Reserved: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  1338  						b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1339  						b2q(sellPlacements[2].CounterTradeRate, lotSize) +
  1340  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1341  				},
  1342  				RequiredCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1343  					b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  1344  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1345  				RemainingCEXBal: b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1346  				UsedDEXBals: map[uint32]uint64{
  1347  					42: 3*lotSize + 3*sellFees.Max.Swap + sellFees.Funding,
  1348  				},
  1349  				UsedCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1350  					b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  1351  			},
  1352  
  1353  			// ---- Buy ----
  1354  			buyDexBalances: map[uint32]uint64{
  1355  				42: 0,
  1356  				0: b2q(buyPlacements[1].Rate, lotSize) +
  1357  					b2q(buyPlacements[2].Rate, 2*lotSize) +
  1358  					b2q(buyPlacements[3].Rate, lotSize) +
  1359  					4*buyFees.Max.Swap + buyFees.Funding,
  1360  			},
  1361  			buyCexBalances: map[uint32]uint64{
  1362  				42: 8 * lotSize,
  1363  				0:  0,
  1364  			},
  1365  			buyPlacements:    buyPlacements,
  1366  			buyPendingOrders: pendingOrders(false, 42, 0),
  1367  			expectedBuyPlacements: []*core.QtyRate{
  1368  				{Qty: lotSize, Rate: buyPlacements[1].Rate},
  1369  				{Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
  1370  				{Qty: lotSize, Rate: buyPlacements[3].Rate},
  1371  			},
  1372  			expectedBuyPlacementsWithDecrement: []*core.QtyRate{
  1373  				{Qty: lotSize, Rate: buyPlacements[1].Rate},
  1374  				{Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
  1375  			},
  1376  			expectedCancels:              []order.OrderID{orderIDs[2]},
  1377  			expectedCancelsWithDecrement: []order.OrderID{orderIDs[2]},
  1378  			multiTradeResult: []*core.MultiTradeResult{
  1379  				{Order: &core.Order{ID: orderIDs[4][:]}},
  1380  				{Order: &core.Order{ID: orderIDs[5][:]}},
  1381  				{Order: &core.Order{ID: orderIDs[6][:]}},
  1382  			},
  1383  			multiTradeResultWithDecrement: []*core.MultiTradeResult{
  1384  				{Order: &core.Order{ID: orderIDs[4][:]}},
  1385  				{Order: &core.Order{ID: orderIDs[5][:]}},
  1386  			},
  1387  			expectedOrderIDs: []order.OrderID{
  1388  				orderIDs[4], orderIDs[5], orderIDs[6],
  1389  			},
  1390  			expectedBuyOrderReport: &OrderReport{
  1391  				Placements: []*TradePlacement{
  1392  					{Lots: 1, Rate: bps[0].Rate, CounterTradeRate: bps[0].CounterTradeRate,
  1393  						StandingLots: 1,
  1394  						RequiredDEX:  map[uint32]uint64{},
  1395  						UsedDEX:      map[uint32]uint64{},
  1396  						RequiredCEX:  0,
  1397  						UsedCEX:      0,
  1398  					},
  1399  					{Lots: 2, Rate: bps[1].Rate, CounterTradeRate: bps[1].CounterTradeRate,
  1400  						StandingLots: 1,
  1401  						RequiredDEX: map[uint32]uint64{
  1402  							0: b2q(buyPlacements[1].Rate, lotSize) + buyFees.Max.Swap,
  1403  						},
  1404  						UsedDEX: map[uint32]uint64{
  1405  							0: b2q(buyPlacements[1].Rate, lotSize) + buyFees.Max.Swap,
  1406  						},
  1407  						RequiredCEX: lotSize,
  1408  						UsedCEX:     lotSize,
  1409  						OrderedLots: 1,
  1410  					},
  1411  					{Lots: 3, Rate: bps[2].Rate, CounterTradeRate: bps[2].CounterTradeRate,
  1412  						StandingLots: 1,
  1413  						RequiredDEX: map[uint32]uint64{
  1414  							0: 2 * (b2q(buyPlacements[2].Rate, lotSize) + buyFees.Max.Swap),
  1415  						},
  1416  						UsedDEX: map[uint32]uint64{
  1417  							0: 2 * (b2q(buyPlacements[2].Rate, lotSize) + buyFees.Max.Swap),
  1418  						},
  1419  						RequiredCEX: 2 * lotSize,
  1420  						UsedCEX:     2 * lotSize,
  1421  						OrderedLots: 2,
  1422  					},
  1423  					{Lots: 2, Rate: bps[3].Rate, CounterTradeRate: bps[3].CounterTradeRate,
  1424  						StandingLots: 1,
  1425  						RequiredDEX: map[uint32]uint64{
  1426  							0: b2q(buyPlacements[3].Rate, lotSize) + buyFees.Max.Swap,
  1427  						},
  1428  						UsedDEX: map[uint32]uint64{
  1429  							0: b2q(buyPlacements[3].Rate, lotSize) + buyFees.Max.Swap,
  1430  						},
  1431  						RequiredCEX: lotSize,
  1432  						UsedCEX:     lotSize,
  1433  						OrderedLots: 1,
  1434  					},
  1435  				},
  1436  				Fees: buyFees,
  1437  				AvailableDEXBals: map[uint32]*BotBalance{
  1438  					0: {
  1439  						Available: b2q(buyPlacements[1].Rate, lotSize) +
  1440  							b2q(buyPlacements[2].Rate, 2*lotSize) +
  1441  							b2q(buyPlacements[3].Rate, lotSize) +
  1442  							4*buyFees.Max.Swap + buyFees.Funding,
  1443  					},
  1444  					42: {},
  1445  				},
  1446  				RequiredDEXBals: map[uint32]uint64{
  1447  					0: b2q(buyPlacements[1].Rate, lotSize) +
  1448  						b2q(buyPlacements[2].Rate, 2*lotSize) +
  1449  						b2q(buyPlacements[3].Rate, lotSize) +
  1450  						4*buyFees.Max.Swap + buyFees.Funding,
  1451  				},
  1452  				RemainingDEXBals: map[uint32]uint64{
  1453  					42: 0,
  1454  					0:  0,
  1455  				},
  1456  				AvailableCEXBal: &BotBalance{
  1457  					Available: 4 * lotSize,
  1458  					Reserved:  4 * lotSize,
  1459  				},
  1460  				RequiredCEXBal:  4 * lotSize,
  1461  				RemainingCEXBal: 0,
  1462  				UsedDEXBals: map[uint32]uint64{
  1463  					0: b2q(buyPlacements[1].Rate, lotSize) +
  1464  						b2q(buyPlacements[2].Rate, 2*lotSize) +
  1465  						b2q(buyPlacements[3].Rate, lotSize) +
  1466  						4*buyFees.Max.Swap + buyFees.Funding,
  1467  				},
  1468  				UsedCEXBal: 4 * lotSize,
  1469  			},
  1470  			expectedBuyOrderReportWithDEXDecrement: &OrderReport{
  1471  				Placements: []*TradePlacement{
  1472  					{Lots: 1, Rate: bps[0].Rate, CounterTradeRate: bps[0].CounterTradeRate,
  1473  						StandingLots: 1,
  1474  						RequiredDEX:  map[uint32]uint64{},
  1475  						UsedDEX:      map[uint32]uint64{},
  1476  						RequiredCEX:  0,
  1477  						UsedCEX:      0,
  1478  					},
  1479  					{Lots: 2, Rate: bps[1].Rate, CounterTradeRate: bps[1].CounterTradeRate,
  1480  						StandingLots: 1,
  1481  						RequiredDEX: map[uint32]uint64{
  1482  							0: b2q(buyPlacements[1].Rate, lotSize) + buyFees.Max.Swap,
  1483  						},
  1484  						UsedDEX: map[uint32]uint64{
  1485  							0: b2q(buyPlacements[1].Rate, lotSize) + buyFees.Max.Swap,
  1486  						},
  1487  						RequiredCEX: lotSize,
  1488  						UsedCEX:     lotSize,
  1489  						OrderedLots: 1,
  1490  					},
  1491  					{Lots: 3, Rate: bps[2].Rate, CounterTradeRate: bps[2].CounterTradeRate,
  1492  						StandingLots: 1,
  1493  						RequiredDEX: map[uint32]uint64{
  1494  							0: 2 * (b2q(buyPlacements[2].Rate, lotSize) + buyFees.Max.Swap),
  1495  						},
  1496  						UsedDEX: map[uint32]uint64{
  1497  							0: 2 * (b2q(buyPlacements[2].Rate, lotSize) + buyFees.Max.Swap),
  1498  						},
  1499  						RequiredCEX: 2 * lotSize,
  1500  						UsedCEX:     2 * lotSize,
  1501  						OrderedLots: 2,
  1502  					},
  1503  					{Lots: 2, Rate: bps[3].Rate, CounterTradeRate: bps[3].CounterTradeRate,
  1504  						StandingLots: 1,
  1505  						RequiredDEX: map[uint32]uint64{
  1506  							0: b2q(buyPlacements[3].Rate, lotSize) + buyFees.Max.Swap,
  1507  						},
  1508  						UsedDEX:     map[uint32]uint64{},
  1509  						RequiredCEX: lotSize,
  1510  						UsedCEX:     0,
  1511  						OrderedLots: 0,
  1512  					},
  1513  				},
  1514  				Fees: buyFees,
  1515  				AvailableDEXBals: map[uint32]*BotBalance{
  1516  					0: {
  1517  						Available: b2q(buyPlacements[1].Rate, lotSize) +
  1518  							b2q(buyPlacements[2].Rate, 2*lotSize) +
  1519  							b2q(buyPlacements[3].Rate, lotSize) +
  1520  							4*buyFees.Max.Swap + buyFees.Funding - 1,
  1521  					},
  1522  					42: {},
  1523  				},
  1524  				RequiredDEXBals: map[uint32]uint64{
  1525  					0: b2q(buyPlacements[1].Rate, lotSize) +
  1526  						b2q(buyPlacements[2].Rate, 2*lotSize) +
  1527  						b2q(buyPlacements[3].Rate, lotSize) +
  1528  						4*buyFees.Max.Swap + buyFees.Funding,
  1529  				},
  1530  				RemainingDEXBals: map[uint32]uint64{
  1531  					42: 0,
  1532  					0:  b2q(buyPlacements[3].Rate, lotSize) + buyFees.Max.Swap - 1,
  1533  				},
  1534  				AvailableCEXBal: &BotBalance{
  1535  					Available: 4 * lotSize,
  1536  					Reserved:  4 * lotSize,
  1537  				},
  1538  				RequiredCEXBal:  4 * lotSize,
  1539  				RemainingCEXBal: lotSize,
  1540  				UsedDEXBals: map[uint32]uint64{
  1541  					0: b2q(buyPlacements[1].Rate, lotSize) +
  1542  						b2q(buyPlacements[2].Rate, 2*lotSize) +
  1543  						3*buyFees.Max.Swap + buyFees.Funding,
  1544  				},
  1545  				UsedCEXBal: 3 * lotSize,
  1546  			},
  1547  			expectedOrderIDsWithDecrement: []order.OrderID{
  1548  				orderIDs[4], orderIDs[5],
  1549  			},
  1550  		},
  1551  		{
  1552  			name:    "non account locker - multi split buffer",
  1553  			baseID:  42,
  1554  			quoteID: 0,
  1555  
  1556  			multiSplitBuffer: 0.1,
  1557  
  1558  			// ---- Sell ----
  1559  			sellDexBalances: map[uint32]uint64{
  1560  				42: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
  1561  				0:  0,
  1562  			},
  1563  			sellCexBalances: map[uint32]uint64{
  1564  				42: 0,
  1565  				0: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  1566  					b2q(sellPlacements[1].CounterTradeRate, 2*lotSize) +
  1567  					b2q(sellPlacements[2].CounterTradeRate, 3*lotSize) +
  1568  					b2q(sellPlacements[3].CounterTradeRate, 2*lotSize),
  1569  			},
  1570  			sellPlacements:    sellPlacements,
  1571  			sellPendingOrders: pendingOrders(true, 42, 0),
  1572  			expectedSellPlacements: []*core.QtyRate{
  1573  				{Qty: lotSize, Rate: sellPlacements[1].Rate},
  1574  				{Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
  1575  				{Qty: lotSize, Rate: sellPlacements[3].Rate},
  1576  			},
  1577  			expectedSellOrderReport: &OrderReport{
  1578  				Placements: []*TradePlacement{
  1579  					{Lots: 1, Rate: sps[0].Rate, CounterTradeRate: sps[0].CounterTradeRate,
  1580  						StandingLots: 1,
  1581  						RequiredDEX:  map[uint32]uint64{},
  1582  						UsedDEX:      map[uint32]uint64{},
  1583  						RequiredCEX:  0,
  1584  						UsedCEX:      0,
  1585  					},
  1586  					{Lots: 2, Rate: sps[1].Rate, CounterTradeRate: sps[1].CounterTradeRate,
  1587  						StandingLots: 1,
  1588  						RequiredDEX: map[uint32]uint64{
  1589  							42: lotSize + sellFees.Max.Swap,
  1590  						},
  1591  						UsedDEX: map[uint32]uint64{
  1592  							42: lotSize + sellFees.Max.Swap,
  1593  						},
  1594  						RequiredCEX: b2q(sellPlacements[1].CounterTradeRate, lotSize),
  1595  						UsedCEX:     b2q(sellPlacements[1].CounterTradeRate, lotSize),
  1596  						OrderedLots: 1,
  1597  					},
  1598  					{Lots: 3, Rate: sps[2].Rate, CounterTradeRate: sps[2].CounterTradeRate,
  1599  						StandingLots: 1,
  1600  						RequiredDEX: map[uint32]uint64{
  1601  							42: 2 * (lotSize + sellFees.Max.Swap),
  1602  						},
  1603  						UsedDEX: map[uint32]uint64{
  1604  							42: 2 * (lotSize + sellFees.Max.Swap),
  1605  						},
  1606  						RequiredCEX: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  1607  						UsedCEX:     b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  1608  						OrderedLots: 2,
  1609  					},
  1610  					{Lots: 2, Rate: sps[3].Rate, CounterTradeRate: sps[3].CounterTradeRate,
  1611  						StandingLots: 1,
  1612  						RequiredDEX: map[uint32]uint64{
  1613  							42: lotSize + sellFees.Max.Swap,
  1614  						},
  1615  						UsedDEX: map[uint32]uint64{
  1616  							42: lotSize + sellFees.Max.Swap,
  1617  						},
  1618  						RequiredCEX: b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1619  						UsedCEX:     b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1620  						OrderedLots: 1,
  1621  					},
  1622  				},
  1623  				Fees: sellFees,
  1624  				AvailableDEXBals: map[uint32]*BotBalance{
  1625  					42: {
  1626  						Available: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
  1627  					},
  1628  					0: {},
  1629  				},
  1630  				RequiredDEXBals: map[uint32]uint64{
  1631  					42: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
  1632  				},
  1633  				RemainingDEXBals: map[uint32]uint64{
  1634  					42: 0,
  1635  					0:  0,
  1636  				},
  1637  				AvailableCEXBal: &BotBalance{
  1638  					Available: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1639  						b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  1640  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1641  					Reserved: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  1642  						b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1643  						b2q(sellPlacements[2].CounterTradeRate, lotSize) +
  1644  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1645  				},
  1646  				RequiredCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1647  					b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  1648  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1649  				RemainingCEXBal: 0,
  1650  				UsedDEXBals: map[uint32]uint64{
  1651  					42: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
  1652  				},
  1653  				UsedCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1654  					b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  1655  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1656  			},
  1657  			expectedSellPlacementsWithDecrement: []*core.QtyRate{
  1658  				{Qty: lotSize, Rate: sellPlacements[1].Rate},
  1659  				{Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
  1660  			},
  1661  			expectedSellOrderReportWithDEXDecrement: &OrderReport{
  1662  				Placements: []*TradePlacement{
  1663  					{Lots: 1, Rate: sps[0].Rate, CounterTradeRate: sps[0].CounterTradeRate,
  1664  						StandingLots: 1,
  1665  						RequiredDEX:  map[uint32]uint64{},
  1666  						UsedDEX:      map[uint32]uint64{},
  1667  						RequiredCEX:  0,
  1668  						UsedCEX:      0,
  1669  					},
  1670  					{Lots: 2, Rate: sps[1].Rate, CounterTradeRate: sps[1].CounterTradeRate,
  1671  						StandingLots: 1,
  1672  						RequiredDEX: map[uint32]uint64{
  1673  							42: lotSize + sellFees.Max.Swap,
  1674  						},
  1675  						UsedDEX: map[uint32]uint64{
  1676  							42: lotSize + sellFees.Max.Swap,
  1677  						},
  1678  						RequiredCEX: b2q(sellPlacements[1].CounterTradeRate, lotSize),
  1679  						UsedCEX:     b2q(sellPlacements[1].CounterTradeRate, lotSize),
  1680  						OrderedLots: 1,
  1681  					},
  1682  					{Lots: 3, Rate: sps[2].Rate, CounterTradeRate: sps[2].CounterTradeRate,
  1683  						StandingLots: 1,
  1684  						RequiredDEX: map[uint32]uint64{
  1685  							42: 2 * (lotSize + sellFees.Max.Swap),
  1686  						},
  1687  						UsedDEX: map[uint32]uint64{
  1688  							42: 2 * (lotSize + sellFees.Max.Swap),
  1689  						},
  1690  						RequiredCEX: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  1691  						UsedCEX:     b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  1692  						OrderedLots: 2,
  1693  					},
  1694  					{Lots: 2, Rate: sps[3].Rate, CounterTradeRate: sps[3].CounterTradeRate,
  1695  						StandingLots: 1,
  1696  						RequiredDEX: map[uint32]uint64{
  1697  							42: lotSize + sellFees.Max.Swap,
  1698  						},
  1699  						UsedDEX:     map[uint32]uint64{},
  1700  						RequiredCEX: b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1701  						UsedCEX:     0,
  1702  						OrderedLots: 0,
  1703  					},
  1704  				},
  1705  				Fees: sellFees,
  1706  				AvailableDEXBals: map[uint32]*BotBalance{
  1707  					42: {
  1708  						Available: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding - 1,
  1709  					},
  1710  					0: {},
  1711  				},
  1712  				RequiredDEXBals: map[uint32]uint64{
  1713  					42: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
  1714  				},
  1715  				RemainingDEXBals: map[uint32]uint64{
  1716  					42: lotSize + sellFees.Max.Swap - 1,
  1717  					0:  0,
  1718  				},
  1719  				AvailableCEXBal: &BotBalance{
  1720  					Available: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1721  						b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  1722  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1723  					Reserved: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  1724  						b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1725  						b2q(sellPlacements[2].CounterTradeRate, lotSize) +
  1726  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1727  				},
  1728  				RequiredCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1729  					b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  1730  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1731  				RemainingCEXBal: b2q(sellPlacements[3].CounterTradeRate, lotSize),
  1732  				UsedDEXBals: map[uint32]uint64{
  1733  					42: 3*lotSize + 3*sellFees.Max.Swap + sellFees.Funding,
  1734  				},
  1735  				UsedCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  1736  					b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  1737  			},
  1738  
  1739  			// ---- Buy ----
  1740  			buyDexBalances: map[uint32]uint64{
  1741  				42: 0,
  1742  				0: addBuffer(b2q(buyPlacements[1].Rate, lotSize)+
  1743  					b2q(buyPlacements[2].Rate, 2*lotSize)+
  1744  					b2q(buyPlacements[3].Rate, lotSize)+
  1745  					4*buyFees.Max.Swap, 0.1) + buyFees.Funding,
  1746  			},
  1747  			buyCexBalances: map[uint32]uint64{
  1748  				42: 8 * lotSize,
  1749  				0:  0,
  1750  			},
  1751  			buyPlacements:    buyPlacements,
  1752  			buyPendingOrders: pendingOrders(false, 42, 0),
  1753  			expectedBuyPlacements: []*core.QtyRate{
  1754  				{Qty: lotSize, Rate: buyPlacements[1].Rate},
  1755  				{Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
  1756  				{Qty: lotSize, Rate: buyPlacements[3].Rate},
  1757  			},
  1758  			expectedBuyPlacementsWithDecrement: []*core.QtyRate{
  1759  				{Qty: lotSize, Rate: buyPlacements[1].Rate},
  1760  				{Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
  1761  			},
  1762  			expectedCancels:              []order.OrderID{orderIDs[2]},
  1763  			expectedCancelsWithDecrement: []order.OrderID{orderIDs[2]},
  1764  			multiTradeResult: []*core.MultiTradeResult{
  1765  				{Order: &core.Order{ID: orderIDs[4][:]}},
  1766  				{Order: &core.Order{ID: orderIDs[5][:]}},
  1767  				{Order: &core.Order{ID: orderIDs[6][:]}},
  1768  			},
  1769  			multiTradeResultWithDecrement: []*core.MultiTradeResult{
  1770  				{Order: &core.Order{ID: orderIDs[4][:]}},
  1771  				{Order: &core.Order{ID: orderIDs[5][:]}},
  1772  			},
  1773  			expectedOrderIDs: []order.OrderID{
  1774  				orderIDs[4], orderIDs[5], orderIDs[6],
  1775  			},
  1776  			expectedBuyOrderReport: &OrderReport{
  1777  				Placements: []*TradePlacement{
  1778  					{Lots: 1, Rate: bps[0].Rate, CounterTradeRate: bps[0].CounterTradeRate,
  1779  						StandingLots: 1,
  1780  						RequiredDEX:  map[uint32]uint64{},
  1781  						UsedDEX:      map[uint32]uint64{},
  1782  						RequiredCEX:  0,
  1783  						UsedCEX:      0,
  1784  					},
  1785  					{Lots: 2, Rate: bps[1].Rate, CounterTradeRate: bps[1].CounterTradeRate,
  1786  						StandingLots: 1,
  1787  						RequiredDEX: map[uint32]uint64{
  1788  							0: addBuffer(b2q(buyPlacements[1].Rate, lotSize)+buyFees.Max.Swap, 0.1),
  1789  						},
  1790  						UsedDEX: map[uint32]uint64{
  1791  							0: addBuffer(b2q(buyPlacements[1].Rate, lotSize)+buyFees.Max.Swap, 0.1),
  1792  						},
  1793  						RequiredCEX: lotSize,
  1794  						UsedCEX:     lotSize,
  1795  						OrderedLots: 1,
  1796  					},
  1797  					{Lots: 3, Rate: bps[2].Rate, CounterTradeRate: bps[2].CounterTradeRate,
  1798  						StandingLots: 1,
  1799  						RequiredDEX: map[uint32]uint64{
  1800  							0: addBuffer(2*(b2q(buyPlacements[2].Rate, lotSize)+buyFees.Max.Swap), 0.1),
  1801  						},
  1802  						UsedDEX: map[uint32]uint64{
  1803  							0: addBuffer(2*(b2q(buyPlacements[2].Rate, lotSize)+buyFees.Max.Swap), 0.1),
  1804  						},
  1805  						RequiredCEX: 2 * lotSize,
  1806  						UsedCEX:     2 * lotSize,
  1807  						OrderedLots: 2,
  1808  					},
  1809  					{Lots: 2, Rate: bps[3].Rate, CounterTradeRate: bps[3].CounterTradeRate,
  1810  						StandingLots: 1,
  1811  						RequiredDEX: map[uint32]uint64{
  1812  							0: addBuffer(b2q(buyPlacements[3].Rate, lotSize)+buyFees.Max.Swap, 0.1),
  1813  						},
  1814  						UsedDEX: map[uint32]uint64{
  1815  							0: addBuffer(b2q(buyPlacements[3].Rate, lotSize)+buyFees.Max.Swap, 0.1),
  1816  						},
  1817  						RequiredCEX: lotSize,
  1818  						UsedCEX:     lotSize,
  1819  						OrderedLots: 1,
  1820  					},
  1821  				},
  1822  				Fees: buyFees,
  1823  				AvailableDEXBals: map[uint32]*BotBalance{
  1824  					0: {
  1825  						Available: addBuffer(b2q(buyPlacements[1].Rate, lotSize)+
  1826  							b2q(buyPlacements[2].Rate, 2*lotSize)+
  1827  							b2q(buyPlacements[3].Rate, lotSize)+
  1828  							4*buyFees.Max.Swap, 0.1) + buyFees.Funding,
  1829  					},
  1830  					42: {},
  1831  				},
  1832  				RequiredDEXBals: map[uint32]uint64{
  1833  					0: addBuffer(b2q(buyPlacements[1].Rate, lotSize)+
  1834  						b2q(buyPlacements[2].Rate, 2*lotSize)+
  1835  						b2q(buyPlacements[3].Rate, lotSize)+
  1836  						4*buyFees.Max.Swap, 0.1) + buyFees.Funding,
  1837  				},
  1838  				RemainingDEXBals: map[uint32]uint64{
  1839  					42: 0,
  1840  					0:  0,
  1841  				},
  1842  				AvailableCEXBal: &BotBalance{
  1843  					Available: 4 * lotSize,
  1844  					Reserved:  4 * lotSize,
  1845  				},
  1846  				RequiredCEXBal:  4 * lotSize,
  1847  				RemainingCEXBal: 0,
  1848  				UsedDEXBals: map[uint32]uint64{
  1849  					0: addBuffer(b2q(buyPlacements[1].Rate, lotSize)+
  1850  						b2q(buyPlacements[2].Rate, 2*lotSize)+
  1851  						b2q(buyPlacements[3].Rate, lotSize)+
  1852  						4*buyFees.Max.Swap, 0.1) + buyFees.Funding,
  1853  				},
  1854  				UsedCEXBal: 4 * lotSize,
  1855  			},
  1856  			expectedBuyOrderReportWithDEXDecrement: &OrderReport{
  1857  				Placements: []*TradePlacement{
  1858  					{Lots: 1, Rate: bps[0].Rate, CounterTradeRate: bps[0].CounterTradeRate,
  1859  						StandingLots: 1,
  1860  						RequiredDEX:  map[uint32]uint64{},
  1861  						UsedDEX:      map[uint32]uint64{},
  1862  						RequiredCEX:  0,
  1863  						UsedCEX:      0,
  1864  					},
  1865  					{Lots: 2, Rate: bps[1].Rate, CounterTradeRate: bps[1].CounterTradeRate,
  1866  						StandingLots: 1,
  1867  						RequiredDEX: map[uint32]uint64{
  1868  							0: addBuffer(b2q(buyPlacements[1].Rate, lotSize)+buyFees.Max.Swap, 0.1),
  1869  						},
  1870  						UsedDEX: map[uint32]uint64{
  1871  							0: addBuffer(b2q(buyPlacements[1].Rate, lotSize)+buyFees.Max.Swap, 0.1),
  1872  						},
  1873  						RequiredCEX: lotSize,
  1874  						UsedCEX:     lotSize,
  1875  						OrderedLots: 1,
  1876  					},
  1877  					{Lots: 3, Rate: bps[2].Rate, CounterTradeRate: bps[2].CounterTradeRate,
  1878  						StandingLots: 1,
  1879  						RequiredDEX: map[uint32]uint64{
  1880  							0: addBuffer(2*(b2q(buyPlacements[2].Rate, lotSize)+buyFees.Max.Swap), 0.1),
  1881  						},
  1882  						UsedDEX: map[uint32]uint64{
  1883  							0: addBuffer(2*(b2q(buyPlacements[2].Rate, lotSize)+buyFees.Max.Swap), 0.1),
  1884  						},
  1885  						RequiredCEX: 2 * lotSize,
  1886  						UsedCEX:     2 * lotSize,
  1887  						OrderedLots: 2,
  1888  					},
  1889  					{Lots: 2, Rate: bps[3].Rate, CounterTradeRate: bps[3].CounterTradeRate,
  1890  						StandingLots: 1,
  1891  						RequiredDEX: map[uint32]uint64{
  1892  							0: addBuffer(b2q(buyPlacements[3].Rate, lotSize)+buyFees.Max.Swap, 0.1),
  1893  						},
  1894  						UsedDEX:     map[uint32]uint64{},
  1895  						RequiredCEX: lotSize,
  1896  						UsedCEX:     0,
  1897  						OrderedLots: 0,
  1898  					},
  1899  				},
  1900  				Fees: buyFees,
  1901  				AvailableDEXBals: map[uint32]*BotBalance{
  1902  					0: {
  1903  						Available: addBuffer(b2q(buyPlacements[1].Rate, lotSize)+
  1904  							b2q(buyPlacements[2].Rate, 2*lotSize)+
  1905  							b2q(buyPlacements[3].Rate, lotSize)+
  1906  							4*buyFees.Max.Swap, 0.1) + buyFees.Funding - 1,
  1907  					},
  1908  					42: {},
  1909  				},
  1910  				RequiredDEXBals: map[uint32]uint64{
  1911  					0: addBuffer(b2q(buyPlacements[1].Rate, lotSize)+
  1912  						b2q(buyPlacements[2].Rate, 2*lotSize)+
  1913  						b2q(buyPlacements[3].Rate, lotSize)+
  1914  						4*buyFees.Max.Swap, 0.1) + buyFees.Funding,
  1915  				},
  1916  				RemainingDEXBals: map[uint32]uint64{
  1917  					42: 0,
  1918  					0:  addBuffer(b2q(buyPlacements[3].Rate, lotSize)+buyFees.Max.Swap, 0.1) - 1,
  1919  				},
  1920  				AvailableCEXBal: &BotBalance{
  1921  					Available: 4 * lotSize,
  1922  					Reserved:  4 * lotSize,
  1923  				},
  1924  				RequiredCEXBal:  4 * lotSize,
  1925  				RemainingCEXBal: lotSize,
  1926  				UsedDEXBals: map[uint32]uint64{
  1927  					0: addBuffer(b2q(buyPlacements[1].Rate, lotSize)+
  1928  						b2q(buyPlacements[2].Rate, 2*lotSize)+
  1929  						3*buyFees.Max.Swap, 0.1) + buyFees.Funding,
  1930  				},
  1931  				UsedCEXBal: 3 * lotSize,
  1932  			},
  1933  			expectedOrderIDsWithDecrement: []order.OrderID{
  1934  				orderIDs[4], orderIDs[5],
  1935  			},
  1936  		},
  1937  		{
  1938  			name:    "not enough bonding for last placement",
  1939  			baseID:  42,
  1940  			quoteID: 0,
  1941  
  1942  			// ---- Sell ----
  1943  			sellDexBalances: map[uint32]uint64{
  1944  				42: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
  1945  				0:  0,
  1946  			},
  1947  			sellCexBalances: map[uint32]uint64{
  1948  				42: 0,
  1949  				0: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  1950  					b2q(sellPlacements[1].CounterTradeRate, 2*lotSize) +
  1951  					b2q(sellPlacements[2].CounterTradeRate, 3*lotSize) +
  1952  					b2q(sellPlacements[3].CounterTradeRate, 2*lotSize),
  1953  			},
  1954  			sellPlacements:    sellPlacements,
  1955  			sellPendingOrders: pendingOrders(true, 42, 0),
  1956  			expectedSellPlacements: []*core.QtyRate{
  1957  				{Qty: lotSize, Rate: sellPlacements[1].Rate},
  1958  				{Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
  1959  				{Qty: lotSize, Rate: sellPlacements[3].Rate},
  1960  			},
  1961  			expectedSellOrderReport: &OrderReport{
  1962  				Placements: []*TradePlacement{
  1963  					{Lots: 1, Rate: sps[0].Rate, CounterTradeRate: sps[0].CounterTradeRate,
  1964  						StandingLots: 1,
  1965  						RequiredDEX:  map[uint32]uint64{},
  1966  						UsedDEX:      map[uint32]uint64{},
  1967  						RequiredCEX:  0,
  1968  						UsedCEX:      0,
  1969  					},
  1970  					{Lots: 2, Rate: sps[1].Rate, CounterTradeRate: sps[1].CounterTradeRate,
  1971  						StandingLots: 1,
  1972  						RequiredDEX: map[uint32]uint64{
  1973  							42: lotSize + sellFees.Max.Swap,
  1974  						},
  1975  						UsedDEX: map[uint32]uint64{
  1976  							42: lotSize + sellFees.Max.Swap,
  1977  						},
  1978  						RequiredCEX: b2q(sellPlacements[1].CounterTradeRate, lotSize),
  1979  						UsedCEX:     b2q(sellPlacements[1].CounterTradeRate, lotSize),
  1980  						OrderedLots: 1,
  1981  					},
  1982  					{Lots: 3, Rate: sps[2].Rate, CounterTradeRate: sps[2].CounterTradeRate,
  1983  						StandingLots: 1,
  1984  						RequiredDEX: map[uint32]uint64{
  1985  							42: 2 * (lotSize + sellFees.Max.Swap),
  1986  						},
  1987  						UsedDEX: map[uint32]uint64{
  1988  							42: 2 * (lotSize + sellFees.Max.Swap),
  1989  						},
  1990  						RequiredCEX: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  1991  						UsedCEX:     b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  1992  						OrderedLots: 2,
  1993  					},
  1994  					{Lots: 2, Rate: sps[3].Rate, CounterTradeRate: sps[3].CounterTradeRate,
  1995  						StandingLots: 1,
  1996  						RequiredDEX: map[uint32]uint64{
  1997  							42: lotSize + sellFees.Max.Swap,
  1998  						},
  1999  						UsedDEX:     map[uint32]uint64{},
  2000  						RequiredCEX: b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2001  						UsedCEX:     0,
  2002  						OrderedLots: 0,
  2003  						Error: &BotProblems{
  2004  							UserLimitTooLow: true,
  2005  						},
  2006  					},
  2007  				},
  2008  				Fees: sellFees,
  2009  				AvailableDEXBals: map[uint32]*BotBalance{
  2010  					42: {
  2011  						Available: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
  2012  					},
  2013  					0: {},
  2014  				},
  2015  				RequiredDEXBals: map[uint32]uint64{
  2016  					42: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
  2017  				},
  2018  				RemainingDEXBals: map[uint32]uint64{
  2019  					42: 0,
  2020  					0:  0,
  2021  				},
  2022  				AvailableCEXBal: &BotBalance{
  2023  					Available: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2024  						b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2025  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2026  					Reserved: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  2027  						b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2028  						b2q(sellPlacements[2].CounterTradeRate, lotSize) +
  2029  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2030  				},
  2031  				RequiredCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2032  					b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2033  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2034  				RemainingCEXBal: 0,
  2035  				UsedDEXBals: map[uint32]uint64{
  2036  					42: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
  2037  				},
  2038  				UsedCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2039  					b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2040  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2041  			},
  2042  			expectedSellPlacementsWithDecrement: []*core.QtyRate{
  2043  				{Qty: lotSize, Rate: sellPlacements[1].Rate},
  2044  				{Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
  2045  			},
  2046  
  2047  			// ---- Buy ----
  2048  			buyDexBalances: map[uint32]uint64{
  2049  				42: 0,
  2050  				0: b2q(buyPlacements[1].Rate, lotSize) +
  2051  					b2q(buyPlacements[2].Rate, 2*lotSize) +
  2052  					b2q(buyPlacements[3].Rate, lotSize) +
  2053  					4*buyFees.Max.Swap + buyFees.Funding,
  2054  			},
  2055  			buyCexBalances: map[uint32]uint64{
  2056  				42: 8 * lotSize,
  2057  				0:  0,
  2058  			},
  2059  			buyPlacements:    buyPlacements,
  2060  			buyPendingOrders: pendingOrders(false, 42, 0),
  2061  			expectedBuyPlacements: []*core.QtyRate{
  2062  				{Qty: lotSize, Rate: buyPlacements[1].Rate},
  2063  				{Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
  2064  				{Qty: lotSize, Rate: buyPlacements[3].Rate},
  2065  			},
  2066  			expectedBuyPlacementsWithDecrement: []*core.QtyRate{
  2067  				{Qty: lotSize, Rate: buyPlacements[1].Rate},
  2068  				{Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
  2069  			},
  2070  			expectedCancels:              []order.OrderID{orderIDs[2]},
  2071  			expectedCancelsWithDecrement: []order.OrderID{orderIDs[2]},
  2072  			multiTradeResult: []*core.MultiTradeResult{
  2073  				{Order: &core.Order{ID: orderIDs[4][:]}},
  2074  				{Order: &core.Order{ID: orderIDs[5][:]}},
  2075  				{Error: &msgjson.Error{Code: msgjson.OrderQuantityTooHigh}},
  2076  			},
  2077  			multiTradeResultWithDecrement: []*core.MultiTradeResult{
  2078  				{Order: &core.Order{ID: orderIDs[4][:]}},
  2079  				{Order: &core.Order{ID: orderIDs[5][:]}},
  2080  			},
  2081  			expectedOrderIDs: []order.OrderID{
  2082  				orderIDs[4], orderIDs[5],
  2083  			},
  2084  			expectedOrderIDsWithDecrement: []order.OrderID{
  2085  				orderIDs[4], orderIDs[5],
  2086  			},
  2087  		},
  2088  		{
  2089  			name:    "non account locker, reconfig to less placements",
  2090  			baseID:  42,
  2091  			quoteID: 0,
  2092  
  2093  			// ---- Sell ----
  2094  			sellDexBalances: map[uint32]uint64{
  2095  				42: 3*lotSize + 3*sellFees.Max.Swap + sellFees.Funding,
  2096  				0:  0,
  2097  			},
  2098  			sellCexBalances: map[uint32]uint64{
  2099  				42: 0,
  2100  				0: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  2101  					b2q(sellPlacements[1].CounterTradeRate, 2*lotSize) +
  2102  					b2q(sellPlacements[2].CounterTradeRate, 3*lotSize) +
  2103  					b2q(sellPlacements[3].CounterTradeRate, 2*lotSize),
  2104  			},
  2105  			sellPlacements:    reconfigToLessPlacements(true),
  2106  			sellPendingOrders: secondPendingOrderNotFilled(true, 42, 0),
  2107  			expectedSellPlacements: []*core.QtyRate{
  2108  				// {Qty: lotSize, Rate: sellPlacements[1].Rate},
  2109  				{Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
  2110  				{Qty: lotSize, Rate: sellPlacements[3].Rate},
  2111  			},
  2112  			expectedSellOrderReport: &OrderReport{
  2113  				Placements: []*TradePlacement{
  2114  					{Lots: 1, Rate: sps[0].Rate, CounterTradeRate: sps[0].CounterTradeRate,
  2115  						StandingLots: 1,
  2116  						RequiredDEX:  map[uint32]uint64{},
  2117  						UsedDEX:      map[uint32]uint64{},
  2118  						RequiredCEX:  0,
  2119  						UsedCEX:      0,
  2120  					},
  2121  					{Lots: 1, Rate: sps[1].Rate, CounterTradeRate: sps[1].CounterTradeRate,
  2122  						StandingLots: 2,
  2123  						RequiredDEX:  map[uint32]uint64{},
  2124  						UsedDEX:      map[uint32]uint64{},
  2125  						RequiredCEX:  0,
  2126  						UsedCEX:      0,
  2127  						OrderedLots:  0,
  2128  					},
  2129  					{Lots: 3, Rate: sps[2].Rate, CounterTradeRate: sps[2].CounterTradeRate,
  2130  						StandingLots: 1,
  2131  						RequiredDEX: map[uint32]uint64{
  2132  							42: 2 * (lotSize + sellFees.Max.Swap),
  2133  						},
  2134  						UsedDEX: map[uint32]uint64{
  2135  							42: 2 * (lotSize + sellFees.Max.Swap),
  2136  						},
  2137  						RequiredCEX: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  2138  						UsedCEX:     b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  2139  						OrderedLots: 2,
  2140  					},
  2141  					{Lots: 2, Rate: sps[3].Rate, CounterTradeRate: sps[3].CounterTradeRate,
  2142  						StandingLots: 1,
  2143  						RequiredDEX: map[uint32]uint64{
  2144  							42: lotSize + sellFees.Max.Swap,
  2145  						},
  2146  						UsedDEX: map[uint32]uint64{
  2147  							42: lotSize + sellFees.Max.Swap,
  2148  						},
  2149  						RequiredCEX: b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2150  						UsedCEX:     b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2151  						OrderedLots: 1,
  2152  					},
  2153  				},
  2154  				Fees: sellFees,
  2155  				AvailableDEXBals: map[uint32]*BotBalance{
  2156  					42: {
  2157  						Available: 3*lotSize + 3*sellFees.Max.Swap + sellFees.Funding,
  2158  					},
  2159  					0: {},
  2160  				},
  2161  				RequiredDEXBals: map[uint32]uint64{
  2162  					42: 3*lotSize + 3*sellFees.Max.Swap + sellFees.Funding,
  2163  				},
  2164  				RemainingDEXBals: map[uint32]uint64{
  2165  					42: 0,
  2166  					0:  0,
  2167  				},
  2168  				AvailableCEXBal: &BotBalance{
  2169  					Available: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2170  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2171  					Reserved: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  2172  						2*b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2173  						b2q(sellPlacements[2].CounterTradeRate, lotSize) +
  2174  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2175  				},
  2176  				RequiredCEXBal: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2177  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2178  				RemainingCEXBal: 0,
  2179  				UsedDEXBals: map[uint32]uint64{
  2180  					42: 3*lotSize + 3*sellFees.Max.Swap + sellFees.Funding,
  2181  				},
  2182  				UsedCEXBal: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2183  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2184  			},
  2185  			expectedSellPlacementsWithDecrement: []*core.QtyRate{
  2186  				// {Qty: lotSize, Rate: sellPlacements[1].Rate},
  2187  				{Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
  2188  			},
  2189  			expectedSellOrderReportWithDEXDecrement: &OrderReport{
  2190  				Placements: []*TradePlacement{
  2191  					{Lots: 1, Rate: sps[0].Rate, CounterTradeRate: sps[0].CounterTradeRate,
  2192  						StandingLots: 1,
  2193  						RequiredDEX:  map[uint32]uint64{},
  2194  						UsedDEX:      map[uint32]uint64{},
  2195  						RequiredCEX:  0,
  2196  						UsedCEX:      0,
  2197  					},
  2198  					{Lots: 1, Rate: sps[1].Rate, CounterTradeRate: sps[1].CounterTradeRate,
  2199  						StandingLots: 2,
  2200  						RequiredDEX:  map[uint32]uint64{},
  2201  						UsedDEX:      map[uint32]uint64{},
  2202  						RequiredCEX:  0,
  2203  						UsedCEX:      0,
  2204  						OrderedLots:  0,
  2205  					},
  2206  					{Lots: 3, Rate: sps[2].Rate, CounterTradeRate: sps[2].CounterTradeRate,
  2207  						StandingLots: 1,
  2208  						RequiredDEX: map[uint32]uint64{
  2209  							42: 2 * (lotSize + sellFees.Max.Swap),
  2210  						},
  2211  						UsedDEX: map[uint32]uint64{
  2212  							42: 2 * (lotSize + sellFees.Max.Swap),
  2213  						},
  2214  						RequiredCEX: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  2215  						UsedCEX:     b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  2216  						OrderedLots: 2,
  2217  					},
  2218  					{Lots: 2, Rate: sps[3].Rate, CounterTradeRate: sps[3].CounterTradeRate,
  2219  						StandingLots: 1,
  2220  						RequiredDEX: map[uint32]uint64{
  2221  							42: lotSize + sellFees.Max.Swap,
  2222  						},
  2223  						UsedDEX:     map[uint32]uint64{},
  2224  						RequiredCEX: b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2225  						UsedCEX:     0,
  2226  						OrderedLots: 0,
  2227  					},
  2228  				},
  2229  				Fees: sellFees,
  2230  				AvailableDEXBals: map[uint32]*BotBalance{
  2231  					42: {
  2232  						Available: 3*lotSize + 3*sellFees.Max.Swap + sellFees.Funding - 1,
  2233  					},
  2234  					0: {},
  2235  				},
  2236  				RequiredDEXBals: map[uint32]uint64{
  2237  					42: 3*lotSize + 3*sellFees.Max.Swap + sellFees.Funding,
  2238  				},
  2239  				RemainingDEXBals: map[uint32]uint64{
  2240  					42: lotSize + sellFees.Max.Swap - 1,
  2241  					0:  0,
  2242  				},
  2243  				AvailableCEXBal: &BotBalance{
  2244  					Available: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2245  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2246  					Reserved: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  2247  						2*b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2248  						b2q(sellPlacements[2].CounterTradeRate, lotSize) +
  2249  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2250  				},
  2251  				RequiredCEXBal: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2252  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2253  				RemainingCEXBal: b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2254  				UsedDEXBals: map[uint32]uint64{
  2255  					42: 2*lotSize + 2*sellFees.Max.Swap + sellFees.Funding,
  2256  				},
  2257  				UsedCEXBal: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  2258  			},
  2259  
  2260  			// ---- Buy ----
  2261  			buyDexBalances: map[uint32]uint64{
  2262  				42: 0,
  2263  				0: b2q(buyPlacements[2].Rate, 2*lotSize) +
  2264  					b2q(buyPlacements[3].Rate, lotSize) +
  2265  					3*buyFees.Max.Swap + buyFees.Funding,
  2266  			},
  2267  			buyCexBalances: map[uint32]uint64{
  2268  				42: 8 * lotSize,
  2269  				0:  0,
  2270  			},
  2271  			buyPlacements:    reconfigToLessPlacements(false),
  2272  			buyPendingOrders: secondPendingOrderNotFilled(false, 42, 0),
  2273  			expectedBuyPlacements: []*core.QtyRate{
  2274  				// {Qty: lotSize, Rate: buyPlacements[1].Rate},
  2275  				{Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
  2276  				{Qty: lotSize, Rate: buyPlacements[3].Rate},
  2277  			},
  2278  			expectedBuyOrderReport: &OrderReport{
  2279  				Placements: []*TradePlacement{
  2280  					{Lots: 1, Rate: bps[0].Rate, CounterTradeRate: bps[0].CounterTradeRate,
  2281  						StandingLots: 1,
  2282  						RequiredDEX:  map[uint32]uint64{},
  2283  						UsedDEX:      map[uint32]uint64{},
  2284  						RequiredCEX:  0,
  2285  						UsedCEX:      0,
  2286  					},
  2287  					{Lots: 1, Rate: bps[1].Rate, CounterTradeRate: bps[1].CounterTradeRate,
  2288  						StandingLots: 2,
  2289  						RequiredDEX:  map[uint32]uint64{},
  2290  						UsedDEX:      map[uint32]uint64{},
  2291  						RequiredCEX:  0,
  2292  						UsedCEX:      0,
  2293  						OrderedLots:  0,
  2294  					},
  2295  					{Lots: 3, Rate: bps[2].Rate, CounterTradeRate: bps[2].CounterTradeRate,
  2296  						StandingLots: 1,
  2297  						RequiredDEX: map[uint32]uint64{
  2298  							0: 2 * (b2q(buyPlacements[2].Rate, lotSize) + buyFees.Max.Swap),
  2299  						},
  2300  						UsedDEX: map[uint32]uint64{
  2301  							0: 2 * (b2q(buyPlacements[2].Rate, lotSize) + buyFees.Max.Swap),
  2302  						},
  2303  						RequiredCEX: 2 * lotSize,
  2304  						UsedCEX:     2 * lotSize,
  2305  						OrderedLots: 2,
  2306  					},
  2307  					{Lots: 2, Rate: bps[3].Rate, CounterTradeRate: bps[3].CounterTradeRate,
  2308  						StandingLots: 1,
  2309  						RequiredDEX: map[uint32]uint64{
  2310  							0: b2q(buyPlacements[3].Rate, lotSize) + buyFees.Max.Swap,
  2311  						},
  2312  						UsedDEX: map[uint32]uint64{
  2313  							0: b2q(buyPlacements[3].Rate, lotSize) + buyFees.Max.Swap,
  2314  						},
  2315  						RequiredCEX: lotSize,
  2316  						UsedCEX:     lotSize,
  2317  						OrderedLots: 1,
  2318  					},
  2319  				},
  2320  				Fees: buyFees,
  2321  				AvailableDEXBals: map[uint32]*BotBalance{
  2322  					0: {
  2323  						Available: b2q(buyPlacements[2].Rate, 2*lotSize) +
  2324  							b2q(buyPlacements[3].Rate, lotSize) +
  2325  							3*buyFees.Max.Swap + buyFees.Funding,
  2326  					},
  2327  					42: {},
  2328  				},
  2329  				RequiredDEXBals: map[uint32]uint64{
  2330  					0: b2q(buyPlacements[2].Rate, 2*lotSize) +
  2331  						b2q(buyPlacements[3].Rate, lotSize) +
  2332  						3*buyFees.Max.Swap + buyFees.Funding,
  2333  				},
  2334  				RemainingDEXBals: map[uint32]uint64{
  2335  					42: 0,
  2336  					0:  0,
  2337  				},
  2338  				AvailableCEXBal: &BotBalance{
  2339  					Available: 3 * lotSize,
  2340  					Reserved:  5 * lotSize,
  2341  				},
  2342  				RequiredCEXBal:  3 * lotSize,
  2343  				RemainingCEXBal: 0,
  2344  				UsedDEXBals: map[uint32]uint64{
  2345  					0: b2q(buyPlacements[2].Rate, 2*lotSize) +
  2346  						b2q(buyPlacements[3].Rate, lotSize) +
  2347  						3*buyFees.Max.Swap + buyFees.Funding,
  2348  				},
  2349  				UsedCEXBal: 3 * lotSize,
  2350  			},
  2351  			expectedBuyOrderReportWithDEXDecrement: &OrderReport{
  2352  				Placements: []*TradePlacement{
  2353  					{Lots: 1, Rate: bps[0].Rate, CounterTradeRate: bps[0].CounterTradeRate,
  2354  						StandingLots: 1,
  2355  						RequiredDEX:  map[uint32]uint64{},
  2356  						UsedDEX:      map[uint32]uint64{},
  2357  						RequiredCEX:  0,
  2358  						UsedCEX:      0,
  2359  					},
  2360  					{Lots: 1, Rate: bps[1].Rate, CounterTradeRate: bps[1].CounterTradeRate,
  2361  						StandingLots: 2,
  2362  						RequiredDEX:  map[uint32]uint64{},
  2363  						UsedDEX:      map[uint32]uint64{},
  2364  						RequiredCEX:  0,
  2365  						UsedCEX:      0,
  2366  						OrderedLots:  0,
  2367  					},
  2368  					{Lots: 3, Rate: bps[2].Rate, CounterTradeRate: bps[2].CounterTradeRate,
  2369  						StandingLots: 1,
  2370  						RequiredDEX: map[uint32]uint64{
  2371  							0: 2 * (b2q(buyPlacements[2].Rate, lotSize) + buyFees.Max.Swap),
  2372  						},
  2373  						UsedDEX: map[uint32]uint64{
  2374  							0: 2 * (b2q(buyPlacements[2].Rate, lotSize) + buyFees.Max.Swap),
  2375  						},
  2376  						RequiredCEX: 2 * lotSize,
  2377  						UsedCEX:     2 * lotSize,
  2378  						OrderedLots: 2,
  2379  					},
  2380  					{Lots: 2, Rate: bps[3].Rate, CounterTradeRate: bps[3].CounterTradeRate,
  2381  						StandingLots: 1,
  2382  						RequiredDEX: map[uint32]uint64{
  2383  							0: b2q(buyPlacements[3].Rate, lotSize) + buyFees.Max.Swap,
  2384  						},
  2385  						UsedDEX:     map[uint32]uint64{},
  2386  						RequiredCEX: lotSize,
  2387  						UsedCEX:     0,
  2388  						OrderedLots: 0,
  2389  					},
  2390  				},
  2391  				Fees: buyFees,
  2392  				AvailableDEXBals: map[uint32]*BotBalance{
  2393  					0: {
  2394  						Available: b2q(buyPlacements[2].Rate, 2*lotSize) +
  2395  							b2q(buyPlacements[3].Rate, lotSize) +
  2396  							3*buyFees.Max.Swap + buyFees.Funding - 1,
  2397  					},
  2398  					42: {},
  2399  				},
  2400  				RequiredDEXBals: map[uint32]uint64{
  2401  					0: b2q(buyPlacements[2].Rate, 2*lotSize) +
  2402  						b2q(buyPlacements[3].Rate, lotSize) +
  2403  						3*buyFees.Max.Swap + buyFees.Funding,
  2404  				},
  2405  				RemainingDEXBals: map[uint32]uint64{
  2406  					42: 0,
  2407  					0:  b2q(buyPlacements[3].Rate, lotSize) + buyFees.Max.Swap - 1,
  2408  				},
  2409  				AvailableCEXBal: &BotBalance{
  2410  					Available: 3 * lotSize,
  2411  					Reserved:  5 * lotSize,
  2412  				},
  2413  				RequiredCEXBal:  3 * lotSize,
  2414  				RemainingCEXBal: lotSize,
  2415  				UsedDEXBals: map[uint32]uint64{
  2416  					0: b2q(buyPlacements[2].Rate, 2*lotSize) +
  2417  						2*buyFees.Max.Swap + buyFees.Funding,
  2418  				},
  2419  				UsedCEXBal: 2 * lotSize,
  2420  			},
  2421  			expectedBuyPlacementsWithDecrement: []*core.QtyRate{
  2422  				// {Qty: lotSize, Rate: buyPlacements[1].Rate},
  2423  				{Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
  2424  			},
  2425  
  2426  			expectedCancels:              []order.OrderID{orderIDs[1], orderIDs[2]},
  2427  			expectedCancelsWithDecrement: []order.OrderID{orderIDs[1], orderIDs[2]},
  2428  			multiTradeResult: []*core.MultiTradeResult{
  2429  				{Order: &core.Order{ID: orderIDs[4][:]}},
  2430  				{Order: &core.Order{ID: orderIDs[5][:]}},
  2431  				// {ID: orderIDs[6][:]},
  2432  			},
  2433  			multiTradeResultWithDecrement: []*core.MultiTradeResult{
  2434  				{Order: &core.Order{ID: orderIDs[4][:]}},
  2435  				// {Order: &core.Order{ID: orderIDs[5][:]}},
  2436  			},
  2437  			expectedOrderIDs: []order.OrderID{
  2438  				orderIDs[4], orderIDs[5],
  2439  			},
  2440  			expectedOrderIDsWithDecrement: []order.OrderID{
  2441  				orderIDs[4],
  2442  			},
  2443  		},
  2444  		{
  2445  			name:    "non account locker, self-match",
  2446  			baseID:  42,
  2447  			quoteID: 0,
  2448  
  2449  			// ---- Sell ----
  2450  			sellDexBalances: map[uint32]uint64{
  2451  				42: 3*lotSize + 3*sellFees.Max.Swap + sellFees.Funding,
  2452  				0:  0,
  2453  			},
  2454  			sellCexBalances: map[uint32]uint64{
  2455  				42: 0,
  2456  				0: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  2457  					b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2458  					b2q(sellPlacements[2].CounterTradeRate, 3*lotSize) +
  2459  					b2q(sellPlacements[3].CounterTradeRate, 2*lotSize),
  2460  			},
  2461  			sellPlacements:    sellPlacements,
  2462  			sellPendingOrders: pendingOrdersSelfMatch(true, 42, 0),
  2463  			expectedSellPlacements: []*core.QtyRate{
  2464  				{Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
  2465  				{Qty: lotSize, Rate: sellPlacements[3].Rate},
  2466  			},
  2467  			expectedSellOrderReport: &OrderReport{
  2468  				Placements: []*TradePlacement{
  2469  					{Lots: 1, Rate: sps[0].Rate, CounterTradeRate: sps[0].CounterTradeRate,
  2470  						StandingLots: 1,
  2471  						RequiredDEX:  map[uint32]uint64{},
  2472  						UsedDEX:      map[uint32]uint64{},
  2473  						RequiredCEX:  0,
  2474  						UsedCEX:      0,
  2475  					},
  2476  					{Lots: 2, Rate: sps[1].Rate, CounterTradeRate: sps[1].CounterTradeRate,
  2477  						StandingLots: 1,
  2478  						RequiredDEX: map[uint32]uint64{
  2479  							42: lotSize + sellFees.Max.Swap,
  2480  						},
  2481  						UsedDEX:     map[uint32]uint64{},
  2482  						RequiredCEX: b2q(sellPlacements[1].CounterTradeRate, lotSize),
  2483  						UsedCEX:     0,
  2484  						OrderedLots: 0,
  2485  						Error:       &BotProblems{CausesSelfMatch: true},
  2486  					},
  2487  					{Lots: 3, Rate: sps[2].Rate, CounterTradeRate: sps[2].CounterTradeRate,
  2488  						StandingLots: 1,
  2489  						RequiredDEX: map[uint32]uint64{
  2490  							42: 2 * (lotSize + sellFees.Max.Swap),
  2491  						},
  2492  						UsedDEX: map[uint32]uint64{
  2493  							42: 2 * (lotSize + sellFees.Max.Swap),
  2494  						},
  2495  						RequiredCEX: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  2496  						UsedCEX:     b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  2497  						OrderedLots: 2,
  2498  					},
  2499  					{Lots: 2, Rate: sps[3].Rate, CounterTradeRate: sps[3].CounterTradeRate,
  2500  						StandingLots: 1,
  2501  						RequiredDEX: map[uint32]uint64{
  2502  							42: lotSize + sellFees.Max.Swap,
  2503  						},
  2504  						UsedDEX: map[uint32]uint64{
  2505  							42: lotSize + sellFees.Max.Swap,
  2506  						},
  2507  						RequiredCEX: b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2508  						UsedCEX:     b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2509  						OrderedLots: 1,
  2510  					},
  2511  				},
  2512  				Fees: sellFees,
  2513  				AvailableDEXBals: map[uint32]*BotBalance{
  2514  					42: {
  2515  						Available: 3*lotSize + 3*sellFees.Max.Swap + sellFees.Funding,
  2516  					},
  2517  					0: {},
  2518  				},
  2519  				RequiredDEXBals: map[uint32]uint64{
  2520  					42: 4*lotSize + 4*sellFees.Max.Swap + sellFees.Funding,
  2521  				},
  2522  				RemainingDEXBals: map[uint32]uint64{
  2523  					42: 0,
  2524  					0:  0,
  2525  				},
  2526  				AvailableCEXBal: &BotBalance{
  2527  					Available: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2528  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2529  					Reserved: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  2530  						b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2531  						b2q(sellPlacements[2].CounterTradeRate, lotSize) +
  2532  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2533  				},
  2534  				RequiredCEXBal: b2q(sellPlacements[1].CounterTradeRate, 1*lotSize) +
  2535  					b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2536  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2537  				RemainingCEXBal: 0,
  2538  				UsedDEXBals: map[uint32]uint64{
  2539  					42: 3*lotSize + 3*sellFees.Max.Swap + sellFees.Funding,
  2540  				},
  2541  				UsedCEXBal: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2542  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2543  			},
  2544  			expectedSellPlacementsWithDecrement: []*core.QtyRate{
  2545  				{Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
  2546  			},
  2547  
  2548  			// ---- Buy ----
  2549  			buyDexBalances: map[uint32]uint64{
  2550  				42: 0,
  2551  				0: b2q(buyPlacements[2].Rate, 2*lotSize) +
  2552  					b2q(buyPlacements[3].Rate, lotSize) +
  2553  					3*buyFees.Max.Swap + buyFees.Funding,
  2554  			},
  2555  			buyCexBalances: map[uint32]uint64{
  2556  				42: 7 * lotSize,
  2557  				0:  0,
  2558  			},
  2559  			buyPlacements:    buyPlacements,
  2560  			buyPendingOrders: pendingOrdersSelfMatch(false, 42, 0),
  2561  			expectedBuyPlacements: []*core.QtyRate{
  2562  				{Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
  2563  				{Qty: lotSize, Rate: buyPlacements[3].Rate},
  2564  			},
  2565  			expectedBuyPlacementsWithDecrement: []*core.QtyRate{
  2566  				{Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
  2567  			},
  2568  
  2569  			expectedCancels:              []order.OrderID{orderIDs[2]},
  2570  			expectedCancelsWithDecrement: []order.OrderID{orderIDs[2]},
  2571  			multiTradeResult: []*core.MultiTradeResult{
  2572  				{Order: &core.Order{ID: orderIDs[5][:]}},
  2573  				{Order: &core.Order{ID: orderIDs[6][:]}},
  2574  			},
  2575  			multiTradeResultWithDecrement: []*core.MultiTradeResult{
  2576  				{Order: &core.Order{ID: orderIDs[5][:]}},
  2577  			},
  2578  			expectedOrderIDs: []order.OrderID{
  2579  				orderIDs[5], orderIDs[6],
  2580  			},
  2581  			expectedOrderIDsWithDecrement: []order.OrderID{
  2582  				orderIDs[5],
  2583  			},
  2584  		},
  2585  		{
  2586  			name:    "non account locker, cancel last placement",
  2587  			baseID:  42,
  2588  			quoteID: 0,
  2589  			// ---- Sell ----
  2590  			sellDexBalances: map[uint32]uint64{
  2591  				42: 3*lotSize + 3*sellFees.Max.Swap + sellFees.Funding,
  2592  				0:  0,
  2593  			},
  2594  			sellCexBalances: map[uint32]uint64{
  2595  				42: 0,
  2596  				0: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  2597  					b2q(sellPlacements[1].CounterTradeRate, 2*lotSize) +
  2598  					b2q(sellPlacements[2].CounterTradeRate, 3*lotSize) +
  2599  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2600  			},
  2601  			sellPlacements:    cancelLastPlacement(true),
  2602  			sellPendingOrders: pendingOrders(true, 42, 0),
  2603  			expectedSellPlacements: []*core.QtyRate{
  2604  				{Qty: lotSize, Rate: sellPlacements[1].Rate},
  2605  				{Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
  2606  			},
  2607  			expectedSellPlacementsWithDecrement: []*core.QtyRate{
  2608  				{Qty: lotSize, Rate: sellPlacements[1].Rate},
  2609  				{Qty: lotSize, Rate: sellPlacements[2].Rate},
  2610  			},
  2611  
  2612  			// ---- Buy ----
  2613  			buyDexBalances: map[uint32]uint64{
  2614  				42: 0,
  2615  				0: b2q(buyPlacements[1].Rate, lotSize) +
  2616  					b2q(buyPlacements[2].Rate, 2*lotSize) +
  2617  					3*buyFees.Max.Swap + buyFees.Funding,
  2618  			},
  2619  			buyCexBalances: map[uint32]uint64{
  2620  				42: 7 * lotSize,
  2621  				0:  0,
  2622  			},
  2623  			buyPlacements:    cancelLastPlacement(false),
  2624  			buyPendingOrders: pendingOrders(false, 42, 0),
  2625  			expectedBuyPlacements: []*core.QtyRate{
  2626  				{Qty: lotSize, Rate: buyPlacements[1].Rate},
  2627  				{Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
  2628  			},
  2629  			expectedBuyPlacementsWithDecrement: []*core.QtyRate{
  2630  				{Qty: lotSize, Rate: buyPlacements[1].Rate},
  2631  				{Qty: lotSize, Rate: buyPlacements[2].Rate},
  2632  			},
  2633  
  2634  			expectedCancels:              []order.OrderID{orderIDs[3], orderIDs[2]},
  2635  			expectedCancelsWithDecrement: []order.OrderID{orderIDs[3], orderIDs[2]},
  2636  			multiTradeResult: []*core.MultiTradeResult{
  2637  				{Order: &core.Order{ID: orderIDs[4][:]}},
  2638  				{Order: &core.Order{ID: orderIDs[5][:]}},
  2639  			},
  2640  			multiTradeResultWithDecrement: []*core.MultiTradeResult{
  2641  				{Order: &core.Order{ID: orderIDs[4][:]}},
  2642  				{Order: &core.Order{ID: orderIDs[5][:]}},
  2643  			},
  2644  			expectedOrderIDs: []order.OrderID{
  2645  				orderIDs[4], orderIDs[5],
  2646  			},
  2647  			expectedOrderIDsWithDecrement: []order.OrderID{
  2648  				orderIDs[4], orderIDs[5],
  2649  			},
  2650  		},
  2651  		{
  2652  			name:    "non account locker, remove last placement",
  2653  			baseID:  42,
  2654  			quoteID: 0,
  2655  			// ---- Sell ----
  2656  			sellDexBalances: map[uint32]uint64{
  2657  				42: 3*lotSize + 3*sellFees.Max.Swap + sellFees.Funding,
  2658  				0:  0,
  2659  			},
  2660  			sellCexBalances: map[uint32]uint64{
  2661  				42: 0,
  2662  				0: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  2663  					b2q(sellPlacements[1].CounterTradeRate, 2*lotSize) +
  2664  					b2q(sellPlacements[2].CounterTradeRate, 3*lotSize) +
  2665  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2666  			},
  2667  			sellPlacements:    removeLastPlacement(true),
  2668  			sellPendingOrders: pendingOrders(true, 42, 0),
  2669  			expectedSellPlacements: []*core.QtyRate{
  2670  				{Qty: lotSize, Rate: sellPlacements[1].Rate},
  2671  				{Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
  2672  			},
  2673  			expectedSellPlacementsWithDecrement: []*core.QtyRate{
  2674  				{Qty: lotSize, Rate: sellPlacements[1].Rate},
  2675  				{Qty: lotSize, Rate: sellPlacements[2].Rate},
  2676  			},
  2677  
  2678  			// ---- Buy ----
  2679  			buyDexBalances: map[uint32]uint64{
  2680  				42: 0,
  2681  				0: b2q(buyPlacements[1].Rate, lotSize) +
  2682  					b2q(buyPlacements[2].Rate, 2*lotSize) +
  2683  					3*buyFees.Max.Swap + buyFees.Funding,
  2684  			},
  2685  			buyCexBalances: map[uint32]uint64{
  2686  				42: 7 * lotSize,
  2687  				0:  0,
  2688  			},
  2689  			buyPlacements:    removeLastPlacement(false),
  2690  			buyPendingOrders: pendingOrders(false, 42, 0),
  2691  			expectedBuyPlacements: []*core.QtyRate{
  2692  				{Qty: lotSize, Rate: buyPlacements[1].Rate},
  2693  				{Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
  2694  			},
  2695  			expectedBuyPlacementsWithDecrement: []*core.QtyRate{
  2696  				{Qty: lotSize, Rate: buyPlacements[1].Rate},
  2697  				{Qty: lotSize, Rate: buyPlacements[2].Rate},
  2698  			},
  2699  
  2700  			expectedCancels:              []order.OrderID{orderIDs[3], orderIDs[2]},
  2701  			expectedCancelsWithDecrement: []order.OrderID{orderIDs[3], orderIDs[2]},
  2702  			multiTradeResult: []*core.MultiTradeResult{
  2703  				{Order: &core.Order{ID: orderIDs[4][:]}},
  2704  				{Order: &core.Order{ID: orderIDs[5][:]}},
  2705  			},
  2706  			multiTradeResultWithDecrement: []*core.MultiTradeResult{
  2707  				{Order: &core.Order{ID: orderIDs[4][:]}},
  2708  				{Order: &core.Order{ID: orderIDs[5][:]}},
  2709  			},
  2710  			expectedOrderIDs: []order.OrderID{
  2711  				orderIDs[4], orderIDs[5],
  2712  			},
  2713  			expectedOrderIDsWithDecrement: []order.OrderID{
  2714  				orderIDs[4], orderIDs[5],
  2715  			},
  2716  		},
  2717  		{
  2718  			name:    "account locker token",
  2719  			baseID:  966001,
  2720  			quoteID: 60,
  2721  			isAccountLocker: map[uint32]bool{
  2722  				966001: true,
  2723  				60:     true,
  2724  			},
  2725  
  2726  			// ---- Sell ----
  2727  			sellDexBalances: map[uint32]uint64{
  2728  				966001: 4 * lotSize,
  2729  				966:    4*(sellFees.Max.Swap+sellFees.Max.Refund) + sellFees.Funding,
  2730  				60:     4 * sellFees.Max.Redeem,
  2731  			},
  2732  			sellCexBalances: map[uint32]uint64{
  2733  				96601: 0,
  2734  				60: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  2735  					b2q(sellPlacements[1].CounterTradeRate, 2*lotSize) +
  2736  					b2q(sellPlacements[2].CounterTradeRate, 3*lotSize) +
  2737  					b2q(sellPlacements[3].CounterTradeRate, 2*lotSize),
  2738  			},
  2739  			sellPlacements:    sellPlacements,
  2740  			sellPendingOrders: pendingOrders(true, 966001, 60),
  2741  			expectedSellPlacements: []*core.QtyRate{
  2742  				{Qty: lotSize, Rate: sellPlacements[1].Rate},
  2743  				{Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
  2744  				{Qty: lotSize, Rate: sellPlacements[3].Rate},
  2745  			},
  2746  			expectedSellOrderReport: &OrderReport{
  2747  				Placements: []*TradePlacement{
  2748  					{Lots: 1, Rate: sps[0].Rate, CounterTradeRate: sps[0].CounterTradeRate,
  2749  						StandingLots: 1,
  2750  						RequiredDEX:  map[uint32]uint64{},
  2751  						UsedDEX:      map[uint32]uint64{},
  2752  						RequiredCEX:  0,
  2753  						UsedCEX:      0,
  2754  					},
  2755  					{Lots: 2, Rate: sps[1].Rate, CounterTradeRate: sps[1].CounterTradeRate,
  2756  						StandingLots: 1,
  2757  						RequiredDEX: map[uint32]uint64{
  2758  							966001: lotSize,
  2759  							966:    sellFees.Max.Swap + sellFees.Max.Refund,
  2760  							60:     sellFees.Max.Redeem,
  2761  						},
  2762  						UsedDEX: map[uint32]uint64{
  2763  							966001: lotSize,
  2764  							966:    sellFees.Max.Swap + sellFees.Max.Refund,
  2765  							60:     sellFees.Max.Redeem,
  2766  						},
  2767  						RequiredCEX: b2q(sellPlacements[1].CounterTradeRate, lotSize),
  2768  						UsedCEX:     b2q(sellPlacements[1].CounterTradeRate, lotSize),
  2769  						OrderedLots: 1,
  2770  					},
  2771  					{Lots: 3, Rate: sps[2].Rate, CounterTradeRate: sps[2].CounterTradeRate,
  2772  						StandingLots: 1,
  2773  						RequiredDEX: map[uint32]uint64{
  2774  							966001: 2 * lotSize,
  2775  							966:    2 * (sellFees.Max.Swap + sellFees.Max.Refund),
  2776  							60:     2 * sellFees.Max.Redeem,
  2777  						},
  2778  						UsedDEX: map[uint32]uint64{
  2779  							966001: 2 * lotSize,
  2780  							966:    2 * (sellFees.Max.Swap + sellFees.Max.Refund),
  2781  							60:     2 * sellFees.Max.Redeem,
  2782  						},
  2783  						RequiredCEX: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  2784  						UsedCEX:     b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  2785  						OrderedLots: 2,
  2786  					},
  2787  					{Lots: 2, Rate: sps[3].Rate, CounterTradeRate: sps[3].CounterTradeRate,
  2788  						StandingLots: 1,
  2789  						RequiredDEX: map[uint32]uint64{
  2790  							966001: lotSize,
  2791  							966:    sellFees.Max.Swap + sellFees.Max.Refund,
  2792  							60:     sellFees.Max.Redeem,
  2793  						},
  2794  						UsedDEX: map[uint32]uint64{
  2795  							966001: lotSize,
  2796  							966:    sellFees.Max.Swap + sellFees.Max.Refund,
  2797  							60:     sellFees.Max.Redeem,
  2798  						},
  2799  						RequiredCEX: b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2800  						UsedCEX:     b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2801  						OrderedLots: 1,
  2802  					},
  2803  				},
  2804  				Fees: sellFees,
  2805  				AvailableDEXBals: map[uint32]*BotBalance{
  2806  					966001: {
  2807  						Available: 4 * lotSize,
  2808  					},
  2809  					966: {
  2810  						Available: 4*(sellFees.Max.Swap+sellFees.Max.Refund) + sellFees.Funding,
  2811  					},
  2812  					60: {
  2813  						Available: 4 * sellFees.Max.Redeem,
  2814  					},
  2815  				},
  2816  				RequiredDEXBals: map[uint32]uint64{
  2817  					966001: 4 * lotSize,
  2818  					966:    4*(sellFees.Max.Swap+sellFees.Max.Refund) + sellFees.Funding,
  2819  					60:     4 * sellFees.Max.Redeem,
  2820  				},
  2821  				RemainingDEXBals: map[uint32]uint64{
  2822  					966001: 0,
  2823  					966:    0,
  2824  					60:     0,
  2825  				},
  2826  				AvailableCEXBal: &BotBalance{
  2827  					Available: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2828  						b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2829  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2830  					Reserved: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  2831  						b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2832  						b2q(sellPlacements[2].CounterTradeRate, lotSize) +
  2833  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2834  				},
  2835  				RequiredCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2836  					b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2837  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2838  				RemainingCEXBal: 0,
  2839  				UsedDEXBals: map[uint32]uint64{
  2840  					966001: 4 * lotSize,
  2841  					966:    4*(sellFees.Max.Swap+sellFees.Max.Refund) + sellFees.Funding,
  2842  					60:     4 * sellFees.Max.Redeem,
  2843  				},
  2844  				UsedCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2845  					b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2846  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2847  			},
  2848  			expectedSellPlacementsWithDecrement: []*core.QtyRate{
  2849  				{Qty: lotSize, Rate: sellPlacements[1].Rate},
  2850  				{Qty: 2 * lotSize, Rate: sellPlacements[2].Rate},
  2851  			},
  2852  			expectedSellOrderReportWithDEXDecrement: &OrderReport{
  2853  				Placements: []*TradePlacement{
  2854  					{Lots: 1, Rate: sps[0].Rate, CounterTradeRate: sps[0].CounterTradeRate,
  2855  						StandingLots: 1,
  2856  						RequiredDEX:  map[uint32]uint64{},
  2857  						UsedDEX:      map[uint32]uint64{},
  2858  						RequiredCEX:  0,
  2859  						UsedCEX:      0,
  2860  					},
  2861  					{Lots: 2, Rate: sps[1].Rate, CounterTradeRate: sps[1].CounterTradeRate,
  2862  						StandingLots: 1,
  2863  						RequiredDEX: map[uint32]uint64{
  2864  							966001: lotSize,
  2865  							966:    sellFees.Max.Swap + sellFees.Max.Refund,
  2866  							60:     sellFees.Max.Redeem,
  2867  						},
  2868  						UsedDEX: map[uint32]uint64{
  2869  							966001: lotSize,
  2870  							966:    sellFees.Max.Swap + sellFees.Max.Refund,
  2871  							60:     sellFees.Max.Redeem,
  2872  						},
  2873  						RequiredCEX: b2q(sellPlacements[1].CounterTradeRate, lotSize),
  2874  						UsedCEX:     b2q(sellPlacements[1].CounterTradeRate, lotSize),
  2875  						OrderedLots: 1,
  2876  					},
  2877  					{Lots: 3, Rate: sps[2].Rate, CounterTradeRate: sps[2].CounterTradeRate,
  2878  						StandingLots: 1,
  2879  						RequiredDEX: map[uint32]uint64{
  2880  							966001: 2 * lotSize,
  2881  							966:    2 * (sellFees.Max.Swap + sellFees.Max.Refund),
  2882  							60:     2 * sellFees.Max.Redeem,
  2883  						},
  2884  						UsedDEX: map[uint32]uint64{
  2885  							966001: 2 * lotSize,
  2886  							966:    2 * (sellFees.Max.Swap + sellFees.Max.Refund),
  2887  							60:     2 * sellFees.Max.Redeem,
  2888  						},
  2889  						RequiredCEX: b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  2890  						UsedCEX:     b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  2891  						OrderedLots: 2,
  2892  					},
  2893  					{Lots: 2, Rate: sps[3].Rate, CounterTradeRate: sps[3].CounterTradeRate,
  2894  						StandingLots: 1,
  2895  						RequiredDEX: map[uint32]uint64{
  2896  							966001: lotSize,
  2897  							966:    sellFees.Max.Swap + sellFees.Max.Refund,
  2898  							60:     sellFees.Max.Redeem,
  2899  						},
  2900  						UsedDEX:     map[uint32]uint64{},
  2901  						RequiredCEX: b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2902  						UsedCEX:     0,
  2903  						OrderedLots: 0,
  2904  					},
  2905  				},
  2906  				Fees: sellFees,
  2907  				AvailableDEXBals: map[uint32]*BotBalance{
  2908  					966001: {
  2909  						Available: 4*lotSize - 1,
  2910  					},
  2911  					966: {
  2912  						Available: 4*(sellFees.Max.Swap+sellFees.Max.Refund) + sellFees.Funding,
  2913  					},
  2914  					60: {
  2915  						Available: 4 * sellFees.Max.Redeem,
  2916  					},
  2917  				},
  2918  				RequiredDEXBals: map[uint32]uint64{
  2919  					966001: 4 * lotSize,
  2920  					966:    4*(sellFees.Max.Swap+sellFees.Max.Refund) + sellFees.Funding,
  2921  					60:     4 * sellFees.Max.Redeem,
  2922  				},
  2923  				RemainingDEXBals: map[uint32]uint64{
  2924  					966001: lotSize - 1,
  2925  					966:    sellFees.Max.Swap + sellFees.Max.Refund,
  2926  					60:     sellFees.Max.Redeem,
  2927  				},
  2928  				AvailableCEXBal: &BotBalance{
  2929  					Available: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2930  						b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2931  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2932  					Reserved: b2q(sellPlacements[0].CounterTradeRate, lotSize) +
  2933  						b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2934  						b2q(sellPlacements[2].CounterTradeRate, lotSize) +
  2935  						b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2936  				},
  2937  				RequiredCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2938  					b2q(sellPlacements[2].CounterTradeRate, 2*lotSize) +
  2939  					b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2940  				RemainingCEXBal: b2q(sellPlacements[3].CounterTradeRate, lotSize),
  2941  				UsedDEXBals: map[uint32]uint64{
  2942  					966001: 3 * lotSize,
  2943  					966:    3*(sellFees.Max.Swap+sellFees.Max.Refund) + sellFees.Funding,
  2944  					60:     3 * sellFees.Max.Redeem,
  2945  				},
  2946  				UsedCEXBal: b2q(sellPlacements[1].CounterTradeRate, lotSize) +
  2947  					b2q(sellPlacements[2].CounterTradeRate, 2*lotSize),
  2948  			},
  2949  
  2950  			// ---- Buy ----
  2951  			buyDexBalances: map[uint32]uint64{
  2952  				966: 4 * buyFees.Max.Redeem,
  2953  				60: b2q(buyPlacements[1].Rate, lotSize) +
  2954  					b2q(buyPlacements[2].Rate, 2*lotSize) +
  2955  					b2q(buyPlacements[3].Rate, lotSize) +
  2956  					4*buyFees.Max.Swap + 4*buyFees.Max.Refund + buyFees.Funding,
  2957  			},
  2958  			buyCexBalances: map[uint32]uint64{
  2959  				966001: 8 * lotSize,
  2960  				0:      0,
  2961  			},
  2962  			buyPlacements:    buyPlacements,
  2963  			buyPendingOrders: pendingOrders(false, 966001, 60),
  2964  			expectedBuyPlacements: []*core.QtyRate{
  2965  				{Qty: lotSize, Rate: buyPlacements[1].Rate},
  2966  				{Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
  2967  				{Qty: lotSize, Rate: buyPlacements[3].Rate},
  2968  			},
  2969  			expectedBuyPlacementsWithDecrement: []*core.QtyRate{
  2970  				{Qty: lotSize, Rate: buyPlacements[1].Rate},
  2971  				{Qty: 2 * lotSize, Rate: buyPlacements[2].Rate},
  2972  			},
  2973  
  2974  			expectedCancels:              []order.OrderID{orderIDs[2]},
  2975  			expectedCancelsWithDecrement: []order.OrderID{orderIDs[2]},
  2976  			multiTradeResult: []*core.MultiTradeResult{
  2977  				{Order: &core.Order{ID: orderIDs[3][:]}},
  2978  				{Order: &core.Order{ID: orderIDs[4][:]}},
  2979  				{Order: &core.Order{ID: orderIDs[5][:]}},
  2980  			},
  2981  			multiTradeResultWithDecrement: []*core.MultiTradeResult{
  2982  				{Order: &core.Order{ID: orderIDs[3][:]}},
  2983  				{Order: &core.Order{ID: orderIDs[4][:]}},
  2984  			},
  2985  			expectedOrderIDs: []order.OrderID{
  2986  				orderIDs[3], orderIDs[4], orderIDs[5],
  2987  			},
  2988  			expectedOrderIDsWithDecrement: []order.OrderID{
  2989  				orderIDs[3], orderIDs[4],
  2990  			},
  2991  		},
  2992  	}
  2993  
  2994  	for _, test := range tests {
  2995  		t.Run(test.name, func(t *testing.T) {
  2996  			testWithDecrement := func(sell, decrement, cex bool, assetID uint32) {
  2997  				t.Run(fmt.Sprintf("sell=%v, decrement=%v, cex=%v, assetID=%d", sell, decrement, cex, assetID), func(t *testing.T) {
  2998  					tCore := newTCore()
  2999  					tCore.isAccountLocker = test.isAccountLocker
  3000  					tCore.market = &core.Market{
  3001  						BaseID:  test.baseID,
  3002  						QuoteID: test.quoteID,
  3003  						LotSize: lotSize,
  3004  					}
  3005  					tCore.multiTradeResult = test.multiTradeResult
  3006  					if decrement {
  3007  						tCore.multiTradeResult = test.multiTradeResultWithDecrement
  3008  					}
  3009  
  3010  					var dexBalances, cexBalances map[uint32]uint64
  3011  					if sell {
  3012  						dexBalances = test.sellDexBalances
  3013  						cexBalances = test.sellCexBalances
  3014  					} else {
  3015  						dexBalances = test.buyDexBalances
  3016  						cexBalances = test.buyCexBalances
  3017  					}
  3018  
  3019  					adaptor := mustParseAdaptor(&exchangeAdaptorCfg{
  3020  						core:            tCore,
  3021  						baseDexBalances: dexBalances,
  3022  						baseCexBalances: cexBalances,
  3023  						mwh: &MarketWithHost{
  3024  							Host:    "dex.com",
  3025  							BaseID:  test.baseID,
  3026  							QuoteID: test.quoteID,
  3027  						},
  3028  						eventLogDB: &tEventLogDB{},
  3029  					})
  3030  
  3031  					if test.multiSplitBuffer > 0 {
  3032  						adaptor.botCfg().QuoteWalletOptions = map[string]string{
  3033  							"multisplitbuffer": fmt.Sprintf("%f", test.multiSplitBuffer),
  3034  						}
  3035  					}
  3036  
  3037  					var pendingOrders map[order.OrderID]*pendingDEXOrder
  3038  					if sell {
  3039  						pendingOrders = test.sellPendingOrders
  3040  					} else {
  3041  						pendingOrders = test.buyPendingOrders
  3042  					}
  3043  
  3044  					pendingOrdersCopy := make(map[order.OrderID]*pendingDEXOrder)
  3045  					for id, order := range pendingOrders {
  3046  						pendingOrdersCopy[id] = order
  3047  					}
  3048  					adaptor.pendingDEXOrders = pendingOrdersCopy
  3049  
  3050  					adaptor.buyFees = buyFees
  3051  					adaptor.sellFees = sellFees
  3052  
  3053  					var placements []*TradePlacement
  3054  					if sell {
  3055  						placements = test.sellPlacements
  3056  					} else {
  3057  						placements = test.buyPlacements
  3058  					}
  3059  
  3060  					res, orderReport := adaptor.multiTrade(placements, sell, driftTolerance, currEpoch)
  3061  
  3062  					expectedOrderIDs := test.expectedOrderIDs
  3063  					if decrement {
  3064  						expectedOrderIDs = test.expectedOrderIDsWithDecrement
  3065  					}
  3066  					if len(res) != len(expectedOrderIDs) {
  3067  						t.Fatalf("expected %d orders, got %d", len(expectedOrderIDs), len(res))
  3068  					}
  3069  					for oid := range res {
  3070  						if _, found := res[oid]; !found {
  3071  							t.Fatalf("order id %s not in results", oid)
  3072  						}
  3073  					}
  3074  
  3075  					var expectedPlacements []*core.QtyRate
  3076  					var expectedOrderReport *OrderReport
  3077  					if sell {
  3078  						expectedPlacements = test.expectedSellPlacements
  3079  						if decrement {
  3080  							expectedPlacements = test.expectedSellPlacementsWithDecrement
  3081  							if !cex && ((sell && assetID == test.baseID) || (!sell && assetID == test.quoteID)) {
  3082  								expectedOrderReport = test.expectedSellOrderReportWithDEXDecrement
  3083  							}
  3084  						} else {
  3085  							expectedOrderReport = test.expectedSellOrderReport
  3086  						}
  3087  					} else {
  3088  						expectedPlacements = test.expectedBuyPlacements
  3089  						if decrement {
  3090  							expectedPlacements = test.expectedBuyPlacementsWithDecrement
  3091  							if !cex {
  3092  								expectedOrderReport = test.expectedBuyOrderReportWithDEXDecrement
  3093  							}
  3094  						} else {
  3095  							expectedOrderReport = test.expectedBuyOrderReport
  3096  						}
  3097  					}
  3098  					if len(expectedPlacements) > 0 != (len(tCore.multiTradesPlaced) > 0) {
  3099  						t.Fatalf("%s: expected placements %v, got %v", test.name, len(expectedPlacements) > 0, len(tCore.multiTradesPlaced) > 0)
  3100  					}
  3101  					if len(expectedPlacements) > 0 {
  3102  						placements := tCore.multiTradesPlaced[0].Placements
  3103  						if !reflect.DeepEqual(placements, expectedPlacements) {
  3104  							t.Fatal(spew.Sprintf("%s: expected placements:\n%#+v\ngot:\n%+#v", test.name, expectedPlacements, placements))
  3105  						}
  3106  					}
  3107  
  3108  					if expectedOrderReport != nil {
  3109  						if !reflect.DeepEqual(orderReport.AvailableCEXBal, expectedOrderReport.AvailableCEXBal) {
  3110  							t.Fatal(spew.Sprintf("%s: expected available cex bal:\n%#+v\ngot:\n%+v", test.name, expectedOrderReport.AvailableCEXBal, orderReport.AvailableCEXBal))
  3111  						}
  3112  						if !reflect.DeepEqual(orderReport.RemainingCEXBal, expectedOrderReport.RemainingCEXBal) {
  3113  							t.Fatal(spew.Sprintf("%s: expected remaining cex bal:\n%#+v\ngot:\n%+v", test.name, expectedOrderReport.RemainingCEXBal, orderReport.RemainingCEXBal))
  3114  						}
  3115  						if !reflect.DeepEqual(orderReport.RequiredCEXBal, expectedOrderReport.RequiredCEXBal) {
  3116  							t.Fatal(spew.Sprintf("%s: expected required cex bal:\n%#+v\ngot:\n%+v", test.name, expectedOrderReport.RequiredCEXBal, orderReport.RequiredCEXBal))
  3117  						}
  3118  						if !reflect.DeepEqual(orderReport.Fees, expectedOrderReport.Fees) {
  3119  							t.Fatal(spew.Sprintf("%s: expected fees:\n%#+v\ngot:\n%+v", test.name, expectedOrderReport.Fees, orderReport.Fees))
  3120  						}
  3121  						if !reflect.DeepEqual(orderReport.AvailableDEXBals, expectedOrderReport.AvailableDEXBals) {
  3122  							t.Fatal(spew.Sprintf("%s: expected available dex bals:\n%#+v\ngot:\n%+v", test.name, expectedOrderReport.AvailableDEXBals, orderReport.AvailableDEXBals))
  3123  						}
  3124  						if !reflect.DeepEqual(orderReport.RequiredDEXBals, expectedOrderReport.RequiredDEXBals) {
  3125  							t.Fatal(spew.Sprintf("%s: expected required dex bals:\n%#+v\ngot:\n%+v", test.name, expectedOrderReport.RequiredDEXBals, orderReport.RequiredDEXBals))
  3126  						}
  3127  						if !reflect.DeepEqual(orderReport.RemainingDEXBals, expectedOrderReport.RemainingDEXBals) {
  3128  							t.Fatal(spew.Sprintf("%s: expected remaining dex bals:\n%#+v\ngot:\n%+v", test.name, expectedOrderReport.RemainingDEXBals, orderReport.RemainingDEXBals))
  3129  						}
  3130  						if len(orderReport.Placements) != len(expectedOrderReport.Placements) {
  3131  							t.Fatalf("%s: expected %d placements, got %d", test.name, len(expectedOrderReport.Placements), len(orderReport.Placements))
  3132  						}
  3133  						for i, placement := range orderReport.Placements {
  3134  							if !reflect.DeepEqual(placement, expectedOrderReport.Placements[i]) {
  3135  								t.Fatal(spew.Sprintf("%s: expected placement %d:\n%#+v\ngot:\n%+v", test.name, i, expectedOrderReport.Placements[i], placement))
  3136  							}
  3137  						}
  3138  						if !reflect.DeepEqual(orderReport.UsedDEXBals, expectedOrderReport.UsedDEXBals) {
  3139  							t.Fatal(spew.Sprintf("%s: expected used dex bals:\n%#+v\ngot:\n%+v", test.name, expectedOrderReport.UsedDEXBals, orderReport.UsedDEXBals))
  3140  						}
  3141  						if orderReport.UsedCEXBal != expectedOrderReport.UsedCEXBal {
  3142  							t.Fatalf("%s: expected used cex bal: %d, got: %d", test.name, expectedOrderReport.UsedCEXBal, orderReport.UsedCEXBal)
  3143  						}
  3144  					}
  3145  
  3146  					expectedCancels := test.expectedCancels
  3147  					if decrement {
  3148  						expectedCancels = test.expectedCancelsWithDecrement
  3149  					}
  3150  					sort.Slice(tCore.cancelsPlaced, func(i, j int) bool {
  3151  						return bytes.Compare(tCore.cancelsPlaced[i][:], tCore.cancelsPlaced[j][:]) < 0
  3152  					})
  3153  					sort.Slice(expectedCancels, func(i, j int) bool {
  3154  						return bytes.Compare(expectedCancels[i][:], expectedCancels[j][:]) < 0
  3155  					})
  3156  					if !reflect.DeepEqual(tCore.cancelsPlaced, expectedCancels) {
  3157  						t.Fatalf("expected cancels %v, got %v", expectedCancels, tCore.cancelsPlaced)
  3158  					}
  3159  				})
  3160  			}
  3161  
  3162  			for _, sell := range []bool{true, false} {
  3163  				var dexBalances, cexBalances map[uint32]uint64
  3164  				if sell {
  3165  					dexBalances = test.sellDexBalances
  3166  					cexBalances = test.sellCexBalances
  3167  				} else {
  3168  					dexBalances = test.buyDexBalances
  3169  					cexBalances = test.buyCexBalances
  3170  				}
  3171  
  3172  				testWithDecrement(sell, false, false, 0)
  3173  				for assetID, bal := range dexBalances {
  3174  					if bal == 0 {
  3175  						continue
  3176  					}
  3177  					dexBalances[assetID]--
  3178  					testWithDecrement(sell, true, false, assetID)
  3179  					dexBalances[assetID]++
  3180  				}
  3181  				for assetID, bal := range cexBalances {
  3182  					if bal == 0 {
  3183  						continue
  3184  					}
  3185  					cexBalances[assetID]--
  3186  					testWithDecrement(sell, true, true, assetID)
  3187  					cexBalances[assetID]++
  3188  				}
  3189  			}
  3190  		})
  3191  	}
  3192  }
  3193  
  3194  func TestDEXTrade(t *testing.T) {
  3195  	orderIDs := make([]order.OrderID, 5)
  3196  	for i := range orderIDs {
  3197  		var id order.OrderID
  3198  		copy(id[:], encode.RandomBytes(order.OrderIDSize))
  3199  		orderIDs[i] = id
  3200  	}
  3201  	matchIDs := make([]order.MatchID, 5)
  3202  	for i := range matchIDs {
  3203  		var id order.MatchID
  3204  		copy(id[:], encode.RandomBytes(order.MatchIDSize))
  3205  		matchIDs[i] = id
  3206  	}
  3207  	coinIDs := make([]string, 6)
  3208  	for i := range coinIDs {
  3209  		coinIDs[i] = hex.EncodeToString(encode.RandomBytes(32))
  3210  	}
  3211  
  3212  	type matchUpdate struct {
  3213  		swapCoin   *dex.Bytes
  3214  		redeemCoin *dex.Bytes
  3215  		refundCoin *dex.Bytes
  3216  		qty, rate  uint64
  3217  	}
  3218  	newMatchUpdate := func(swapCoin, redeemCoin, refundCoin *string, qty, rate uint64) *matchUpdate {
  3219  		stringToBytes := func(s *string) *dex.Bytes {
  3220  			if s == nil {
  3221  				return nil
  3222  			}
  3223  			b, _ := hex.DecodeString(*s)
  3224  			d := dex.Bytes(b)
  3225  			return &d
  3226  		}
  3227  
  3228  		return &matchUpdate{
  3229  			swapCoin:   stringToBytes(swapCoin),
  3230  			redeemCoin: stringToBytes(redeemCoin),
  3231  			refundCoin: stringToBytes(refundCoin),
  3232  			qty:        qty,
  3233  			rate:       rate,
  3234  		}
  3235  	}
  3236  
  3237  	type orderUpdate struct {
  3238  		id                   order.OrderID
  3239  		lockedAmt            uint64
  3240  		parentAssetLockedAmt uint64
  3241  		redeemLockedAmt      uint64
  3242  		refundLockedAmt      uint64
  3243  		status               order.OrderStatus
  3244  		matches              []*matchUpdate
  3245  		allFeesConfirmed     bool
  3246  	}
  3247  	newOrderUpdate := func(id order.OrderID, lockedAmt, parentAssetLockedAmt, redeemLockedAmt, refundLockedAmt uint64, status order.OrderStatus, allFeesConfirmed bool, matches ...*matchUpdate) *orderUpdate {
  3248  		return &orderUpdate{
  3249  			id:                   id,
  3250  			lockedAmt:            lockedAmt,
  3251  			parentAssetLockedAmt: parentAssetLockedAmt,
  3252  			redeemLockedAmt:      redeemLockedAmt,
  3253  			refundLockedAmt:      refundLockedAmt,
  3254  			status:               status,
  3255  			matches:              matches,
  3256  			allFeesConfirmed:     allFeesConfirmed,
  3257  		}
  3258  	}
  3259  
  3260  	type orderLockedFunds struct {
  3261  		id                   order.OrderID
  3262  		lockedAmt            uint64
  3263  		parentAssetLockedAmt uint64
  3264  		redeemLockedAmt      uint64
  3265  		refundLockedAmt      uint64
  3266  	}
  3267  	newOrderLockedFunds := func(id order.OrderID, lockedAmt, parentAssetLockedAmt, redeemLockedAmt, refundLockedAmt uint64) *orderLockedFunds {
  3268  		return &orderLockedFunds{
  3269  			id:                   id,
  3270  			lockedAmt:            lockedAmt,
  3271  			parentAssetLockedAmt: parentAssetLockedAmt,
  3272  			redeemLockedAmt:      redeemLockedAmt,
  3273  			refundLockedAmt:      refundLockedAmt,
  3274  		}
  3275  	}
  3276  
  3277  	newWalletTx := func(id string, txType asset.TransactionType, amount, fees uint64, confirmed bool) *asset.WalletTransaction {
  3278  		return &asset.WalletTransaction{
  3279  			ID:        id,
  3280  			Amount:    amount,
  3281  			Fees:      fees,
  3282  			Confirmed: confirmed,
  3283  			Type:      txType,
  3284  		}
  3285  	}
  3286  
  3287  	b2q := calc.BaseToQuote
  3288  
  3289  	type updatesAndBalances struct {
  3290  		orderUpdate      *orderUpdate
  3291  		txUpdates        map[string]*asset.WalletTransaction
  3292  		stats            *RunStats
  3293  		numPendingTrades int
  3294  	}
  3295  
  3296  	type test struct {
  3297  		name               string
  3298  		isDynamicSwapper   map[uint32]bool
  3299  		initialBalances    map[uint32]uint64
  3300  		baseID             uint32
  3301  		quoteID            uint32
  3302  		sell               bool
  3303  		placements         []*TradePlacement
  3304  		initialLockedFunds []*orderLockedFunds
  3305  
  3306  		postTradeBalances  map[uint32]*BotBalance
  3307  		updatesAndBalances []*updatesAndBalances
  3308  	}
  3309  
  3310  	const host = "dex.com"
  3311  	const lotSize = 1e6
  3312  	const rate1, rate2 = 5e7, 6e7
  3313  	const swapFees, redeemFees, refundFees = 1000, 1100, 1200
  3314  	const sellFees, buyFees = 2000, 50 // booking fees per lot
  3315  	const basePerLot = lotSize + sellFees
  3316  	quoteLot1, quoteLot2 := b2q(rate1, lotSize), b2q(rate2, lotSize)
  3317  	quotePerLot1, quotePerLot2 := quoteLot1+buyFees, quoteLot2+buyFees
  3318  
  3319  	// This emulates the coinIDs of UTXO coins, which have the
  3320  	// vout appended to the tx id.
  3321  	suffixedCoinID := func(id string, suffix int) *string {
  3322  		s := fmt.Sprintf("%s0%d", id, suffix)
  3323  		return &s
  3324  	}
  3325  
  3326  	tests := []*test{
  3327  		{
  3328  			name: "non dynamic swapper, sell",
  3329  			initialBalances: map[uint32]uint64{
  3330  				42: 1e8,
  3331  				0:  1e8,
  3332  			},
  3333  			sell:    true,
  3334  			baseID:  42,
  3335  			quoteID: 0,
  3336  			placements: []*TradePlacement{
  3337  				{Lots: 5, Rate: rate1},
  3338  				{Lots: 5, Rate: rate2},
  3339  			},
  3340  			initialLockedFunds: []*orderLockedFunds{
  3341  				newOrderLockedFunds(orderIDs[0], basePerLot*5, 0, 0, 0),
  3342  				newOrderLockedFunds(orderIDs[1], basePerLot*5, 0, 0, 0),
  3343  			},
  3344  			postTradeBalances: map[uint32]*BotBalance{
  3345  				42: {1e8 - 10*basePerLot, 10 * basePerLot, 0, 0},
  3346  				0:  {1e8, 0, 0, 0},
  3347  			},
  3348  			updatesAndBalances: []*updatesAndBalances{
  3349  				// First order has a match and sends a swap tx
  3350  				{
  3351  					txUpdates: map[string]*asset.WalletTransaction{
  3352  						coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*lotSize, swapFees, false),
  3353  					},
  3354  					orderUpdate: newOrderUpdate(orderIDs[0], 3*basePerLot, 0, 0, 0, order.OrderStatusBooked, false,
  3355  						newMatchUpdate(&coinIDs[0], nil, nil, 2*lotSize, rate1)),
  3356  					stats: &RunStats{
  3357  						DEXBalances: map[uint32]*BotBalance{
  3358  							42: {1e8 - 8*basePerLot - 2*lotSize - swapFees, 8 * basePerLot, 0, 0},
  3359  							0:  {1e8, 0, 2 * quoteLot1, 0},
  3360  						},
  3361  					},
  3362  					numPendingTrades: 2,
  3363  				},
  3364  				// Second order has a match and sends swap tx
  3365  				{
  3366  					txUpdates: map[string]*asset.WalletTransaction{
  3367  						coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*lotSize, swapFees, false),
  3368  					},
  3369  					orderUpdate: newOrderUpdate(orderIDs[1], 2*basePerLot, 0, 0, 0, order.OrderStatusBooked, false,
  3370  						newMatchUpdate(&coinIDs[1], nil, nil, 3*lotSize, rate2)),
  3371  					stats: &RunStats{
  3372  						DEXBalances: map[uint32]*BotBalance{
  3373  							42: {1e8 - 5*basePerLot - 5*lotSize - 2*swapFees, 5 * basePerLot, 0, 0},
  3374  							0:  {1e8, 0, 2*quoteLot1 + 3*quoteLot2, 0},
  3375  						},
  3376  					},
  3377  					numPendingTrades: 2,
  3378  				},
  3379  				// First order swap is confirmed, and redemption is sent
  3380  				{
  3381  					txUpdates: map[string]*asset.WalletTransaction{
  3382  						coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*lotSize, swapFees, true),
  3383  						coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, 2*quoteLot1, redeemFees, false),
  3384  					},
  3385  					orderUpdate: newOrderUpdate(orderIDs[0], 3*basePerLot, 0, 0, 0, order.OrderStatusBooked, false,
  3386  						newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)),
  3387  					stats: &RunStats{
  3388  						DEXBalances: map[uint32]*BotBalance{
  3389  							42: {1e8 - 5*basePerLot - 5*lotSize - 2*swapFees, 5 * basePerLot, 0, 0},
  3390  							0:  {1e8, 0, 2*quoteLot1 + 3*quoteLot2 - redeemFees, 0},
  3391  						},
  3392  					},
  3393  					numPendingTrades: 2,
  3394  				},
  3395  				// First order redemption confirmed
  3396  				{
  3397  					txUpdates: map[string]*asset.WalletTransaction{
  3398  						coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, b2q(5e7, 2e6), redeemFees, true),
  3399  					},
  3400  					orderUpdate: newOrderUpdate(orderIDs[0], 3*basePerLot, 0, 0, 0, order.OrderStatusBooked, false,
  3401  						newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)),
  3402  					stats: &RunStats{
  3403  						DEXBalances: map[uint32]*BotBalance{
  3404  							42: {1e8 - 5*basePerLot - 5*lotSize - 2*swapFees, 5 * basePerLot, 0, 0},
  3405  							0:  {1e8 + 2*quoteLot1 - redeemFees, 0, 3 * quoteLot2, 0},
  3406  						},
  3407  					},
  3408  					numPendingTrades: 2,
  3409  				},
  3410  				// First order cancelled
  3411  				{
  3412  					orderUpdate: newOrderUpdate(orderIDs[0], 0, 0, 0, 0, order.OrderStatusCanceled, true,
  3413  						newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)),
  3414  					stats: &RunStats{
  3415  						DEXBalances: map[uint32]*BotBalance{
  3416  							42: {1e8 - 2*basePerLot - 5*lotSize - 2*swapFees, 2 * basePerLot, 0, 0},
  3417  							0:  {1e8 + 2*quoteLot1 - redeemFees, 0, 3 * quoteLot2, 0},
  3418  						},
  3419  					},
  3420  					numPendingTrades: 1,
  3421  				},
  3422  				// Second order second match, swap sent, and first match refunded
  3423  				{
  3424  					txUpdates: map[string]*asset.WalletTransaction{
  3425  						coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*lotSize, swapFees, true),
  3426  						coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*lotSize, refundFees, false),
  3427  						coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*lotSize, swapFees, false),
  3428  					},
  3429  					orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 0, 0, order.OrderStatusExecuted, false,
  3430  						newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 3*lotSize, rate2),
  3431  						newMatchUpdate(&coinIDs[4], nil, nil, 2*lotSize, rate2)),
  3432  					stats: &RunStats{
  3433  						DEXBalances: map[uint32]*BotBalance{
  3434  							42: {1e8 - 7*lotSize - 3*swapFees, 0, 3*lotSize - refundFees /* refund */, 0},
  3435  							0:  {1e8 + 2*quoteLot1 - redeemFees, 0, 2 * quoteLot2 /* new swap */, 0},
  3436  						},
  3437  					},
  3438  					numPendingTrades: 1,
  3439  				},
  3440  				// Second order second match redeemed and confirmed, first match refund confirmed
  3441  				{
  3442  					txUpdates: map[string]*asset.WalletTransaction{
  3443  						coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*lotSize, refundFees, true),
  3444  						coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*lotSize, swapFees, true),
  3445  						coinIDs[5]: newWalletTx(coinIDs[5], asset.Redeem, 2*quoteLot2, redeemFees, true),
  3446  					},
  3447  					orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 0, 0, order.OrderStatusExecuted, true,
  3448  						newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 3e6, 6e7),
  3449  						newMatchUpdate(&coinIDs[4], &coinIDs[5], nil, 2e6, 6e7)),
  3450  					stats: &RunStats{
  3451  						DEXBalances: map[uint32]*BotBalance{
  3452  							42: {1e8 - 4*lotSize - 3*swapFees - refundFees, 0, 0, 0},
  3453  							0:  {1e8 + 2*quoteLot1 + 2*quoteLot2 - 2*redeemFees, 0, 0, 0},
  3454  						},
  3455  					},
  3456  				},
  3457  			},
  3458  		},
  3459  		{
  3460  			name: "non dynamic swapper, buy",
  3461  			initialBalances: map[uint32]uint64{
  3462  				42: 1e8,
  3463  				0:  1e8,
  3464  			},
  3465  			baseID:  42,
  3466  			quoteID: 0,
  3467  			placements: []*TradePlacement{
  3468  				{Lots: 5, Rate: rate1},
  3469  				{Lots: 5, Rate: rate2},
  3470  			},
  3471  			initialLockedFunds: []*orderLockedFunds{
  3472  				newOrderLockedFunds(orderIDs[0], 5*quotePerLot1, 0, 0, 0),
  3473  				newOrderLockedFunds(orderIDs[1], 5*quotePerLot2, 0, 0, 0),
  3474  			},
  3475  			postTradeBalances: map[uint32]*BotBalance{
  3476  				42: {1e8, 0, 0, 0},
  3477  				0:  {1e8 - 5*quotePerLot1 - 5*quotePerLot2, 5*quotePerLot1 + 5*quotePerLot2, 0, 0},
  3478  			},
  3479  			updatesAndBalances: []*updatesAndBalances{
  3480  				// First order has a match and sends a swap tx
  3481  				{
  3482  					txUpdates: map[string]*asset.WalletTransaction{
  3483  						coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*quoteLot1, swapFees, false),
  3484  					},
  3485  					orderUpdate: newOrderUpdate(orderIDs[0], 3*quotePerLot1, 0, 0, 0, order.OrderStatusBooked, false,
  3486  						newMatchUpdate(&coinIDs[0], nil, nil, 2*lotSize, rate1)),
  3487  					stats: &RunStats{
  3488  						DEXBalances: map[uint32]*BotBalance{
  3489  							42: {1e8, 0, 2 * lotSize, 0},
  3490  							0:  {1e8 - 3*quotePerLot1 - 5*quotePerLot2 - 2*quoteLot1 - swapFees, 3*quotePerLot1 + 5*quotePerLot2, 0, 0},
  3491  						},
  3492  					},
  3493  					numPendingTrades: 2,
  3494  				},
  3495  				// Second order has a match and sends swap tx
  3496  				{
  3497  					txUpdates: map[string]*asset.WalletTransaction{
  3498  						coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*quoteLot2, swapFees, false),
  3499  					},
  3500  					orderUpdate: newOrderUpdate(orderIDs[1], 2*quotePerLot2, 0, 0, 0, order.OrderStatusBooked, false,
  3501  						newMatchUpdate(&coinIDs[1], nil, nil, 3*lotSize, rate2)),
  3502  					stats: &RunStats{
  3503  						DEXBalances: map[uint32]*BotBalance{
  3504  							42: {1e8, 0, 5 * lotSize, 0},
  3505  							0:  {1e8 - 3*quotePerLot1 - 2*quotePerLot2 - 2*quoteLot1 - 3*quoteLot2 - 2*swapFees, 3*quotePerLot1 + 2*quotePerLot2, 0, 0},
  3506  						},
  3507  					},
  3508  					numPendingTrades: 2,
  3509  				},
  3510  				// First order swap is confirmed, and redemption is sent
  3511  				{
  3512  					txUpdates: map[string]*asset.WalletTransaction{
  3513  						coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*quoteLot1, swapFees, true),
  3514  						coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, 2*lotSize, redeemFees, false),
  3515  					},
  3516  					orderUpdate: newOrderUpdate(orderIDs[0], 3*quotePerLot1, 0, 0, 0, order.OrderStatusBooked, false,
  3517  						newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*quoteLot1, rate1)),
  3518  					stats: &RunStats{
  3519  						DEXBalances: map[uint32]*BotBalance{
  3520  							42: {1e8, 0, 5*lotSize - redeemFees, 0},
  3521  							0:  {1e8 - 3*quotePerLot1 - 2*quotePerLot2 - 2*quoteLot1 - 3*quoteLot2 - 2*swapFees, 3*quotePerLot1 + 2*quotePerLot2, 0, 0},
  3522  						},
  3523  					},
  3524  					numPendingTrades: 2,
  3525  				},
  3526  				// First order redemption confirmed
  3527  				{
  3528  					txUpdates: map[string]*asset.WalletTransaction{
  3529  						coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, 2*lotSize, redeemFees, true),
  3530  					},
  3531  					orderUpdate: newOrderUpdate(orderIDs[0], 3*quotePerLot1, 0, 0, 0, order.OrderStatusBooked, false,
  3532  						newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)),
  3533  					stats: &RunStats{
  3534  						DEXBalances: map[uint32]*BotBalance{
  3535  							42: {1e8 + 2*lotSize - redeemFees, 0, 3 * lotSize, 0},
  3536  							0:  {1e8 - 3*quotePerLot1 - 2*quotePerLot2 - 2*quoteLot1 - 3*quoteLot2 - 2*swapFees, 3*quotePerLot1 + 2*quotePerLot2, 0, 0},
  3537  						},
  3538  					},
  3539  					numPendingTrades: 2,
  3540  				},
  3541  				// First order cancelled
  3542  				{
  3543  					orderUpdate: newOrderUpdate(orderIDs[0], 0, 0, 0, 0, order.OrderStatusCanceled, true,
  3544  						newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)),
  3545  					stats: &RunStats{
  3546  						DEXBalances: map[uint32]*BotBalance{
  3547  							42: {1e8 + 2*lotSize - redeemFees, 0, 3 * lotSize, 0},
  3548  							0:  {1e8 - 2*quotePerLot2 - 2*quoteLot1 - 3*quoteLot2 - 2*swapFees, 2 * quotePerLot2, 0, 0},
  3549  						},
  3550  					},
  3551  					numPendingTrades: 1,
  3552  				},
  3553  				// Second order second match, swap sent, and first match refunded
  3554  				{
  3555  					txUpdates: map[string]*asset.WalletTransaction{
  3556  						coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*quoteLot2, swapFees, true),
  3557  						coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*quoteLot2, refundFees, false),
  3558  						coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*quoteLot2, swapFees, false),
  3559  					},
  3560  					orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 0, 0, order.OrderStatusExecuted, false,
  3561  						newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 3*lotSize, rate2),
  3562  						newMatchUpdate(&coinIDs[4], nil, nil, 2*lotSize, rate2)),
  3563  					stats: &RunStats{
  3564  						DEXBalances: map[uint32]*BotBalance{
  3565  							42: {1e8 + 2*lotSize - redeemFees, 0, 2 * lotSize, 0},
  3566  							0:  {1e8 - 2*quoteLot1 - 5*quoteLot2 - 3*swapFees, 0, 3*quoteLot2 - refundFees, 0},
  3567  						},
  3568  					},
  3569  					numPendingTrades: 1,
  3570  				},
  3571  				// Second order second match redeemed and confirmed, first match refund confirmed
  3572  				{
  3573  					txUpdates: map[string]*asset.WalletTransaction{
  3574  						coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*quoteLot2, refundFees, true),
  3575  						coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*quoteLot2, swapFees, true),
  3576  						coinIDs[5]: newWalletTx(coinIDs[5], asset.Redeem, 2*lotSize, redeemFees, true),
  3577  					},
  3578  					orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 0, 0, order.OrderStatusExecuted, true,
  3579  						newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 3*lotSize, rate2),
  3580  						newMatchUpdate(&coinIDs[4], &coinIDs[5], nil, 2*lotSize, rate2)),
  3581  					stats: &RunStats{
  3582  						DEXBalances: map[uint32]*BotBalance{
  3583  							42: {1e8 + 4*lotSize - 2*redeemFees, 0, 0, 0},
  3584  							0:  {1e8 - 2*quoteLot1 - 2*quoteLot2 - 3*swapFees - refundFees, 0, 0, 0},
  3585  						},
  3586  					},
  3587  				},
  3588  			},
  3589  		},
  3590  		{
  3591  			name: "dynamic swapper, token, sell",
  3592  			initialBalances: map[uint32]uint64{
  3593  				966001: 1e8,
  3594  				966:    1e8,
  3595  				60:     1e8,
  3596  			},
  3597  			isDynamicSwapper: map[uint32]bool{
  3598  				966001: true,
  3599  				966:    true,
  3600  				60:     true,
  3601  			},
  3602  			sell:    true,
  3603  			baseID:  60,
  3604  			quoteID: 966001,
  3605  			placements: []*TradePlacement{
  3606  				{Lots: 5, Rate: rate1},
  3607  				{Lots: 5, Rate: rate2},
  3608  			},
  3609  			initialLockedFunds: []*orderLockedFunds{
  3610  				newOrderLockedFunds(orderIDs[1], 5*basePerLot, 0, 5*redeemFees, 5*refundFees),
  3611  				newOrderLockedFunds(orderIDs[0], 5*basePerLot, 0, 5*redeemFees, 5*refundFees),
  3612  			},
  3613  			postTradeBalances: map[uint32]*BotBalance{
  3614  				966001: {1e8, 0, 0, 0},
  3615  				966:    {1e8 - 10*redeemFees, 10 * redeemFees, 0, 0},
  3616  				60:     {1e8 - 10*(basePerLot+refundFees), 10 * (basePerLot + refundFees), 0, 0},
  3617  			},
  3618  			updatesAndBalances: []*updatesAndBalances{
  3619  				// First order has a match and sends a swap tx
  3620  				{
  3621  					txUpdates: map[string]*asset.WalletTransaction{
  3622  						coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*lotSize, swapFees, false),
  3623  					},
  3624  					orderUpdate: newOrderUpdate(orderIDs[0], 3*basePerLot, 0, 5*redeemFees, 5*refundFees, order.OrderStatusBooked, false,
  3625  						newMatchUpdate(&coinIDs[0], nil, nil, 2*lotSize, rate1)),
  3626  					stats: &RunStats{
  3627  						DEXBalances: map[uint32]*BotBalance{
  3628  							966001: {1e8, 0, 2 * quoteLot1, 0},
  3629  							966:    {1e8 - 10*redeemFees, 10 * redeemFees, 0, 0},
  3630  							60:     {1e8 - 8*(basePerLot+refundFees) - 2*lotSize - 2*refundFees - swapFees, 8*(basePerLot+refundFees) + 2*refundFees, 0, 0},
  3631  						},
  3632  					},
  3633  					numPendingTrades: 2,
  3634  				},
  3635  				// Second order has a match and sends swap tx
  3636  				{
  3637  					txUpdates: map[string]*asset.WalletTransaction{
  3638  						coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*lotSize, swapFees, false),
  3639  					},
  3640  					orderUpdate: newOrderUpdate(orderIDs[1], 2*basePerLot, 0, 5*redeemFees, 5*refundFees, order.OrderStatusBooked, false,
  3641  						newMatchUpdate(&coinIDs[1], nil, nil, 3*lotSize, rate2)),
  3642  					stats: &RunStats{
  3643  						DEXBalances: map[uint32]*BotBalance{
  3644  							966001: {1e8, 0, 2*quoteLot1 + 3*quoteLot2, 0},
  3645  							966:    {1e8 - 10*redeemFees, 10 * redeemFees, 0, 0},
  3646  							60:     {1e8 - 5*(basePerLot+refundFees) - 5*lotSize - 5*refundFees - 2*swapFees, 5*(basePerLot+refundFees) + 5*refundFees, 0, 0},
  3647  						},
  3648  					},
  3649  					numPendingTrades: 2,
  3650  				},
  3651  				// First order swap is confirmed, and redemption is sent
  3652  				{
  3653  					txUpdates: map[string]*asset.WalletTransaction{
  3654  						coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*lotSize, swapFees, true),
  3655  						coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, 2*quoteLot1, redeemFees, false),
  3656  					},
  3657  					orderUpdate: newOrderUpdate(orderIDs[0], 3*basePerLot, 0, 3*redeemFees, 3*refundFees, order.OrderStatusBooked, false,
  3658  						newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)),
  3659  					stats: &RunStats{
  3660  						DEXBalances: map[uint32]*BotBalance{
  3661  							966001: {1e8, 0, 2*quoteLot1 + 3*quoteLot2, 0},
  3662  							966:    {1e8 - 9*redeemFees, 8 * redeemFees, 0, 0},
  3663  							60:     {1e8 - 5*(basePerLot+refundFees) - 5*lotSize - 3*refundFees - 2*swapFees, 5*(basePerLot+refundFees) + 3*refundFees, 0, 0},
  3664  						},
  3665  					},
  3666  					numPendingTrades: 2,
  3667  				},
  3668  				// First order redemption confirmed
  3669  				{
  3670  					txUpdates: map[string]*asset.WalletTransaction{
  3671  						coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, 2*quoteLot1, redeemFees, true),
  3672  					},
  3673  					orderUpdate: newOrderUpdate(orderIDs[0], 3*basePerLot, 0, 3*redeemFees, 3*refundFees, order.OrderStatusBooked, false,
  3674  						newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)),
  3675  					stats: &RunStats{
  3676  						DEXBalances: map[uint32]*BotBalance{
  3677  							966001: {1e8 + 2*quoteLot1, 0, 3 * quoteLot2, 0},
  3678  							966:    {1e8 - 9*redeemFees, 8 * redeemFees, 0, 0},
  3679  							60:     {1e8 - 5*(basePerLot+refundFees) - 5*lotSize - 3*refundFees - 2*swapFees, 5*(basePerLot+refundFees) + 3*refundFees, 0, 0},
  3680  						},
  3681  					},
  3682  					numPendingTrades: 2,
  3683  				},
  3684  				// First order cancelled
  3685  				{
  3686  					orderUpdate: newOrderUpdate(orderIDs[0], 0, 0, 0, 0, order.OrderStatusCanceled, true,
  3687  						newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)),
  3688  					stats: &RunStats{
  3689  						DEXBalances: map[uint32]*BotBalance{
  3690  							966001: {1e8 + 2*quoteLot1, 0, 3 * quoteLot2, 0},
  3691  							966:    {1e8 - 6*redeemFees, 5 * redeemFees, 0, 0},
  3692  							60:     {1e8 - 2*(basePerLot+refundFees) - 5*lotSize - 3*refundFees - 2*swapFees, 2*(basePerLot+refundFees) + 3*refundFees, 0, 0},
  3693  						},
  3694  					},
  3695  					numPendingTrades: 1,
  3696  				},
  3697  				// Second order second match, swap sent, and first match refunded
  3698  				{
  3699  					txUpdates: map[string]*asset.WalletTransaction{
  3700  						coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*lotSize, swapFees, true),
  3701  						coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*lotSize, refundFees, false),
  3702  						coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*lotSize, swapFees, false),
  3703  					},
  3704  					orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 2*redeemFees, 2*refundFees, order.OrderStatusExecuted, false,
  3705  						newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 3*lotSize, rate2),
  3706  						newMatchUpdate(&coinIDs[4], nil, nil, 2*lotSize, rate2)),
  3707  					stats: &RunStats{
  3708  						DEXBalances: map[uint32]*BotBalance{
  3709  							966001: {1e8 + 2*quoteLot1, 0, 2 * quoteLot2, 0},
  3710  							966:    {1e8 - 3*redeemFees, 2 * redeemFees, 0, 0},
  3711  							60:     {1e8 - 7*lotSize - 2*refundFees - 3*swapFees - refundFees, 2 * refundFees, 3 * lotSize, 0},
  3712  						},
  3713  					},
  3714  					numPendingTrades: 1,
  3715  				},
  3716  				// Second order second match redeemed and confirmed, first match refund confirmed
  3717  				{
  3718  					txUpdates: map[string]*asset.WalletTransaction{
  3719  						coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*lotSize, refundFees, true),
  3720  						coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*lotSize, swapFees, true),
  3721  						coinIDs[5]: newWalletTx(coinIDs[5], asset.Redeem, 2*quoteLot2, redeemFees, true),
  3722  					},
  3723  					orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 0, 0, order.OrderStatusExecuted, true, newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 0, 0), newMatchUpdate(&coinIDs[4], &coinIDs[5], nil, 0, 0)),
  3724  					stats: &RunStats{
  3725  						DEXBalances: map[uint32]*BotBalance{
  3726  							966001: {1e8 + 2*quoteLot1 + 2*quoteLot2, 0, 0, 0},
  3727  							966:    {1e8 - 2*redeemFees, 0, 0, 0},
  3728  							60:     {1e8 - 4*lotSize - 3*swapFees - refundFees, 0, 0, 0},
  3729  						},
  3730  					},
  3731  				},
  3732  			},
  3733  		},
  3734  		{
  3735  			name: "dynamic swapper, token, buy",
  3736  			initialBalances: map[uint32]uint64{
  3737  				966001: 1e8,
  3738  				966:    1e8,
  3739  				60:     1e8,
  3740  			},
  3741  			isDynamicSwapper: map[uint32]bool{
  3742  				966001: true,
  3743  				966:    true,
  3744  				60:     true,
  3745  			},
  3746  			baseID:  60,
  3747  			quoteID: 966001,
  3748  			placements: []*TradePlacement{
  3749  				{Lots: 5, Rate: rate1},
  3750  				{Lots: 5, Rate: rate2},
  3751  			},
  3752  			initialLockedFunds: []*orderLockedFunds{
  3753  				newOrderLockedFunds(orderIDs[0], 5*quoteLot1, 5*buyFees, 5*redeemFees, 5*refundFees),
  3754  				newOrderLockedFunds(orderIDs[1], 5*quoteLot2, 5*buyFees, 5*redeemFees, 5*refundFees),
  3755  			},
  3756  			postTradeBalances: map[uint32]*BotBalance{
  3757  				966001: {1e8 - 5*quoteLot1 - 5*quoteLot2, 5*quoteLot1 + 5*quoteLot2, 0, 0},
  3758  				966:    {1e8 - 10*(buyFees+refundFees), 10 * (buyFees + refundFees), 0, 0},
  3759  				60:     {1e8 - 10*redeemFees, 10 * redeemFees, 0, 0},
  3760  			},
  3761  			updatesAndBalances: []*updatesAndBalances{
  3762  				// First order has a match and sends a swap tx
  3763  				{
  3764  					txUpdates: map[string]*asset.WalletTransaction{
  3765  						coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*quoteLot1, swapFees, false),
  3766  					},
  3767  					orderUpdate: newOrderUpdate(orderIDs[0], 3*quoteLot1, 3*buyFees, 5*redeemFees, 5*refundFees, order.OrderStatusBooked, false,
  3768  						newMatchUpdate(&coinIDs[0], nil, nil, 2*lotSize, rate1)),
  3769  					stats: &RunStats{
  3770  						DEXBalances: map[uint32]*BotBalance{
  3771  							966001: {1e8 - 5*quoteLot1 - 5*quoteLot2, 3*quoteLot1 + 5*quoteLot2, 0, 0},
  3772  							966:    {1e8 - 8*(buyFees+refundFees) - swapFees - 2*refundFees, 8*(buyFees+refundFees) + 2*refundFees, 0, 0},
  3773  							60:     {1e8 - 10*redeemFees, 10 * redeemFees, 2 * lotSize, 0},
  3774  						},
  3775  					},
  3776  					numPendingTrades: 2,
  3777  				},
  3778  				// Second order has a match and sends swap tx
  3779  				{
  3780  					txUpdates: map[string]*asset.WalletTransaction{
  3781  						coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*quoteLot2, swapFees, false),
  3782  					},
  3783  					orderUpdate: newOrderUpdate(orderIDs[1], 2*quoteLot2, 2*buyFees, 5*redeemFees, 5*refundFees, order.OrderStatusBooked, false,
  3784  						newMatchUpdate(&coinIDs[1], nil, nil, 3*lotSize, rate2)),
  3785  					stats: &RunStats{
  3786  						DEXBalances: map[uint32]*BotBalance{
  3787  							966001: {1e8 - 5*quoteLot1 - 5*quoteLot2, 3*quoteLot1 + 2*quoteLot2, 0, 0},
  3788  							966:    {1e8 - 5*(buyFees+refundFees) - 2*swapFees - 5*refundFees, 5*(buyFees+refundFees) + 5*refundFees, 0, 0},
  3789  							60:     {1e8 - 10*redeemFees, 10 * redeemFees, 5 * lotSize, 0},
  3790  						},
  3791  					},
  3792  					numPendingTrades: 2,
  3793  				},
  3794  				// First order swap is confirmed, and redemption is sent
  3795  				{
  3796  					txUpdates: map[string]*asset.WalletTransaction{
  3797  						coinIDs[0]: newWalletTx(coinIDs[0], asset.Swap, 2*quoteLot1, swapFees, true),
  3798  						coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, 2*lotSize, redeemFees, false),
  3799  					},
  3800  					orderUpdate: newOrderUpdate(orderIDs[0], 3*quoteLot1, 3*buyFees, 3*redeemFees, 3*refundFees, order.OrderStatusBooked, false,
  3801  						newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)),
  3802  					stats: &RunStats{
  3803  						DEXBalances: map[uint32]*BotBalance{
  3804  							966001: {1e8 - 5*quoteLot1 - 5*quoteLot2, 3*quoteLot1 + 2*quoteLot2, 0, 0},
  3805  							966:    {1e8 - 5*(buyFees+refundFees) - 2*swapFees - 3*refundFees, 5*(buyFees+refundFees) + 3*refundFees, 0, 0},
  3806  							60:     {1e8 - 8*redeemFees - redeemFees, 8 * redeemFees, 5 * lotSize, 0},
  3807  						},
  3808  					},
  3809  					numPendingTrades: 2,
  3810  				},
  3811  				// First order redemption confirmed
  3812  				{
  3813  					txUpdates: map[string]*asset.WalletTransaction{
  3814  						coinIDs[2]: newWalletTx(coinIDs[2], asset.Redeem, 2*lotSize, redeemFees, true),
  3815  					},
  3816  					orderUpdate: newOrderUpdate(orderIDs[0], 3*quoteLot1, 3*buyFees, 3*redeemFees, 3*refundFees, order.OrderStatusBooked, false,
  3817  						newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)),
  3818  					stats: &RunStats{
  3819  						DEXBalances: map[uint32]*BotBalance{
  3820  							966001: {1e8 - 5*quoteLot1 - 5*quoteLot2, 3*quoteLot1 + 2*quoteLot2, 0, 0},
  3821  							966:    {1e8 - 5*(buyFees+refundFees) - 2*swapFees - 3*refundFees, 5*(buyFees+refundFees) + 3*refundFees, 0, 0},
  3822  							60:     {1e8 + 2*lotSize - 8*redeemFees - redeemFees, 8 * redeemFees, 3 * lotSize, 0},
  3823  						},
  3824  					},
  3825  					numPendingTrades: 2,
  3826  				},
  3827  				// First order cancelled
  3828  				{
  3829  					orderUpdate: newOrderUpdate(orderIDs[0], 0, 0, 0, 0, order.OrderStatusCanceled, true,
  3830  						newMatchUpdate(&coinIDs[0], &coinIDs[2], nil, 2*lotSize, rate1)),
  3831  					stats: &RunStats{
  3832  						DEXBalances: map[uint32]*BotBalance{
  3833  							966001: {1e8 - 2*quoteLot1 - 5*quoteLot2, 2 * quoteLot2, 0, 0},
  3834  							966:    {1e8 - 2*(buyFees+refundFees) - 2*swapFees - 3*refundFees, 2*(buyFees+refundFees) + 3*refundFees, 0, 0},
  3835  							60:     {1e8 + 2*lotSize - 5*redeemFees - redeemFees, 5 * redeemFees, 3 * lotSize, 0},
  3836  						},
  3837  					},
  3838  					numPendingTrades: 1,
  3839  				},
  3840  				// Second order second match, swap sent, and first match refunded
  3841  				{
  3842  					txUpdates: map[string]*asset.WalletTransaction{
  3843  						coinIDs[1]: newWalletTx(coinIDs[1], asset.Swap, 3*quoteLot2, swapFees, true),
  3844  						coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*quoteLot2, refundFees, false),
  3845  						coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*quoteLot2, swapFees, false),
  3846  					},
  3847  					orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 2*redeemFees, 2*refundFees, order.OrderStatusExecuted, false,
  3848  						newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 3*lotSize, rate2),
  3849  						newMatchUpdate(&coinIDs[4], nil, nil, 2*lotSize, rate2)),
  3850  					stats: &RunStats{
  3851  						DEXBalances: map[uint32]*BotBalance{
  3852  							966001: {1e8 - 2*quoteLot1 - 5*quoteLot2, 0, 3 * quoteLot2, 0},
  3853  							966:    {1e8 - 3*swapFees - 3*refundFees, 2 * refundFees, 0, 0},
  3854  							60:     {1e8 + 2*lotSize - 2*redeemFees - redeemFees, 2 * redeemFees, 2 * lotSize, 0},
  3855  						},
  3856  					},
  3857  					numPendingTrades: 1,
  3858  				},
  3859  				// Second order second match redeemed and confirmed, first match refund confirmed
  3860  				{
  3861  					txUpdates: map[string]*asset.WalletTransaction{
  3862  						coinIDs[3]: newWalletTx(coinIDs[3], asset.Refund, 3*quoteLot2, refundFees, true),
  3863  						coinIDs[4]: newWalletTx(coinIDs[4], asset.Swap, 2*quoteLot2, swapFees, true),
  3864  						coinIDs[5]: newWalletTx(coinIDs[5], asset.Redeem, 2*lotSize, redeemFees, true),
  3865  					},
  3866  					orderUpdate: newOrderUpdate(orderIDs[1], 0, 0, 0, 0, order.OrderStatusExecuted, true,
  3867  						newMatchUpdate(&coinIDs[1], nil, &coinIDs[3], 3*lotSize, rate2),
  3868  						newMatchUpdate(&coinIDs[4], &coinIDs[5], nil, 2*lotSize, rate2)),
  3869  					stats: &RunStats{
  3870  						DEXBalances: map[uint32]*BotBalance{
  3871  							966001: {1e8 - 2*quoteLot1 - 2*quoteLot2, 0, 0, 0},
  3872  							966:    {1e8 - 3*swapFees - 1*refundFees, 0, 0, 0},
  3873  							60:     {1e8 + 4*lotSize - 2*redeemFees, 0, 0, 0},
  3874  						},
  3875  					},
  3876  				},
  3877  			},
  3878  		},
  3879  		{
  3880  			name: "non dynamic swapper, sell, shared swap and redeem txs",
  3881  			initialBalances: map[uint32]uint64{
  3882  				42: 1e8,
  3883  				0:  1e8,
  3884  			},
  3885  			sell:    true,
  3886  			baseID:  42,
  3887  			quoteID: 0,
  3888  			placements: []*TradePlacement{
  3889  				{Lots: 5, Rate: rate1},
  3890  			},
  3891  			initialLockedFunds: []*orderLockedFunds{
  3892  				newOrderLockedFunds(orderIDs[0], basePerLot*5, 0, 0, 0),
  3893  			},
  3894  			postTradeBalances: map[uint32]*BotBalance{
  3895  				42: {1e8 - 5*basePerLot, 5 * basePerLot, 0, 0},
  3896  				0:  {1e8, 0, 0, 0},
  3897  			},
  3898  			updatesAndBalances: []*updatesAndBalances{
  3899  				// Order has two matches, sends one swap tx for both
  3900  				{
  3901  					txUpdates: map[string]*asset.WalletTransaction{
  3902  						*suffixedCoinID(coinIDs[0], 0): newWalletTx(coinIDs[0], asset.Swap, 5*lotSize, swapFees, false),
  3903  						*suffixedCoinID(coinIDs[0], 1): newWalletTx(coinIDs[0], asset.Swap, 5*lotSize, swapFees, false),
  3904  					},
  3905  					orderUpdate: newOrderUpdate(orderIDs[0], 0, 0, 0, 0, order.OrderStatusBooked, false,
  3906  						newMatchUpdate(suffixedCoinID(coinIDs[0], 0), nil, nil, 2*lotSize, rate1),
  3907  						newMatchUpdate(suffixedCoinID(coinIDs[0], 1), nil, nil, 3*lotSize, rate1),
  3908  					),
  3909  					stats: &RunStats{
  3910  						DEXBalances: map[uint32]*BotBalance{
  3911  							42: {1e8 - 5*lotSize - swapFees, 0, 0, 0},
  3912  							0:  {1e8, 0, 5 * quoteLot1, 0},
  3913  						},
  3914  					},
  3915  					numPendingTrades: 1,
  3916  				},
  3917  				// Both matches redeemed with same tx
  3918  				{
  3919  					txUpdates: map[string]*asset.WalletTransaction{
  3920  						*suffixedCoinID(coinIDs[1], 0): newWalletTx(coinIDs[1], asset.Redeem, 5*quoteLot1, redeemFees, true),
  3921  						*suffixedCoinID(coinIDs[1], 1): newWalletTx(coinIDs[1], asset.Redeem, 5*quoteLot1, redeemFees, true),
  3922  					},
  3923  					orderUpdate: newOrderUpdate(orderIDs[0], 0, 0, 0, 0, order.OrderStatusExecuted, true,
  3924  						newMatchUpdate(suffixedCoinID(coinIDs[0], 0), suffixedCoinID(coinIDs[1], 0), nil, 2*lotSize, rate1),
  3925  						newMatchUpdate(suffixedCoinID(coinIDs[0], 1), suffixedCoinID(coinIDs[1], 1), nil, 3*lotSize, rate1),
  3926  					),
  3927  					stats: &RunStats{
  3928  						DEXBalances: map[uint32]*BotBalance{
  3929  							42: {1e8 - 5*lotSize - swapFees, 0, 0, 0},
  3930  							0:  {1e8 + 5*quoteLot1 - redeemFees, 0, 0, 0},
  3931  						},
  3932  					},
  3933  					numPendingTrades: 0,
  3934  				},
  3935  			},
  3936  		},
  3937  	}
  3938  
  3939  	runTest := func(test *test) {
  3940  		tCore := newTCore()
  3941  		tCore.market = &core.Market{
  3942  			BaseID:  test.baseID,
  3943  			QuoteID: test.quoteID,
  3944  			LotSize: lotSize,
  3945  		}
  3946  		tCore.isDynamicSwapper = test.isDynamicSwapper
  3947  
  3948  		multiTradeResult := make([]*core.MultiTradeResult, 0, len(test.initialLockedFunds))
  3949  		for i, o := range test.initialLockedFunds {
  3950  			multiTradeResult = append(multiTradeResult, &core.MultiTradeResult{
  3951  				Order: &core.Order{
  3952  					Host:                 host,
  3953  					BaseID:               test.baseID,
  3954  					QuoteID:              test.quoteID,
  3955  					Sell:                 test.sell,
  3956  					LockedAmt:            o.lockedAmt,
  3957  					ID:                   o.id[:],
  3958  					ParentAssetLockedAmt: o.parentAssetLockedAmt,
  3959  					RedeemLockedAmt:      o.redeemLockedAmt,
  3960  					RefundLockedAmt:      o.refundLockedAmt,
  3961  					Rate:                 test.placements[i].Rate,
  3962  					Qty:                  test.placements[i].Lots * lotSize,
  3963  				},
  3964  				Error: nil,
  3965  			})
  3966  		}
  3967  		tCore.multiTradeResult = multiTradeResult
  3968  
  3969  		// These don't effect the test, but need to be non-nil.
  3970  		tCore.singleLotBuyFees = tFees(0, 0, 0, 0)
  3971  		tCore.singleLotSellFees = tFees(0, 0, 0, 0)
  3972  
  3973  		ctx, cancel := context.WithCancel(context.Background())
  3974  		defer cancel()
  3975  
  3976  		botID := dexMarketID(host, test.baseID, test.quoteID)
  3977  		eventLogDB := newTEventLogDB()
  3978  		adaptor := mustParseAdaptor(&exchangeAdaptorCfg{
  3979  			botID:           botID,
  3980  			core:            tCore,
  3981  			baseDexBalances: test.initialBalances,
  3982  			mwh: &MarketWithHost{
  3983  				Host:    host,
  3984  				BaseID:  test.baseID,
  3985  				QuoteID: test.quoteID,
  3986  			},
  3987  			eventLogDB: eventLogDB,
  3988  		})
  3989  		_, err := adaptor.Connect(ctx)
  3990  		if err != nil {
  3991  			t.Fatalf("%s: Connect error: %v", test.name, err)
  3992  		}
  3993  
  3994  		orders, _ := adaptor.multiTrade(test.placements, test.sell, 0.01, 100)
  3995  		if len(orders) == 0 {
  3996  			t.Fatalf("%s: multi trade did not place orders", test.name)
  3997  		}
  3998  
  3999  		checkBalances := func(expected map[uint32]*BotBalance, updateNum int) {
  4000  			t.Helper()
  4001  			stats := adaptor.stats()
  4002  			for assetID, expectedBal := range expected {
  4003  				bal := adaptor.DEXBalance(assetID)
  4004  				statsBal := stats.DEXBalances[assetID]
  4005  				if *statsBal != *bal {
  4006  					t.Fatalf("%s: stats bal != bal for asset %d. stats bal: %+v, bal: %+v", test.name, assetID, statsBal, bal)
  4007  				}
  4008  				if *bal != *expectedBal {
  4009  					var updateStr string
  4010  					if updateNum <= 0 {
  4011  						updateStr = "post trade"
  4012  					} else {
  4013  						updateStr = fmt.Sprintf("after update #%d", updateNum)
  4014  					}
  4015  					t.Fatalf("%s: unexpected asset %d balance %s. want %+v, got %+v",
  4016  						test.name, assetID, updateStr, expectedBal, bal)
  4017  				}
  4018  			}
  4019  		}
  4020  
  4021  		// Check that the correct initial events are logged
  4022  		oidToEventID := make(map[order.OrderID]uint64)
  4023  		for i, trade := range test.placements {
  4024  			o := test.initialLockedFunds[i]
  4025  			oidToEventID[o.id] = uint64(i + 1)
  4026  			e := &MarketMakingEvent{
  4027  				ID: uint64(i + 1),
  4028  				DEXOrderEvent: &DEXOrderEvent{
  4029  					ID:           o.id.String(),
  4030  					Rate:         trade.Rate,
  4031  					Qty:          trade.Lots * lotSize,
  4032  					Sell:         test.sell,
  4033  					Transactions: []*asset.WalletTransaction{},
  4034  				},
  4035  				Pending: true,
  4036  			}
  4037  
  4038  			if !eventLogDB.storedEventAtIndexEquals(e, i) {
  4039  				t.Fatalf("%s: unexpected event logged. want:\n%+v,\ngot:\n%+v", test.name, e, eventLogDB.latestStoredEvent())
  4040  			}
  4041  		}
  4042  
  4043  		checkBalances(test.postTradeBalances, 0)
  4044  
  4045  		for i, update := range test.updatesAndBalances {
  4046  			tCore.walletTxsMtx.Lock()
  4047  			for coinID, txUpdate := range update.txUpdates {
  4048  				tCore.walletTxs[coinID] = txUpdate
  4049  				tCore.walletTxs[txUpdate.ID] = txUpdate
  4050  			}
  4051  			tCore.walletTxsMtx.Unlock()
  4052  
  4053  			o := &core.Order{
  4054  				Host:                 host,
  4055  				BaseID:               test.baseID,
  4056  				QuoteID:              test.quoteID,
  4057  				Sell:                 test.sell,
  4058  				LockedAmt:            update.orderUpdate.lockedAmt,
  4059  				ID:                   update.orderUpdate.id[:],
  4060  				ParentAssetLockedAmt: update.orderUpdate.parentAssetLockedAmt,
  4061  				RedeemLockedAmt:      update.orderUpdate.redeemLockedAmt,
  4062  				RefundLockedAmt:      update.orderUpdate.refundLockedAmt,
  4063  				Status:               update.orderUpdate.status,
  4064  				Matches:              make([]*core.Match, len(update.orderUpdate.matches)),
  4065  				AllFeesConfirmed:     update.orderUpdate.allFeesConfirmed,
  4066  			}
  4067  
  4068  			for i, matchUpdate := range update.orderUpdate.matches {
  4069  				o.Matches[i] = &core.Match{
  4070  					Rate: matchUpdate.rate,
  4071  					Qty:  matchUpdate.qty,
  4072  				}
  4073  				if matchUpdate.swapCoin != nil {
  4074  					o.Matches[i].Swap = &core.Coin{
  4075  						ID: *matchUpdate.swapCoin,
  4076  					}
  4077  				}
  4078  				if matchUpdate.redeemCoin != nil {
  4079  					o.Matches[i].Redeem = &core.Coin{
  4080  						ID: *matchUpdate.redeemCoin,
  4081  					}
  4082  				}
  4083  				if matchUpdate.refundCoin != nil {
  4084  					o.Matches[i].Refund = &core.Coin{
  4085  						ID: *matchUpdate.refundCoin,
  4086  					}
  4087  				}
  4088  			}
  4089  
  4090  			note := core.OrderNote{
  4091  				Order: o,
  4092  			}
  4093  			tCore.noteFeed <- &note
  4094  			tCore.noteFeed <- &core.BondPostNote{} // dummy note
  4095  
  4096  			checkBalances(update.stats.DEXBalances, i+1)
  4097  
  4098  			stats := adaptor.stats()
  4099  			stats.CEXBalances = nil
  4100  			stats.StartTime = 0
  4101  
  4102  			if !reflect.DeepEqual(stats.DEXBalances, update.stats.DEXBalances) {
  4103  				t.Fatalf("%s: stats mismatch after update %d.\nwant: %+v\n\ngot: %+v", test.name, i+1, update.stats, stats)
  4104  			}
  4105  
  4106  			if len(adaptor.pendingDEXOrders) != update.numPendingTrades {
  4107  				t.Fatalf("%s: update #%d, expected %d pending trades, got %d", test.name, i+1, update.numPendingTrades, len(adaptor.pendingDEXOrders))
  4108  			}
  4109  		}
  4110  	}
  4111  
  4112  	for _, test := range tests {
  4113  		runTest(test)
  4114  	}
  4115  }
  4116  
  4117  func TestDeposit(t *testing.T) {
  4118  	type test struct {
  4119  		name              string
  4120  		isWithdrawer      bool
  4121  		isDynamicSwapper  bool
  4122  		depositAmt        uint64
  4123  		sendCoin          *tCoin
  4124  		unconfirmedTx     *asset.WalletTransaction
  4125  		confirmedTx       *asset.WalletTransaction
  4126  		receivedAmt       uint64
  4127  		initialDEXBalance uint64
  4128  		initialCEXBalance uint64
  4129  		assetID           uint32
  4130  		initialEvent      *MarketMakingEvent
  4131  		postConfirmEvent  *MarketMakingEvent
  4132  
  4133  		preConfirmDEXBalance  *BotBalance
  4134  		preConfirmCEXBalance  *BotBalance
  4135  		postConfirmDEXBalance *BotBalance
  4136  		postConfirmCEXBalance *BotBalance
  4137  	}
  4138  
  4139  	coinID := encode.RandomBytes(32)
  4140  	txID := hex.EncodeToString(coinID)
  4141  
  4142  	tests := []test{
  4143  		{
  4144  			name:         "withdrawer, not dynamic swapper",
  4145  			assetID:      42,
  4146  			isWithdrawer: true,
  4147  			depositAmt:   1e6,
  4148  			sendCoin: &tCoin{
  4149  				coinID: coinID,
  4150  				value:  1e6 - 2000,
  4151  			},
  4152  			unconfirmedTx: &asset.WalletTransaction{
  4153  				ID:     txID,
  4154  				Amount: 1e6 - 2000,
  4155  				Fees:   2000,
  4156  			},
  4157  			confirmedTx: &asset.WalletTransaction{
  4158  				ID:     txID,
  4159  				Amount: 1e6 - 2000,
  4160  				Fees:   2000,
  4161  			},
  4162  			receivedAmt:       1e6 - 2000,
  4163  			initialDEXBalance: 3e6,
  4164  			initialCEXBalance: 1e6,
  4165  			preConfirmDEXBalance: &BotBalance{
  4166  				Available: 2e6,
  4167  			},
  4168  			preConfirmCEXBalance: &BotBalance{
  4169  				Available: 1e6,
  4170  				Pending:   1e6 - 2000,
  4171  			},
  4172  			postConfirmDEXBalance: &BotBalance{
  4173  				Available: 2e6,
  4174  			},
  4175  			postConfirmCEXBalance: &BotBalance{
  4176  				Available: 2e6 - 2000,
  4177  			},
  4178  			initialEvent: &MarketMakingEvent{
  4179  				ID: 1,
  4180  				BalanceEffects: &BalanceEffects{
  4181  					Settled: map[uint32]int64{
  4182  						42: -2000,
  4183  					},
  4184  				},
  4185  				Pending: true,
  4186  				DepositEvent: &DepositEvent{
  4187  					AssetID: 42,
  4188  					Transaction: &asset.WalletTransaction{
  4189  						ID:     txID,
  4190  						Amount: 1e6 - 2000,
  4191  						Fees:   2000,
  4192  					},
  4193  				},
  4194  			},
  4195  			postConfirmEvent: &MarketMakingEvent{
  4196  				ID: 1,
  4197  				BalanceEffects: &BalanceEffects{
  4198  					Settled: map[uint32]int64{
  4199  						42: -2000,
  4200  					},
  4201  				},
  4202  				Pending: false,
  4203  				DepositEvent: &DepositEvent{
  4204  					AssetID: 42,
  4205  					Transaction: &asset.WalletTransaction{
  4206  						ID:     txID,
  4207  						Amount: 1e6 - 2000,
  4208  						Fees:   2000,
  4209  					},
  4210  					CEXCredit: 1e6 - 2000,
  4211  				},
  4212  			},
  4213  		},
  4214  		{
  4215  			name:       "not withdrawer, not dynamic swapper",
  4216  			assetID:    42,
  4217  			depositAmt: 1e6,
  4218  			sendCoin: &tCoin{
  4219  				coinID: coinID,
  4220  				value:  1e6,
  4221  			},
  4222  			unconfirmedTx: &asset.WalletTransaction{
  4223  				ID:     txID,
  4224  				Amount: 1e6,
  4225  				Fees:   2000,
  4226  			},
  4227  			confirmedTx: &asset.WalletTransaction{
  4228  				ID:     txID,
  4229  				Amount: 1e6,
  4230  				Fees:   2000,
  4231  			},
  4232  			receivedAmt:       1e6,
  4233  			initialDEXBalance: 3e6,
  4234  			initialCEXBalance: 1e6,
  4235  			preConfirmDEXBalance: &BotBalance{
  4236  				Available: 2e6 - 2000,
  4237  			},
  4238  			preConfirmCEXBalance: &BotBalance{
  4239  				Available: 1e6,
  4240  				Pending:   1e6,
  4241  			},
  4242  			postConfirmDEXBalance: &BotBalance{
  4243  				Available: 2e6 - 2000,
  4244  			},
  4245  			postConfirmCEXBalance: &BotBalance{
  4246  				Available: 2e6,
  4247  			},
  4248  			initialEvent: &MarketMakingEvent{
  4249  				ID: 1,
  4250  				BalanceEffects: &BalanceEffects{
  4251  					Settled: map[uint32]int64{
  4252  						42: -2000,
  4253  					},
  4254  				},
  4255  				Pending: true,
  4256  				DepositEvent: &DepositEvent{
  4257  					AssetID: 42,
  4258  					Transaction: &asset.WalletTransaction{
  4259  						ID:     txID,
  4260  						Amount: 1e6,
  4261  						Fees:   2000,
  4262  					},
  4263  				},
  4264  			},
  4265  			postConfirmEvent: &MarketMakingEvent{
  4266  				ID: 1,
  4267  				BalanceEffects: &BalanceEffects{
  4268  					Settled: map[uint32]int64{
  4269  						42: -2000,
  4270  					},
  4271  				},
  4272  				Pending: false,
  4273  				DepositEvent: &DepositEvent{
  4274  					AssetID: 42,
  4275  					Transaction: &asset.WalletTransaction{
  4276  						ID:     txID,
  4277  						Amount: 1e6,
  4278  						Fees:   2000,
  4279  					},
  4280  					CEXCredit: 1e6,
  4281  				},
  4282  			},
  4283  		},
  4284  		{
  4285  			name:             "not withdrawer, dynamic swapper",
  4286  			assetID:          42,
  4287  			isDynamicSwapper: true,
  4288  			depositAmt:       1e6,
  4289  			sendCoin: &tCoin{
  4290  				coinID: coinID,
  4291  				value:  1e6,
  4292  			},
  4293  			unconfirmedTx: &asset.WalletTransaction{
  4294  				ID:        txID,
  4295  				Amount:    1e6,
  4296  				Fees:      4000,
  4297  				Confirmed: false,
  4298  			},
  4299  			confirmedTx: &asset.WalletTransaction{
  4300  				ID:        txID,
  4301  				Amount:    1e6,
  4302  				Fees:      2000,
  4303  				Confirmed: true,
  4304  			},
  4305  			receivedAmt:       1e6,
  4306  			initialDEXBalance: 3e6,
  4307  			initialCEXBalance: 1e6,
  4308  			preConfirmDEXBalance: &BotBalance{
  4309  				Available: 2e6 - 4000,
  4310  			},
  4311  			preConfirmCEXBalance: &BotBalance{
  4312  				Available: 1e6,
  4313  				Pending:   1e6,
  4314  			},
  4315  			postConfirmDEXBalance: &BotBalance{
  4316  				Available: 2e6 - 2000,
  4317  			},
  4318  			postConfirmCEXBalance: &BotBalance{
  4319  				Available: 2e6,
  4320  			},
  4321  			initialEvent: &MarketMakingEvent{
  4322  				ID: 1,
  4323  				BalanceEffects: &BalanceEffects{
  4324  					Settled: map[uint32]int64{
  4325  						42: -2000,
  4326  					},
  4327  				},
  4328  				Pending: true,
  4329  				DepositEvent: &DepositEvent{
  4330  					AssetID: 42,
  4331  					Transaction: &asset.WalletTransaction{
  4332  						ID:     txID,
  4333  						Amount: 1e6,
  4334  						Fees:   4000,
  4335  					},
  4336  				},
  4337  			},
  4338  			postConfirmEvent: &MarketMakingEvent{
  4339  				ID: 1,
  4340  				BalanceEffects: &BalanceEffects{
  4341  					Settled: map[uint32]int64{
  4342  						42: -2000,
  4343  					},
  4344  				},
  4345  				Pending: false,
  4346  				DepositEvent: &DepositEvent{
  4347  					AssetID: 42,
  4348  					Transaction: &asset.WalletTransaction{
  4349  						ID:        txID,
  4350  						Amount:    1e6,
  4351  						Fees:      2000,
  4352  						Confirmed: true,
  4353  					},
  4354  					CEXCredit: 1e6,
  4355  				},
  4356  			},
  4357  		},
  4358  		{
  4359  			name:             "not withdrawer, dynamic swapper, token",
  4360  			assetID:          966001,
  4361  			isDynamicSwapper: true,
  4362  			depositAmt:       1e6,
  4363  			sendCoin: &tCoin{
  4364  				coinID: coinID,
  4365  				value:  1e6,
  4366  			},
  4367  			unconfirmedTx: &asset.WalletTransaction{
  4368  				ID:        txID,
  4369  				Amount:    1e6,
  4370  				Fees:      4000,
  4371  				Confirmed: false,
  4372  			},
  4373  			confirmedTx: &asset.WalletTransaction{
  4374  				ID:        txID,
  4375  				Amount:    1e6,
  4376  				Fees:      2000,
  4377  				Confirmed: true,
  4378  			},
  4379  			receivedAmt:       1e6,
  4380  			initialDEXBalance: 3e6,
  4381  			initialCEXBalance: 1e6,
  4382  			preConfirmDEXBalance: &BotBalance{
  4383  				Available: 2e6,
  4384  			},
  4385  			preConfirmCEXBalance: &BotBalance{
  4386  				Available: 1e6,
  4387  				Pending:   1e6,
  4388  			},
  4389  			postConfirmDEXBalance: &BotBalance{
  4390  				Available: 2e6,
  4391  			},
  4392  			postConfirmCEXBalance: &BotBalance{
  4393  				Available: 2e6,
  4394  			},
  4395  			initialEvent: &MarketMakingEvent{
  4396  				ID: 1,
  4397  				BalanceEffects: &BalanceEffects{
  4398  					Settled: map[uint32]int64{
  4399  						966001: -4000,
  4400  					},
  4401  				},
  4402  				Pending: true,
  4403  				DepositEvent: &DepositEvent{
  4404  					AssetID: 966001,
  4405  					Transaction: &asset.WalletTransaction{
  4406  						ID:     txID,
  4407  						Amount: 1e6,
  4408  						Fees:   4000,
  4409  					},
  4410  				},
  4411  			},
  4412  			postConfirmEvent: &MarketMakingEvent{
  4413  				ID: 1,
  4414  				BalanceEffects: &BalanceEffects{
  4415  					Settled: map[uint32]int64{
  4416  						966001: -2000,
  4417  					},
  4418  				},
  4419  				Pending: false,
  4420  				DepositEvent: &DepositEvent{
  4421  					AssetID: 966001,
  4422  					Transaction: &asset.WalletTransaction{
  4423  						ID:        txID,
  4424  						Amount:    1e6,
  4425  						Fees:      2000,
  4426  						Confirmed: true,
  4427  					},
  4428  					CEXCredit: 1e6,
  4429  				},
  4430  			},
  4431  		},
  4432  	}
  4433  
  4434  	runTest := func(test *test) {
  4435  		t.Run(test.name, func(t *testing.T) {
  4436  			tCore := newTCore()
  4437  			tCore.isWithdrawer[test.assetID] = test.isWithdrawer
  4438  			tCore.isDynamicSwapper[test.assetID] = test.isDynamicSwapper
  4439  			tCore.setAssetBalances(map[uint32]uint64{test.assetID: test.initialDEXBalance, 0: 2e6, 966: 2e6})
  4440  			tCore.walletTxsMtx.Lock()
  4441  			tCore.walletTxs[test.unconfirmedTx.ID] = test.unconfirmedTx
  4442  			tCore.walletTxsMtx.Unlock()
  4443  			tCore.sendCoin = test.sendCoin
  4444  
  4445  			tCEX := newTCEX()
  4446  			tCEX.balances[test.assetID] = &libxc.ExchangeBalance{
  4447  				Available: test.initialCEXBalance,
  4448  			}
  4449  			tCEX.balances[0] = &libxc.ExchangeBalance{
  4450  				Available: 2e6,
  4451  			}
  4452  			tCEX.balances[966] = &libxc.ExchangeBalance{
  4453  				Available: 1e8,
  4454  			}
  4455  
  4456  			dexBalances := map[uint32]uint64{
  4457  				test.assetID: test.initialDEXBalance,
  4458  				0:            2e6,
  4459  				966:          2e6,
  4460  			}
  4461  			cexBalances := map[uint32]uint64{
  4462  				0:   2e6,
  4463  				966: 1e8,
  4464  			}
  4465  
  4466  			ctx, cancel := context.WithCancel(context.Background())
  4467  			defer cancel()
  4468  
  4469  			botID := dexMarketID("host1", test.assetID, 0)
  4470  			eventLogDB := newTEventLogDB()
  4471  			adaptor := mustParseAdaptor(&exchangeAdaptorCfg{
  4472  				botID:           botID,
  4473  				core:            tCore,
  4474  				cex:             tCEX,
  4475  				baseDexBalances: dexBalances,
  4476  				baseCexBalances: cexBalances,
  4477  				mwh: &MarketWithHost{
  4478  					Host:    "host1",
  4479  					BaseID:  test.assetID,
  4480  					QuoteID: 0,
  4481  				},
  4482  				eventLogDB: eventLogDB,
  4483  			})
  4484  
  4485  			tCore.singleLotBuyFees = tFees(0, 0, 0, 0)
  4486  			tCore.singleLotSellFees = tFees(0, 0, 0, 0)
  4487  
  4488  			_, err := adaptor.Connect(ctx)
  4489  			if err != nil {
  4490  				t.Fatalf("%s: Connect error: %v", test.name, err)
  4491  			}
  4492  
  4493  			err = adaptor.deposit(ctx, test.assetID, test.depositAmt)
  4494  			if err != nil {
  4495  				t.Fatalf("%s: unexpected error: %v", test.name, err)
  4496  			}
  4497  
  4498  			preConfirmBal := adaptor.DEXBalance(test.assetID)
  4499  			if *preConfirmBal != *test.preConfirmDEXBalance {
  4500  				t.Fatalf("%s: unexpected pre confirm dex balance. want %d, got %d", test.name, test.preConfirmDEXBalance, preConfirmBal.Available)
  4501  			}
  4502  
  4503  			if test.assetID == 966001 {
  4504  				preConfirmParentBal := adaptor.DEXBalance(966)
  4505  				if preConfirmParentBal.Available != 2e6-test.unconfirmedTx.Fees {
  4506  					t.Fatalf("%s: unexpected pre confirm dex balance. want %d, got %d", test.name, test.preConfirmDEXBalance, preConfirmBal.Available)
  4507  				}
  4508  			}
  4509  
  4510  			if !eventLogDB.latestStoredEventEquals(test.initialEvent) {
  4511  				t.Fatalf("%s: unexpected event logged. want:\n%+v,\ngot:\n%+v", test.name, test.initialEvent, eventLogDB.latestStoredEvent())
  4512  			}
  4513  
  4514  			tCore.walletTxsMtx.Lock()
  4515  			tCore.walletTxs[test.unconfirmedTx.ID] = test.confirmedTx
  4516  			tCore.walletTxsMtx.Unlock()
  4517  
  4518  			tCEX.confirmDepositMtx.Lock()
  4519  			tCEX.confirmedDeposit = &test.receivedAmt
  4520  			tCEX.confirmDepositMtx.Unlock()
  4521  
  4522  			adaptor.confirmDeposit(ctx, txID)
  4523  
  4524  			checkPostConfirmBalance := func() error {
  4525  				postConfirmBal := adaptor.DEXBalance(test.assetID)
  4526  				if *postConfirmBal != *test.postConfirmDEXBalance {
  4527  					return fmt.Errorf("%s: unexpected post confirm dex balance. want %d, got %d", test.name, test.postConfirmDEXBalance, postConfirmBal.Available)
  4528  				}
  4529  
  4530  				if test.assetID == 966001 {
  4531  					postConfirmParentBal := adaptor.DEXBalance(966)
  4532  					if postConfirmParentBal.Available != 2e6-test.confirmedTx.Fees {
  4533  						return fmt.Errorf("%s: unexpected post confirm fee balance. want %d, got %d", test.name, 2e6-test.confirmedTx.Fees, postConfirmParentBal.Available)
  4534  					}
  4535  				}
  4536  				return nil
  4537  			}
  4538  
  4539  			tryWithTimeout := func(f func() error) {
  4540  				t.Helper()
  4541  				var err error
  4542  				for i := 0; i < 20; i++ {
  4543  					time.Sleep(100 * time.Millisecond)
  4544  					err = f()
  4545  					if err == nil {
  4546  						return
  4547  					}
  4548  				}
  4549  				t.Fatal(err)
  4550  			}
  4551  
  4552  			// Synchronizing because the event may not yet be when confirmDeposit
  4553  			// returns if two calls to confirmDeposit happen in parallel.
  4554  			tryWithTimeout(func() error {
  4555  				err = checkPostConfirmBalance()
  4556  				if err != nil {
  4557  					return err
  4558  				}
  4559  
  4560  				if !eventLogDB.latestStoredEventEquals(test.postConfirmEvent) {
  4561  					return fmt.Errorf("%s: unexpected event logged. want:\n%+v,\ngot:\n%+v", test.name, test.postConfirmEvent, eventLogDB.latestStoredEvent())
  4562  				}
  4563  				return nil
  4564  			})
  4565  		})
  4566  	}
  4567  
  4568  	for _, test := range tests {
  4569  		runTest(&test)
  4570  	}
  4571  }
  4572  
  4573  func TestWithdraw(t *testing.T) {
  4574  	assetID := uint32(42)
  4575  	coinID := encode.RandomBytes(32)
  4576  	txID := hex.EncodeToString(coinID)
  4577  	withdrawalID := hex.EncodeToString(encode.RandomBytes(32))
  4578  
  4579  	type test struct {
  4580  		name              string
  4581  		withdrawAmt       uint64
  4582  		tx                *asset.WalletTransaction
  4583  		initialDEXBalance uint64
  4584  		initialCEXBalance uint64
  4585  
  4586  		preConfirmDEXBalance  *BotBalance
  4587  		preConfirmCEXBalance  *BotBalance
  4588  		postConfirmDEXBalance *BotBalance
  4589  		postConfirmCEXBalance *BotBalance
  4590  
  4591  		initialEvent     *MarketMakingEvent
  4592  		postConfirmEvent *MarketMakingEvent
  4593  	}
  4594  
  4595  	tests := []test{
  4596  		{
  4597  			name:        "ok",
  4598  			withdrawAmt: 1e6,
  4599  			tx: &asset.WalletTransaction{
  4600  				ID:        txID,
  4601  				Amount:    0.9e6 - 2000,
  4602  				Fees:      2000,
  4603  				Confirmed: true,
  4604  			},
  4605  			initialCEXBalance: 3e6,
  4606  			initialDEXBalance: 1e6,
  4607  			preConfirmDEXBalance: &BotBalance{
  4608  				Available: 1e6,
  4609  				Pending:   1e6,
  4610  			},
  4611  			preConfirmCEXBalance: &BotBalance{
  4612  				Available: 1.9e6,
  4613  			},
  4614  			postConfirmDEXBalance: &BotBalance{
  4615  				Available: 1.9e6 - 2000,
  4616  			},
  4617  			postConfirmCEXBalance: &BotBalance{
  4618  				Available: 2e6,
  4619  			},
  4620  			initialEvent: &MarketMakingEvent{
  4621  				ID:      1,
  4622  				Pending: true,
  4623  				WithdrawalEvent: &WithdrawalEvent{
  4624  					AssetID:  42,
  4625  					CEXDebit: 1e6,
  4626  					ID:       withdrawalID,
  4627  				},
  4628  			},
  4629  			postConfirmEvent: &MarketMakingEvent{
  4630  				ID:      1,
  4631  				Pending: false,
  4632  				BalanceEffects: &BalanceEffects{
  4633  					Settled: map[uint32]int64{
  4634  						42: -(0.1e6 + 2000),
  4635  					},
  4636  				},
  4637  				WithdrawalEvent: &WithdrawalEvent{
  4638  					AssetID:  42,
  4639  					CEXDebit: 1e6,
  4640  					ID:       withdrawalID,
  4641  					Transaction: &asset.WalletTransaction{
  4642  						ID:        txID,
  4643  						Amount:    0.9e6 - 2000,
  4644  						Fees:      2000,
  4645  						Confirmed: true,
  4646  					},
  4647  				},
  4648  			},
  4649  		},
  4650  	}
  4651  
  4652  	runTest := func(test *test) {
  4653  		tCore := newTCore()
  4654  
  4655  		tCore.walletTxsMtx.Lock()
  4656  		tCore.walletTxs[test.tx.ID] = test.tx
  4657  		tCore.walletTxsMtx.Unlock()
  4658  
  4659  		tCEX := newTCEX()
  4660  
  4661  		dexBalances := map[uint32]uint64{
  4662  			assetID: test.initialDEXBalance,
  4663  			0:       2e6,
  4664  		}
  4665  		cexBalances := map[uint32]uint64{
  4666  			assetID: test.initialCEXBalance,
  4667  			966:     1e8,
  4668  		}
  4669  
  4670  		tCEX.withdrawalID = withdrawalID
  4671  
  4672  		ctx, cancel := context.WithCancel(context.Background())
  4673  		defer cancel()
  4674  
  4675  		botID := dexMarketID("host1", assetID, 0)
  4676  		eventLogDB := newTEventLogDB()
  4677  		adaptor := mustParseAdaptor(&exchangeAdaptorCfg{
  4678  			botID:           botID,
  4679  			core:            tCore,
  4680  			cex:             tCEX,
  4681  			baseDexBalances: dexBalances,
  4682  			baseCexBalances: cexBalances,
  4683  			mwh: &MarketWithHost{
  4684  				Host:    "host1",
  4685  				BaseID:  assetID,
  4686  				QuoteID: 0,
  4687  			},
  4688  			eventLogDB: eventLogDB,
  4689  		})
  4690  		tCore.singleLotBuyFees = tFees(0, 0, 0, 0)
  4691  		tCore.singleLotSellFees = tFees(0, 0, 0, 0)
  4692  
  4693  		_, err := adaptor.Connect(ctx)
  4694  		if err != nil {
  4695  			t.Fatalf("%s: Connect error: %v", test.name, err)
  4696  		}
  4697  
  4698  		err = adaptor.withdraw(ctx, assetID, test.withdrawAmt)
  4699  		if err != nil {
  4700  			t.Fatalf("%s: unexpected error: %v", test.name, err)
  4701  		}
  4702  
  4703  		if !eventLogDB.latestStoredEventEquals(test.initialEvent) {
  4704  			t.Fatalf("%s: unexpected event logged. want:\n%+v,\ngot:\n%+v", test.name, test.initialEvent, eventLogDB.latestStoredEvent())
  4705  		}
  4706  		preConfirmBal := adaptor.DEXBalance(assetID)
  4707  		if *preConfirmBal != *test.preConfirmDEXBalance {
  4708  			t.Fatalf("%s: unexpected pre confirm dex balance. want %+v, got %+v", test.name, test.preConfirmDEXBalance, preConfirmBal)
  4709  		}
  4710  
  4711  		tCEX.confirmWithdrawalMtx.Lock()
  4712  		tCEX.confirmWithdrawal = &withdrawArgs{
  4713  			assetID: assetID,
  4714  			amt:     test.withdrawAmt,
  4715  			txID:    test.tx.ID,
  4716  		}
  4717  		tCEX.confirmWithdrawalMtx.Unlock()
  4718  
  4719  		adaptor.confirmWithdrawal(ctx, withdrawalID)
  4720  
  4721  		tryWithTimeout := func(f func() error) {
  4722  			t.Helper()
  4723  			var err error
  4724  			for i := 0; i < 20; i++ {
  4725  				time.Sleep(100 * time.Millisecond)
  4726  				err = f()
  4727  				if err == nil {
  4728  					return
  4729  				}
  4730  			}
  4731  			t.Fatal(err)
  4732  		}
  4733  
  4734  		// Synchronizing because the event may not yet be when confirmWithdrawal
  4735  		// returns if two calls to confirmWithdrawal happen in parallel.
  4736  		tryWithTimeout(func() error {
  4737  			postConfirmBal := adaptor.DEXBalance(assetID)
  4738  			if *postConfirmBal != *test.postConfirmDEXBalance {
  4739  				return fmt.Errorf("%s: unexpected post confirm dex balance. want %+v, got %+v", test.name, test.postConfirmDEXBalance, postConfirmBal)
  4740  			}
  4741  			if !eventLogDB.latestStoredEventEquals(test.postConfirmEvent) {
  4742  				return fmt.Errorf("%s: unexpected event logged. want:\n%s,\ngot:\n%s", test.name, spew.Sdump(test.postConfirmEvent), spew.Sdump(eventLogDB.latestStoredEvent()))
  4743  			}
  4744  			return nil
  4745  		})
  4746  	}
  4747  
  4748  	for _, test := range tests {
  4749  		runTest(&test)
  4750  	}
  4751  }
  4752  
  4753  func TestCEXTrade(t *testing.T) {
  4754  	baseID := uint32(42)
  4755  	quoteID := uint32(0)
  4756  	tradeID := "123"
  4757  
  4758  	type updateAndStats struct {
  4759  		update *libxc.Trade
  4760  		stats  *RunStats
  4761  		event  *MarketMakingEvent
  4762  	}
  4763  
  4764  	type test struct {
  4765  		name     string
  4766  		sell     bool
  4767  		rate     uint64
  4768  		qty      uint64
  4769  		balances map[uint32]uint64
  4770  
  4771  		wantErr           bool
  4772  		postTradeBalances map[uint32]*BotBalance
  4773  		postTradeEvent    *MarketMakingEvent
  4774  		updates           []*updateAndStats
  4775  	}
  4776  
  4777  	b2q := calc.BaseToQuote
  4778  
  4779  	tests := []*test{
  4780  		{
  4781  			name: "fully filled sell",
  4782  			sell: true,
  4783  			rate: 5e7,
  4784  			qty:  5e6,
  4785  			balances: map[uint32]uint64{
  4786  				42: 1e7,
  4787  				0:  1e7,
  4788  			},
  4789  			postTradeBalances: map[uint32]*BotBalance{
  4790  				42: {
  4791  					Available: 5e6,
  4792  					Locked:    5e6,
  4793  				},
  4794  				0: {
  4795  					Available: 1e7,
  4796  				},
  4797  			},
  4798  			postTradeEvent: &MarketMakingEvent{
  4799  				ID:      1,
  4800  				Pending: true,
  4801  				CEXOrderEvent: &CEXOrderEvent{
  4802  					ID:   tradeID,
  4803  					Rate: 5e7,
  4804  					Qty:  5e6,
  4805  					Sell: true,
  4806  				},
  4807  			},
  4808  			updates: []*updateAndStats{
  4809  				{
  4810  					update: &libxc.Trade{
  4811  						Rate:        5e7,
  4812  						Qty:         5e6,
  4813  						BaseFilled:  3e6,
  4814  						QuoteFilled: 1.6e6,
  4815  					},
  4816  					event: &MarketMakingEvent{
  4817  						ID:      1,
  4818  						Pending: true,
  4819  						BalanceEffects: &BalanceEffects{
  4820  							Settled: map[uint32]int64{
  4821  								42: -5e6,
  4822  								0:  1.6e6,
  4823  							},
  4824  							Locked: map[uint32]uint64{
  4825  								42: 2e6,
  4826  							},
  4827  						},
  4828  						CEXOrderEvent: &CEXOrderEvent{
  4829  							ID:          tradeID,
  4830  							Rate:        5e7,
  4831  							Qty:         5e6,
  4832  							Sell:        true,
  4833  							BaseFilled:  3e6,
  4834  							QuoteFilled: 1.6e6,
  4835  						},
  4836  					},
  4837  					stats: &RunStats{
  4838  						CEXBalances: map[uint32]*BotBalance{
  4839  							42: {5e6, 5e6 - 3e6, 0, 0},
  4840  							0:  {1e7 + 1.6e6, 0, 0, 0},
  4841  						},
  4842  					},
  4843  				},
  4844  				{
  4845  					update: &libxc.Trade{
  4846  						Rate:        5e7,
  4847  						Qty:         5e6,
  4848  						BaseFilled:  5e6,
  4849  						QuoteFilled: 2.8e6,
  4850  						Complete:    true,
  4851  					},
  4852  					event: &MarketMakingEvent{
  4853  						ID:      1,
  4854  						Pending: false,
  4855  						BalanceEffects: &BalanceEffects{
  4856  							Settled: map[uint32]int64{
  4857  								42: -5e6,
  4858  								0:  2.8e6,
  4859  							},
  4860  						},
  4861  						CEXOrderEvent: &CEXOrderEvent{
  4862  							ID:          tradeID,
  4863  							Rate:        5e7,
  4864  							Qty:         5e6,
  4865  							Sell:        true,
  4866  							BaseFilled:  5e6,
  4867  							QuoteFilled: 2.8e6,
  4868  						},
  4869  					},
  4870  					stats: &RunStats{
  4871  						CEXBalances: map[uint32]*BotBalance{
  4872  							42: {5e6, 0, 0, 0},
  4873  							0:  {1e7 + 2.8e6, 0, 0, 0},
  4874  						},
  4875  					},
  4876  				},
  4877  				{
  4878  					update: &libxc.Trade{
  4879  						Rate:        5e7,
  4880  						Qty:         5e6,
  4881  						BaseFilled:  5e6,
  4882  						QuoteFilled: 2.8e6,
  4883  						Complete:    true,
  4884  					},
  4885  					stats: &RunStats{
  4886  						CEXBalances: map[uint32]*BotBalance{
  4887  							42: {5e6, 0, 0, 0},
  4888  							0:  {1e7 + 2.8e6, 0, 0, 0},
  4889  						},
  4890  					},
  4891  				},
  4892  			},
  4893  		},
  4894  		{
  4895  			name: "partially filled sell",
  4896  			sell: true,
  4897  			rate: 5e7,
  4898  			qty:  5e6,
  4899  			balances: map[uint32]uint64{
  4900  				42: 1e7,
  4901  				0:  1e7,
  4902  			},
  4903  			postTradeBalances: map[uint32]*BotBalance{
  4904  				42: {
  4905  					Available: 5e6,
  4906  					Locked:    5e6,
  4907  				},
  4908  				0: {
  4909  					Available: 1e7,
  4910  				},
  4911  			},
  4912  			postTradeEvent: &MarketMakingEvent{
  4913  				ID:      1,
  4914  				Pending: true,
  4915  				CEXOrderEvent: &CEXOrderEvent{
  4916  					ID:   tradeID,
  4917  					Rate: 5e7,
  4918  					Qty:  5e6,
  4919  					Sell: true,
  4920  				},
  4921  			},
  4922  			updates: []*updateAndStats{
  4923  				{
  4924  					update: &libxc.Trade{
  4925  						Rate:        5e7,
  4926  						Qty:         5e6,
  4927  						BaseFilled:  3e6,
  4928  						QuoteFilled: 1.6e6,
  4929  						Complete:    true,
  4930  					},
  4931  					event: &MarketMakingEvent{
  4932  						ID:      1,
  4933  						Pending: false,
  4934  						BalanceEffects: &BalanceEffects{
  4935  							Settled: map[uint32]int64{
  4936  								42: -3e6,
  4937  								0:  1.6e6,
  4938  							},
  4939  						},
  4940  						CEXOrderEvent: &CEXOrderEvent{
  4941  							ID:          tradeID,
  4942  							Rate:        5e7,
  4943  							Qty:         5e6,
  4944  							Sell:        true,
  4945  							BaseFilled:  3e6,
  4946  							QuoteFilled: 1.6e6,
  4947  						},
  4948  					},
  4949  					stats: &RunStats{
  4950  						CEXBalances: map[uint32]*BotBalance{
  4951  							42: {7e6, 0, 0, 0},
  4952  							0:  {1e7 + 1.6e6, 0, 0, 0},
  4953  						},
  4954  					},
  4955  				},
  4956  			},
  4957  		},
  4958  		{
  4959  			name: "fully filled buy",
  4960  			sell: false,
  4961  			rate: 5e7,
  4962  			qty:  5e6,
  4963  			balances: map[uint32]uint64{
  4964  				42: 1e7,
  4965  				0:  1e7,
  4966  			},
  4967  			postTradeBalances: map[uint32]*BotBalance{
  4968  				42: {
  4969  					Available: 1e7,
  4970  				},
  4971  				0: {
  4972  					Available: 1e7 - b2q(5e7, 5e6),
  4973  					Locked:    b2q(5e7, 5e6),
  4974  				},
  4975  			},
  4976  			postTradeEvent: &MarketMakingEvent{
  4977  				ID:      1,
  4978  				Pending: true,
  4979  				CEXOrderEvent: &CEXOrderEvent{
  4980  					ID:   tradeID,
  4981  					Rate: 5e7,
  4982  					Qty:  5e6,
  4983  					Sell: false,
  4984  				},
  4985  			},
  4986  			updates: []*updateAndStats{
  4987  				{
  4988  					update: &libxc.Trade{
  4989  						Rate:        5e7,
  4990  						Qty:         5e6,
  4991  						BaseFilled:  3e6,
  4992  						QuoteFilled: 1.6e6,
  4993  					},
  4994  					event: &MarketMakingEvent{
  4995  						ID:      1,
  4996  						Pending: true,
  4997  						BalanceEffects: &BalanceEffects{
  4998  							Settled: map[uint32]int64{
  4999  								42: 3e6,
  5000  								0:  -1.6e6,
  5001  							},
  5002  							Locked: map[uint32]uint64{
  5003  								0: b2q(5e7, 2e6),
  5004  							},
  5005  						},
  5006  						CEXOrderEvent: &CEXOrderEvent{
  5007  							ID:          tradeID,
  5008  							Rate:        5e7,
  5009  							Qty:         5e6,
  5010  							Sell:        false,
  5011  							BaseFilled:  3e6,
  5012  							QuoteFilled: 1.6e6,
  5013  						},
  5014  					},
  5015  					stats: &RunStats{
  5016  						CEXBalances: map[uint32]*BotBalance{
  5017  							42: {1e7 + 3e6, 0, 0, 0},
  5018  							0:  {1e7 - b2q(5e7, 5e6), b2q(5e7, 5e6) - 1.6e6, 0, 0},
  5019  						},
  5020  					},
  5021  				},
  5022  				{
  5023  					update: &libxc.Trade{
  5024  						Rate:        5e7,
  5025  						Qty:         5e6,
  5026  						BaseFilled:  5.1e6,
  5027  						QuoteFilled: calc.BaseToQuote(5e7, 5e6),
  5028  						Complete:    true,
  5029  					},
  5030  					event: &MarketMakingEvent{
  5031  						ID:      1,
  5032  						Pending: false,
  5033  						BalanceEffects: &BalanceEffects{
  5034  							Settled: map[uint32]int64{
  5035  								42: 5.1e6,
  5036  								0:  -int64(b2q(5e7, 5e6)),
  5037  							},
  5038  						},
  5039  						CEXOrderEvent: &CEXOrderEvent{
  5040  							ID:          tradeID,
  5041  							Rate:        5e7,
  5042  							Qty:         5e6,
  5043  							Sell:        false,
  5044  							BaseFilled:  5.1e6,
  5045  							QuoteFilled: b2q(5e7, 5e6),
  5046  						},
  5047  					},
  5048  					stats: &RunStats{
  5049  						CEXBalances: map[uint32]*BotBalance{
  5050  							42: {1e7 + 5.1e6, 0, 0, 0},
  5051  							0:  {1e7 - b2q(5e7, 5e6), 0, 0, 0},
  5052  						},
  5053  					},
  5054  				},
  5055  				{
  5056  					update: &libxc.Trade{
  5057  						Rate:        5e7,
  5058  						Qty:         5e6,
  5059  						BaseFilled:  5.1e6,
  5060  						QuoteFilled: b2q(5e7, 5e6),
  5061  						Complete:    true,
  5062  					},
  5063  					stats: &RunStats{
  5064  						CEXBalances: map[uint32]*BotBalance{
  5065  							42: {1e7 + 5.1e6, 0, 0, 0},
  5066  							0:  {1e7 - b2q(5e7, 5e6), 0, 0, 0},
  5067  						},
  5068  					},
  5069  				},
  5070  			},
  5071  		},
  5072  		{
  5073  			name: "partially filled buy",
  5074  			sell: false,
  5075  			rate: 5e7,
  5076  			qty:  5e6,
  5077  			balances: map[uint32]uint64{
  5078  				42: 1e7,
  5079  				0:  1e7,
  5080  			},
  5081  			postTradeBalances: map[uint32]*BotBalance{
  5082  				42: {
  5083  					Available: 1e7,
  5084  				},
  5085  				0: {
  5086  					Available: 1e7 - calc.BaseToQuote(5e7, 5e6),
  5087  					Locked:    calc.BaseToQuote(5e7, 5e6),
  5088  				},
  5089  			},
  5090  			postTradeEvent: &MarketMakingEvent{
  5091  				ID:      1,
  5092  				Pending: true,
  5093  				CEXOrderEvent: &CEXOrderEvent{
  5094  					ID:   tradeID,
  5095  					Rate: 5e7,
  5096  					Qty:  5e6,
  5097  					Sell: false,
  5098  				},
  5099  			},
  5100  			updates: []*updateAndStats{
  5101  				{
  5102  					update: &libxc.Trade{
  5103  						Rate:        5e7,
  5104  						Qty:         5e6,
  5105  						BaseFilled:  3e6,
  5106  						QuoteFilled: 1.6e6,
  5107  						Complete:    true,
  5108  					},
  5109  					event: &MarketMakingEvent{
  5110  						ID:      1,
  5111  						Pending: false,
  5112  						BalanceEffects: &BalanceEffects{
  5113  							Settled: map[uint32]int64{
  5114  								42: 3e6,
  5115  								0:  -1.6e6,
  5116  							},
  5117  						},
  5118  						CEXOrderEvent: &CEXOrderEvent{
  5119  							ID:          tradeID,
  5120  							Rate:        5e7,
  5121  							Qty:         5e6,
  5122  							Sell:        false,
  5123  							BaseFilled:  3e6,
  5124  							QuoteFilled: 1.6e6,
  5125  						},
  5126  					},
  5127  					stats: &RunStats{
  5128  						CEXBalances: map[uint32]*BotBalance{
  5129  							42: {1e7 + 3e6, 0, 0, 0},
  5130  							0:  {1e7 - 1.6e6, 0, 0, 0},
  5131  						},
  5132  					},
  5133  				},
  5134  			},
  5135  		},
  5136  	}
  5137  
  5138  	botCfg := &BotConfig{
  5139  		Host:    "host1",
  5140  		BaseID:  baseID,
  5141  		QuoteID: quoteID,
  5142  		CEXName: "Binance",
  5143  	}
  5144  
  5145  	runTest := func(test *test) {
  5146  		tCore := newTCore()
  5147  		tCEX := newTCEX()
  5148  		tCEX.tradeID = tradeID
  5149  
  5150  		ctx, cancel := context.WithCancel(context.Background())
  5151  		defer cancel()
  5152  
  5153  		botID := dexMarketID(botCfg.Host, botCfg.BaseID, botCfg.QuoteID)
  5154  		eventLogDB := newTEventLogDB()
  5155  		adaptor := mustParseAdaptor(&exchangeAdaptorCfg{
  5156  			botID:           botID,
  5157  			core:            tCore,
  5158  			cex:             tCEX,
  5159  			baseDexBalances: test.balances,
  5160  			baseCexBalances: test.balances,
  5161  			mwh: &MarketWithHost{
  5162  				Host:    "host1",
  5163  				BaseID:  botCfg.BaseID,
  5164  				QuoteID: botCfg.QuoteID,
  5165  			},
  5166  			eventLogDB: eventLogDB,
  5167  		})
  5168  		tCore.singleLotBuyFees = tFees(0, 0, 0, 0)
  5169  		tCore.singleLotSellFees = tFees(0, 0, 0, 0)
  5170  		_, err := adaptor.Connect(ctx)
  5171  		if err != nil {
  5172  			t.Fatalf("%s: Connect error: %v", test.name, err)
  5173  		}
  5174  
  5175  		adaptor.SubscribeTradeUpdates()
  5176  
  5177  		_, err = adaptor.CEXTrade(ctx, baseID, quoteID, test.sell, test.rate, test.qty)
  5178  		if test.wantErr {
  5179  			if err == nil {
  5180  				t.Fatalf("%s: expected error but did not get", test.name)
  5181  			}
  5182  			return
  5183  		}
  5184  		if err != nil {
  5185  			t.Fatalf("%s: unexpected error: %v", test.name, err)
  5186  		}
  5187  
  5188  		checkBalances := func(expected map[uint32]*BotBalance, i int) {
  5189  			t.Helper()
  5190  			for assetID, expectedBal := range expected {
  5191  				bal := adaptor.CEXBalance(assetID)
  5192  				if *bal != *expectedBal {
  5193  					step := "post trade"
  5194  					if i > 0 {
  5195  						step = fmt.Sprintf("after update #%d", i)
  5196  					}
  5197  					t.Fatalf("%s: unexpected cex balance %s for asset %d. want %+v, got %+v",
  5198  						test.name, step, assetID, expectedBal, bal)
  5199  				}
  5200  			}
  5201  		}
  5202  
  5203  		checkBalances(test.postTradeBalances, 0)
  5204  
  5205  		checkLatestEvent := func(expected *MarketMakingEvent, i int) {
  5206  			t.Helper()
  5207  			step := "post trade"
  5208  			if i > 0 {
  5209  				step = fmt.Sprintf("after update #%d", i)
  5210  			}
  5211  			if !eventLogDB.latestStoredEventEquals(expected) {
  5212  				t.Fatalf("%s: unexpected event %s. want:\n%+v,\ngot:\n%+v", test.name, step, expected, eventLogDB.latestStoredEvent())
  5213  			}
  5214  		}
  5215  
  5216  		checkLatestEvent(test.postTradeEvent, 0)
  5217  
  5218  		for i, updateAndStats := range test.updates {
  5219  			update := updateAndStats.update
  5220  			update.ID = tradeID
  5221  			update.BaseID = baseID
  5222  			update.QuoteID = quoteID
  5223  			update.Sell = test.sell
  5224  			eventLogDB.storedEventsMtx.Lock()
  5225  			eventLogDB.storedEvents = []*MarketMakingEvent{}
  5226  			eventLogDB.storedEventsMtx.Unlock()
  5227  			tCEX.tradeUpdates <- updateAndStats.update
  5228  			tCEX.tradeUpdates <- &libxc.Trade{} // dummy update
  5229  			checkBalances(updateAndStats.stats.CEXBalances, i+1)
  5230  			checkLatestEvent(updateAndStats.event, i+1)
  5231  
  5232  			stats := adaptor.stats()
  5233  			stats.DEXBalances = nil
  5234  			stats.StartTime = 0
  5235  			if !reflect.DeepEqual(stats.CEXBalances, updateAndStats.stats.CEXBalances) {
  5236  				t.Fatalf("%s: stats mismatch after update %d.\nwant: %+v\n\ngot: %+v", test.name, i+1, updateAndStats.stats, stats)
  5237  			}
  5238  		}
  5239  	}
  5240  
  5241  	for _, test := range tests {
  5242  		runTest(test)
  5243  	}
  5244  }
  5245  
  5246  func TestOrderFeesInUnits(t *testing.T) {
  5247  	type test struct {
  5248  		name      string
  5249  		buyFees   *OrderFees
  5250  		sellFees  *OrderFees
  5251  		rate      uint64
  5252  		market    *MarketWithHost
  5253  		fiatRates map[uint32]float64
  5254  
  5255  		expectedSellBase  uint64
  5256  		expectedSellQuote uint64
  5257  		expectedBuyBase   uint64
  5258  		expectedBuyQuote  uint64
  5259  	}
  5260  
  5261  	tests := []*test{
  5262  		{
  5263  			name: "dcr/btc",
  5264  			market: &MarketWithHost{
  5265  				BaseID:  42,
  5266  				QuoteID: 0,
  5267  			},
  5268  			buyFees:           tFees(5e5, 1.1e4, 0, 0),
  5269  			sellFees:          tFees(1.085e4, 4e5, 0, 0),
  5270  			rate:              5e7,
  5271  			expectedSellBase:  810850,
  5272  			expectedBuyBase:   1011000,
  5273  			expectedSellQuote: 405425,
  5274  			expectedBuyQuote:  505500,
  5275  		},
  5276  		{
  5277  			name: "btc/usdc.eth",
  5278  			market: &MarketWithHost{
  5279  				BaseID:  0,
  5280  				QuoteID: 60001,
  5281  			},
  5282  			buyFees:  tFees(1e7, 4e4, 0, 0),
  5283  			sellFees: tFees(5e4, 1.1e7, 0, 0),
  5284  			fiatRates: map[uint32]float64{
  5285  				60001: 0.99,
  5286  				60:    2300,
  5287  				0:     42999,
  5288  			},
  5289  			rate: calc.MessageRateAlt(43000, 1e8, 1e6),
  5290  			// We first convert from the parent asset to the child.
  5291  			// 5e4 sats + (1.1e7 gwei / 1e9 * 2300 / 0.99 * 1e6) = 25555555 microUSDC
  5292  			// Then we use QuoteToBase with the message-rate.
  5293  			// r = 43000 * 1e8 / 1e8 * 1e6 = 43_000_000_000
  5294  			// 25555555 * 1e8 / 43_000_000_000 = 59431 Sats
  5295  			// 5e4 + 59431 = 109431
  5296  			expectedSellBase: 109431,
  5297  			// 1e7 gwei * / 1e9 * 2300 / 0.99 * 1e6 = 23232323 microUSDC
  5298  			// 23232323 * 1e8 / 43_000_000_000 = 54028 Sats
  5299  			// 4e4 + 54028 = 94028
  5300  			expectedBuyBase:   94028,
  5301  			expectedSellQuote: 47055556,
  5302  			expectedBuyQuote:  40432323,
  5303  		},
  5304  		{
  5305  			name: "wbtc.polygon/usdc.eth",
  5306  			market: &MarketWithHost{
  5307  				BaseID:  966003,
  5308  				QuoteID: 60001,
  5309  			},
  5310  			buyFees:  tFees(1e7, 2e8, 0, 0),
  5311  			sellFees: tFees(5e8, 1.1e7, 0, 0),
  5312  			fiatRates: map[uint32]float64{
  5313  				60001:  0.99,
  5314  				60:     2300,
  5315  				966003: 42500,
  5316  				966:    0.8,
  5317  			},
  5318  			rate: calc.MessageRateAlt(43000, 1e8, 1e6),
  5319  			// 1.1e7 gwei / 1e9 * 2300 / 0.99 * 1e6 = 25555556 micoUSDC
  5320  			// 25555556 * 1e8 / 43_000_000_000 = 59431 Sats
  5321  			// 5e8 gwei / 1e9 * 0.8 / 42500 * 1e8 = 941 wSats
  5322  			// 59431 + 941 = 60372
  5323  			expectedSellBase: 60372,
  5324  			// 1e7 gwei / 1e9 * 2300 / 0.99 = 23232323 microUSDC
  5325  			// 23232323 * 1e8 / 43_000_000_000 = 54028 wSats
  5326  			// 2e8 / 1e9 * 0.8 / 42500 * 1e8 = 376 wSats
  5327  			// 54028 + 376 = 54404
  5328  			expectedBuyBase: 54404,
  5329  			// 5e8 gwei / 1e9 * 0.8 / 42500 * 1e8 = 941 wSats
  5330  			// 941 * 43_000_000_000 / 1e8 = 404630 microUSDC
  5331  			// 1.1e7 gwei / 1e9 * 2300 / 0.99 * 1e6 = 25555556 microUSDC
  5332  			// 404630 + 25555556 = 25960186
  5333  			expectedSellQuote: 25960186,
  5334  			// 1e7 / 1e9 * 2300 / 0.99 * 1e6 = 23232323 microUSDC
  5335  			// 2e8 / 1e9 * 0.8 / 42500 * 1e8 = 376 wSats
  5336  			// 376 * 43_000_000_000 / 1e8 = 161680 microUSDC
  5337  			// 23232323 + 161680 = 23394003
  5338  			expectedBuyQuote: 23394003,
  5339  		},
  5340  	}
  5341  
  5342  	runTest := func(tt *test) {
  5343  		tCore := newTCore()
  5344  		tCore.fiatRates = tt.fiatRates
  5345  		tCore.singleLotBuyFees = tt.buyFees
  5346  		tCore.singleLotSellFees = tt.sellFees
  5347  		adaptor := mustParseAdaptor(&exchangeAdaptorCfg{
  5348  			core:       tCore,
  5349  			mwh:        tt.market,
  5350  			eventLogDB: &tEventLogDB{},
  5351  		})
  5352  		ctx, cancel := context.WithCancel(context.Background())
  5353  		defer cancel()
  5354  		_, err := adaptor.Connect(ctx)
  5355  		if err != nil {
  5356  			t.Fatalf("%s: Connect error: %v", tt.name, err)
  5357  		}
  5358  
  5359  		sellBase, err := adaptor.OrderFeesInUnits(true, true, tt.rate)
  5360  		if err != nil {
  5361  			t.Fatalf("%s: unexpected error: %v", tt.name, err)
  5362  		}
  5363  		if sellBase != tt.expectedSellBase {
  5364  			t.Fatalf("%s: unexpected sell base fee. want %d, got %d", tt.name, tt.expectedSellBase, sellBase)
  5365  		}
  5366  
  5367  		sellQuote, err := adaptor.OrderFeesInUnits(true, false, tt.rate)
  5368  		if err != nil {
  5369  			t.Fatalf("%s: unexpected error: %v", tt.name, err)
  5370  		}
  5371  		if sellQuote != tt.expectedSellQuote {
  5372  			t.Fatalf("%s: unexpected sell quote fee. want %d, got %d", tt.name, tt.expectedSellQuote, sellQuote)
  5373  		}
  5374  
  5375  		buyBase, err := adaptor.OrderFeesInUnits(false, true, tt.rate)
  5376  		if err != nil {
  5377  			t.Fatalf("%s: unexpected error: %v", tt.name, err)
  5378  		}
  5379  		if buyBase != tt.expectedBuyBase {
  5380  			t.Fatalf("%s: unexpected buy base fee. want %d, got %d", tt.name, tt.expectedBuyBase, buyBase)
  5381  		}
  5382  
  5383  		buyQuote, err := adaptor.OrderFeesInUnits(false, false, tt.rate)
  5384  		if err != nil {
  5385  			t.Fatalf("%s: unexpected error: %v", tt.name, err)
  5386  		}
  5387  		if buyQuote != tt.expectedBuyQuote {
  5388  			t.Fatalf("%s: unexpected buy quote fee. want %d, got %d", tt.name, tt.expectedBuyQuote, buyQuote)
  5389  		}
  5390  	}
  5391  
  5392  	for _, test := range tests {
  5393  		runTest(test)
  5394  	}
  5395  }
  5396  
  5397  func TestCalcProfitLoss(t *testing.T) {
  5398  	initialBalances := map[uint32]uint64{
  5399  		42: 1e9,
  5400  		0:  1e6,
  5401  	}
  5402  	finalBalances := map[uint32]uint64{
  5403  		42: 0.9e9,
  5404  		0:  1.1e6,
  5405  	}
  5406  	fiatRates := map[uint32]float64{
  5407  		42: 23,
  5408  		0:  65000,
  5409  	}
  5410  	pl := newProfitLoss(initialBalances, finalBalances, nil, fiatRates)
  5411  	expProfitLoss := (9-10)*23 + (0.011-0.01)*65000
  5412  	if math.Abs(pl.Profit-expProfitLoss) > 1e-6 {
  5413  		t.Fatalf("unexpected profit loss. want %f, got %f", expProfitLoss, pl.Profit)
  5414  	}
  5415  	initialFiatValue := 10*23 + 0.01*65000
  5416  	expProfitRatio := expProfitLoss / initialFiatValue
  5417  	if math.Abs(pl.ProfitRatio-expProfitRatio) > 1e-6 {
  5418  		t.Fatalf("unexpected profit ratio. want %f, got %f", expProfitRatio, pl.ProfitRatio)
  5419  	}
  5420  
  5421  	// Add mods and decrease initial balances by the same amount. P/L should be the same.
  5422  	mods := map[uint32]int64{
  5423  		42: 1e6,
  5424  		0:  2e6,
  5425  	}
  5426  	initialBalances[42] -= 1e6
  5427  	initialBalances[0] -= 2e6
  5428  	pl = newProfitLoss(initialBalances, finalBalances, mods, fiatRates)
  5429  	if math.Abs(pl.Profit-expProfitLoss) > 1e-6 {
  5430  		t.Fatalf("unexpected profit loss. want %f, got %f", expProfitLoss, pl.Profit)
  5431  	}
  5432  	if math.Abs(pl.ProfitRatio-expProfitRatio) > 1e-6 {
  5433  		t.Fatalf("unexpected profit ratio. want %f, got %f", expProfitRatio, pl.ProfitRatio)
  5434  	}
  5435  }
  5436  
  5437  func TestRefreshPendingEvents(t *testing.T) {
  5438  	tCore := newTCore()
  5439  	tCEX := newTCEX()
  5440  
  5441  	dexBalances := map[uint32]uint64{
  5442  		42: 1e9,
  5443  		0:  1e9,
  5444  	}
  5445  	cexBalances := map[uint32]uint64{
  5446  		42: 1e9,
  5447  		0:  1e9,
  5448  	}
  5449  
  5450  	adaptor := mustParseAdaptor(&exchangeAdaptorCfg{
  5451  		core: tCore,
  5452  		cex:  tCEX,
  5453  		mwh: &MarketWithHost{
  5454  			Host:    "host1",
  5455  			BaseID:  42,
  5456  			QuoteID: 0,
  5457  		},
  5458  		baseDexBalances: dexBalances,
  5459  		baseCexBalances: cexBalances,
  5460  		eventLogDB:      &tEventLogDB{},
  5461  	})
  5462  
  5463  	// These will be updated throughout the test
  5464  	expectedDEXAvailableBalance := map[uint32]uint64{
  5465  		42: 1e9,
  5466  		0:  1e9,
  5467  	}
  5468  	expectedCEXAvailableBalance := map[uint32]uint64{
  5469  		42: 1e9,
  5470  		0:  1e9,
  5471  	}
  5472  	checkAvailableBalances := func() {
  5473  		t.Helper()
  5474  		for assetID, expectedBal := range expectedDEXAvailableBalance {
  5475  			bal := adaptor.DEXBalance(assetID)
  5476  			if bal.Available != expectedBal {
  5477  				t.Fatalf("unexpected dex balance for asset %d. want %d, got %d", assetID, expectedBal, bal.Available)
  5478  			}
  5479  		}
  5480  
  5481  		for assetID, expectedBal := range expectedCEXAvailableBalance {
  5482  			bal := adaptor.CEXBalance(assetID)
  5483  			if bal.Available != expectedBal {
  5484  				t.Fatalf("unexpected cex balance for asset %d. want %d, got %d", assetID, expectedBal, bal.Available)
  5485  			}
  5486  		}
  5487  	}
  5488  
  5489  	// Add a pending dex order, then refresh pending events
  5490  	var dexOrderID order.OrderID
  5491  	copy(dexOrderID[:], encode.RandomBytes(32))
  5492  	swapCoinID := encode.RandomBytes(32)
  5493  	redeemCoinID := encode.RandomBytes(32)
  5494  	tCore.walletTxs = map[string]*asset.WalletTransaction{
  5495  		hex.EncodeToString(swapCoinID): {
  5496  			Confirmed: true,
  5497  			Fees:      2000,
  5498  			Amount:    5e6,
  5499  		},
  5500  		hex.EncodeToString(redeemCoinID): {
  5501  			Confirmed: true,
  5502  			Fees:      1000,
  5503  			Amount:    calc.BaseToQuote(5e6, 5e7),
  5504  		},
  5505  	}
  5506  	pord := &pendingDEXOrder{
  5507  		swaps:              map[string]*asset.WalletTransaction{},
  5508  		redeems:            map[string]*asset.WalletTransaction{},
  5509  		refunds:            map[string]*asset.WalletTransaction{},
  5510  		swapCoinIDToTxID:   map[string]string{},
  5511  		redeemCoinIDToTxID: map[string]string{},
  5512  		refundCoinIDToTxID: map[string]string{},
  5513  	}
  5514  	adaptor.pendingDEXOrders[dexOrderID] = pord
  5515  	pord.state.Store(&dexOrderState{
  5516  		order: &core.Order{
  5517  			ID:      dexOrderID[:],
  5518  			Sell:    true,
  5519  			Rate:    5e6,
  5520  			Qty:     5e7,
  5521  			BaseID:  42,
  5522  			QuoteID: 0,
  5523  			Matches: []*core.Match{
  5524  				{
  5525  					Rate: 5e6,
  5526  					Qty:  5e7,
  5527  					Swap: &core.Coin{
  5528  						ID: swapCoinID,
  5529  					},
  5530  					Redeem: &core.Coin{
  5531  						ID: redeemCoinID,
  5532  					},
  5533  				},
  5534  			},
  5535  		},
  5536  		dexBalanceEffects: &BalanceEffects{},
  5537  		cexBalanceEffects: &BalanceEffects{},
  5538  		counterTradeRate:  pord.counterTradeRate,
  5539  	})
  5540  	ctx := context.Background()
  5541  	adaptor.refreshAllPendingEvents(ctx)
  5542  	expectedDEXAvailableBalance[42] -= 5e6 + 2000
  5543  	expectedDEXAvailableBalance[0] += calc.BaseToQuote(5e6, 5e7) - 1000
  5544  	checkAvailableBalances()
  5545  
  5546  	// Add a pending unfilled CEX order, then refresh pending events
  5547  	cexOrderID := "123"
  5548  	adaptor.pendingCEXOrders = map[string]*pendingCEXOrder{
  5549  		cexOrderID: {
  5550  			trade: &libxc.Trade{
  5551  				ID:      cexOrderID,
  5552  				Sell:    true,
  5553  				Rate:    5e6,
  5554  				Qty:     5e7,
  5555  				BaseID:  42,
  5556  				QuoteID: 0,
  5557  			},
  5558  		},
  5559  	}
  5560  	tCEX.tradeStatus = &libxc.Trade{
  5561  		ID:          cexOrderID,
  5562  		Sell:        true,
  5563  		Rate:        5e6,
  5564  		Qty:         5e7,
  5565  		BaseID:      42,
  5566  		QuoteID:     0,
  5567  		BaseFilled:  5e7,
  5568  		QuoteFilled: calc.BaseToQuote(5e6, 5e7),
  5569  		Complete:    true,
  5570  	}
  5571  	adaptor.refreshAllPendingEvents(ctx)
  5572  	expectedCEXAvailableBalance[42] -= 5e7
  5573  	expectedCEXAvailableBalance[0] += calc.BaseToQuote(5e6, 5e7)
  5574  	checkAvailableBalances()
  5575  
  5576  	// Add a pending deposit, then refresh pending events
  5577  	depositTxID := hex.EncodeToString(encode.RandomBytes(32))
  5578  	adaptor.pendingDeposits[depositTxID] = &pendingDeposit{
  5579  		assetID: 42,
  5580  		tx: &asset.WalletTransaction{
  5581  			ID:        depositTxID,
  5582  			Fees:      1000,
  5583  			Amount:    1e7,
  5584  			Confirmed: true,
  5585  		},
  5586  		feeConfirmed: true,
  5587  	}
  5588  	amtReceived := uint64(1e7 - 1000)
  5589  	tCEX.confirmDepositMtx.Lock()
  5590  	tCEX.confirmedDeposit = &amtReceived
  5591  	tCEX.confirmDepositMtx.Unlock()
  5592  	adaptor.refreshAllPendingEvents(ctx)
  5593  	expectedDEXAvailableBalance[42] -= 1e7 + 1000
  5594  	expectedCEXAvailableBalance[42] += amtReceived
  5595  	checkAvailableBalances()
  5596  
  5597  	// Add a pending withdrawal, then refresh pending events
  5598  	withdrawalID := "456"
  5599  	adaptor.pendingWithdrawals[withdrawalID] = &pendingWithdrawal{
  5600  		withdrawalID: withdrawalID,
  5601  		assetID:      42,
  5602  		amtWithdrawn: 2e7,
  5603  	}
  5604  
  5605  	withdrawalTxID := hex.EncodeToString(encode.RandomBytes(32))
  5606  	tCore.walletTxs[withdrawalTxID] = &asset.WalletTransaction{
  5607  		ID:        withdrawalTxID,
  5608  		Amount:    2e7 - 3000,
  5609  		Confirmed: true,
  5610  	}
  5611  
  5612  	tCEX.confirmWithdrawalMtx.Lock()
  5613  	tCEX.confirmWithdrawal = &withdrawArgs{
  5614  		assetID: 42,
  5615  		amt:     2e7,
  5616  		txID:    withdrawalTxID,
  5617  	}
  5618  	tCEX.confirmWithdrawalMtx.Unlock()
  5619  
  5620  	adaptor.refreshAllPendingEvents(ctx)
  5621  	expectedDEXAvailableBalance[42] += 2e7 - 3000
  5622  	expectedCEXAvailableBalance[42] -= 2e7
  5623  	checkAvailableBalances()
  5624  }