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 */