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