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 <- ¬e 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 }