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

     1  package mm
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"math"
     9  	"testing"
    10  
    11  	"decred.org/dcrdex/client/core"
    12  	"decred.org/dcrdex/client/mm/libxc"
    13  	"decred.org/dcrdex/dex"
    14  	"decred.org/dcrdex/dex/calc"
    15  	"decred.org/dcrdex/dex/encode"
    16  	"decred.org/dcrdex/dex/order"
    17  )
    18  
    19  func TestArbRebalance(t *testing.T) {
    20  	lotSize := uint64(40e8)
    21  	baseID := uint32(42)
    22  	quoteID := uint32(0)
    23  
    24  	orderIDs := make([]order.OrderID, 5)
    25  	for i := 0; i < 5; i++ {
    26  		copy(orderIDs[i][:], encode.RandomBytes(32))
    27  	}
    28  
    29  	cexTradeIDs := make([]string, 0, 5)
    30  	for i := 0; i < 5; i++ {
    31  		cexTradeIDs = append(cexTradeIDs, fmt.Sprintf("%x", encode.RandomBytes(32)))
    32  	}
    33  
    34  	const currEpoch uint64 = 100
    35  	const numEpochsLeaveOpen uint32 = 10
    36  	const maxActiveArbs uint32 = 5
    37  	const profitTrigger float64 = 0.01
    38  	const feesInQuoteUnits uint64 = 5e5
    39  	const rateStep = 1e5
    40  
    41  	edgeSellRate := func(buyRate, qty uint64, profitable bool) uint64 {
    42  		quoteToBuy := calc.BaseToQuote(buyRate, qty)
    43  		reqFromSell := quoteToBuy + feesInQuoteUnits + calc.BaseToQuote(buyRate, uint64(float64(qty)*profitTrigger))
    44  		sellRate := calc.QuoteToBase(qty, reqFromSell) // quote * 1e8 / base = sellRate
    45  		var steps float64
    46  		if profitable {
    47  			steps = math.Ceil(float64(sellRate) / float64(rateStep))
    48  		} else {
    49  			steps = math.Floor(float64(sellRate) / float64(rateStep))
    50  		}
    51  		return uint64(steps) * rateStep
    52  	}
    53  
    54  	type testBooks struct {
    55  		dexBidsAvg     []uint64
    56  		dexBidsExtrema []uint64
    57  
    58  		dexAsksAvg     []uint64
    59  		dexAsksExtrema []uint64
    60  
    61  		cexBidsAvg     []uint64
    62  		cexBidsExtrema []uint64
    63  
    64  		cexAsksAvg     []uint64
    65  		cexAsksExtrema []uint64
    66  	}
    67  
    68  	noArbBooks := &testBooks{
    69  		dexBidsAvg:     []uint64{1.8e6, 1.7e6},
    70  		dexBidsExtrema: []uint64{1.7e6, 1.6e6},
    71  
    72  		dexAsksAvg:     []uint64{2e6, 2.5e6},
    73  		dexAsksExtrema: []uint64{2e6, 3e6},
    74  
    75  		cexBidsAvg:     []uint64{edgeSellRate(2e6, lotSize, false), 2.1e6},
    76  		cexBidsExtrema: []uint64{2.2e6, 1.9e6},
    77  
    78  		cexAsksAvg:     []uint64{2.4e6, 2.6e6},
    79  		cexAsksExtrema: []uint64{2.5e6, 2.7e6},
    80  	}
    81  
    82  	arbBuyOnDEXBooks := &testBooks{
    83  		dexBidsAvg:     []uint64{1.8e6, 1.7e6},
    84  		dexBidsExtrema: []uint64{1.7e6, 1.6e6},
    85  
    86  		dexAsksAvg:     []uint64{2e6, 2.5e6},
    87  		dexAsksExtrema: []uint64{2e6, 3e6},
    88  
    89  		cexBidsAvg:     []uint64{edgeSellRate(2e6, lotSize, true), 2.1e6},
    90  		cexBidsExtrema: []uint64{2.2e6, 1.9e6},
    91  
    92  		cexAsksAvg:     []uint64{2.4e6, 2.6e6},
    93  		cexAsksExtrema: []uint64{2.5e6, 2.7e6},
    94  	}
    95  
    96  	arbSellOnDEXBooks := &testBooks{
    97  		cexBidsAvg:     []uint64{1.8e6, 1.7e6},
    98  		cexBidsExtrema: []uint64{1.7e6, 1.6e6},
    99  
   100  		cexAsksAvg:     []uint64{2e6, 2.5e6},
   101  		cexAsksExtrema: []uint64{2e6, 3e6},
   102  
   103  		dexBidsAvg:     []uint64{edgeSellRate(2e6, lotSize, true), 2.1e6},
   104  		dexBidsExtrema: []uint64{2.2e6, 1.9e6},
   105  
   106  		dexAsksAvg:     []uint64{2.4e6, 2.6e6},
   107  		dexAsksExtrema: []uint64{2.5e6, 2.7e6},
   108  	}
   109  
   110  	arb2LotsBuyOnDEXBooks := &testBooks{
   111  		dexBidsAvg:     []uint64{1.8e6, 1.7e6},
   112  		dexBidsExtrema: []uint64{1.7e6, 1.6e6},
   113  
   114  		dexAsksAvg:     []uint64{2e6, 2e6, 2.5e6},
   115  		dexAsksExtrema: []uint64{2e6, 2e6, 3e6},
   116  
   117  		cexBidsAvg:     []uint64{2.3e6, 2.2e6, 2.1e6},
   118  		cexBidsExtrema: []uint64{2.2e6, 2.2e6, 1.9e6},
   119  
   120  		cexAsksAvg:     []uint64{2.4e6, 2.6e6},
   121  		cexAsksExtrema: []uint64{2.5e6, 2.7e6},
   122  	}
   123  
   124  	arb2LotsSellOnDEXBooks := &testBooks{
   125  		cexBidsAvg:     []uint64{1.8e6, 1.7e6},
   126  		cexBidsExtrema: []uint64{1.7e6, 1.6e6},
   127  
   128  		cexAsksAvg:     []uint64{2e6, 2e6, 2.5e6},
   129  		cexAsksExtrema: []uint64{2e6, 2e6, 3e6},
   130  
   131  		dexBidsAvg:     []uint64{edgeSellRate(2e6, lotSize, true), edgeSellRate(2e6, lotSize, true), 2.1e6},
   132  		dexBidsExtrema: []uint64{2.2e6, 2.2e6, 1.9e6},
   133  
   134  		dexAsksAvg:     []uint64{2.4e6, 2.6e6},
   135  		dexAsksExtrema: []uint64{2.5e6, 2.7e6},
   136  	}
   137  
   138  	// Arbing 2 lots worth would still be above profit trigger, but the
   139  	// second lot on its own would not be.
   140  	arb2LotsButOneWorth := &testBooks{
   141  		dexBidsAvg:     []uint64{1.8e6, 1.7e6},
   142  		dexBidsExtrema: []uint64{1.7e6, 1.6e6},
   143  
   144  		dexAsksAvg:     []uint64{2e6, 2.1e6},
   145  		dexAsksExtrema: []uint64{2e6, 2.2e6},
   146  
   147  		cexBidsAvg:     []uint64{2.3e6, 2.122e6},
   148  		cexBidsExtrema: []uint64{2.2e6, 2.1e6},
   149  
   150  		cexAsksAvg:     []uint64{2.4e6, 2.6e6},
   151  		cexAsksExtrema: []uint64{2.5e6, 2.7e6},
   152  	}
   153  
   154  	type test struct {
   155  		name          string
   156  		books         *testBooks
   157  		dexVWAPErr    error
   158  		cexVWAPErr    error
   159  		cexTradeErr   error
   160  		existingArbs  []*arbSequence
   161  		dexMaxBuyQty  uint64
   162  		dexMaxSellQty uint64
   163  		cexMaxBuyQty  uint64
   164  		cexMaxSellQty uint64
   165  
   166  		expectedDexOrder   *dexOrder
   167  		expectedCexOrder   *libxc.Trade
   168  		expectedDEXCancels []dex.Bytes
   169  		expectedCEXCancels []string
   170  	}
   171  
   172  	tests := []test{
   173  		// "no arb"
   174  		{
   175  			name:  "no arb",
   176  			books: noArbBooks,
   177  		},
   178  		// "1 lot, buy on dex, sell on cex"
   179  		{
   180  			name:          "1 lot, buy on dex, sell on cex",
   181  			books:         arbBuyOnDEXBooks,
   182  			dexMaxSellQty: 5 * lotSize,
   183  			dexMaxBuyQty:  5 * lotSize,
   184  			cexMaxSellQty: 5 * lotSize,
   185  			cexMaxBuyQty:  5 * lotSize,
   186  			expectedDexOrder: &dexOrder{
   187  				qty:  lotSize,
   188  				rate: 2e6,
   189  				sell: false,
   190  			},
   191  			expectedCexOrder: &libxc.Trade{
   192  				BaseID:  42,
   193  				QuoteID: 0,
   194  				Qty:     lotSize,
   195  				Rate:    2.2e6,
   196  				Sell:    true,
   197  			},
   198  		},
   199  		// "1 lot, sell on dex, buy on cex"
   200  		{
   201  			name:          "1 lot, sell on dex, buy on cex",
   202  			books:         arbSellOnDEXBooks,
   203  			dexMaxSellQty: 5 * lotSize,
   204  			dexMaxBuyQty:  5 * lotSize,
   205  			cexMaxSellQty: 5 * lotSize,
   206  			cexMaxBuyQty:  5 * lotSize,
   207  			expectedDexOrder: &dexOrder{
   208  				qty:  lotSize,
   209  				rate: 2.2e6,
   210  				sell: true,
   211  			},
   212  			expectedCexOrder: &libxc.Trade{
   213  				BaseID:  42,
   214  				QuoteID: 0,
   215  				Qty:     lotSize,
   216  				Rate:    2e6,
   217  				Sell:    false,
   218  			},
   219  		},
   220  		// "1 lot, buy on dex, sell on cex, but dex base balance not enough"
   221  		{
   222  			name:          "1 lot, buy on dex, sell on cex, but cex balance not enough",
   223  			books:         arbBuyOnDEXBooks,
   224  			dexMaxSellQty: 5 * lotSize,
   225  			dexMaxBuyQty:  5 * lotSize,
   226  			cexMaxSellQty: 0,
   227  			cexMaxBuyQty:  5 * lotSize,
   228  		},
   229  		// "2 lot, buy on dex, sell on cex, but dex quote balance only enough for 1"
   230  		{
   231  			name:          "2 lot, buy on dex, sell on cex, but dex quote balance only enough for 1",
   232  			books:         arb2LotsBuyOnDEXBooks,
   233  			dexMaxBuyQty:  1 * lotSize,
   234  			cexMaxSellQty: 5 * lotSize,
   235  			expectedDexOrder: &dexOrder{
   236  				qty:  lotSize,
   237  				rate: 2e6,
   238  				sell: false,
   239  			},
   240  			expectedCexOrder: &libxc.Trade{
   241  				BaseID:  42,
   242  				QuoteID: 0,
   243  				Qty:     lotSize,
   244  				Rate:    2.2e6,
   245  				Sell:    true,
   246  			},
   247  		},
   248  		// "2 lot, buy on cex, sell on dex, but cex quote balance only enough for 1"
   249  		{
   250  			name:          "2 lot, buy on cex, sell on dex, but cex quote balance only enough for 1",
   251  			books:         arb2LotsSellOnDEXBooks,
   252  			dexMaxSellQty: 5 * lotSize,
   253  			cexMaxBuyQty:  lotSize,
   254  			expectedDexOrder: &dexOrder{
   255  				qty:  lotSize,
   256  				rate: 2.2e6,
   257  				sell: true,
   258  			},
   259  			expectedCexOrder: &libxc.Trade{
   260  				BaseID:  42,
   261  				QuoteID: 0,
   262  				Qty:     lotSize,
   263  				Rate:    2e6,
   264  				Sell:    false,
   265  			},
   266  		},
   267  		// "2 lots arb still above profit trigger, but second not worth it on its own"
   268  		{
   269  			name:          "2 lots arb still above profit trigger, but second not worth it on its own",
   270  			books:         arb2LotsButOneWorth,
   271  			dexMaxSellQty: 5 * lotSize,
   272  			dexMaxBuyQty:  5 * lotSize,
   273  			cexMaxSellQty: 5 * lotSize,
   274  			cexMaxBuyQty:  5 * lotSize,
   275  			expectedDexOrder: &dexOrder{
   276  				qty:  lotSize,
   277  				rate: 2e6,
   278  				sell: false,
   279  			},
   280  			expectedCexOrder: &libxc.Trade{
   281  				BaseID:  42,
   282  				QuoteID: 0,
   283  				Qty:     lotSize,
   284  				Rate:    2.2e6,
   285  				Sell:    true,
   286  			},
   287  		},
   288  		// "cex no asks"
   289  		{
   290  			name: "cex no asks",
   291  			books: &testBooks{
   292  				dexBidsAvg:     []uint64{1.8e6, 1.7e6},
   293  				dexBidsExtrema: []uint64{1.7e6, 1.6e6},
   294  
   295  				dexAsksAvg:     []uint64{2e6, 2.5e6},
   296  				dexAsksExtrema: []uint64{2e6, 3e6},
   297  
   298  				cexBidsAvg:     []uint64{1.9e6, 1.8e6},
   299  				cexBidsExtrema: []uint64{1.85e6, 1.75e6},
   300  
   301  				cexAsksAvg:     []uint64{},
   302  				cexAsksExtrema: []uint64{},
   303  			},
   304  			dexMaxSellQty: 5 * lotSize,
   305  			dexMaxBuyQty:  5 * lotSize,
   306  			cexMaxSellQty: 5 * lotSize,
   307  			cexMaxBuyQty:  5 * lotSize,
   308  		},
   309  		// "dex no asks"
   310  		{
   311  			name: "dex no asks",
   312  			books: &testBooks{
   313  				dexBidsAvg:     []uint64{1.8e6, 1.7e6},
   314  				dexBidsExtrema: []uint64{1.7e6, 1.6e6},
   315  
   316  				dexAsksAvg:     []uint64{},
   317  				dexAsksExtrema: []uint64{},
   318  
   319  				cexBidsAvg:     []uint64{1.9e6, 1.8e6},
   320  				cexBidsExtrema: []uint64{1.85e6, 1.75e6},
   321  
   322  				cexAsksAvg:     []uint64{2.1e6, 2.2e6},
   323  				cexAsksExtrema: []uint64{2.2e6, 2.3e6},
   324  			},
   325  			dexMaxSellQty: 5 * lotSize,
   326  			dexMaxBuyQty:  5 * lotSize,
   327  			cexMaxSellQty: 5 * lotSize,
   328  			cexMaxBuyQty:  5 * lotSize,
   329  		},
   330  		// "self-match"
   331  		{
   332  			name:  "self-match",
   333  			books: arbSellOnDEXBooks,
   334  			existingArbs: []*arbSequence{{
   335  				dexOrder: &core.Order{
   336  					ID:   orderIDs[0][:],
   337  					Rate: 2.2e6,
   338  				},
   339  				cexOrderID: cexTradeIDs[0],
   340  				sellOnDEX:  false,
   341  				startEpoch: currEpoch - 2,
   342  			}},
   343  			dexMaxSellQty: 5 * lotSize,
   344  			dexMaxBuyQty:  5 * lotSize,
   345  			cexMaxSellQty: 5 * lotSize,
   346  			cexMaxBuyQty:  5 * lotSize,
   347  
   348  			expectedCEXCancels: []string{cexTradeIDs[0]},
   349  			expectedDEXCancels: []dex.Bytes{orderIDs[0][:]},
   350  		},
   351  		// "remove expired active arbs"
   352  		{
   353  			name:          "remove expired active arbs",
   354  			books:         noArbBooks,
   355  			dexMaxSellQty: 5 * lotSize,
   356  			dexMaxBuyQty:  5 * lotSize,
   357  			cexMaxSellQty: 5 * lotSize,
   358  			cexMaxBuyQty:  5 * lotSize,
   359  			existingArbs: []*arbSequence{
   360  				{
   361  					dexOrder: &core.Order{
   362  						ID: orderIDs[0][:],
   363  					},
   364  					cexOrderID: cexTradeIDs[0],
   365  					sellOnDEX:  false,
   366  					startEpoch: currEpoch - 2,
   367  				},
   368  				{
   369  					dexOrder: &core.Order{
   370  						ID: orderIDs[1][:],
   371  					},
   372  					cexOrderID: cexTradeIDs[1],
   373  					sellOnDEX:  false,
   374  					startEpoch: currEpoch - (uint64(numEpochsLeaveOpen) + 2),
   375  				},
   376  				{
   377  					dexOrder: &core.Order{
   378  						ID: orderIDs[2][:],
   379  					},
   380  					cexOrderID:     cexTradeIDs[2],
   381  					sellOnDEX:      false,
   382  					cexOrderFilled: true,
   383  					startEpoch:     currEpoch - (uint64(numEpochsLeaveOpen) + 2),
   384  				},
   385  				{
   386  					dexOrder: &core.Order{
   387  						ID: orderIDs[3][:],
   388  					},
   389  					cexOrderID:     cexTradeIDs[3],
   390  					sellOnDEX:      false,
   391  					dexOrderFilled: true,
   392  					startEpoch:     currEpoch - (uint64(numEpochsLeaveOpen) + 2),
   393  				},
   394  			},
   395  			expectedCEXCancels: []string{cexTradeIDs[1], cexTradeIDs[3]},
   396  			expectedDEXCancels: []dex.Bytes{orderIDs[1][:], orderIDs[2][:]},
   397  		},
   398  		// "already max active arbs"
   399  		{
   400  			name:          "already max active arbs",
   401  			books:         arbBuyOnDEXBooks,
   402  			dexMaxSellQty: 5 * lotSize,
   403  			dexMaxBuyQty:  5 * lotSize,
   404  			cexMaxSellQty: 5 * lotSize,
   405  			cexMaxBuyQty:  5 * lotSize,
   406  			existingArbs: []*arbSequence{
   407  				{
   408  					dexOrder: &core.Order{
   409  						ID: orderIDs[0][:],
   410  					},
   411  					cexOrderID: cexTradeIDs[0],
   412  					sellOnDEX:  false,
   413  					startEpoch: currEpoch - 1,
   414  				},
   415  				{
   416  					dexOrder: &core.Order{
   417  						ID: orderIDs[1][:],
   418  					},
   419  					cexOrderID: cexTradeIDs[2],
   420  					sellOnDEX:  false,
   421  					startEpoch: currEpoch - 2,
   422  				},
   423  				{
   424  					dexOrder: &core.Order{
   425  						ID: orderIDs[2][:],
   426  					},
   427  					cexOrderID: cexTradeIDs[2],
   428  					sellOnDEX:  false,
   429  					startEpoch: currEpoch - 3,
   430  				},
   431  				{
   432  					dexOrder: &core.Order{
   433  						ID: orderIDs[3][:],
   434  					},
   435  					cexOrderID: cexTradeIDs[3],
   436  					sellOnDEX:  false,
   437  					startEpoch: currEpoch - 4,
   438  				},
   439  				{
   440  					dexOrder: &core.Order{
   441  						ID: orderIDs[4][:],
   442  					},
   443  					cexOrderID: cexTradeIDs[4],
   444  					sellOnDEX:  false,
   445  					startEpoch: currEpoch - 5,
   446  				},
   447  			},
   448  		},
   449  		// "cex trade error"
   450  		{
   451  			name:          "cex trade error",
   452  			books:         arbBuyOnDEXBooks,
   453  			dexMaxSellQty: 5 * lotSize,
   454  			dexMaxBuyQty:  5 * lotSize,
   455  			cexMaxSellQty: 5 * lotSize,
   456  			cexMaxBuyQty:  5 * lotSize,
   457  			cexTradeErr:   errors.New(""),
   458  		},
   459  	}
   460  
   461  	runTest := func(test *test) {
   462  		t.Run(test.name, func(t *testing.T) {
   463  			cex := newTBotCEXAdaptor()
   464  			tcex := newTCEX()
   465  			tcex.vwapErr = test.cexVWAPErr
   466  			cex.tradeErr = test.cexTradeErr
   467  			cex.maxBuyQty = test.cexMaxBuyQty
   468  			cex.maxSellQty = test.cexMaxSellQty
   469  
   470  			tc := newTCore()
   471  			coreAdaptor := newTBotCoreAdaptor(tc)
   472  			coreAdaptor.buyFeesInQuote = feesInQuoteUnits
   473  			coreAdaptor.sellFeesInQuote = feesInQuoteUnits
   474  			coreAdaptor.maxBuyQty = test.dexMaxBuyQty
   475  			coreAdaptor.maxSellQty = test.dexMaxSellQty
   476  
   477  			if test.expectedDexOrder != nil {
   478  				coreAdaptor.tradeResult = &core.Order{
   479  					Qty:  test.expectedDexOrder.qty,
   480  					Rate: test.expectedDexOrder.rate,
   481  					Sell: test.expectedDexOrder.sell,
   482  				}
   483  			}
   484  
   485  			orderBook := &tOrderBook{
   486  				bidsVWAP: make(map[uint64]vwapResult),
   487  				asksVWAP: make(map[uint64]vwapResult),
   488  				vwapErr:  test.dexVWAPErr,
   489  			}
   490  			for i := range test.books.dexBidsAvg {
   491  				orderBook.bidsVWAP[uint64(i+1)] = vwapResult{test.books.dexBidsAvg[i], test.books.dexBidsExtrema[i]}
   492  			}
   493  			for i := range test.books.dexAsksAvg {
   494  				orderBook.asksVWAP[uint64(i+1)] = vwapResult{test.books.dexAsksAvg[i], test.books.dexAsksExtrema[i]}
   495  			}
   496  			for i := range test.books.cexBidsAvg {
   497  				tcex.bidsVWAP[uint64(i+1)*lotSize] = vwapResult{test.books.cexBidsAvg[i], test.books.cexBidsExtrema[i]}
   498  			}
   499  			for i := range test.books.cexAsksAvg {
   500  				tcex.asksVWAP[uint64(i+1)*lotSize] = vwapResult{test.books.cexAsksAvg[i], test.books.cexAsksExtrema[i]}
   501  			}
   502  
   503  			u := mustParseAdaptorFromMarket(&core.Market{
   504  				LotSize:  lotSize,
   505  				BaseID:   baseID,
   506  				QuoteID:  quoteID,
   507  				RateStep: 1e2,
   508  			})
   509  			u.clientCore.(*tCore).userParcels = 0
   510  			u.clientCore.(*tCore).parcelLimit = 1
   511  
   512  			a := &simpleArbMarketMaker{
   513  				unifiedExchangeAdaptor: u,
   514  				cex:                    cex,
   515  				core:                   coreAdaptor,
   516  				activeArbs:             test.existingArbs,
   517  			}
   518  			const sellSwapFees, sellRedeemFees = 3e5, 1e5
   519  			const buySwapFees, buyRedeemFees = 2e4, 1e4
   520  			const buyRate, sellRate = 1e7, 1.1e7
   521  			a.CEX = tcex
   522  			a.buyFees = &OrderFees{
   523  				LotFeeRange: &LotFeeRange{
   524  					Max: &LotFees{
   525  						Redeem: buyRedeemFees,
   526  					},
   527  					Estimated: &LotFees{
   528  						Swap:   buySwapFees,
   529  						Redeem: buyRedeemFees,
   530  					},
   531  				},
   532  				BookingFeesPerLot: buySwapFees,
   533  			}
   534  			a.sellFees = &OrderFees{
   535  				LotFeeRange: &LotFeeRange{
   536  					Max: &LotFees{
   537  						Redeem: sellRedeemFees,
   538  					},
   539  					Estimated: &LotFees{
   540  						Swap:   sellSwapFees,
   541  						Redeem: sellRedeemFees,
   542  					},
   543  				},
   544  				BookingFeesPerLot: sellSwapFees,
   545  			}
   546  			// arbEngine.setBotLoop(arbEngine.botLoop)
   547  			a.cfgV.Store(&SimpleArbConfig{
   548  				ProfitTrigger:      profitTrigger,
   549  				MaxActiveArbs:      maxActiveArbs,
   550  				NumEpochsLeaveOpen: numEpochsLeaveOpen,
   551  			})
   552  			a.book = orderBook
   553  			a.rebalance(currEpoch)
   554  
   555  			// Check dex trade
   556  			if test.expectedDexOrder == nil != (coreAdaptor.lastTradePlaced == nil) {
   557  				t.Fatalf("%s: expected dex order %v but got %v", test.name, (test.expectedDexOrder != nil), (coreAdaptor.lastTradePlaced != nil))
   558  			}
   559  			if test.expectedDexOrder != nil {
   560  				if test.expectedDexOrder.rate != coreAdaptor.lastTradePlaced.rate {
   561  					t.Fatalf("%s: expected sell order rate %d but got %d", test.name, test.expectedDexOrder.rate, coreAdaptor.lastTradePlaced.rate)
   562  				}
   563  				if test.expectedDexOrder.qty != coreAdaptor.lastTradePlaced.qty {
   564  					t.Fatalf("%s: expected sell order qty %d but got %d", test.name, test.expectedDexOrder.qty, coreAdaptor.lastTradePlaced.qty)
   565  				}
   566  				if test.expectedDexOrder.sell != coreAdaptor.lastTradePlaced.sell {
   567  					t.Fatalf("%s: expected sell order sell %v but got %v", test.name, test.expectedDexOrder.sell, coreAdaptor.lastTradePlaced.sell)
   568  				}
   569  			}
   570  
   571  			// Check cex trade
   572  			if (test.expectedCexOrder == nil) != (cex.lastTrade == nil) {
   573  				t.Fatalf("%s: expected cex order %v but got %v", test.name, (test.expectedCexOrder != nil), (cex.lastTrade != nil))
   574  			}
   575  			if cex.lastTrade != nil &&
   576  				*cex.lastTrade != *test.expectedCexOrder {
   577  				t.Fatalf("%s: cex order %+v != expected %+v", test.name, cex.lastTrade, test.expectedCexOrder)
   578  			}
   579  
   580  			// Check dex cancels
   581  			if len(test.expectedDEXCancels) != len(tc.cancelsPlaced) {
   582  				t.Fatalf("%s: expected %d cancels but got %d", test.name, len(test.expectedDEXCancels), len(tc.cancelsPlaced))
   583  			}
   584  			for i := range test.expectedDEXCancels {
   585  				if !bytes.Equal(test.expectedDEXCancels[i], tc.cancelsPlaced[i][:]) {
   586  					t.Fatalf("%s: expected cancel %x but got %x", test.name, test.expectedDEXCancels[i], tc.cancelsPlaced[i])
   587  				}
   588  			}
   589  
   590  			// Check cex cancels
   591  			if len(test.expectedCEXCancels) != len(cex.cancelledTrades) {
   592  				t.Fatalf("%s: expected %d cex cancels but got %d", test.name, len(test.expectedCEXCancels), len(cex.cancelledTrades))
   593  			}
   594  			for i := range test.expectedCEXCancels {
   595  				if test.expectedCEXCancels[i] != cex.cancelledTrades[i] {
   596  					t.Fatalf("%s: expected cex cancel %s but got %s", test.name, test.expectedCEXCancels[i], cex.cancelledTrades[i])
   597  				}
   598  			}
   599  		})
   600  	}
   601  
   602  	for _, test := range tests {
   603  		runTest(&test)
   604  	}
   605  }
   606  
   607  func TestArbDexTradeUpdates(t *testing.T) {
   608  	orderIDs := make([]order.OrderID, 5)
   609  	for i := 0; i < 5; i++ {
   610  		copy(orderIDs[i][:], encode.RandomBytes(32))
   611  	}
   612  
   613  	cexTradeIDs := make([]string, 0, 5)
   614  	for i := 0; i < 5; i++ {
   615  		cexTradeIDs = append(cexTradeIDs, fmt.Sprintf("%x", encode.RandomBytes(32)))
   616  	}
   617  
   618  	type test struct {
   619  		name               string
   620  		activeArbs         []*arbSequence
   621  		updatedOrderID     []byte
   622  		updatedOrderStatus order.OrderStatus
   623  		expectedActiveArbs []*arbSequence
   624  	}
   625  
   626  	dexOrder := &core.Order{
   627  		ID: orderIDs[0][:],
   628  	}
   629  
   630  	tests := []*test{
   631  		{
   632  			name: "dex order still booked",
   633  			activeArbs: []*arbSequence{
   634  				{
   635  					dexOrder:   dexOrder,
   636  					cexOrderID: cexTradeIDs[0],
   637  				},
   638  			},
   639  			updatedOrderID:     orderIDs[0][:],
   640  			updatedOrderStatus: order.OrderStatusBooked,
   641  			expectedActiveArbs: []*arbSequence{
   642  				{
   643  					dexOrder:   dexOrder,
   644  					cexOrderID: cexTradeIDs[0],
   645  				},
   646  			},
   647  		},
   648  		{
   649  			name: "dex order executed, but cex not yet filled",
   650  			activeArbs: []*arbSequence{
   651  				{
   652  					dexOrder:   dexOrder,
   653  					cexOrderID: cexTradeIDs[0],
   654  				},
   655  			},
   656  			updatedOrderID:     orderIDs[0][:],
   657  			updatedOrderStatus: order.OrderStatusExecuted,
   658  			expectedActiveArbs: []*arbSequence{
   659  				{
   660  					dexOrder:       dexOrder,
   661  					cexOrderID:     cexTradeIDs[0],
   662  					dexOrderFilled: true,
   663  				},
   664  			},
   665  		},
   666  		{
   667  			name: "dex order executed, but cex already filled",
   668  			activeArbs: []*arbSequence{
   669  				{
   670  					dexOrder:       dexOrder,
   671  					cexOrderID:     cexTradeIDs[0],
   672  					cexOrderFilled: true,
   673  				},
   674  			},
   675  			updatedOrderID:     orderIDs[0][:],
   676  			updatedOrderStatus: order.OrderStatusExecuted,
   677  			expectedActiveArbs: []*arbSequence{},
   678  		},
   679  	}
   680  
   681  	runTest := func(test *test) {
   682  		cex := newTBotCEXAdaptor()
   683  		coreAdaptor := newTBotCoreAdaptor(newTCore())
   684  
   685  		ctx, cancel := context.WithCancel(context.Background())
   686  		defer cancel()
   687  
   688  		arbEngine := &simpleArbMarketMaker{
   689  			unifiedExchangeAdaptor: mustParseAdaptorFromMarket(&core.Market{
   690  				BaseID:  42,
   691  				QuoteID: 0,
   692  			}),
   693  			cex:        cex,
   694  			core:       coreAdaptor,
   695  			activeArbs: test.activeArbs,
   696  		}
   697  		arbEngine.clientCore = newTCore()
   698  		arbEngine.CEX = newTCEX()
   699  		arbEngine.ctx = ctx
   700  		arbEngine.setBotLoop(arbEngine.botLoop)
   701  		arbEngine.cfgV.Store(&SimpleArbConfig{
   702  			ProfitTrigger:      0.01,
   703  			MaxActiveArbs:      5,
   704  			NumEpochsLeaveOpen: 10,
   705  		})
   706  		err := arbEngine.runBotLoop(ctx)
   707  		if err != nil {
   708  			t.Fatalf("%s: Connect error: %v", test.name, err)
   709  		}
   710  
   711  		coreAdaptor.orderUpdates <- &core.Order{
   712  			Status: test.updatedOrderStatus,
   713  			ID:     test.updatedOrderID,
   714  		}
   715  		coreAdaptor.orderUpdates <- &core.Order{}
   716  
   717  		if len(test.expectedActiveArbs) != len(arbEngine.activeArbs) {
   718  			t.Fatalf("%s: expected %d active arbs but got %d", test.name, len(test.expectedActiveArbs), len(arbEngine.activeArbs))
   719  		}
   720  
   721  		for i := range test.expectedActiveArbs {
   722  			if *arbEngine.activeArbs[i] != *test.expectedActiveArbs[i] {
   723  				t.Fatalf("%s: active arb %+v != expected active arb %+v", test.name, arbEngine.activeArbs[i], test.expectedActiveArbs[i])
   724  			}
   725  		}
   726  	}
   727  
   728  	for _, test := range tests {
   729  		runTest(test)
   730  	}
   731  }
   732  
   733  func TestCexTradeUpdates(t *testing.T) {
   734  	orderIDs := make([]order.OrderID, 5)
   735  	for i := 0; i < 5; i++ {
   736  		copy(orderIDs[i][:], encode.RandomBytes(32))
   737  	}
   738  
   739  	cexTradeIDs := make([]string, 0, 5)
   740  	for i := 0; i < 5; i++ {
   741  		cexTradeIDs = append(cexTradeIDs, fmt.Sprintf("%x", encode.RandomBytes(32)))
   742  	}
   743  
   744  	dexOrder := &core.Order{
   745  		ID: orderIDs[0][:],
   746  	}
   747  
   748  	type test struct {
   749  		name               string
   750  		activeArbs         []*arbSequence
   751  		updatedOrderID     string
   752  		orderComplete      bool
   753  		expectedActiveArbs []*arbSequence
   754  	}
   755  
   756  	tests := []*test{
   757  		{
   758  			name: "neither complete",
   759  			activeArbs: []*arbSequence{
   760  				{
   761  					dexOrder:   dexOrder,
   762  					cexOrderID: cexTradeIDs[0],
   763  				},
   764  			},
   765  			updatedOrderID: cexTradeIDs[0],
   766  			orderComplete:  false,
   767  			expectedActiveArbs: []*arbSequence{
   768  				{
   769  					dexOrder:   dexOrder,
   770  					cexOrderID: cexTradeIDs[0],
   771  				},
   772  			},
   773  		},
   774  		{
   775  			name: "cex complete, but dex order not complete",
   776  			activeArbs: []*arbSequence{
   777  				{
   778  					dexOrder:   dexOrder,
   779  					cexOrderID: cexTradeIDs[0],
   780  				},
   781  			},
   782  			updatedOrderID: cexTradeIDs[0],
   783  			orderComplete:  true,
   784  			expectedActiveArbs: []*arbSequence{
   785  				{
   786  					dexOrder:       dexOrder,
   787  					cexOrderID:     cexTradeIDs[0],
   788  					cexOrderFilled: true,
   789  				},
   790  			},
   791  		},
   792  		{
   793  			name: "both complete",
   794  			activeArbs: []*arbSequence{
   795  				{
   796  					dexOrder:       dexOrder,
   797  					cexOrderID:     cexTradeIDs[0],
   798  					dexOrderFilled: true,
   799  				},
   800  			},
   801  			updatedOrderID: cexTradeIDs[0],
   802  			orderComplete:  true,
   803  		},
   804  	}
   805  
   806  	runTest := func(test *test) {
   807  		cex := newTBotCEXAdaptor()
   808  		ctx, cancel := context.WithCancel(context.Background())
   809  		defer cancel()
   810  
   811  		arbEngine := &simpleArbMarketMaker{
   812  			unifiedExchangeAdaptor: mustParseAdaptorFromMarket(&core.Market{
   813  				BaseID:  42,
   814  				QuoteID: 0,
   815  			}),
   816  			cex:        cex,
   817  			core:       newTBotCoreAdaptor(newTCore()),
   818  			activeArbs: test.activeArbs,
   819  		}
   820  		arbEngine.ctx = ctx
   821  		arbEngine.CEX = newTCEX()
   822  		arbEngine.setBotLoop(arbEngine.botLoop)
   823  		arbEngine.cfgV.Store(&SimpleArbConfig{
   824  			ProfitTrigger:      0.01,
   825  			MaxActiveArbs:      5,
   826  			NumEpochsLeaveOpen: 10,
   827  		})
   828  
   829  		err := arbEngine.runBotLoop(ctx)
   830  		if err != nil {
   831  			t.Fatalf("%s: Connect error: %v", test.name, err)
   832  		}
   833  
   834  		cex.tradeUpdates <- &libxc.Trade{
   835  			ID:       test.updatedOrderID,
   836  			Complete: test.orderComplete,
   837  		}
   838  		// send dummy update
   839  		cex.tradeUpdates <- &libxc.Trade{
   840  			ID: "",
   841  		}
   842  
   843  		if len(test.expectedActiveArbs) != len(arbEngine.activeArbs) {
   844  			t.Fatalf("%s: expected %d active arbs but got %d", test.name, len(test.expectedActiveArbs), len(arbEngine.activeArbs))
   845  		}
   846  		for i := range test.expectedActiveArbs {
   847  			if *arbEngine.activeArbs[i] != *test.expectedActiveArbs[i] {
   848  				t.Fatalf("%s: active arb %+v != expected active arb %+v", test.name, arbEngine.activeArbs[i], test.expectedActiveArbs[i])
   849  			}
   850  		}
   851  	}
   852  
   853  	for _, test := range tests {
   854  		runTest(test)
   855  	}
   856  }
   857  
   858  /*func TestArbBotProblems(t *testing.T) {
   859  	const baseID, quoteID = 42, 0
   860  	const lotSize uint64 = 5e9
   861  	const sellSwapFees, sellRedeemFees = 3e6, 1e6
   862  	const buySwapFees, buyRedeemFees = 2e5, 1e5
   863  	const buyRate, sellRate = 1e7, 1.1e7
   864  
   865  	type test struct {
   866  		name            string
   867  		userLimitTooLow bool
   868  		dexBalanceDefs  map[uint32]uint64
   869  		cexBalanceDefs  map[uint32]uint64
   870  
   871  		expBotProblems *BotProblems
   872  	}
   873  
   874  	updateBotProblems := func(f func(*BotProblems)) *BotProblems {
   875  		bp := newBotProblems()
   876  		f(bp)
   877  		return bp
   878  	}
   879  
   880  	tests := []*test{
   881  		{
   882  			name:           "no problems",
   883  			expBotProblems: newBotProblems(),
   884  		},
   885  		{
   886  			name:            "user limit too low",
   887  			userLimitTooLow: true,
   888  			expBotProblems: updateBotProblems(func(bp *BotProblems) {
   889  				bp.UserLimitTooLow = true
   890  			}),
   891  		},
   892  		{
   893  			name: "balance deficiencies",
   894  			dexBalanceDefs: map[uint32]uint64{
   895  				baseID:  lotSize + sellSwapFees,
   896  				quoteID: calc.BaseToQuote(buyRate, lotSize) + buySwapFees,
   897  			},
   898  			cexBalanceDefs: map[uint32]uint64{
   899  				baseID:  lotSize,
   900  				quoteID: calc.BaseToQuote(sellRate, lotSize),
   901  			},
   902  			expBotProblems: updateBotProblems(func(bp *BotProblems) {
   903  				// All these values are multiplied by 2 because the same deficiencies
   904  				// are returned for buys and sells, and they are summed.
   905  				bp.DEXBalanceDeficiencies = map[uint32]uint64{
   906  					baseID:  (lotSize + sellSwapFees) * 2,
   907  					quoteID: (calc.BaseToQuote(buyRate, lotSize) + buySwapFees) * 2,
   908  				}
   909  				bp.CEXBalanceDeficiencies = map[uint32]uint64{
   910  					baseID:  lotSize * 2,
   911  					quoteID: calc.BaseToQuote(sellRate, lotSize) * 2,
   912  				}
   913  			}),
   914  		},
   915  	}
   916  
   917  	runTest := func(tt *test) {
   918  		t.Run(tt.name, func(t *testing.T) {
   919  			cex := newTCEX()
   920  			mkt := &core.Market{
   921  				RateStep:   1e3,
   922  				AtomToConv: 1,
   923  				LotSize:    lotSize,
   924  				BaseID:     baseID,
   925  				QuoteID:    quoteID,
   926  			}
   927  			u := mustParseAdaptorFromMarket(mkt)
   928  			u.CEX = cex
   929  			u.botCfgV.Store(&BotConfig{})
   930  			c := newTCore()
   931  			if !tt.userLimitTooLow {
   932  				u.clientCore.(*tCore).userParcels = 0
   933  				u.clientCore.(*tCore).parcelLimit = 1
   934  			}
   935  			u.fiatRates.Store(map[uint32]float64{baseID: 1, quoteID: 1})
   936  			cexAdaptor := newTBotCEXAdaptor()
   937  			coreAdaptor := newTBotCoreAdaptor(c)
   938  			a := &simpleArbMarketMaker{
   939  				unifiedExchangeAdaptor: u,
   940  				cex:                    cexAdaptor,
   941  				core:                   coreAdaptor,
   942  			}
   943  
   944  			coreAdaptor.balanceDefs = tt.dexBalanceDefs
   945  			cexAdaptor.balanceDefs = tt.cexBalanceDefs
   946  
   947  			a.cfgV.Store(&SimpleArbConfig{})
   948  
   949  			cex.asksVWAP[lotSize] = vwapResult{
   950  				avg:     buyRate,
   951  				extrema: buyRate,
   952  			}
   953  			cex.bidsVWAP[lotSize] = vwapResult{
   954  				avg:     sellRate,
   955  				extrema: sellRate,
   956  			}
   957  
   958  			a.book = &tOrderBook{
   959  				bidsVWAP: map[uint64]vwapResult{
   960  					1: {
   961  						avg:     buyRate,
   962  						extrema: buyRate,
   963  					},
   964  				},
   965  				asksVWAP: map[uint64]vwapResult{
   966  					1: {
   967  						avg:     sellRate,
   968  						extrema: sellRate,
   969  					},
   970  				},
   971  			}
   972  
   973  			a.buyFees = &OrderFees{
   974  				LotFeeRange: &LotFeeRange{
   975  					Max: &LotFees{
   976  						Redeem: buyRedeemFees,
   977  						Swap:   buySwapFees,
   978  					},
   979  					Estimated: &LotFees{},
   980  				},
   981  				BookingFeesPerLot: buySwapFees,
   982  			}
   983  			a.sellFees = &OrderFees{
   984  				LotFeeRange: &LotFeeRange{
   985  					Max: &LotFees{
   986  						Redeem: sellRedeemFees,
   987  						Swap:   sellSwapFees,
   988  					},
   989  					Estimated: &LotFees{},
   990  				},
   991  				BookingFeesPerLot: sellSwapFees,
   992  			}
   993  
   994  			a.rebalance(1)
   995  
   996  			problems := a.problems()
   997  			if !reflect.DeepEqual(tt.expBotProblems, problems) {
   998  				t.Fatalf("expected bot problems %v, got %v", tt.expBotProblems, problems)
   999  			}
  1000  		})
  1001  	}
  1002  
  1003  	for _, test := range tests {
  1004  		runTest(test)
  1005  	}
  1006  }
  1007  */