github.com/dylantkx/matching-engine-core@v0.0.0-20220929025549-6afe3c7e6e9c/engine_test.go (about) 1 package matchingenginecore_test 2 3 import ( 4 "fmt" 5 "math/rand" 6 "sync" 7 "testing" 8 "time" 9 10 me "github.com/dylantkx/matching-engine-core" 11 "github.com/dylantkx/matching-engine-core/model" 12 "github.com/shopspring/decimal" 13 ) 14 15 func TestGetHighestBuyPrice(t *testing.T) { 16 engine := me.NewMatchingEngine() 17 18 p := engine.GetHighestBuyPrice() 19 if !p.IsZero() { 20 t.Fatalf("expect highest buy to be 0, but got %s", p) 21 } 22 23 ord := model.OrderLimit{ 24 ID: "1", 25 Units: decimal.NewFromFloat(1), 26 Price: decimal.NewFromFloat(100), 27 Side: model.OrderSide_Buy, 28 } 29 engine.ProcessLimitOrder(&ord) 30 31 p = engine.GetHighestBuyPrice() 32 if !p.Equal(decimal.NewFromFloat(100)) { 33 t.Fatalf("expect highest buy to be 100, but got %s", p) 34 } 35 } 36 37 func TestGetLowestSellPrice(t *testing.T) { 38 engine := me.NewMatchingEngine() 39 40 p := engine.GetLowestSellPrice() 41 if !p.IsZero() { 42 t.Fatalf("expect lowest sell to be 0, but got %s", p) 43 } 44 45 ord := model.OrderLimit{ 46 ID: "1", 47 Units: decimal.NewFromFloat(1), 48 Price: decimal.NewFromFloat(100), 49 Side: model.OrderSide_Sell, 50 } 51 engine.ProcessLimitOrder(&ord) 52 53 p = engine.GetLowestSellPrice() 54 if !p.Equal(decimal.NewFromFloat(100)) { 55 t.Fatalf("expect lowest sell to be 100, but got %s", p) 56 } 57 } 58 59 func TestProcessLimitOneBuyOrder(t *testing.T) { 60 engine := me.NewMatchingEngine() 61 62 ord := model.OrderLimit{ 63 ID: "1", 64 Units: decimal.NewFromFloat(1), 65 Price: decimal.NewFromFloat(100), 66 Side: model.OrderSide_Buy, 67 } 68 r := engine.ProcessLimitOrder(&ord) 69 sn := engine.GetOrderBookFullSnapshot() 70 71 if len(r.Trades) > 0 { 72 t.Fatalf("expect no trades but got %d", len(r.Trades)) 73 } 74 if sn == nil || len(sn.Buys) != 1 { 75 t.Fatalf("expect order buy book size = 1 but got %d", len(sn.Buys)) 76 } 77 } 78 79 func TestProcessLimitOneSellOrder(t *testing.T) { 80 engine := me.NewMatchingEngine() 81 82 ord := model.OrderLimit{ 83 ID: "1", 84 Units: decimal.NewFromFloat(1), 85 Price: decimal.NewFromFloat(100), 86 Side: model.OrderSide_Sell, 87 } 88 r := engine.ProcessLimitOrder(&ord) 89 sn := engine.GetOrderBookFullSnapshot() 90 91 if len(r.Trades) > 0 { 92 t.Fatalf("expect no trades but got %d", len(r.Trades)) 93 } 94 if sn == nil || len(sn.Sells) != 1 { 95 t.Fatalf("expect order book sell size = 1 but got %d", len(sn.Sells)) 96 } 97 } 98 99 func TestProcessLimitOrderWithoutTrades(t *testing.T) { 100 engine := me.NewMatchingEngine() 101 102 sellOrders := []model.OrderLimit{ 103 { 104 ID: "1", 105 Units: decimal.NewFromFloat(1), 106 Price: decimal.NewFromFloat(100), 107 Side: model.OrderSide_Sell, 108 }, 109 { 110 ID: "2", 111 Units: decimal.NewFromFloat(1), 112 Price: decimal.NewFromFloat(200), 113 Side: model.OrderSide_Sell, 114 }, 115 } 116 for _, ord := range sellOrders { 117 engine.ProcessLimitOrder(&ord) 118 } 119 sn1 := engine.GetOrderBookFullSnapshot() 120 if sn1 == nil || len(sn1.Sells) != 2 { 121 t.Fatalf("expect order book sell size = 2 but got %d", len(sn1.Sells)) 122 } 123 124 buyOrder := model.OrderLimit{ 125 ID: "3", 126 Units: decimal.NewFromFloat(1), 127 Price: decimal.NewFromFloat(50), 128 Side: model.OrderSide_Buy, 129 } 130 r := engine.ProcessLimitOrder(&buyOrder) 131 sn2 := engine.GetOrderBookFullSnapshot() 132 133 if len(r.Trades) != 0 { 134 t.Fatalf("expect 1 trade but got %d", len(r.Trades)) 135 } 136 if sn2 == nil || len(sn2.Buys) != 1 { 137 t.Fatalf("expect order buy book size = 1 but got %d", len(sn2.Buys)) 138 } 139 } 140 141 func TestLimitBuyOrderProducingTrades(t *testing.T) { 142 engine := me.NewMatchingEngine() 143 144 sellOrders := []model.OrderLimit{ 145 { 146 ID: "1", 147 Units: decimal.NewFromFloat(1), 148 Price: decimal.NewFromFloat(100), 149 Side: model.OrderSide_Sell, 150 }, 151 { 152 ID: "2", 153 Units: decimal.NewFromFloat(1), 154 Price: decimal.NewFromFloat(200), 155 Side: model.OrderSide_Sell, 156 }, 157 } 158 for _, ord := range sellOrders { 159 engine.ProcessLimitOrder(&ord) 160 } 161 sn1 := engine.GetOrderBookFullSnapshot() 162 if sn1 == nil || len(sn1.Sells) != 2 { 163 t.Fatalf("expect order book sell size = 2 but got %d", len(sn1.Sells)) 164 } 165 166 buyOrder := model.OrderLimit{ 167 ID: "3", 168 Units: decimal.NewFromFloat(1.5), 169 Price: decimal.NewFromFloat(200), 170 Side: model.OrderSide_Buy, 171 } 172 r := engine.ProcessLimitOrder(&buyOrder) 173 sn2 := engine.GetOrderBookFullSnapshot() 174 175 if len(r.Trades) != 2 { 176 t.Fatalf("expect 2 trade but got %d", len(r.Trades)) 177 } 178 tr := r.Trades[0] 179 if tr.BuyOrderID != buyOrder.ID || tr.IsBuyerMaker || tr.Price.GreaterThan(buyOrder.Price) || !tr.Units.Equal(sellOrders[0].Units) { 180 t.Fatalf("wrong trade output: %+v", tr) 181 } 182 183 if sn2 == nil || len(sn2.Sells) != 1 { 184 t.Fatalf("expect order sell book size = 1 but got %d", len(sn2.Sells)) 185 } 186 } 187 188 func TestLimitSellOrderProducingTrades(t *testing.T) { 189 engine := me.NewMatchingEngine() 190 191 buyOrders := []model.OrderLimit{ 192 { 193 ID: "1", 194 Units: decimal.NewFromFloat(1), 195 Price: decimal.NewFromFloat(100), 196 Side: model.OrderSide_Buy, 197 }, 198 { 199 ID: "2", 200 Units: decimal.NewFromFloat(1), 201 Price: decimal.NewFromFloat(200), 202 Side: model.OrderSide_Buy, 203 }, 204 } 205 for _, ord := range buyOrders { 206 engine.ProcessLimitOrder(&ord) 207 } 208 sn1 := engine.GetOrderBookFullSnapshot() 209 if sn1 == nil || len(sn1.Buys) != 2 { 210 t.Fatalf("expect order book buy size = 2 but got %d", len(sn1.Buys)) 211 } 212 213 sellOrder := model.OrderLimit{ 214 ID: "3", 215 Units: decimal.NewFromFloat(2), 216 Price: decimal.NewFromFloat(200), 217 Side: model.OrderSide_Sell, 218 } 219 r := engine.ProcessLimitOrder(&sellOrder) 220 sn2 := engine.GetOrderBookFullSnapshot() 221 222 if len(r.Trades) != 1 { 223 t.Fatalf("expect 1 trade but got %d", len(r.Trades)) 224 } 225 tr := r.Trades[0] 226 if tr.SellOrderID != sellOrder.ID || !tr.IsBuyerMaker || tr.Price.LessThan(sellOrder.Price) || !tr.Units.Equal(decimal.NewFromFloat(1)) { 227 t.Fatalf("wrong trade output: %+v", tr) 228 } 229 230 if sn2 == nil || len(sn2.Sells) != 1 { 231 t.Fatalf("expect order sell book size = 1 but got %d", len(sn2.Sells)) 232 } 233 if sn2 == nil || len(sn2.Buys) != 1 { 234 t.Fatalf("expect order buy book size = 1 but got %d", len(sn2.Buys)) 235 } 236 } 237 238 func TestProcessMarketOneBuyOrder(t *testing.T) { 239 engine := me.NewMatchingEngine() 240 241 ord := model.OrderMarket{ 242 ID: "1", 243 Units: decimal.NewFromFloat(1), 244 Side: model.OrderSide_Buy, 245 } 246 r := engine.ProcessMarketOrder(&ord) 247 248 if len(r.Trades) > 0 { 249 t.Fatalf("expect no trades but got %d", len(r.Trades)) 250 } 251 if len(r.Cancellations) != 1 { 252 t.Fatalf("expect 1 cancellation but got %d", len(r.Cancellations)) 253 } 254 if !r.Cancellations[0].Units.Equal(ord.Units) { 255 t.Fatalf("expect to cancel whole order, but got %s", r.Cancellations[0].Units) 256 } 257 } 258 259 func TestProcessMarketOneSellOrder(t *testing.T) { 260 engine := me.NewMatchingEngine() 261 262 ord := model.OrderMarket{ 263 ID: "1", 264 Units: decimal.NewFromFloat(1), 265 Side: model.OrderSide_Sell, 266 } 267 r := engine.ProcessMarketOrder(&ord) 268 269 if len(r.Trades) > 0 { 270 t.Fatalf("expect no trades but got %d", len(r.Trades)) 271 } 272 if len(r.Cancellations) != 1 { 273 t.Fatalf("expect 1 cancellation but got %d", len(r.Cancellations)) 274 } 275 if !r.Cancellations[0].Units.Equal(ord.Units) { 276 t.Fatalf("expect to cancel whole order, but got %s", r.Cancellations[0].Units) 277 } 278 } 279 280 func TestMarketBuyOrderProducingTrades(t *testing.T) { 281 engine := me.NewMatchingEngine() 282 283 sellOrders := []model.OrderLimit{ 284 { 285 ID: "1", 286 Units: decimal.NewFromFloat(1), 287 Price: decimal.NewFromFloat(100), 288 Side: model.OrderSide_Sell, 289 }, 290 { 291 ID: "2", 292 Units: decimal.NewFromFloat(1), 293 Price: decimal.NewFromFloat(200), 294 Side: model.OrderSide_Sell, 295 }, 296 } 297 for _, ord := range sellOrders { 298 engine.ProcessLimitOrder(&ord) 299 } 300 301 buyOrder := model.OrderMarket{ 302 ID: "3", 303 Units: decimal.NewFromFloat(1.5), 304 Side: model.OrderSide_Buy, 305 } 306 r := engine.ProcessMarketOrder(&buyOrder) 307 sn2 := engine.GetOrderBookFullSnapshot() 308 309 if len(r.Trades) != 2 { 310 t.Fatalf("expect 2 trades but got %d", len(r.Trades)) 311 } 312 if len(r.Cancellations) != 0 { 313 t.Fatalf("expect 0 cancels but got %d", len(r.Cancellations)) 314 } 315 tr1, tr2 := r.Trades[0], r.Trades[1] 316 if tr1.BuyOrderID != buyOrder.ID || tr1.IsBuyerMaker || !tr1.Price.Equal(sellOrders[0].Price) || !tr1.Units.Equal(sellOrders[0].Units) { 317 t.Fatalf("wrong trade output: %+v", tr1) 318 } 319 if tr2.BuyOrderID != buyOrder.ID || tr2.IsBuyerMaker || !tr2.Price.Equal(sellOrders[1].Price) || !tr2.Units.Equal(buyOrder.Units.Sub(sellOrders[0].Units)) { 320 t.Fatalf("wrong trade output: %+v", tr2) 321 } 322 323 if sn2 == nil || len(sn2.Sells) != 1 { 324 t.Fatalf("expect order sell book size = 1 but got %d", len(sn2.Sells)) 325 } 326 } 327 328 func TestMarketSellOrderProducingTrades(t *testing.T) { 329 engine := me.NewMatchingEngine() 330 331 buyOrders := []model.OrderLimit{ 332 { 333 ID: "1", 334 Units: decimal.NewFromFloat(1), 335 Price: decimal.NewFromFloat(200), 336 Side: model.OrderSide_Buy, 337 }, 338 { 339 ID: "2", 340 Units: decimal.NewFromFloat(1), 341 Price: decimal.NewFromFloat(100), 342 Side: model.OrderSide_Buy, 343 }, 344 } 345 for _, ord := range buyOrders { 346 engine.ProcessLimitOrder(&ord) 347 } 348 349 sellOrder := model.OrderMarket{ 350 ID: "3", 351 Units: decimal.NewFromFloat(1.5), 352 Side: model.OrderSide_Sell, 353 } 354 r := engine.ProcessMarketOrder(&sellOrder) 355 sn2 := engine.GetOrderBookFullSnapshot() 356 357 if len(r.Trades) != 2 { 358 t.Fatalf("expect 2 trade but got %d", len(r.Trades)) 359 } 360 if len(r.Cancellations) != 0 { 361 t.Fatalf("expect 0 cancels but got %d", len(r.Cancellations)) 362 } 363 tr1, tr2 := r.Trades[0], r.Trades[1] 364 if tr1.SellOrderID != sellOrder.ID || !tr1.IsBuyerMaker || !tr1.Price.Equal(buyOrders[0].Price) || !tr1.Units.Equal(buyOrders[0].Units) { 365 t.Fatalf("wrong trade output: %+v", tr1) 366 } 367 if tr2.SellOrderID != sellOrder.ID || !tr2.IsBuyerMaker || !tr2.Price.Equal(buyOrders[1].Price) || !tr2.Units.Equal(sellOrder.Units.Sub(buyOrders[0].Units)) { 368 t.Fatalf("wrong trade output: %+v", tr2) 369 } 370 371 if sn2 == nil || len(sn2.Buys) != 1 { 372 t.Fatalf("expect order buy book size = 1 but got %d", len(sn2.Buys)) 373 } 374 } 375 376 func TestMarketBuyOrderProducingTradesWithCancels(t *testing.T) { 377 engine := me.NewMatchingEngine() 378 379 sellOrders := []model.OrderLimit{ 380 { 381 ID: "1", 382 Units: decimal.NewFromFloat(1), 383 Price: decimal.NewFromFloat(100), 384 Side: model.OrderSide_Sell, 385 }, 386 { 387 ID: "2", 388 Units: decimal.NewFromFloat(1), 389 Price: decimal.NewFromFloat(200), 390 Side: model.OrderSide_Sell, 391 }, 392 } 393 for _, ord := range sellOrders { 394 engine.ProcessLimitOrder(&ord) 395 } 396 397 buyOrder := model.OrderMarket{ 398 ID: "3", 399 Units: decimal.NewFromFloat(2.5), 400 Side: model.OrderSide_Buy, 401 } 402 r := engine.ProcessMarketOrder(&buyOrder) 403 sn2 := engine.GetOrderBookFullSnapshot() 404 405 if len(r.Trades) != 2 { 406 t.Fatalf("expect 2 trade but got %d", len(r.Trades)) 407 } 408 if len(r.Cancellations) != 1 { 409 t.Fatalf("expect 1 cancels but got %d", len(r.Cancellations)) 410 } 411 tr1, tr2 := r.Trades[0], r.Trades[1] 412 if tr1.BuyOrderID != buyOrder.ID || tr1.IsBuyerMaker || !tr1.Price.Equal(sellOrders[0].Price) || !tr1.Units.Equal(sellOrders[0].Units) { 413 t.Fatalf("wrong trade output: %+v", tr1) 414 } 415 if tr2.BuyOrderID != buyOrder.ID || tr2.IsBuyerMaker || !tr2.Price.Equal(sellOrders[1].Price) || !tr2.Units.Equal(sellOrders[0].Units) { 416 t.Fatalf("wrong trade output: %+v", tr2) 417 } 418 if !r.Cancellations[0].Units.Equal(buyOrder.Units.Sub(sellOrders[0].Units.Add(sellOrders[1].Units))) { 419 t.Fatalf("wrong cancelled units, got %s", r.Cancellations[0].Units) 420 } 421 422 if sn2 == nil || len(sn2.Sells) != 0 { 423 fmt.Printf("%+v\n", sn2.Sells) 424 t.Fatalf("expect order sell book size = 0 but got %d", len(sn2.Sells)) 425 } 426 } 427 428 func TestNearlyConcurrentMarketBuys(t *testing.T) { 429 engine := me.NewMatchingEngine() 430 431 sellOrders := []model.OrderLimit{ 432 { 433 ID: "1", 434 Units: decimal.NewFromFloat(1), 435 Price: decimal.NewFromFloat(100), 436 Side: model.OrderSide_Sell, 437 }, 438 { 439 ID: "2", 440 Units: decimal.NewFromFloat(1), 441 Price: decimal.NewFromFloat(200), 442 Side: model.OrderSide_Sell, 443 }, 444 } 445 for _, ord := range sellOrders { 446 engine.ProcessLimitOrder(&ord) 447 } 448 449 buyOrder1 := model.OrderMarket{ 450 ID: "3", 451 Units: decimal.NewFromFloat(1), 452 Side: model.OrderSide_Buy, 453 } 454 buyOrder2 := model.OrderMarket{ 455 ID: "4", 456 Units: decimal.NewFromFloat(1), 457 Side: model.OrderSide_Buy, 458 } 459 460 var trades_1, trades_2 []model.Trade 461 462 wg := sync.WaitGroup{} 463 wg.Add(2) 464 465 go func() { 466 defer wg.Done() 467 r := engine.ProcessMarketOrder(&buyOrder1) 468 trades_1 = r.Trades 469 }() 470 go func() { 471 time.Sleep(time.Millisecond * 1) 472 defer wg.Done() 473 r := engine.ProcessMarketOrder(&buyOrder2) 474 trades_2 = r.Trades 475 }() 476 477 wg.Wait() 478 479 if len(trades_1) != 1 { 480 t.Fatalf("expect 1 trade but got %d", len(trades_1)) 481 } 482 483 if !trades_1[0].Price.Equal(sellOrders[0].Price) { 484 t.Fatalf("expect trade 1 to be filled with price of %s, but got %s", sellOrders[0].Price, trades_1[0].Price) 485 } 486 if !trades_2[0].Price.Equal(sellOrders[1].Price) { 487 t.Fatalf("expect trade 2 to be filled with price of %s, but got %s", sellOrders[1].Price, trades_2[0].Price) 488 } 489 } 490 491 func TestCancelOrder(t *testing.T) { 492 engine := me.NewMatchingEngine() 493 494 ord := model.OrderLimit{ 495 ID: "1", 496 Units: decimal.NewFromFloat(1), 497 Price: decimal.NewFromFloat(100), 498 Side: model.OrderSide_Buy, 499 } 500 engine.ProcessLimitOrder(&ord) 501 502 cancels, err := engine.CancelOrder(model.Order{ 503 ID: ord.ID, 504 Units: ord.Units.Copy(), 505 Price: ord.Price.Copy(), 506 Side: ord.Side, 507 }) 508 if err != nil { 509 t.Fatalf(err.Error()) 510 } 511 512 sn := engine.GetOrderBookFullSnapshot() 513 514 if len(cancels) != 1 { 515 t.Fatalf("expect 1 cancel but got %d", len(cancels)) 516 } 517 if cancels[0].OrderID != ord.ID || !cancels[0].Units.Equal(ord.Units) { 518 t.Fatalf("wrong cancel output: %+v", cancels) 519 } 520 if sn == nil || len(sn.Buys) != 0 { 521 t.Fatalf("expect order buy book size = 0 but got %d", len(sn.Buys)) 522 } 523 } 524 525 func BenchmarkProcessLimitOrders(b *testing.B) { 526 b.StopTimer() 527 engine := me.NewMatchingEngine() 528 orders := make([]*model.OrderLimit, 0, b.N) 529 for i := 0; i < b.N; i++ { 530 order := &model.OrderLimit{ 531 ID: fmt.Sprintf("%d", i+1), 532 Units: decimal.NewFromFloat((rand.Float64() + float64(rand.Intn(2))) * 10), 533 Price: decimal.NewFromFloat((rand.Float64() + float64(rand.Intn(2))) * 100), 534 Side: model.OrderSide_Buy, 535 } 536 orders = append(orders, order) 537 } 538 b.StartTimer() 539 for _, order := range orders { 540 engine.ProcessLimitOrder(order) 541 } 542 } 543 544 func BenchmarkProcessLimitOrdersAsync(b *testing.B) { 545 b.StopTimer() 546 engine := me.NewMatchingEngine() 547 orders := make([]*model.OrderLimit, 0, b.N) 548 for i := 0; i < b.N; i++ { 549 order := &model.OrderLimit{ 550 ID: fmt.Sprintf("%d", i+1), 551 Units: decimal.NewFromFloat((rand.Float64() + float64(rand.Intn(2))) * 10), 552 Price: decimal.NewFromFloat((rand.Float64() + float64(rand.Intn(2))) * 100), 553 Side: model.OrderSide_Buy, 554 } 555 orders = append(orders, order) 556 } 557 b.StartTimer() 558 wg := sync.WaitGroup{} 559 for _, order := range orders { 560 wg.Add(1) 561 go func(order *model.OrderLimit) { 562 defer wg.Done() 563 engine.ProcessLimitOrder(order) 564 }(order) 565 } 566 wg.Wait() 567 } 568 569 func BenchmarkProcessOneMarketOrderProducingTrades(b *testing.B) { 570 b.StopTimer() 571 engine := me.NewMatchingEngine() 572 var n int = 1e5 573 orders := make([]*model.OrderLimit, 0, n) 574 for i := 0; i < n; i++ { 575 order := &model.OrderLimit{ 576 ID: fmt.Sprintf("%d", i+1), 577 Units: decimal.NewFromFloat((rand.Float64() + float64(rand.Intn(2))) * 10), 578 Price: decimal.NewFromFloat((rand.Float64() + float64(rand.Intn(2))) * 100), 579 Side: model.OrderSide_Buy, 580 } 581 orders = append(orders, order) 582 } 583 for _, order := range orders { 584 engine.ProcessLimitOrder(order) 585 } 586 587 order := &model.OrderMarket{ 588 ID: fmt.Sprintf("%d", n+1), 589 Units: decimal.NewFromFloat(100), 590 Side: model.OrderSide_Sell, 591 } 592 593 b.StartTimer() 594 engine.ProcessMarketOrder(order) 595 }