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