decred.org/dcrdex@v1.0.5/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.unifiedExchangeAdaptor.botCfgV.Store(&BotConfig{ 548 SimpleArbConfig: &SimpleArbConfig{ 549 ProfitTrigger: profitTrigger, 550 MaxActiveArbs: maxActiveArbs, 551 NumEpochsLeaveOpen: numEpochsLeaveOpen, 552 }, 553 }) 554 a.book = orderBook 555 a.rebalance(currEpoch) 556 557 // Check dex trade 558 if test.expectedDexOrder == nil != (coreAdaptor.lastTradePlaced == nil) { 559 t.Fatalf("%s: expected dex order %v but got %v", test.name, (test.expectedDexOrder != nil), (coreAdaptor.lastTradePlaced != nil)) 560 } 561 if test.expectedDexOrder != nil { 562 if test.expectedDexOrder.rate != coreAdaptor.lastTradePlaced.rate { 563 t.Fatalf("%s: expected sell order rate %d but got %d", test.name, test.expectedDexOrder.rate, coreAdaptor.lastTradePlaced.rate) 564 } 565 if test.expectedDexOrder.qty != coreAdaptor.lastTradePlaced.qty { 566 t.Fatalf("%s: expected sell order qty %d but got %d", test.name, test.expectedDexOrder.qty, coreAdaptor.lastTradePlaced.qty) 567 } 568 if test.expectedDexOrder.sell != coreAdaptor.lastTradePlaced.sell { 569 t.Fatalf("%s: expected sell order sell %v but got %v", test.name, test.expectedDexOrder.sell, coreAdaptor.lastTradePlaced.sell) 570 } 571 } 572 573 // Check cex trade 574 if (test.expectedCexOrder == nil) != (cex.lastTrade == nil) { 575 t.Fatalf("%s: expected cex order %v but got %v", test.name, (test.expectedCexOrder != nil), (cex.lastTrade != nil)) 576 } 577 if cex.lastTrade != nil && 578 *cex.lastTrade != *test.expectedCexOrder { 579 t.Fatalf("%s: cex order %+v != expected %+v", test.name, cex.lastTrade, test.expectedCexOrder) 580 } 581 582 // Check dex cancels 583 if len(test.expectedDEXCancels) != len(tc.cancelsPlaced) { 584 t.Fatalf("%s: expected %d cancels but got %d", test.name, len(test.expectedDEXCancels), len(tc.cancelsPlaced)) 585 } 586 for i := range test.expectedDEXCancels { 587 if !bytes.Equal(test.expectedDEXCancels[i], tc.cancelsPlaced[i][:]) { 588 t.Fatalf("%s: expected cancel %x but got %x", test.name, test.expectedDEXCancels[i], tc.cancelsPlaced[i]) 589 } 590 } 591 592 // Check cex cancels 593 if len(test.expectedCEXCancels) != len(cex.cancelledTrades) { 594 t.Fatalf("%s: expected %d cex cancels but got %d", test.name, len(test.expectedCEXCancels), len(cex.cancelledTrades)) 595 } 596 for i := range test.expectedCEXCancels { 597 if test.expectedCEXCancels[i] != cex.cancelledTrades[i] { 598 t.Fatalf("%s: expected cex cancel %s but got %s", test.name, test.expectedCEXCancels[i], cex.cancelledTrades[i]) 599 } 600 } 601 }) 602 } 603 604 for _, test := range tests { 605 runTest(&test) 606 } 607 } 608 609 func TestArbDexTradeUpdates(t *testing.T) { 610 orderIDs := make([]order.OrderID, 5) 611 for i := 0; i < 5; i++ { 612 copy(orderIDs[i][:], encode.RandomBytes(32)) 613 } 614 615 cexTradeIDs := make([]string, 0, 5) 616 for i := 0; i < 5; i++ { 617 cexTradeIDs = append(cexTradeIDs, fmt.Sprintf("%x", encode.RandomBytes(32))) 618 } 619 620 type test struct { 621 name string 622 activeArbs []*arbSequence 623 updatedOrderID []byte 624 updatedOrderStatus order.OrderStatus 625 expectedActiveArbs []*arbSequence 626 } 627 628 dexOrder := &core.Order{ 629 ID: orderIDs[0][:], 630 } 631 632 tests := []*test{ 633 { 634 name: "dex order still booked", 635 activeArbs: []*arbSequence{ 636 { 637 dexOrder: dexOrder, 638 cexOrderID: cexTradeIDs[0], 639 }, 640 }, 641 updatedOrderID: orderIDs[0][:], 642 updatedOrderStatus: order.OrderStatusBooked, 643 expectedActiveArbs: []*arbSequence{ 644 { 645 dexOrder: dexOrder, 646 cexOrderID: cexTradeIDs[0], 647 }, 648 }, 649 }, 650 { 651 name: "dex order executed, but cex not yet filled", 652 activeArbs: []*arbSequence{ 653 { 654 dexOrder: dexOrder, 655 cexOrderID: cexTradeIDs[0], 656 }, 657 }, 658 updatedOrderID: orderIDs[0][:], 659 updatedOrderStatus: order.OrderStatusExecuted, 660 expectedActiveArbs: []*arbSequence{ 661 { 662 dexOrder: dexOrder, 663 cexOrderID: cexTradeIDs[0], 664 dexOrderFilled: true, 665 }, 666 }, 667 }, 668 { 669 name: "dex order executed, but cex already filled", 670 activeArbs: []*arbSequence{ 671 { 672 dexOrder: dexOrder, 673 cexOrderID: cexTradeIDs[0], 674 cexOrderFilled: true, 675 }, 676 }, 677 updatedOrderID: orderIDs[0][:], 678 updatedOrderStatus: order.OrderStatusExecuted, 679 expectedActiveArbs: []*arbSequence{}, 680 }, 681 } 682 683 runTest := func(test *test) { 684 cex := newTBotCEXAdaptor() 685 coreAdaptor := newTBotCoreAdaptor(newTCore()) 686 687 ctx, cancel := context.WithCancel(context.Background()) 688 defer cancel() 689 690 arbEngine := &simpleArbMarketMaker{ 691 unifiedExchangeAdaptor: mustParseAdaptorFromMarket(&core.Market{ 692 BaseID: 42, 693 QuoteID: 0, 694 }), 695 cex: cex, 696 core: coreAdaptor, 697 activeArbs: test.activeArbs, 698 } 699 arbEngine.clientCore = newTCore() 700 arbEngine.CEX = newTCEX() 701 arbEngine.ctx = ctx 702 arbEngine.setBotLoop(arbEngine.botLoop) 703 arbEngine.unifiedExchangeAdaptor.botCfgV.Store(&BotConfig{ 704 SimpleArbConfig: &SimpleArbConfig{ 705 ProfitTrigger: 0.01, 706 MaxActiveArbs: 5, 707 NumEpochsLeaveOpen: 10, 708 }, 709 }) 710 711 err := arbEngine.runBotLoop(ctx) 712 if err != nil { 713 t.Fatalf("%s: Connect error: %v", test.name, err) 714 } 715 716 coreAdaptor.orderUpdates <- &core.Order{ 717 Status: test.updatedOrderStatus, 718 ID: test.updatedOrderID, 719 } 720 coreAdaptor.orderUpdates <- &core.Order{} 721 722 if len(test.expectedActiveArbs) != len(arbEngine.activeArbs) { 723 t.Fatalf("%s: expected %d active arbs but got %d", test.name, len(test.expectedActiveArbs), len(arbEngine.activeArbs)) 724 } 725 726 for i := range test.expectedActiveArbs { 727 if *arbEngine.activeArbs[i] != *test.expectedActiveArbs[i] { 728 t.Fatalf("%s: active arb %+v != expected active arb %+v", test.name, arbEngine.activeArbs[i], test.expectedActiveArbs[i]) 729 } 730 } 731 } 732 733 for _, test := range tests { 734 runTest(test) 735 } 736 } 737 738 func TestCexTradeUpdates(t *testing.T) { 739 orderIDs := make([]order.OrderID, 5) 740 for i := 0; i < 5; i++ { 741 copy(orderIDs[i][:], encode.RandomBytes(32)) 742 } 743 744 cexTradeIDs := make([]string, 0, 5) 745 for i := 0; i < 5; i++ { 746 cexTradeIDs = append(cexTradeIDs, fmt.Sprintf("%x", encode.RandomBytes(32))) 747 } 748 749 dexOrder := &core.Order{ 750 ID: orderIDs[0][:], 751 } 752 753 type test struct { 754 name string 755 activeArbs []*arbSequence 756 updatedOrderID string 757 orderComplete bool 758 expectedActiveArbs []*arbSequence 759 } 760 761 tests := []*test{ 762 { 763 name: "neither complete", 764 activeArbs: []*arbSequence{ 765 { 766 dexOrder: dexOrder, 767 cexOrderID: cexTradeIDs[0], 768 }, 769 }, 770 updatedOrderID: cexTradeIDs[0], 771 orderComplete: false, 772 expectedActiveArbs: []*arbSequence{ 773 { 774 dexOrder: dexOrder, 775 cexOrderID: cexTradeIDs[0], 776 }, 777 }, 778 }, 779 { 780 name: "cex complete, but dex order not complete", 781 activeArbs: []*arbSequence{ 782 { 783 dexOrder: dexOrder, 784 cexOrderID: cexTradeIDs[0], 785 }, 786 }, 787 updatedOrderID: cexTradeIDs[0], 788 orderComplete: true, 789 expectedActiveArbs: []*arbSequence{ 790 { 791 dexOrder: dexOrder, 792 cexOrderID: cexTradeIDs[0], 793 cexOrderFilled: true, 794 }, 795 }, 796 }, 797 { 798 name: "both complete", 799 activeArbs: []*arbSequence{ 800 { 801 dexOrder: dexOrder, 802 cexOrderID: cexTradeIDs[0], 803 dexOrderFilled: true, 804 }, 805 }, 806 updatedOrderID: cexTradeIDs[0], 807 orderComplete: true, 808 }, 809 } 810 811 runTest := func(test *test) { 812 cex := newTBotCEXAdaptor() 813 ctx, cancel := context.WithCancel(context.Background()) 814 defer cancel() 815 816 arbEngine := &simpleArbMarketMaker{ 817 unifiedExchangeAdaptor: mustParseAdaptorFromMarket(&core.Market{ 818 BaseID: 42, 819 QuoteID: 0, 820 }), 821 cex: cex, 822 core: newTBotCoreAdaptor(newTCore()), 823 activeArbs: test.activeArbs, 824 } 825 arbEngine.ctx = ctx 826 arbEngine.CEX = newTCEX() 827 arbEngine.setBotLoop(arbEngine.botLoop) 828 arbEngine.unifiedExchangeAdaptor.botCfgV.Store(&BotConfig{ 829 SimpleArbConfig: &SimpleArbConfig{ 830 ProfitTrigger: 0.01, 831 MaxActiveArbs: 5, 832 NumEpochsLeaveOpen: 10, 833 }, 834 }) 835 836 err := arbEngine.runBotLoop(ctx) 837 if err != nil { 838 t.Fatalf("%s: Connect error: %v", test.name, err) 839 } 840 841 cex.tradeUpdates <- &libxc.Trade{ 842 ID: test.updatedOrderID, 843 Complete: test.orderComplete, 844 } 845 // send dummy update 846 cex.tradeUpdates <- &libxc.Trade{ 847 ID: "", 848 } 849 850 if len(test.expectedActiveArbs) != len(arbEngine.activeArbs) { 851 t.Fatalf("%s: expected %d active arbs but got %d", test.name, len(test.expectedActiveArbs), len(arbEngine.activeArbs)) 852 } 853 for i := range test.expectedActiveArbs { 854 if *arbEngine.activeArbs[i] != *test.expectedActiveArbs[i] { 855 t.Fatalf("%s: active arb %+v != expected active arb %+v", test.name, arbEngine.activeArbs[i], test.expectedActiveArbs[i]) 856 } 857 } 858 } 859 860 for _, test := range tests { 861 runTest(test) 862 } 863 } 864 865 /*func TestArbBotProblems(t *testing.T) { 866 const baseID, quoteID = 42, 0 867 const lotSize uint64 = 5e9 868 const sellSwapFees, sellRedeemFees = 3e6, 1e6 869 const buySwapFees, buyRedeemFees = 2e5, 1e5 870 const buyRate, sellRate = 1e7, 1.1e7 871 872 type test struct { 873 name string 874 userLimitTooLow bool 875 dexBalanceDefs map[uint32]uint64 876 cexBalanceDefs map[uint32]uint64 877 878 expBotProblems *BotProblems 879 } 880 881 updateBotProblems := func(f func(*BotProblems)) *BotProblems { 882 bp := newBotProblems() 883 f(bp) 884 return bp 885 } 886 887 tests := []*test{ 888 { 889 name: "no problems", 890 expBotProblems: newBotProblems(), 891 }, 892 { 893 name: "user limit too low", 894 userLimitTooLow: true, 895 expBotProblems: updateBotProblems(func(bp *BotProblems) { 896 bp.UserLimitTooLow = true 897 }), 898 }, 899 { 900 name: "balance deficiencies", 901 dexBalanceDefs: map[uint32]uint64{ 902 baseID: lotSize + sellSwapFees, 903 quoteID: calc.BaseToQuote(buyRate, lotSize) + buySwapFees, 904 }, 905 cexBalanceDefs: map[uint32]uint64{ 906 baseID: lotSize, 907 quoteID: calc.BaseToQuote(sellRate, lotSize), 908 }, 909 expBotProblems: updateBotProblems(func(bp *BotProblems) { 910 // All these values are multiplied by 2 because the same deficiencies 911 // are returned for buys and sells, and they are summed. 912 bp.DEXBalanceDeficiencies = map[uint32]uint64{ 913 baseID: (lotSize + sellSwapFees) * 2, 914 quoteID: (calc.BaseToQuote(buyRate, lotSize) + buySwapFees) * 2, 915 } 916 bp.CEXBalanceDeficiencies = map[uint32]uint64{ 917 baseID: lotSize * 2, 918 quoteID: calc.BaseToQuote(sellRate, lotSize) * 2, 919 } 920 }), 921 }, 922 } 923 924 runTest := func(tt *test) { 925 t.Run(tt.name, func(t *testing.T) { 926 cex := newTCEX() 927 mkt := &core.Market{ 928 RateStep: 1e3, 929 AtomToConv: 1, 930 LotSize: lotSize, 931 BaseID: baseID, 932 QuoteID: quoteID, 933 } 934 u := mustParseAdaptorFromMarket(mkt) 935 u.CEX = cex 936 u.botCfgV.Store(&BotConfig{}) 937 c := newTCore() 938 if !tt.userLimitTooLow { 939 u.clientCore.(*tCore).userParcels = 0 940 u.clientCore.(*tCore).parcelLimit = 1 941 } 942 u.fiatRates.Store(map[uint32]float64{baseID: 1, quoteID: 1}) 943 cexAdaptor := newTBotCEXAdaptor() 944 coreAdaptor := newTBotCoreAdaptor(c) 945 a := &simpleArbMarketMaker{ 946 unifiedExchangeAdaptor: u, 947 cex: cexAdaptor, 948 core: coreAdaptor, 949 } 950 951 coreAdaptor.balanceDefs = tt.dexBalanceDefs 952 cexAdaptor.balanceDefs = tt.cexBalanceDefs 953 954 a.cfgV.Store(&SimpleArbConfig{}) 955 956 cex.asksVWAP[lotSize] = vwapResult{ 957 avg: buyRate, 958 extrema: buyRate, 959 } 960 cex.bidsVWAP[lotSize] = vwapResult{ 961 avg: sellRate, 962 extrema: sellRate, 963 } 964 965 a.book = &tOrderBook{ 966 bidsVWAP: map[uint64]vwapResult{ 967 1: { 968 avg: buyRate, 969 extrema: buyRate, 970 }, 971 }, 972 asksVWAP: map[uint64]vwapResult{ 973 1: { 974 avg: sellRate, 975 extrema: sellRate, 976 }, 977 }, 978 } 979 980 a.buyFees = &OrderFees{ 981 LotFeeRange: &LotFeeRange{ 982 Max: &LotFees{ 983 Redeem: buyRedeemFees, 984 Swap: buySwapFees, 985 }, 986 Estimated: &LotFees{}, 987 }, 988 BookingFeesPerLot: buySwapFees, 989 } 990 a.sellFees = &OrderFees{ 991 LotFeeRange: &LotFeeRange{ 992 Max: &LotFees{ 993 Redeem: sellRedeemFees, 994 Swap: sellSwapFees, 995 }, 996 Estimated: &LotFees{}, 997 }, 998 BookingFeesPerLot: sellSwapFees, 999 } 1000 1001 a.rebalance(1) 1002 1003 problems := a.problems() 1004 if !reflect.DeepEqual(tt.expBotProblems, problems) { 1005 t.Fatalf("expected bot problems %v, got %v", tt.expBotProblems, problems) 1006 } 1007 }) 1008 } 1009 1010 for _, test := range tests { 1011 runTest(test) 1012 } 1013 } 1014 */