decred.org/dcrdex@v1.0.5/server/matcher/match_test.go (about) 1 package matcher 2 3 import ( 4 "bytes" 5 "encoding/hex" 6 "fmt" 7 "math/rand" 8 "reflect" 9 "testing" 10 "time" 11 12 "decred.org/dcrdex/dex" 13 "decred.org/dcrdex/dex/order" 14 "decred.org/dcrdex/server/account" 15 ) 16 17 // An arbitrary account ID for test orders. 18 var acct0 = account.AccountID{ 19 0x22, 0x4c, 0xba, 0xaa, 0xfa, 0x80, 0xbf, 0x3b, 20 0xd1, 0xff, 0x73, 0x15, 0x90, 0xbc, 0xbd, 0xda, 21 0x5a, 0x76, 0xf9, 0x1e, 0x60, 0xa1, 0x56, 0x99, 22 0x46, 0x34, 0xe9, 0x1c, 0xec, 0x25, 0xd5, 0x40, // 32 bytes 23 } 24 25 const ( 26 AssetDCR uint32 = iota 27 AssetBTC 28 29 LotSize = uint64(10 * 1e8) 30 ) 31 32 var rnd = rand.New(rand.NewSource(1)) 33 34 func randomPreimage() (pe order.Preimage) { 35 rnd.Read(pe[:]) 36 return 37 } 38 39 func startLogger() { 40 logger := dex.StdOutLogger("MATCHTEST", dex.LevelTrace) 41 UseLogger(logger) 42 } 43 44 var ( 45 marketPreimages = []order.Preimage{ 46 randomPreimage(), 47 randomPreimage(), 48 } 49 marketOrders = []*OrderRevealed{ 50 { // market BUY of 4 lots 51 &order.MarketOrder{ 52 P: order.Prefix{ 53 AccountID: acct0, 54 BaseAsset: AssetDCR, 55 QuoteAsset: AssetBTC, 56 OrderType: order.MarketOrderType, 57 ClientTime: time.Unix(1566497653, 0), 58 ServerTime: time.Unix(1566497656, 0), 59 Commit: marketPreimages[0].Commit(), 60 }, 61 T: order.Trade{ 62 Coins: []order.CoinID{}, 63 Sell: false, 64 Quantity: 4 * LotSize, 65 Address: "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui", 66 }, 67 }, 68 marketPreimages[0], 69 }, 70 { // market SELL of 2 lots 71 &order.MarketOrder{ 72 P: order.Prefix{ 73 AccountID: acct0, 74 BaseAsset: AssetDCR, 75 QuoteAsset: AssetBTC, 76 OrderType: order.MarketOrderType, 77 ClientTime: time.Unix(1566497654, 0), 78 ServerTime: time.Unix(1566497656, 0), 79 Commit: marketPreimages[1].Commit(), 80 }, 81 T: order.Trade{ 82 Coins: []order.CoinID{}, 83 Sell: true, 84 Quantity: 2 * LotSize, 85 Address: "149RQGLaHf2gGiL4NXZdH7aA8nYEuLLrgm", 86 }, 87 }, 88 marketPreimages[1], 89 }, 90 } 91 92 limitPreimages = []order.Preimage{ 93 randomPreimage(), 94 randomPreimage(), 95 randomPreimage(), 96 randomPreimage(), 97 } 98 limitOrders = []*OrderRevealed{ 99 { // limit BUY of 2 lots at 0.043 100 &order.LimitOrder{ 101 P: order.Prefix{ 102 AccountID: acct0, 103 BaseAsset: AssetDCR, 104 QuoteAsset: AssetBTC, 105 OrderType: order.LimitOrderType, 106 ClientTime: time.Unix(1566497653, 0), 107 ServerTime: time.Unix(1566497656, 0), 108 Commit: limitPreimages[0].Commit(), 109 }, 110 T: order.Trade{ 111 Coins: []order.CoinID{}, 112 Sell: false, 113 Quantity: 2 * LotSize, 114 Address: "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui", 115 }, 116 Rate: 4300000, 117 Force: order.StandingTiF, 118 }, 119 limitPreimages[0], 120 }, 121 { // limit SELL of 3 lots at 0.045 122 &order.LimitOrder{ 123 P: order.Prefix{ 124 AccountID: acct0, 125 BaseAsset: AssetDCR, 126 QuoteAsset: AssetBTC, 127 OrderType: order.LimitOrderType, 128 ClientTime: time.Unix(1566497651, 0), 129 ServerTime: time.Unix(1566497652, 0), 130 Commit: limitPreimages[1].Commit(), 131 }, 132 T: order.Trade{ 133 Coins: []order.CoinID{}, 134 Sell: true, 135 Quantity: 3 * LotSize, 136 Address: "149RQGLaHf2gGiL4NXZdH7aA8nYEuLLrgm", 137 }, 138 Rate: 4500000, 139 Force: order.StandingTiF, 140 }, 141 limitPreimages[1], 142 }, 143 { // limit BUY of 1 lot at 0.046 144 &order.LimitOrder{ 145 P: order.Prefix{ 146 AccountID: acct0, 147 BaseAsset: AssetDCR, 148 QuoteAsset: AssetBTC, 149 OrderType: order.LimitOrderType, 150 ClientTime: time.Unix(1566497655, 0), 151 ServerTime: time.Unix(1566497656, 0), 152 Commit: limitPreimages[2].Commit(), 153 }, 154 T: order.Trade{ 155 Coins: []order.CoinID{}, 156 Sell: false, 157 Quantity: 1 * LotSize, 158 Address: "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui", 159 }, 160 Rate: 4600000, 161 Force: order.StandingTiF, 162 }, 163 limitPreimages[2], 164 }, 165 { // limit BUY of 1 lot at 0.045 166 &order.LimitOrder{ 167 P: order.Prefix{ 168 AccountID: acct0, 169 BaseAsset: AssetDCR, 170 QuoteAsset: AssetBTC, 171 OrderType: order.LimitOrderType, 172 ClientTime: time.Unix(1566497649, 0), 173 ServerTime: time.Unix(1566497651, 0), 174 Commit: limitPreimages[3].Commit(), 175 }, 176 T: order.Trade{ 177 Coins: []order.CoinID{}, 178 Sell: false, 179 Quantity: 1 * LotSize, 180 Address: "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui", 181 }, 182 Rate: 4500000, 183 Force: order.StandingTiF, 184 }, 185 limitPreimages[3], 186 }, 187 } 188 ) 189 190 type BookStub struct { 191 lotSize uint64 192 sellOrders []*order.LimitOrder // sorted descending in this stub 193 buyOrders []*order.LimitOrder // sorted ascending 194 } 195 196 func (b *BookStub) LotSize() uint64 { 197 return b.lotSize 198 } 199 200 func (b *BookStub) BestSell() *order.LimitOrder { 201 if len(b.sellOrders) == 0 { 202 return nil 203 } 204 return b.sellOrders[len(b.sellOrders)-1] 205 } 206 207 func (b *BookStub) BestBuy() *order.LimitOrder { 208 if len(b.buyOrders) == 0 { 209 return nil 210 } 211 return b.buyOrders[len(b.buyOrders)-1] 212 } 213 214 func (b *BookStub) SellCount() int { 215 return len(b.sellOrders) 216 } 217 218 func (b *BookStub) BuyCount() int { 219 return len(b.buyOrders) 220 } 221 222 func (b *BookStub) Insert(ord *order.LimitOrder) bool { 223 // Only "inserts" by making it the best order. 224 if ord.Sell { 225 b.sellOrders = append(b.sellOrders, ord) 226 } else { 227 b.buyOrders = append(b.buyOrders, ord) 228 } 229 return true 230 } 231 232 func (b *BookStub) Remove(orderID order.OrderID) (*order.LimitOrder, bool) { 233 for i := range b.buyOrders { 234 if b.buyOrders[i].ID() == orderID { 235 //fmt.Println("Removing", orderID) 236 removed := b.buyOrders[i] 237 b.buyOrders = append(b.buyOrders[:i], b.buyOrders[i+1:]...) 238 return removed, true 239 } 240 } 241 for i := range b.sellOrders { 242 if b.sellOrders[i].ID() == orderID { 243 //fmt.Println("Removing", orderID) 244 removed := b.sellOrders[i] 245 b.sellOrders = append(b.sellOrders[:i], b.sellOrders[i+1:]...) 246 return removed, true 247 } 248 } 249 return nil, false 250 } 251 252 func (b *BookStub) BuyOrders() []*order.LimitOrder { return b.buyOrders } 253 func (b *BookStub) SellOrders() []*order.LimitOrder { return b.sellOrders } 254 255 var _ Booker = (*BookStub)(nil) 256 257 func newLimitOrder(sell bool, rate, quantityLots uint64, force order.TimeInForce, timeOffset int64) *order.LimitOrder { 258 return newLimit(sell, rate, quantityLots, force, timeOffset).Order.(*order.LimitOrder) 259 } 260 261 func newLimit(sell bool, rate, quantityLots uint64, force order.TimeInForce, timeOffset int64) *OrderRevealed { 262 addr := "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui" 263 if sell { 264 addr = "149RQGLaHf2gGiL4NXZdH7aA8nYEuLLrgm" 265 } 266 pe := randomPreimage() 267 return &OrderRevealed{ 268 &order.LimitOrder{ 269 P: order.Prefix{ 270 AccountID: acct0, 271 BaseAsset: AssetDCR, 272 QuoteAsset: AssetBTC, 273 OrderType: order.LimitOrderType, 274 ClientTime: time.Unix(1566497653+timeOffset, 0), 275 ServerTime: time.Unix(1566497656+timeOffset, 0), 276 Commit: pe.Commit(), 277 }, 278 T: order.Trade{ 279 Coins: []order.CoinID{}, 280 Sell: sell, 281 Quantity: quantityLots * LotSize, 282 Address: addr, 283 }, 284 Rate: rate, 285 Force: force, 286 }, 287 pe, 288 } 289 } 290 291 func newMarketSellOrder(quantityLots uint64, timeOffset int64) *OrderRevealed { 292 pe := randomPreimage() 293 return &OrderRevealed{ 294 &order.MarketOrder{ 295 P: order.Prefix{ 296 AccountID: acct0, 297 BaseAsset: AssetDCR, 298 QuoteAsset: AssetBTC, 299 OrderType: order.MarketOrderType, 300 ClientTime: time.Unix(1566497653+timeOffset, 0), 301 ServerTime: time.Unix(1566497656+timeOffset, 0), 302 Commit: pe.Commit(), 303 }, 304 T: order.Trade{ 305 Coins: []order.CoinID{}, 306 Sell: true, 307 Quantity: quantityLots * LotSize, 308 Address: "149RQGLaHf2gGiL4NXZdH7aA8nYEuLLrgm", 309 }, 310 }, 311 pe, 312 } 313 } 314 315 func newMarketBuyOrder(quantityQuoteAsset uint64, timeOffset int64) *OrderRevealed { 316 pe := randomPreimage() 317 return &OrderRevealed{ 318 &order.MarketOrder{ 319 P: order.Prefix{ 320 AccountID: acct0, 321 BaseAsset: AssetDCR, 322 QuoteAsset: AssetBTC, 323 OrderType: order.MarketOrderType, 324 ClientTime: time.Unix(1566497653+timeOffset, 0), 325 ServerTime: time.Unix(1566497656+timeOffset, 0), 326 Commit: pe.Commit(), 327 }, 328 T: order.Trade{ 329 Coins: []order.CoinID{}, 330 Sell: false, 331 Quantity: quantityQuoteAsset, 332 Address: "DcqXswjTPnUcd4FRCkX4vRJxmVtfgGVa5ui", 333 }, 334 }, 335 pe, 336 } 337 } 338 339 var ( 340 // Create a coherent order book of standing orders and sorted rates. 341 bookBuyOrders = []*order.LimitOrder{ 342 newLimitOrder(false, 2500000, 2, order.StandingTiF, 0), 343 newLimitOrder(false, 2700000, 2, order.StandingTiF, 0), 344 newLimitOrder(false, 3200000, 2, order.StandingTiF, 0), 345 newLimitOrder(false, 3300000, 1, order.StandingTiF, 2), // newer 346 newLimitOrder(false, 3300000, 2, order.StandingTiF, 0), // older 347 newLimitOrder(false, 3600000, 4, order.StandingTiF, 0), 348 newLimitOrder(false, 3900000, 2, order.StandingTiF, 0), 349 newLimitOrder(false, 4000000, 10, order.StandingTiF, 0), 350 newLimitOrder(false, 4300000, 4, order.StandingTiF, 1), // newer 351 newLimitOrder(false, 4300000, 2, order.StandingTiF, 0), // older 352 newLimitOrder(false, 4500000, 1, order.StandingTiF, 0), 353 } 354 bookSellOrders = []*order.LimitOrder{ 355 newLimitOrder(true, 6200000, 2, order.StandingTiF, 1), // newer 356 newLimitOrder(true, 6200000, 2, order.StandingTiF, 0), // older 357 newLimitOrder(true, 6100000, 2, order.StandingTiF, 0), 358 newLimitOrder(true, 6000000, 2, order.StandingTiF, 0), 359 newLimitOrder(true, 5500000, 1, order.StandingTiF, 0), 360 newLimitOrder(true, 5400000, 4, order.StandingTiF, 0), 361 newLimitOrder(true, 5000000, 2, order.StandingTiF, 0), 362 newLimitOrder(true, 4700000, 4, order.StandingTiF, 1), // newer 363 newLimitOrder(true, 4700000, 10, order.StandingTiF, 0), // older 364 newLimitOrder(true, 4600000, 2, order.StandingTiF, 0), 365 newLimitOrder(true, 4550000, 1, order.StandingTiF, 0), 366 } 367 ) 368 369 const ( 370 bookBuyLots = 32 371 bookSellLots = 32 372 initialMidGap = (4550000 + 4500000) / 2 // 4525000 373 ) 374 375 func newBooker() Booker { 376 resetMakers() 377 buyOrders := make([]*order.LimitOrder, len(bookBuyOrders)) 378 copy(buyOrders, bookBuyOrders) 379 sellOrders := make([]*order.LimitOrder, len(bookSellOrders)) 380 copy(sellOrders, bookSellOrders) 381 return &BookStub{ 382 lotSize: LotSize, 383 buyOrders: buyOrders, 384 sellOrders: sellOrders, 385 } 386 } 387 388 func resetMakers() { 389 for _, o := range bookBuyOrders { 390 o.FillAmt = 0 391 } 392 for _, o := range bookSellOrders { 393 o.FillAmt = 0 394 } 395 } 396 397 func newMatchSet(taker order.Order, makers []*order.LimitOrder, lastPartialAmount ...uint64) *order.MatchSet { 398 amounts := make([]uint64, len(makers)) 399 rates := make([]uint64, len(makers)) 400 var total uint64 401 for i := range makers { 402 total += makers[i].Quantity 403 amounts[i] = makers[i].Quantity 404 rates[i] = makers[i].Rate 405 } 406 if len(lastPartialAmount) > 0 { 407 amounts[len(makers)-1] = lastPartialAmount[0] 408 total -= makers[len(makers)-1].Quantity - lastPartialAmount[0] 409 } 410 return &order.MatchSet{ 411 Taker: taker, 412 Makers: makers, 413 Amounts: amounts, 414 Rates: rates, 415 Total: total, 416 } 417 } 418 419 func Test_matchLimitOrder(t *testing.T) { 420 // Setup the match package's logger. 421 startLogger() 422 423 takers := []*order.LimitOrder{ 424 newLimitOrder(false, 4550000, 1, order.ImmediateTiF, 0), // buy, 1 lot, immediate, equal rate 425 newLimitOrder(true, 4450000, 1, order.ImmediateTiF, 0), // sell, 1 lot, immediate, overlapping rate 426 newLimitOrder(true, 4300000, 3, order.StandingTiF, 0), // sell, 3 lots, immediate, multiple makers 427 newLimitOrder(true, 4300000, 2, order.StandingTiF, 0), // sell, 4 lots, immediate, multiple makers, partial last maker 428 newLimitOrder(true, 4300000, 8, order.StandingTiF, 0), // sell, 8 lots, immediate, multiple makers, partial taker remaining 429 } 430 resetTakers := func() { 431 for _, o := range takers { 432 o.FillAmt = 0 433 } 434 } 435 436 nSell := len(bookSellOrders) 437 nBuy := len(bookBuyOrders) 438 439 type args struct { 440 book Booker 441 ord *order.LimitOrder 442 } 443 tests := []struct { 444 name string 445 args args 446 doesMatch bool 447 wantMatch *order.MatchSet 448 takerRemaining uint64 449 }{ 450 { 451 "OK limit buy immediate rate match", 452 args{ 453 book: newBooker(), 454 ord: takers[0], 455 }, 456 true, 457 newMatchSet(takers[0], []*order.LimitOrder{bookSellOrders[nSell-1]}), 458 0, 459 }, 460 { 461 "OK limit sell immediate rate overlap", 462 args{ 463 book: newBooker(), 464 ord: takers[1], 465 }, 466 true, 467 newMatchSet(takers[1], []*order.LimitOrder{bookBuyOrders[nBuy-1]}), 468 0, 469 }, 470 { 471 "OK limit sell immediate multiple makers", 472 args{ 473 book: newBooker(), 474 ord: takers[2], 475 }, 476 true, 477 newMatchSet(takers[2], []*order.LimitOrder{bookBuyOrders[nBuy-1], bookBuyOrders[nBuy-2]}), 478 0, 479 }, 480 { 481 "OK limit sell immediate multiple makers partial maker fill", 482 args{ 483 book: newBooker(), 484 ord: takers[3], 485 }, 486 true, 487 newMatchSet(takers[3], []*order.LimitOrder{bookBuyOrders[nBuy-1], bookBuyOrders[nBuy-2]}, 1*LotSize), 488 0, 489 }, 490 { 491 "OK limit sell immediate multiple makers partial taker fill", 492 args{ 493 book: newBooker(), 494 ord: takers[4], 495 }, 496 true, 497 newMatchSet(takers[4], []*order.LimitOrder{bookBuyOrders[nBuy-1], bookBuyOrders[nBuy-2], bookBuyOrders[nBuy-3]}), 498 1 * LotSize, 499 }, 500 } 501 for _, tt := range tests { 502 t.Run(tt.name, func(t *testing.T) { 503 // Reset Filled amounts of all pre-defined orders before each test. 504 resetTakers() 505 resetMakers() 506 507 gotMatch := matchLimitOrder(tt.args.book, tt.args.ord) 508 matchMade := gotMatch != nil 509 if tt.doesMatch != matchMade { 510 t.Errorf("Match expected = %v, got = %v", tt.doesMatch, matchMade) 511 } 512 if !reflect.DeepEqual(gotMatch, tt.wantMatch) { 513 t.Errorf("matchLimitOrder() = %v, want %v", gotMatch, tt.wantMatch) 514 } 515 if tt.takerRemaining != tt.args.ord.Remaining() { 516 t.Errorf("Taker remaining incorrect. Expected %d, got %d.", 517 tt.takerRemaining, tt.args.ord.Remaining()) 518 } 519 }) 520 } 521 } 522 523 func newCancelOrder(targetOrderID order.OrderID, serverTime time.Time) *OrderRevealed { 524 pe := randomPreimage() 525 return &OrderRevealed{ 526 &order.CancelOrder{ 527 P: order.Prefix{ 528 ServerTime: serverTime, 529 Commit: pe.Commit(), 530 }, 531 TargetOrderID: targetOrderID, 532 }, pe} 533 } 534 535 func TestMatch_cancelOnly(t *testing.T) { 536 // Setup the match package's logger. 537 startLogger() 538 539 // New matching engine. 540 me := New() 541 542 rnd.Seed(1212121) 543 544 fakeOrder := newLimitOrder(false, 4550000, 1, order.ImmediateTiF, 0) 545 fakeOrder.ServerTime = time.Now() 546 547 // takers is heterogenous w.r.t. type 548 takers := []*OrderRevealed{ 549 newCancelOrder(bookBuyOrders[3].ID(), fakeOrder.ServerTime.Add(time.Second)), 550 newCancelOrder(fakeOrder.ID(), fakeOrder.ServerTime.Add(time.Second)), 551 } 552 553 //nSell := len(bookSellOrders) 554 //nBuy := len(bookBuyOrders) 555 556 type args struct { 557 book Booker 558 queue []*OrderRevealed 559 } 560 tests := []struct { 561 name string 562 args args 563 doesMatch bool 564 wantSeed []byte 565 wantMatches []*order.MatchSet 566 wantNumPassed int 567 wantNumFailed int 568 wantDoneOK int 569 wantNumPartial int 570 wantNumInserted int 571 wantNumUnbooked int 572 wantNumCancelsExecuted int 573 wantNumTradesCanceled int 574 wantNumNomatched int 575 }{ 576 { 577 name: "cancel standing ok", 578 args: args{ 579 book: newBooker(), 580 queue: []*OrderRevealed{takers[0]}, 581 }, 582 doesMatch: true, 583 wantSeed: []byte{ 584 0xbb, 0x20, 0x85, 0x39, 0x78, 0xd5, 0x09, 0xa9, 585 0xa0, 0x56, 0x78, 0x4c, 0x50, 0xb1, 0xb4, 0x3d, 586 0xe3, 0xe9, 0x9b, 0x2b, 0x88, 0x35, 0x2c, 0x71, 587 0xed, 0xff, 0x5d, 0xc2, 0x1e, 0x87, 0xcb, 0xf8}, 588 wantMatches: []*order.MatchSet{ 589 { 590 Taker: takers[0].Order, 591 Makers: []*order.LimitOrder{bookBuyOrders[3]}, 592 Amounts: []uint64{bookBuyOrders[3].Remaining()}, 593 Rates: []uint64{bookBuyOrders[3].Rate}, 594 Total: 0, 595 }, 596 }, 597 wantNumPassed: 1, 598 wantNumFailed: 0, 599 wantDoneOK: 1, 600 wantNumPartial: 0, 601 wantNumInserted: 0, 602 wantNumUnbooked: 1, 603 wantNumCancelsExecuted: 1, 604 wantNumTradesCanceled: 1, 605 }, 606 { 607 name: "cancel non-existent standing", 608 args: args{ 609 book: newBooker(), 610 queue: []*OrderRevealed{takers[1]}, 611 }, 612 doesMatch: false, 613 wantSeed: []byte{ 614 0x9a, 0xdb, 0xbb, 0x7a, 0x9f, 0x7f, 0xe1, 0x72, 615 0xf0, 0x90, 0x38, 0x7a, 0xa6, 0xb9, 0x8a, 0x83, 616 0x5a, 0x75, 0x05, 0x19, 0x66, 0x6d, 0x53, 0x12, 617 0x7e, 0xc8, 0x9d, 0xe3, 0x87, 0x5d, 0xdc, 0x8a}, 618 wantMatches: nil, 619 wantNumPassed: 0, 620 wantNumFailed: 1, 621 wantDoneOK: 0, 622 wantNumPartial: 0, 623 wantNumInserted: 0, 624 wantNumUnbooked: 0, 625 wantNumCancelsExecuted: 0, 626 wantNumTradesCanceled: 0, 627 wantNumNomatched: 1, 628 }, 629 } 630 for _, tt := range tests { 631 t.Run(tt.name, func(t *testing.T) { 632 // Reset Filled amounts of all pre-defined orders before each test. 633 resetMakers() 634 635 numBuys0 := tt.args.book.BuyCount() 636 637 seed, matches, passed, failed, doneOK, partial, booked, nomatched, unbooked, updates, _ := me.Match(tt.args.book, tt.args.queue) 638 matchMade := len(matches) > 0 && matches[0] != nil 639 if tt.doesMatch != matchMade { 640 t.Errorf("Match expected = %v, got = %v", tt.doesMatch, matchMade) 641 } 642 if len(matches) != len(tt.wantMatches) { 643 t.Errorf("number of matches %d, expected %d", len(matches), len(tt.wantMatches)) 644 } 645 for i := range matches { 646 if !reflect.DeepEqual(matches[i], tt.wantMatches[i]) { 647 t.Errorf("matches[%d] = %v, want %v", i, matches[i], tt.wantMatches[i]) 648 } 649 } 650 if !bytes.Equal(seed, tt.wantSeed) { 651 t.Errorf("got seed %#v, expected %x", seed, tt.wantSeed) 652 } 653 if len(passed) != tt.wantNumPassed { 654 t.Errorf("number passed %d, expected %d", len(passed), tt.wantNumPassed) 655 } 656 if len(failed) != tt.wantNumFailed { 657 t.Errorf("number failed %d, expected %d", len(failed), tt.wantNumFailed) 658 } 659 if len(doneOK) != tt.wantDoneOK { 660 t.Errorf("number doneOK %d, expected %d", len(doneOK), tt.wantDoneOK) 661 } 662 if len(partial) != tt.wantNumPartial { 663 t.Errorf("number partial %d, expected %d", len(partial), tt.wantNumPartial) 664 } 665 if len(booked) != tt.wantNumInserted { 666 t.Errorf("number booked %d, expected %d", len(booked), tt.wantNumInserted) 667 } 668 if len(unbooked) != tt.wantNumUnbooked { 669 t.Errorf("number unbooked %d, expected %d", len(unbooked), tt.wantNumUnbooked) 670 } 671 if len(nomatched) != tt.wantNumNomatched { 672 t.Errorf("number nomatched %d, expected %d", len(nomatched), tt.wantNumNomatched) 673 } 674 675 if len(updates.CancelsExecuted) != tt.wantNumCancelsExecuted { 676 t.Errorf("number of cancels executed %d, expected %d", len(updates.CancelsExecuted), tt.wantNumCancelsExecuted) 677 } 678 if len(updates.TradesCanceled) != tt.wantNumTradesCanceled { 679 t.Errorf("number of trades canceled %d, expected %d", len(updates.CancelsExecuted), tt.wantNumCancelsExecuted) 680 } 681 682 numBuys1 := tt.args.book.BuyCount() 683 if numBuys0-len(passed) != numBuys1 { 684 t.Errorf("Buy side order book size %d, expected %d", numBuys1, numBuys0-len(passed)) 685 } 686 }) 687 } 688 } 689 690 func TestMatch_limitsOnly(t *testing.T) { 691 // Setup the match package's logger. 692 startLogger() 693 694 // New matching engine. 695 me := New() 696 697 rnd.Seed(1212121) 698 699 badLotsizeOrder := newLimit(false, 05000000, 1, order.ImmediateTiF, 0) 700 badLotsizeOrder.Order.(*order.LimitOrder).Quantity /= 2 701 702 // takers is heterogenous w.r.t. type 703 takers := []*OrderRevealed{ 704 newLimit(false, 4550000, 1, order.ImmediateTiF, 0), // buy, 1 lot, immediate, equal rate 705 newLimit(false, 4550000, 2, order.StandingTiF, 0), // buy, 2 lot, standing, equal rate, partial taker insert to book 706 newLimit(false, 4550000, 2, order.ImmediateTiF, 0), // buy, 2 lot, immediate, equal rate, partial taker unfilled 707 newLimit(false, 4100000, 1, order.ImmediateTiF, 0), // buy, 1 lot, immediate, unfilled fail 708 newLimit(true, 4540000, 1, order.ImmediateTiF, 0), // sell, 1 lot, immediate 709 } 710 711 // tweak the preimage in takers[4] for the 2-element queue test. 712 takers[4].Preimage[0] += 1 713 takers[4].Order.(*order.LimitOrder).Commit = takers[4].Preimage.Commit() 714 715 resetTakers := func() { 716 for _, o := range takers { 717 switch ot := o.Order.(type) { 718 case *order.MarketOrder: 719 ot.FillAmt = 0 720 case *order.LimitOrder: 721 ot.FillAmt = 0 722 } 723 } 724 } 725 726 nSell := len(bookSellOrders) 727 728 type args struct { 729 book Booker 730 queue []*OrderRevealed 731 } 732 tests := []struct { 733 name string 734 args args 735 doesMatch bool 736 wantSeed []byte 737 wantMatches []*order.MatchSet 738 wantNumPassed int 739 wantNumFailed int 740 wantDoneOK int 741 wantNumPartial int 742 wantNumInserted int 743 wantNumUnbooked int 744 wantNumTradesPartial int 745 wantNumTradesBooked int 746 wantNumTradesCanceled int 747 wantNumTradesCompleted int 748 wantNumTradesFailed int 749 wantNumNomatched int 750 matchStats *MatchCycleStats 751 }{ 752 { 753 name: "limit buy immediate rate match", 754 args: args{ 755 book: newBooker(), 756 queue: []*OrderRevealed{takers[0]}, 757 }, 758 doesMatch: true, 759 wantSeed: []byte{ 760 0xbb, 0x20, 0x85, 0x39, 0x78, 0xd5, 0x09, 0xa9, 761 0xa0, 0x56, 0x78, 0x4c, 0x50, 0xb1, 0xb4, 0x3d, 762 0xe3, 0xe9, 0x9b, 0x2b, 0x88, 0x35, 0x2c, 0x71, 763 0xed, 0xff, 0x5d, 0xc2, 0x1e, 0x87, 0xcb, 0xf8}, 764 wantMatches: []*order.MatchSet{ 765 // 1 lot @ 4550000, laves best sell of 4600000 766 newMatchSet(takers[0].Order, []*order.LimitOrder{bookSellOrders[nSell-1]}), 767 }, 768 wantNumPassed: 1, 769 wantNumFailed: 0, 770 wantDoneOK: 1, 771 wantNumPartial: 0, 772 wantNumInserted: 0, 773 wantNumUnbooked: 1, 774 wantNumTradesCompleted: 2, 775 wantNumNomatched: 0, 776 matchStats: &MatchCycleStats{ 777 BookBuys: bookBuyLots * LotSize, 778 BookSells: (bookSellLots - 1) * LotSize, 779 MatchVolume: LotSize, 780 HighRate: bookSellOrders[nSell-1].Rate, 781 LowRate: bookSellOrders[nSell-1].Rate, 782 StartRate: bookSellOrders[nSell-1].Rate, 783 EndRate: bookSellOrders[nSell-1].Rate, 784 }, 785 }, 786 { 787 name: "limit buy standing partial taker inserted to book", 788 args: args{ 789 book: newBooker(), 790 queue: []*OrderRevealed{takers[1]}, 791 }, 792 doesMatch: true, 793 wantSeed: []byte{ 794 0x9a, 0xdb, 0xbb, 0x7a, 0x9f, 0x7f, 0xe1, 0x72, 795 0xf0, 0x90, 0x38, 0x7a, 0xa6, 0xb9, 0x8a, 0x83, 796 0x5a, 0x75, 0x05, 0x19, 0x66, 0x6d, 0x53, 0x12, 797 0x7e, 0xc8, 0x9d, 0xe3, 0x87, 0x5d, 0xdc, 0x8a}, 798 wantMatches: []*order.MatchSet{ 799 // 1 lot @ 4550000. bookvol + 1 - 1 = 0 800 newMatchSet(takers[1].Order, []*order.LimitOrder{bookSellOrders[nSell-1]}), 801 }, 802 wantNumPassed: 1, 803 wantNumFailed: 0, 804 wantDoneOK: 0, 805 wantNumPartial: 1, 806 wantNumInserted: 1, 807 wantNumUnbooked: 1, 808 wantNumTradesBooked: 1, 809 wantNumTradesCompleted: 1, 810 wantNumNomatched: 0, 811 matchStats: &MatchCycleStats{ 812 BookSells: (bookSellLots - 1) * LotSize, 813 BookBuys: (bookBuyLots + 1) * LotSize, 814 MatchVolume: LotSize, 815 HighRate: bookSellOrders[nSell-1].Rate, 816 LowRate: bookSellOrders[nSell-1].Rate, 817 StartRate: bookSellOrders[nSell-1].Rate, 818 EndRate: bookSellOrders[nSell-1].Rate, 819 }, 820 }, 821 { 822 name: "limit buy immediate partial taker unfilled", 823 args: args{ 824 book: newBooker(), 825 queue: []*OrderRevealed{takers[2]}, 826 }, 827 doesMatch: true, 828 wantSeed: []byte{ 829 0x4e, 0xec, 0x23, 0x4f, 0xfd, 0xb1, 0xd8, 0xd9, 830 0x14, 0xcd, 0xae, 0x34, 0x3c, 0xa2, 0x14, 0x5d, 831 0x6e, 0x2d, 0x92, 0xf3, 0xbf, 0xb2, 0x04, 0xfc, 832 0xee, 0x39, 0x13, 0xbb, 0x55, 0xed, 0x98, 0x81}, 833 wantMatches: []*order.MatchSet{ 834 // 1 lot @ 4550000. bookvol - 1 835 newMatchSet(takers[2].Order, []*order.LimitOrder{bookSellOrders[nSell-1]}), 836 }, 837 wantNumPassed: 1, 838 wantNumFailed: 0, 839 wantDoneOK: 1, 840 wantNumPartial: 1, 841 wantNumInserted: 0, 842 wantNumUnbooked: 1, 843 wantNumTradesCompleted: 2, // not in partial since they are done 844 wantNumNomatched: 0, 845 matchStats: &MatchCycleStats{ 846 BookSells: (bookSellLots - 1) * LotSize, 847 BookBuys: bookBuyLots * LotSize, 848 MatchVolume: LotSize, 849 HighRate: bookSellOrders[nSell-1].Rate, 850 LowRate: bookSellOrders[nSell-1].Rate, 851 StartRate: bookSellOrders[nSell-1].Rate, 852 EndRate: bookSellOrders[nSell-1].Rate, 853 }, 854 }, 855 { 856 name: "limit buy immediate unfilled fail", 857 args: args{ 858 book: newBooker(), 859 queue: []*OrderRevealed{takers[3]}, 860 }, 861 doesMatch: false, 862 wantSeed: []byte{ 863 0x6f, 0x3d, 0x0f, 0xc8, 0x33, 0xab, 0x4d, 0xc4, 864 0xaf, 0xb7, 0x32, 0xcc, 0xd1, 0x77, 0xe9, 0x3e, 865 0x41, 0xdb, 0x0b, 0x7a, 0x9e, 0x6f, 0x34, 0xbe, 866 0xc9, 0xab, 0x6b, 0xb5, 0x17, 0xb2, 0x89, 0x82}, 867 wantMatches: nil, 868 wantNumPassed: 0, 869 wantNumFailed: 1, 870 wantDoneOK: 0, 871 wantNumPartial: 0, 872 wantNumInserted: 0, 873 wantNumUnbooked: 0, 874 wantNumTradesFailed: 1, 875 wantNumNomatched: 1, 876 matchStats: &MatchCycleStats{ 877 BookSells: bookSellLots * LotSize, 878 BookBuys: bookBuyLots * LotSize, 879 HighRate: 0, 880 LowRate: 0, 881 StartRate: 0, 882 EndRate: 0, 883 }, 884 }, 885 { 886 name: "bad lot size order", 887 args: args{ 888 book: newBooker(), 889 queue: []*OrderRevealed{badLotsizeOrder}, 890 }, 891 doesMatch: false, 892 wantSeed: []byte{ 893 0x76, 0x9b, 0xd2, 0x8a, 0x7c, 0x2d, 0xa2, 0x56, 894 0x97, 0xa3, 0xc6, 0x8b, 0x43, 0x0c, 0x9e, 0x9a, 895 0xfa, 0x6c, 0xf6, 0x86, 0x70, 0x97, 0xc7, 0x74, 896 0xb, 0xfb, 0x29, 0x19, 0xbf, 0x86, 0x6a, 0xc8}, 897 wantMatches: nil, 898 wantNumPassed: 0, 899 wantNumFailed: 1, 900 wantDoneOK: 0, 901 wantNumPartial: 0, 902 wantNumInserted: 0, 903 wantNumUnbooked: 0, 904 wantNumTradesFailed: 1, 905 wantNumNomatched: 0, 906 }, 907 { 908 name: "limit buy standing partial taker inserted to book, then filled by down-queue sell", 909 args: args{ 910 book: newBooker(), 911 queue: []*OrderRevealed{ 912 takers[1], 913 takers[4], 914 }, 915 }, 916 doesMatch: true, 917 wantSeed: []byte{ 918 0xb4, 0xc0, 0xcc, 0xc2, 0x3e, 0x50, 0x1d, 0xf5, 919 0x15, 0xe1, 0x29, 0xd7, 0x59, 0xd6, 0x7a, 0xb0, 920 0xd1, 0x24, 0xfa, 0x3c, 0x9d, 0x37, 0x6e, 0x1a, 921 0x8b, 0x15, 0xc5, 0x84, 0xa4, 0xc0, 0x80, 0x98}, 922 wantMatches: []*order.MatchSet{ 923 newMatchSet(takers[1].Order, []*order.LimitOrder{bookSellOrders[nSell-1]}), 924 { // the maker is reduced by matching first item in the queue 925 Taker: takers[4].Order, 926 Makers: []*order.LimitOrder{takers[1].Order.(*order.LimitOrder)}, 927 Amounts: []uint64{1 * LotSize}, // 2 - 1 928 Rates: []uint64{4550000}, 929 Total: 1 * LotSize, 930 }, 931 }, 932 wantNumPassed: 2, 933 wantNumFailed: 0, 934 wantDoneOK: 1, 935 wantNumPartial: 1, 936 wantNumInserted: 1, 937 wantNumUnbooked: 2, 938 wantNumTradesBooked: 1, 939 wantNumTradesCompleted: 3, // both queue orders and one book sell order fully filled 940 }, 941 } 942 for _, tt := range tests { 943 t.Run(tt.name, func(t *testing.T) { 944 // Reset Filled amounts of all pre-defined orders before each test. 945 resetTakers() 946 resetMakers() 947 948 seed, matches, passed, failed, doneOK, partial, booked, nomatched, unbooked, updates, stats := me.Match(tt.args.book, tt.args.queue) 949 matchMade := len(matches) > 0 && matches[0] != nil 950 if tt.doesMatch != matchMade { 951 t.Errorf("Match expected = %v, got = %v", tt.doesMatch, matchMade) 952 } 953 if len(matches) != len(tt.wantMatches) { 954 t.Errorf("number of matches %d, expected %d", len(matches), len(tt.wantMatches)) 955 } 956 for i := range matches { 957 if !reflect.DeepEqual(matches[i], tt.wantMatches[i]) { 958 t.Errorf("matches[%d] = %v, want %v", i, matches[i], tt.wantMatches[i]) 959 } 960 } 961 if !bytes.Equal(seed, tt.wantSeed) { 962 t.Errorf("got seed %#v, expected %x", seed, tt.wantSeed) 963 } 964 if len(passed) != tt.wantNumPassed { 965 t.Errorf("number passed %d, expected %d", len(passed), tt.wantNumPassed) 966 } 967 if len(failed) != tt.wantNumFailed { 968 t.Errorf("number failed %d, expected %d", len(failed), tt.wantNumFailed) 969 } 970 if len(doneOK) != tt.wantDoneOK { 971 t.Errorf("number doneOK %d, expected %d", len(doneOK), tt.wantDoneOK) 972 } 973 if len(partial) != tt.wantNumPartial { 974 t.Errorf("number partial %d, expected %d", len(partial), tt.wantNumPartial) 975 } 976 if len(booked) != tt.wantNumInserted { 977 t.Errorf("number booked %d, expected %d", len(booked), tt.wantNumInserted) 978 } 979 if len(unbooked) != tt.wantNumUnbooked { 980 t.Errorf("number unbooked %d, expected %d", len(unbooked), tt.wantNumUnbooked) 981 } 982 983 if len(updates.TradesCompleted) != tt.wantNumTradesCompleted { 984 t.Errorf("number of trades completed %d, expected %d", len(updates.TradesCompleted), tt.wantNumTradesCompleted) 985 } 986 if len(updates.TradesBooked) != tt.wantNumTradesBooked { 987 t.Errorf("number of trades booked %d, expected %d", len(updates.TradesBooked), tt.wantNumTradesBooked) 988 } 989 if len(updates.TradesFailed) != tt.wantNumTradesFailed { 990 t.Errorf("number of trades failed %d, expected %d", len(updates.TradesFailed), tt.wantNumTradesFailed) 991 } 992 if len(nomatched) != tt.wantNumNomatched { 993 t.Errorf("number of trades nomatched %d, expected %d", len(nomatched), tt.wantNumNomatched) 994 } 995 if tt.matchStats != nil { 996 compareMatchStats(t, tt.matchStats, stats) 997 } 998 }) 999 } 1000 } 1001 1002 func TestMatch_marketSellsOnly(t *testing.T) { 1003 // Setup the match package's logger. 1004 startLogger() 1005 1006 // New matching engine. 1007 me := New() 1008 1009 rnd.Seed(1212121) 1010 1011 badLotsizeOrder := newMarketSellOrder(1, 0) 1012 badLotsizeOrder.Order.(*order.MarketOrder).Quantity /= 2 1013 1014 // takers is heterogenous w.r.t. type 1015 takers := []*OrderRevealed{ 1016 newMarketSellOrder(1, 0), // sell, 1 lot 1017 newMarketSellOrder(3, 0), // sell, 3 lot 1018 newMarketSellOrder(6, 0), // sell, 6 lot, partial maker fill 1019 newMarketSellOrder(99, 0), // sell, 99 lot, partial taker fill 1020 } 1021 1022 partialRate := uint64(2500000) 1023 partialWithRemainder := newLimitOrder(false, partialRate, 2, order.StandingTiF, 0) 1024 partialThenGone := newLimitOrder(false, partialRate, 4, order.StandingTiF, 0) 1025 1026 resetTakers := func() { 1027 for _, o := range takers { 1028 switch ot := o.Order.(type) { 1029 case *order.MarketOrder: 1030 ot.FillAmt = 0 1031 case *order.LimitOrder: 1032 ot.FillAmt = 0 1033 } 1034 } 1035 } 1036 1037 nBuy := len(bookBuyOrders) 1038 1039 type args struct { 1040 book Booker 1041 queue []*OrderRevealed 1042 } 1043 tests := []struct { 1044 name string 1045 args args 1046 doesMatch bool 1047 wantSeed []byte 1048 wantMatches []*order.MatchSet 1049 wantNumPassed int 1050 wantNumFailed int 1051 wantDoneOK int 1052 wantNumPartial int 1053 wantNumInserted int 1054 wantNumUnbooked int 1055 wantNumTradesCompleted int 1056 wantNumTradesPartial int 1057 wantNumTradesFailed int 1058 }{ 1059 { 1060 name: "market sell, 1 maker match", 1061 args: args{ 1062 book: newBooker(), 1063 queue: []*OrderRevealed{takers[0]}, 1064 }, 1065 doesMatch: true, 1066 wantSeed: []byte{ 1067 0xbb, 0x20, 0x85, 0x39, 0x78, 0xd5, 0x09, 0xa9, 1068 0xa0, 0x56, 0x78, 0x4c, 0x50, 0xb1, 0xb4, 0x3d, 1069 0xe3, 0xe9, 0x9b, 0x2b, 0x88, 0x35, 0x2c, 0x71, 1070 0xed, 0xff, 0x5d, 0xc2, 0x1e, 0x87, 0xcb, 0xf8}, 1071 wantMatches: []*order.MatchSet{ 1072 newMatchSet(takers[0].Order, []*order.LimitOrder{bookBuyOrders[nBuy-1]}), 1073 }, 1074 wantNumPassed: 1, 1075 wantNumFailed: 0, 1076 wantDoneOK: 1, 1077 wantNumPartial: 0, 1078 wantNumInserted: 0, 1079 wantNumUnbooked: 1, 1080 wantNumTradesCompleted: 2, // queued market and already booked limit 1081 }, 1082 { 1083 name: "market sell, 2 maker match", 1084 args: args{ 1085 book: newBooker(), 1086 queue: []*OrderRevealed{takers[1]}, 1087 }, 1088 doesMatch: true, 1089 wantSeed: []byte{ 1090 0x9a, 0xdb, 0xbb, 0x7a, 0x9f, 0x7f, 0xe1, 0x72, 1091 0xf0, 0x90, 0x38, 0x7a, 0xa6, 0xb9, 0x8a, 0x83, 1092 0x5a, 0x75, 0x05, 0x19, 0x66, 0x6d, 0x53, 0x12, 1093 0x7e, 0xc8, 0x9d, 0xe3, 0x87, 0x5d, 0xdc, 0x8a}, 1094 wantMatches: []*order.MatchSet{ 1095 newMatchSet(takers[1].Order, []*order.LimitOrder{bookBuyOrders[nBuy-1], bookBuyOrders[nBuy-2]}), 1096 }, 1097 wantNumPassed: 1, 1098 wantNumFailed: 0, 1099 wantDoneOK: 1, 1100 wantNumPartial: 0, 1101 wantNumInserted: 0, 1102 wantNumUnbooked: 2, 1103 wantNumTradesCompleted: 3, // queued market and two already booked limits 1104 }, 1105 { 1106 name: "market sell, 3 maker match, 1 partial maker fill", 1107 args: args{ 1108 book: newBooker(), 1109 queue: []*OrderRevealed{takers[2]}, 1110 }, 1111 doesMatch: true, 1112 wantSeed: []byte{ 1113 0x4e, 0xec, 0x23, 0x4f, 0xfd, 0xb1, 0xd8, 0xd9, 1114 0x14, 0xcd, 0xae, 0x34, 0x3c, 0xa2, 0x14, 0x5d, 1115 0x6e, 0x2d, 0x92, 0xf3, 0xbf, 0xb2, 0x04, 0xfc, 1116 0xee, 0x39, 0x13, 0xbb, 0x55, 0xed, 0x98, 0x81}, 1117 wantMatches: []*order.MatchSet{ 1118 newMatchSet(takers[2].Order, []*order.LimitOrder{bookBuyOrders[nBuy-1], bookBuyOrders[nBuy-2], bookBuyOrders[nBuy-3]}, 3*LotSize), 1119 }, 1120 wantNumPassed: 1, 1121 wantNumFailed: 0, 1122 wantDoneOK: 1, 1123 wantNumPartial: 0, 1124 wantNumInserted: 0, 1125 wantNumUnbooked: 2, 1126 wantNumTradesCompleted: 3, // taker and two already booked limits 1127 wantNumTradesPartial: 1, // partial fill of one already booked limit, which consumed the taker 1128 }, 1129 { 1130 name: "market sell bad lot size", 1131 args: args{ 1132 book: newBooker(), 1133 queue: []*OrderRevealed{badLotsizeOrder}, 1134 }, 1135 doesMatch: false, 1136 wantSeed: []byte{ 1137 0x76, 0x9b, 0xd2, 0x8a, 0x7c, 0x2d, 0xa2, 0x56, 1138 0x97, 0xa3, 0xc6, 0x8b, 0x43, 0x0c, 0x9e, 0x9a, 1139 0xfa, 0x6c, 0xf6, 0x86, 0x70, 0x97, 0xc7, 0x74, 1140 0x0b, 0xfb, 0x29, 0x19, 0xbf, 0x86, 0x6a, 0xc8}, 1141 wantMatches: nil, 1142 wantNumPassed: 0, 1143 wantNumFailed: 1, 1144 wantDoneOK: 0, 1145 wantNumPartial: 0, 1146 wantNumInserted: 0, 1147 wantNumUnbooked: 0, 1148 wantNumTradesFailed: 1, 1149 }, 1150 { 1151 name: "market sell partial fill with remainder", 1152 args: args{ 1153 book: &BookStub{ 1154 lotSize: LotSize, 1155 buyOrders: []*order.LimitOrder{partialWithRemainder}, 1156 }, 1157 queue: []*OrderRevealed{takers[0]}, 1158 }, 1159 doesMatch: true, 1160 wantSeed: []byte{ 1161 0xbb, 0x20, 0x85, 0x39, 0x78, 0xd5, 0x09, 0xa9, 1162 0xa0, 0x56, 0x78, 0x4c, 0x50, 0xb1, 0xb4, 0x3d, 1163 0xe3, 0xe9, 0x9b, 0x2b, 0x88, 0x35, 0x2c, 0x71, 1164 0xed, 0xff, 0x5d, 0xc2, 0x1e, 0x87, 0xcb, 0xf8}, 1165 wantMatches: []*order.MatchSet{ 1166 { 1167 Taker: takers[0].Order, 1168 Makers: []*order.LimitOrder{partialWithRemainder}, 1169 Amounts: []uint64{LotSize}, 1170 Rates: []uint64{partialRate}, 1171 Total: LotSize, 1172 }, 1173 }, 1174 wantNumPassed: 1, 1175 wantNumFailed: 0, 1176 wantDoneOK: 1, 1177 wantNumTradesPartial: 1, 1178 wantNumTradesCompleted: 1, 1179 wantNumUnbooked: 0, 1180 wantNumTradesFailed: 0, 1181 }, 1182 // If a booked order partially matches, but then completes on a subsequent 1183 // order, the updates.TradesPartial should not contain the order. 1184 { 1185 name: "market sell partial fill before complete fill", 1186 args: args{ 1187 book: &BookStub{ 1188 lotSize: LotSize, 1189 buyOrders: []*order.LimitOrder{partialThenGone}, 1190 }, 1191 queue: []*OrderRevealed{takers[0], takers[1]}, 1192 }, 1193 doesMatch: true, 1194 wantSeed: []byte{ 1195 0x29, 0x39, 0xf3, 0x0e, 0x9e, 0x00, 0xc6, 0xe5, 1196 0xc7, 0x1d, 0x56, 0x82, 0x2c, 0x89, 0x92, 0x3c, 1197 0x59, 0x97, 0x84, 0x43, 0xaf, 0x7f, 0x7b, 0x67, 1198 0xeb, 0x54, 0xd0, 0x88, 0x01, 0x2b, 0xf0, 0x58}, 1199 wantMatches: []*order.MatchSet{ 1200 { 1201 Taker: takers[0].Order, 1202 Makers: []*order.LimitOrder{partialThenGone}, 1203 Amounts: []uint64{LotSize}, 1204 Rates: []uint64{partialRate}, 1205 Total: LotSize, 1206 }, 1207 { 1208 Taker: takers[1].Order, 1209 Makers: []*order.LimitOrder{partialThenGone}, 1210 Amounts: []uint64{LotSize * 3}, 1211 Rates: []uint64{partialRate}, 1212 Total: LotSize * 3, 1213 }, 1214 }, 1215 wantNumPassed: 2, 1216 wantNumFailed: 0, 1217 wantDoneOK: 2, 1218 wantNumTradesPartial: 0, 1219 wantNumTradesCompleted: 3, 1220 wantNumUnbooked: 1, 1221 wantNumTradesFailed: 0, 1222 }, 1223 } 1224 for _, tt := range tests { 1225 t.Run(tt.name, func(t *testing.T) { 1226 // Reset Filled amounts of all pre-defined orders before each test. 1227 resetTakers() 1228 resetMakers() 1229 1230 fmt.Printf("%v\n", takers) 1231 1232 seed, matches, passed, failed, doneOK, partial, booked, _, unbooked, updates, _ := me.Match(tt.args.book, tt.args.queue) 1233 matchMade := len(matches) > 0 && matches[0] != nil 1234 if tt.doesMatch != matchMade { 1235 t.Errorf("Match expected = %v, got = %v", tt.doesMatch, matchMade) 1236 } 1237 if len(matches) != len(tt.wantMatches) { 1238 t.Errorf("number of matches %d, expected %d", len(matches), len(tt.wantMatches)) 1239 } 1240 for i := range matches { 1241 if !reflect.DeepEqual(matches[i], tt.wantMatches[i]) { 1242 t.Errorf("matches[%d] = %v, want %v", i, matches[i], tt.wantMatches[i]) 1243 } 1244 } 1245 if !bytes.Equal(seed, tt.wantSeed) { 1246 t.Errorf("got seed %x, expected %x", seed, tt.wantSeed) 1247 } 1248 if len(passed) != tt.wantNumPassed { 1249 t.Errorf("number passed %d, expected %d", len(passed), tt.wantNumPassed) 1250 } 1251 if len(failed) != tt.wantNumFailed { 1252 t.Errorf("number failed %d, expected %d", len(failed), tt.wantNumFailed) 1253 } 1254 if len(doneOK) != tt.wantDoneOK { 1255 t.Errorf("number doneOK %d, expected %d", len(doneOK), tt.wantDoneOK) 1256 } 1257 if len(partial) != tt.wantNumPartial { 1258 t.Errorf("number partial %d, expected %d", len(partial), tt.wantNumPartial) 1259 } 1260 if len(booked) != tt.wantNumInserted { 1261 t.Errorf("number booked %d, expected %d", len(booked), tt.wantNumInserted) 1262 } 1263 if len(unbooked) != tt.wantNumUnbooked { 1264 t.Errorf("number unbooked %d, expected %d", len(unbooked), tt.wantNumUnbooked) 1265 } 1266 1267 if len(updates.TradesCompleted) != tt.wantNumTradesCompleted { 1268 t.Errorf("number of trades completed %d, expected %d", len(updates.TradesCompleted), tt.wantNumTradesCompleted) 1269 } 1270 if len(updates.TradesPartial) != tt.wantNumTradesPartial { 1271 t.Errorf("number of book trades partially filled %d, expected %d", len(updates.TradesPartial), tt.wantNumTradesPartial) 1272 } 1273 if len(updates.TradesFailed) != tt.wantNumTradesFailed { 1274 t.Errorf("number of trades failed %d, expected %d", len(updates.TradesFailed), tt.wantNumTradesFailed) 1275 } 1276 t.Log(updates) 1277 }) 1278 } 1279 } 1280 1281 // marketBuyQuoteAmt gives the exact amount in the quote asset require to 1282 // purchase lots worth of the base asset given the current sell order book. 1283 func marketBuyQuoteAmt(lots uint64) uint64 { 1284 var amt uint64 1285 var i int 1286 nSell := len(bookSellOrders) 1287 for lots > 0 && i < nSell { 1288 sellOrder := bookSellOrders[nSell-1-i] 1289 orderLots := sellOrder.Quantity / LotSize 1290 if orderLots > lots { 1291 orderLots = lots 1292 } 1293 lots -= orderLots 1294 1295 amt += BaseToQuote(sellOrder.Rate, orderLots*LotSize) 1296 i++ 1297 } 1298 return amt 1299 } 1300 1301 // quoteAmt computes the required amount of the quote asset required to purchase 1302 // the specified number of lots given the current order book and required amount 1303 // buffering in the single lot case. 1304 func quoteAmt(lots uint64) uint64 { 1305 amt := marketBuyQuoteAmt(lots) 1306 if lots == 1 { 1307 amt *= 3 1308 amt /= 2 1309 } 1310 return amt 1311 } 1312 1313 func TestMatch_marketBuysOnly(t *testing.T) { 1314 // Setup the match package's logger. 1315 startLogger() 1316 1317 // New matching engine. 1318 me := New() 1319 1320 rnd.Seed(1212121) 1321 1322 nSell := len(bookSellOrders) 1323 1324 // takers is heterogenous w.r.t. type 1325 takers := []*OrderRevealed{ 1326 newMarketBuyOrder(quoteAmt(1), 0), // buy, 1 lot 1327 newMarketBuyOrder(quoteAmt(2), 0), // buy, 2 lot 1328 newMarketBuyOrder(quoteAmt(3), 0), // buy, 3 lot 1329 newMarketBuyOrder(quoteAmt(99), 0), // buy, 99 lot 1330 } 1331 1332 resetTakers := func() { 1333 for _, o := range takers { 1334 switch ot := o.Order.(type) { 1335 case *order.MarketOrder: 1336 ot.FillAmt = 0 1337 case *order.LimitOrder: 1338 ot.FillAmt = 0 1339 } 1340 } 1341 } 1342 1343 bookSellOrdersReverse := make([]*order.LimitOrder, len(bookSellOrders)) 1344 for i := range bookSellOrders { 1345 bookSellOrdersReverse[len(bookSellOrders)-1-i] = bookSellOrders[i] 1346 } 1347 1348 type args struct { 1349 book Booker 1350 queue []*OrderRevealed 1351 } 1352 tests := []struct { 1353 name string 1354 args args 1355 doesMatch bool 1356 wantSeed []byte 1357 wantMatches []*order.MatchSet 1358 remaining []uint64 1359 wantNumPassed int 1360 wantNumFailed int 1361 wantDoneOK int 1362 wantNumPartial int 1363 wantNumInserted int 1364 wantNumUnbooked int 1365 wantNumTradesCompleted int 1366 wantNumTradesPartial int 1367 wantNumTradesFailed int 1368 matchStats *MatchCycleStats 1369 }{ 1370 { 1371 name: "market buy, 1 maker match", 1372 args: args{ 1373 book: newBooker(), 1374 queue: []*OrderRevealed{takers[0]}, 1375 }, 1376 doesMatch: true, 1377 wantSeed: []byte{ 1378 0x76, 0x9b, 0xd2, 0x8a, 0x7c, 0x2d, 0xa2, 0x56, 1379 0x97, 0xa3, 0xc6, 0x8b, 0x43, 0x0c, 0x9e, 0x9a, 1380 0xfa, 0x6c, 0xf6, 0x86, 0x70, 0x97, 0xc7, 0x74, 1381 0x0b, 0xfb, 0x29, 0x19, 0xbf, 0x86, 0x6a, 0xc8}, 1382 wantMatches: []*order.MatchSet{ 1383 // 1 lot @ 4550000, leaving best sell as 4600000 1384 newMatchSet(takers[0].Order, []*order.LimitOrder{bookSellOrders[nSell-1]}), 1385 }, 1386 remaining: []uint64{quoteAmt(1) - marketBuyQuoteAmt(1)}, 1387 wantNumPassed: 1, 1388 wantNumFailed: 0, 1389 wantDoneOK: 1, 1390 wantNumPartial: 0, 1391 wantNumInserted: 0, 1392 wantNumUnbooked: 1, 1393 wantNumTradesCompleted: 2, // the taker and the maker are completed 1394 matchStats: &MatchCycleStats{ 1395 BookSells: (bookSellLots - 1) * LotSize, 1396 BookBuys: bookBuyLots * LotSize, 1397 MatchVolume: LotSize, 1398 HighRate: bookSellOrders[nSell-1].Rate, 1399 LowRate: bookSellOrders[nSell-1].Rate, 1400 StartRate: bookSellOrders[nSell-1].Rate, 1401 EndRate: bookSellOrders[nSell-1].Rate, 1402 }, 1403 }, 1404 { 1405 name: "market buy, 2 maker match", 1406 args: args{ 1407 book: newBooker(), 1408 queue: []*OrderRevealed{takers[1]}, 1409 }, 1410 doesMatch: true, 1411 wantSeed: []byte{ 1412 0xbb, 0x20, 0x85, 0x39, 0x78, 0xd5, 0x09, 0xa9, 1413 0xa0, 0x56, 0x78, 0x4c, 0x50, 0xb1, 0xb4, 0x3d, 1414 0xe3, 0xe9, 0x9b, 0x2b, 0x88, 0x35, 0x2c, 0x71, 1415 0xed, 0xff, 0x5d, 0xc2, 0x1e, 0x87, 0xcb, 0xf8}, 1416 wantMatches: []*order.MatchSet{ 1417 // 1 lot @ 4550000, 1 @ 4600000 1418 newMatchSet(takers[1].Order, []*order.LimitOrder{bookSellOrders[nSell-1], bookSellOrders[nSell-2]}, 1*LotSize), 1419 }, 1420 remaining: []uint64{0}, 1421 wantNumPassed: 1, 1422 wantNumFailed: 0, 1423 wantDoneOK: 1, 1424 wantNumPartial: 0, 1425 wantNumInserted: 0, 1426 wantNumUnbooked: 1, 1427 wantNumTradesCompleted: 2, // taker, and one maker 1428 wantNumTradesPartial: 1, // the second maker 1429 matchStats: &MatchCycleStats{ 1430 BookSells: (bookSellLots - 2) * LotSize, 1431 BookBuys: bookBuyLots * LotSize, 1432 MatchVolume: 2 * LotSize, 1433 HighRate: bookSellOrders[nSell-2].Rate, 1434 LowRate: bookSellOrders[nSell-1].Rate, 1435 StartRate: bookSellOrders[nSell-1].Rate, 1436 EndRate: bookSellOrders[nSell-2].Rate, 1437 }, 1438 }, 1439 { 1440 name: "market buy, 3 maker match", 1441 args: args{ 1442 book: newBooker(), 1443 queue: []*OrderRevealed{takers[2]}, 1444 }, 1445 doesMatch: true, 1446 wantSeed: []byte{ 1447 0x9a, 0xdb, 0xbb, 0x7a, 0x9f, 0x7f, 0xe1, 0x72, 1448 0xf0, 0x90, 0x38, 0x7a, 0xa6, 0xb9, 0x8a, 0x83, 1449 0x5a, 0x75, 0x05, 0x19, 0x66, 0x6d, 0x53, 0x12, 1450 0x7e, 0xc8, 0x9d, 0xe3, 0x87, 0x5d, 0xdc, 0x8a}, 1451 wantMatches: []*order.MatchSet{ 1452 // 1 @ 4550000, 2 @ 4600000, leaves best sell of 4700000 behind/ 1453 newMatchSet(takers[2].Order, []*order.LimitOrder{bookSellOrders[nSell-1], bookSellOrders[nSell-2]}), 1454 }, 1455 remaining: []uint64{0}, 1456 wantNumPassed: 1, 1457 wantNumFailed: 0, 1458 wantDoneOK: 1, 1459 wantNumPartial: 0, 1460 wantNumInserted: 0, 1461 wantNumUnbooked: 2, 1462 wantNumTradesCompleted: 3, // taker, 2 makers 1463 matchStats: &MatchCycleStats{ 1464 BookSells: (bookSellLots - 3) * LotSize, 1465 BookBuys: bookBuyLots * LotSize, 1466 MatchVolume: 3 * LotSize, 1467 HighRate: bookSellOrders[nSell-2].Rate, 1468 LowRate: bookSellOrders[nSell-1].Rate, 1469 StartRate: bookSellOrders[nSell-1].Rate, 1470 EndRate: bookSellOrders[nSell-2].Rate, 1471 }, 1472 }, 1473 { 1474 name: "market buy, 99 maker match", 1475 args: args{ 1476 book: newBooker(), 1477 queue: []*OrderRevealed{takers[3]}, 1478 }, 1479 doesMatch: true, 1480 wantSeed: []byte{ 1481 0x4e, 0xec, 0x23, 0x4f, 0xfd, 0xb1, 0xd8, 0xd9, 1482 0x14, 0xcd, 0xae, 0x34, 0x3c, 0xa2, 0x14, 0x5d, 1483 0x6e, 0x2d, 0x92, 0xf3, 0xbf, 0xb2, 0x04, 0xfc, 1484 0xee, 0x39, 0x13, 0xbb, 0x55, 0xed, 0x98, 0x81}, 1485 wantMatches: []*order.MatchSet{ 1486 newMatchSet(takers[3].Order, bookSellOrdersReverse), 1487 }, 1488 remaining: []uint64{0}, 1489 wantNumPassed: 1, 1490 wantNumFailed: 0, 1491 wantDoneOK: 1, 1492 wantNumPartial: 0, 1493 wantNumInserted: 0, 1494 wantNumUnbooked: 11, 1495 wantNumTradesCompleted: 12, // taker, 11 makers 1496 }, 1497 } 1498 for _, tt := range tests { 1499 t.Run(tt.name, func(t *testing.T) { 1500 // Reset Filled amounts of all pre-defined orders before each test. 1501 resetTakers() 1502 resetMakers() 1503 1504 seed, matches, passed, failed, doneOK, partial, booked, _, unbooked, updates, stats := me.Match(tt.args.book, tt.args.queue) 1505 matchMade := len(matches) > 0 && matches[0] != nil 1506 if tt.doesMatch != matchMade { 1507 t.Errorf("Match expected = %v, got = %v", tt.doesMatch, matchMade) 1508 } 1509 if len(matches) != len(tt.wantMatches) { 1510 t.Errorf("number of matches %d, expected %d", len(matches), len(tt.wantMatches)) 1511 } 1512 1513 for i := range matches { 1514 if !reflect.DeepEqual(matches[i], tt.wantMatches[i]) { 1515 t.Errorf("matches[%d] = %v, want %v", i, matches[i], tt.wantMatches[i]) 1516 } 1517 if matches[i].Taker.Trade().Remaining() != tt.remaining[i] { 1518 t.Errorf("Incorrect taker order amount remaining. Expected %d, got %d", 1519 tt.remaining[i], matches[i].Taker.Trade().Remaining()) 1520 } 1521 } 1522 if !bytes.Equal(seed, tt.wantSeed) { 1523 t.Errorf("got seed %x, expected %x", seed, tt.wantSeed) 1524 } 1525 if len(passed) != tt.wantNumPassed { 1526 t.Errorf("number passed %d, expected %d", len(passed), tt.wantNumPassed) 1527 } 1528 if len(failed) != tt.wantNumFailed { 1529 t.Errorf("number failed %d, expected %d", len(failed), tt.wantNumFailed) 1530 } 1531 if len(doneOK) != tt.wantDoneOK { 1532 t.Errorf("number doneOK %d, expected %d", len(doneOK), tt.wantDoneOK) 1533 } 1534 if len(partial) != tt.wantNumPartial { 1535 t.Errorf("number partial %d, expected %d", len(partial), tt.wantNumPartial) 1536 } 1537 if len(booked) != tt.wantNumInserted { 1538 t.Errorf("number booked %d, expected %d", len(booked), tt.wantNumInserted) 1539 } 1540 if len(unbooked) != tt.wantNumUnbooked { 1541 t.Errorf("number unbooked %d, expected %d", len(unbooked), tt.wantNumUnbooked) 1542 } 1543 1544 if len(updates.TradesCompleted) != tt.wantNumTradesCompleted { 1545 t.Errorf("number of trades completed %d, expected %d", len(updates.TradesCompleted), tt.wantNumTradesCompleted) 1546 } 1547 if len(updates.TradesPartial) != tt.wantNumTradesPartial { 1548 t.Errorf("number of book trades partially filled %d, expected %d", len(updates.TradesPartial), tt.wantNumTradesPartial) 1549 } 1550 if len(updates.TradesFailed) != tt.wantNumTradesFailed { 1551 t.Errorf("number of trades failed %d, expected %d", len(updates.TradesFailed), tt.wantNumTradesFailed) 1552 } 1553 if tt.matchStats != nil { 1554 compareMatchStats(t, tt.matchStats, stats) 1555 } 1556 }) 1557 } 1558 } 1559 func Test_shuffleQueue(t *testing.T) { 1560 // Setup the match package's logger. 1561 startLogger() 1562 1563 // order queues to be shuffled 1564 q3_1 := []*OrderRevealed{ 1565 limitOrders[0], 1566 marketOrders[0], 1567 marketOrders[1], 1568 } 1569 1570 // q3_2 has same orders as q1 in different order 1571 q3_2 := []*OrderRevealed{ 1572 marketOrders[0], 1573 limitOrders[0], 1574 marketOrders[1], 1575 } 1576 1577 // q3Shuffled is the expected result of sorting q3_1 and q3_2 1578 q3Shuffled := []*OrderRevealed{ 1579 marketOrders[1], 1580 limitOrders[0], 1581 marketOrders[0], 1582 } 1583 q3Seed, _ := hex.DecodeString("18ad6e0777c50ce4ea64efcc1a39b8aea26bc96f78ebf163470e0495edd9fb80") 1584 1585 // shuffleQueue should work with nil slice 1586 var qNil []*OrderRevealed 1587 1588 // shuffleQueue should work with empty slice 1589 qEmpty := []*OrderRevealed{} 1590 1591 // shuffleQueue should work with single element slice 1592 q1 := []*OrderRevealed{marketOrders[0]} 1593 q1Seed, _ := hex.DecodeString("d5546f7fbbc4c7761c9a0fa986956f28208d55cb0bf2e132eae01362f51ee1ed") 1594 1595 // shuffleQueue should work with two element slice 1596 q2_a := []*OrderRevealed{ 1597 limitOrders[0], 1598 marketOrders[0], 1599 } 1600 1601 // ... with same output regardless of input order 1602 q2_b := []*OrderRevealed{ 1603 marketOrders[0], 1604 limitOrders[0], 1605 } 1606 1607 q2Shuffled := []*OrderRevealed{ 1608 marketOrders[0], 1609 limitOrders[0], 1610 } 1611 q2Seed, _ := hex.DecodeString("fe51266c189dcc2f5413b62f5e7ef5015bb0d7305188813c5881c1747996ad6b") 1612 1613 tests := []struct { 1614 name string 1615 inOut []*OrderRevealed 1616 want []*OrderRevealed 1617 wantSeed []byte 1618 }{ 1619 { 1620 "q3_1 iter 1", 1621 q3_1, 1622 q3Shuffled, 1623 q3Seed, 1624 }, 1625 { 1626 "q3_1 iter 2", 1627 q3_1, 1628 q3Shuffled, 1629 q3Seed, 1630 }, 1631 { 1632 "q3_1 iter 3", 1633 q3_1, 1634 q3Shuffled, 1635 q3Seed, 1636 }, 1637 { 1638 "q3_2", 1639 q3_2, 1640 q3Shuffled, 1641 q3Seed, 1642 }, 1643 { 1644 "qEmpty", 1645 qEmpty, 1646 []*OrderRevealed{}, 1647 []byte{}, 1648 }, 1649 { 1650 "qNil", 1651 qNil, 1652 []*OrderRevealed(nil), 1653 []byte{}, 1654 }, 1655 { 1656 "q1", 1657 q1, 1658 []*OrderRevealed{q1[0]}, 1659 q1Seed, 1660 }, 1661 { 1662 "q2_a", 1663 q2_a, 1664 q2Shuffled, 1665 q2Seed, 1666 }, 1667 { 1668 "q2_b", 1669 q2_b, 1670 q2Shuffled, 1671 q2Seed, 1672 }, 1673 } 1674 for _, tt := range tests { 1675 t.Run(tt.name, func(t *testing.T) { 1676 seed := shuffleQueue(tt.inOut) 1677 if !reflect.DeepEqual(tt.inOut, tt.want) { 1678 t.Errorf("shuffleQueue(q): q = %#v, want %#v", tt.inOut, tt.want) 1679 } 1680 if !bytes.Equal(seed, tt.wantSeed) { 1681 t.Errorf("got seed %x, expected %x", seed, tt.wantSeed) 1682 } 1683 }) 1684 } 1685 } 1686 1687 func Test_sortQueue(t *testing.T) { 1688 // Setup the match package's logger. 1689 startLogger() 1690 1691 // order queues to be sorted 1692 q3_1 := []*OrderRevealed{ 1693 limitOrders[0], // df8b93e65c03a7ec20f8ae8dce42ba132a613ec3d3848900209d46f515f2d46e 1694 marketOrders[0], // d793e1b4e34d86800ce6a207478d17b4cba73b2b65987b293a361b971697d45c 1695 marketOrders[1], // edf28bf4dc05fce563b1c2725aecec5edef5098c3a9d76fc71d6473ee2dc3fa5 1696 } 1697 1698 // q3_2 has same orders as q1 in different order 1699 q3_2 := []*OrderRevealed{ 1700 marketOrders[0], 1701 limitOrders[0], 1702 marketOrders[1], 1703 } 1704 1705 // q3Sorted is the expected result of sorting q3_1 and q3_2 1706 q3Sorted := []*OrderRevealed{ 1707 marketOrders[0], // #1: d793e1b4e34d86800ce6a207478d17b4cba73b2b65987b293a361b971697d45c 1708 limitOrders[0], // #2: df8b93e65c03a7ec20f8ae8dce42ba132a613ec3d3848900209d46f515f2d46e 1709 marketOrders[1], // #3: edf28bf4dc05fce563b1c2725aecec5edef5098c3a9d76fc71d6473ee2dc3fa5 1710 } 1711 1712 // sortQueue should work with nil slice 1713 var qNil []*OrderRevealed 1714 1715 // sortQueue should work with empty slice 1716 qEmpty := []*OrderRevealed{} 1717 1718 // sortQueue should work with single element slice 1719 q1 := []*OrderRevealed{marketOrders[0]} 1720 1721 // sortQueue should work with two element slice 1722 q2_a := []*OrderRevealed{ 1723 limitOrders[0], 1724 marketOrders[0], 1725 } 1726 1727 // ... with same output regardless of input order 1728 q2_b := []*OrderRevealed{ 1729 marketOrders[0], 1730 limitOrders[0], 1731 } 1732 1733 q2Sorted := []*OrderRevealed{ 1734 marketOrders[0], 1735 limitOrders[0], 1736 } 1737 1738 tests := []struct { 1739 name string 1740 inOut []*OrderRevealed 1741 want []*OrderRevealed 1742 }{ 1743 { 1744 "q3_1 iter 1", 1745 q3_1, 1746 q3Sorted, 1747 }, 1748 { 1749 "q3_1 iter 2", 1750 q3_1, 1751 q3Sorted, 1752 }, 1753 { 1754 "q3_1 iter 3", 1755 q3_1, 1756 q3Sorted, 1757 }, 1758 { 1759 "q3_2", 1760 q3_2, 1761 q3Sorted, 1762 }, 1763 { 1764 "qEmpty", 1765 qEmpty, 1766 []*OrderRevealed{}, 1767 }, 1768 { 1769 "qNil", 1770 qNil, 1771 []*OrderRevealed(nil), 1772 }, 1773 { 1774 "q1", 1775 q1, 1776 []*OrderRevealed{q1[0]}, 1777 }, 1778 { 1779 "q2_a", 1780 q2_a, 1781 q2Sorted, 1782 }, 1783 { 1784 "q2_b", 1785 q2_b, 1786 q2Sorted, 1787 }, 1788 } 1789 for _, tt := range tests { 1790 t.Run(tt.name, func(t *testing.T) { 1791 sortQueueByID(tt.inOut) 1792 if !reflect.DeepEqual(tt.inOut, tt.want) { 1793 t.Errorf("sortQueueByID(q): q = %#v, want %#v", tt.inOut, tt.want) 1794 } 1795 }) 1796 } 1797 } 1798 1799 func TestOrdersMatch(t *testing.T) { 1800 // Setup the match package's logger. 1801 startLogger() 1802 1803 type args struct { 1804 a order.Order 1805 b order.Order 1806 } 1807 tests := []struct { 1808 name string 1809 args args 1810 want bool 1811 }{ 1812 { 1813 "MATCH market buy : limit sell", 1814 args{ 1815 marketOrders[0].Order, 1816 limitOrders[1].Order, 1817 }, 1818 true, 1819 }, 1820 { 1821 "MATCH market sell : limit buy", 1822 args{ 1823 marketOrders[1].Order, 1824 limitOrders[0].Order, 1825 }, 1826 true, 1827 }, 1828 { 1829 "MATCH limit sell : market buy", 1830 args{ 1831 limitOrders[1].Order, 1832 marketOrders[0].Order, 1833 }, 1834 true, 1835 }, 1836 { 1837 "MATCH limit buy : market sell", 1838 args{ 1839 limitOrders[0].Order, 1840 marketOrders[1].Order, 1841 }, 1842 true, 1843 }, 1844 { 1845 "NO MATCH market sell : market buy", 1846 args{ 1847 marketOrders[1].Order, 1848 marketOrders[0].Order, 1849 }, 1850 false, 1851 }, 1852 { 1853 "NO MATCH (rates) limit sell : limit buy", 1854 args{ 1855 limitOrders[0].Order, 1856 limitOrders[1].Order, 1857 }, 1858 false, 1859 }, 1860 { 1861 "NO MATCH (rates) limit buy : limit sell", 1862 args{ 1863 limitOrders[1].Order, 1864 limitOrders[0].Order, 1865 }, 1866 false, 1867 }, 1868 { 1869 "MATCH (overlapping rates) limit sell : limit buy", 1870 args{ 1871 limitOrders[1].Order, 1872 limitOrders[2].Order, 1873 }, 1874 true, 1875 }, 1876 { 1877 "MATCH (same rates) limit sell : limit buy", 1878 args{ 1879 limitOrders[1].Order, 1880 limitOrders[3].Order, 1881 }, 1882 true, 1883 }, 1884 { 1885 "NO MATCH (same side) limit buy : limit buy", 1886 args{ 1887 limitOrders[2].Order, 1888 limitOrders[3].Order, 1889 }, 1890 false, 1891 }, 1892 { 1893 "NO MATCH (cancel) market buy : cancel", 1894 args{ 1895 marketOrders[0].Order, 1896 &order.CancelOrder{}, 1897 }, 1898 false, 1899 }, 1900 { 1901 "NO MATCH (cancel) cancel : market sell", 1902 args{ 1903 &order.CancelOrder{}, 1904 marketOrders[1].Order, 1905 }, 1906 false, 1907 }, 1908 { 1909 "NO MATCH (cancel) limit sell : cancel", 1910 args{ 1911 limitOrders[1].Order, 1912 &order.CancelOrder{}, 1913 }, 1914 false, 1915 }, 1916 } 1917 for _, tt := range tests { 1918 t.Run(tt.name, func(t *testing.T) { 1919 if got := OrdersMatch(tt.args.a, tt.args.b); got != tt.want { 1920 t.Errorf("OrdersMatch() = %v, want %v", got, tt.want) 1921 } 1922 }) 1923 } 1924 } 1925 1926 func TestBaseToQuote(t *testing.T) { 1927 type args struct { 1928 rate uint64 1929 rateFloat float64 1930 base uint64 1931 } 1932 tests := []struct { 1933 name string 1934 args args 1935 wantQuote uint64 1936 }{ 1937 { 1938 name: "ok <1", 1939 args: args{ 1940 rate: 1234132, 1941 rateFloat: 0.01234132, 1942 base: 4200000000, 1943 }, 1944 wantQuote: 51833544, 1945 }, 1946 { 1947 name: "ok 1", 1948 args: args{ 1949 rate: 100000000, 1950 rateFloat: 1.0, 1951 base: 4200000000, 1952 }, 1953 wantQuote: 4200000000, 1954 }, 1955 { 1956 name: "ok >1", 1957 args: args{ 1958 rate: 100000022, 1959 rateFloat: 1.00000022, 1960 base: 4200000000, 1961 }, 1962 wantQuote: 4200000924, 1963 }, 1964 { 1965 name: "ok >>1", 1966 args: args{ 1967 rate: 19900000022, 1968 rateFloat: 199.00000022, 1969 base: 4200000000, 1970 }, 1971 wantQuote: 835800000924, 1972 }, 1973 } 1974 for _, tt := range tests { 1975 t.Run(tt.name, func(t *testing.T) { 1976 gotQuote := BaseToQuote(tt.args.rate, tt.args.base) 1977 if gotQuote != tt.wantQuote { 1978 t.Errorf("BaseToQuote() = %v, want %v", gotQuote, tt.wantQuote) 1979 } 1980 // quote2 := uint64(tt.args.rateFloat * float64(tt.args.base)) 1981 // t.Logf("quote from integer rate = %d, from float rate = %d, diff = %d", 1982 // gotQuote, quote2, int64(gotQuote-quote2)) 1983 }) 1984 } 1985 } 1986 1987 func TestQuoteToBase(t *testing.T) { 1988 type args struct { 1989 rate uint64 1990 rateFloat float64 1991 quote uint64 1992 } 1993 tests := []struct { 1994 name string 1995 args args 1996 wantBase uint64 1997 }{ 1998 { 1999 name: "ok <1", 2000 args: args{ 2001 rate: 1234132, 2002 rateFloat: 0.01234132, 2003 quote: 51833544, 2004 }, 2005 wantBase: 4200000000, 2006 }, 2007 { 2008 name: "ok 1", 2009 args: args{ 2010 rate: 100000000, 2011 rateFloat: 1.0, 2012 quote: 4200000000, 2013 }, 2014 wantBase: 4200000000, 2015 }, 2016 { 2017 name: "ok >1", 2018 args: args{ 2019 rate: 100000022, 2020 rateFloat: 1.00000022, 2021 quote: 4200000924, 2022 }, 2023 wantBase: 4200000000, 2024 }, 2025 { 2026 name: "don't panic on 0 rate", 2027 args: args{ 2028 rate: 0, 2029 rateFloat: 0, 2030 quote: 51833544, 2031 }, 2032 wantBase: 0, 2033 }, 2034 { 2035 name: "ok >>1", 2036 args: args{ 2037 rate: 19900000022, 2038 rateFloat: 199.00000022, 2039 quote: 835800000924, 2040 }, 2041 wantBase: 4200000000, 2042 }, 2043 } 2044 for _, tt := range tests { 2045 t.Run(tt.name, func(t *testing.T) { 2046 gotBase := QuoteToBase(tt.args.rate, tt.args.quote) 2047 if gotBase != tt.wantBase { 2048 t.Errorf("QuoteToBase() = %v, want %v", gotBase, tt.wantBase) 2049 } 2050 // base2 := uint64(float64(tt.args.quote) / tt.args.rateFloat) 2051 // t.Logf("base2 from integer rate = %d, from float rate = %d, diff = %d", 2052 // gotBase, base2, int64(gotBase-base2)) 2053 }) 2054 } 2055 } 2056 2057 func TestQuoteToBaseToQuote(t *testing.T) { 2058 type args struct { 2059 rate uint64 2060 rateFloat float64 2061 quote uint64 2062 } 2063 tests := []struct { 2064 name string 2065 args args 2066 }{ 2067 { 2068 name: "ok <1", 2069 args: args{ 2070 rate: 1234132, 2071 rateFloat: 0.01234132, 2072 quote: 51833544, 2073 }, 2074 }, 2075 { 2076 name: "ok 1", 2077 args: args{ 2078 rate: 100000000, 2079 rateFloat: 1.0, 2080 quote: 4200000000, 2081 }, 2082 }, 2083 { 2084 name: "ok >1", 2085 args: args{ 2086 rate: 100000022, 2087 rateFloat: 1.00000022, 2088 quote: 4200000924, 2089 }, 2090 }, 2091 { 2092 name: "ok >>1", 2093 args: args{ 2094 rate: 19900000022, 2095 rateFloat: 199.00000022, 2096 quote: 835800000924, 2097 }, 2098 }, 2099 } 2100 for _, tt := range tests { 2101 t.Run(tt.name, func(t *testing.T) { 2102 gotBase := QuoteToBase(tt.args.rate, tt.args.quote) 2103 gotQuote := BaseToQuote(tt.args.rate, gotBase) 2104 if gotQuote != tt.args.quote { 2105 t.Errorf("Failed quote->base->quote round trip. %d != %d", 2106 gotQuote, tt.args.quote) 2107 } 2108 2109 // baseFlt := float64(tt.args.quote) / tt.args.rateFloat 2110 // quoteFlt := baseFlt * tt.args.rateFloat 2111 2112 // t.Logf("expected quote = %d, final quote = %d, float quote = %f", 2113 // tt.args.quote, gotQuote, quoteFlt) 2114 }) 2115 } 2116 } 2117 2118 func TestBaseToQuoteToBase(t *testing.T) { 2119 type args struct { 2120 rate uint64 2121 rateFloat float64 2122 base uint64 2123 } 2124 tests := []struct { 2125 name string 2126 args args 2127 }{ 2128 { 2129 name: "ok <1", 2130 args: args{ 2131 rate: 1234132, 2132 rateFloat: 0.01234132, 2133 base: 4200000000, 2134 }, 2135 }, 2136 { 2137 name: "ok 1", 2138 args: args{ 2139 rate: 100000000, 2140 rateFloat: 1.0, 2141 base: 4200000000, 2142 }, 2143 }, 2144 { 2145 name: "ok >1", 2146 args: args{ 2147 rate: 100000022, 2148 rateFloat: 1.00000022, 2149 base: 4200000000, 2150 }, 2151 }, 2152 { 2153 name: "ok >>1", 2154 args: args{ 2155 rate: 19900000022, 2156 rateFloat: 199.00000022, 2157 base: 4200000000, 2158 }, 2159 }, 2160 } 2161 for _, tt := range tests { 2162 t.Run(tt.name, func(t *testing.T) { 2163 gotQuote := BaseToQuote(tt.args.rate, tt.args.base) 2164 gotBase := QuoteToBase(tt.args.rate, gotQuote) 2165 if gotBase != tt.args.base { 2166 t.Errorf("Failed base->quote->base round trip. %d != %d", 2167 gotBase, tt.args.base) 2168 } 2169 2170 // quoteFlt := float64(tt.args.base) * tt.args.rateFloat 2171 // baseFlt := quoteFlt / tt.args.rateFloat 2172 2173 // t.Logf("expected quote = %d, final quote = %d, float quote = %f", 2174 // tt.args.base, gotBase, baseFlt) 2175 }) 2176 } 2177 } 2178 2179 func compareMatchStats(t *testing.T, sWant, sHave *MatchCycleStats) { 2180 t.Helper() 2181 if sWant.BookBuys != sHave.BookBuys { 2182 t.Errorf("wrong BookBuys. wanted %d, got %d", sWant.BookBuys, sHave.BookBuys) 2183 } 2184 // if sWant.BookBuys5 != sHave.BookBuys5 { 2185 // t.Errorf("wrong BookBuys5. wanted %d, got %d", sWant.BookBuys5, sHave.BookBuys5) 2186 // } 2187 // if sWant.BookBuys25 != sHave.BookBuys25 { 2188 // t.Errorf("wrong BookBuys25. wanted %d, got %d", sWant.BookBuys25, sHave.BookBuys25) 2189 // } 2190 if sWant.BookSells != sHave.BookSells { 2191 t.Errorf("wrong BookSells. wanted %d, got %d", sWant.BookSells, sHave.BookSells) 2192 } 2193 // if sWant.BookSells5 != sHave.BookSells5 { 2194 // t.Errorf("wrong BookSells5. wanted %d, got %d", sWant.BookSells5, sHave.BookSells5) 2195 // } 2196 // if sWant.BookSells25 != sHave.BookSells25 { 2197 // t.Errorf("wrong BookSells25. wanted %d, got %d", sWant.BookSells25, sHave.BookSells25) 2198 // } 2199 if sWant.MatchVolume != sHave.MatchVolume { 2200 t.Errorf("wrong MatchVolume. wanted %d, got %d", sWant.MatchVolume, sHave.MatchVolume) 2201 } 2202 if sWant.HighRate != sHave.HighRate { 2203 t.Errorf("wrong HighRate. wanted %d, got %d", sWant.HighRate, sHave.HighRate) 2204 } 2205 if sWant.LowRate != sHave.LowRate { 2206 t.Errorf("wrong LowRate. wanted %d, got %d", sWant.LowRate, sHave.LowRate) 2207 } 2208 if sWant.StartRate != sHave.StartRate { 2209 t.Errorf("wrong StartRate. wanted %d, got %d", sWant.StartRate, sHave.StartRate) 2210 } 2211 if sWant.EndRate != sHave.EndRate { 2212 t.Errorf("wrong EndRate. wanted %d, got %d", sWant.EndRate, sHave.EndRate) 2213 } 2214 } 2215 2216 func TestCSum(t *testing.T) { 2217 // This is just a smoke test to ensure a known result does not change. It 2218 // also corresponds to a csum used in market_test: 2219 // a64ee6372a49f9465910ca0b556818dbc765f3c7fa21d5f40ab25bf4b73f45ed 2220 // and in client's epochqueue_test: 2221 // 8c743c3225b89ffbb50b5d766d3e078cd8e2658fa8cb6e543c4101e1d59a8e8e. 2222 2223 pi0B, _ := hex.DecodeString("e1f796fa0fc16ba7bb90be2a33e87c3d60ab628471a420834383661801bb0bfd") 2224 var pi0 order.Preimage 2225 copy(pi0[:], pi0B) 2226 fmt.Printf("%x\n", pi0) 2227 com0 := pi0.Commit() // aba75140b1f6edf26955a97e1b09d7b17abdc9c0b099fc73d9729501652fbf66 2228 lo0 := &order.LimitOrder{ 2229 P: order.Prefix{ 2230 Commit: com0, 2231 }, 2232 } 2233 2234 pi1B, _ := hex.DecodeString("8e6c140071db1eb2f7a18194f1a045a94c078835c75dff2f3e836180baad9e95") 2235 var pi1 order.Preimage 2236 copy(pi1[:], pi1B) 2237 com1 := pi1.Commit() // 0f4bc030d392cef3f44d0781870ab7fcb78a0cda36c73e50b88c741b4f851600 2238 lo1 := &order.LimitOrder{ 2239 P: order.Prefix{ 2240 Commit: com1, 2241 }, 2242 } 2243 2244 wantCSum, _ := hex.DecodeString("a64ee6372a49f9465910ca0b556818dbc765f3c7fa21d5f40ab25bf4b73f45ed") 2245 csum := CSum([]order.Order{lo0, lo1}) 2246 if !bytes.Equal(wantCSum, csum) { 2247 t.Errorf("got csum %x, wanted %x", csum, wantCSum) 2248 } 2249 2250 // a third for the matching client-side test in epochqueue_test 2251 pi2B, _ := hex.DecodeString("e1f796fa0fc16ba7bb90be2a33e87c3d60ab628471a420834383661801bb0bfd") 2252 var pi2 order.Preimage 2253 copy(pi2[:], pi2B) 2254 com2 := pi2.Commit() // aba75140b1f6edf26955a97e1b09d7b17abdc9c0b099fc73d9729501652fbf66 2255 lo2 := &order.LimitOrder{ 2256 P: order.Prefix{ 2257 Commit: com2, 2258 }, 2259 } 2260 2261 wantCSum, _ = hex.DecodeString("8c743c3225b89ffbb50b5d766d3e078cd8e2658fa8cb6e543c4101e1d59a8e8e") 2262 csum = CSum([]order.Order{lo0, lo1, lo2}) 2263 if !bytes.Equal(wantCSum, csum) { 2264 t.Errorf("got csum %x, wanted %x", csum, wantCSum) 2265 } 2266 }