Spaces:
Sleeping
Sleeping
| // tests/test_order_book.cpp | |
| // Unit tests for the matching engine core | |
| // Build: cmake --build build --target hft_tests && ./build/hft_tests | |
| using namespace hft; | |
| static int tests_passed = 0; | |
| static int tests_failed = 0; | |
| // βββ Price Conversion Tests ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TEST(test_to_price_positive) { | |
| ASSERT_EQ(to_price(185.50), 1855000); | |
| ASSERT_EQ(to_price(0.0001), 1); | |
| ASSERT_EQ(to_price(100.0), 1000000); | |
| PASS(); | |
| } | |
| TEST(test_to_price_negative) { | |
| ASSERT_EQ(to_price(-1.5), -15000); | |
| ASSERT_EQ(to_price(-0.0001), -1); | |
| ASSERT_EQ(to_price(-100.0), -1000000); | |
| PASS(); | |
| } | |
| TEST(test_to_price_roundtrip) { | |
| double original = 185.4567; | |
| Price p = to_price(original); | |
| double back = from_price(p); | |
| ASSERT_TRUE(std::abs(back - original) < 0.00005); | |
| PASS(); | |
| } | |
| // βββ Order Book: Basic Insert ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TEST(test_add_limit_order_no_match) { | |
| OrderBook book("AAPL"); | |
| Order buy; | |
| buy.id = 1; buy.symbol = "AAPL"; buy.side = Side::BUY; | |
| buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 100; | |
| auto trades = book.add_order(buy); | |
| ASSERT_EQ(trades.size(), 0u); | |
| ASSERT_EQ(book.order_count(), 1u); | |
| ASSERT_EQ(book.bid_levels(), 1u); | |
| ASSERT_EQ(book.ask_levels(), 0u); | |
| ASSERT_EQ(book.best_bid(), to_price(185.0)); | |
| PASS(); | |
| } | |
| TEST(test_multiple_levels) { | |
| OrderBook book("AAPL"); | |
| Order o1, o2, o3; | |
| o1.id = 1; o1.symbol = "AAPL"; o1.side = Side::BUY; | |
| o1.type = OrderType::LIMIT; o1.price = to_price(185.0); o1.qty = 100; | |
| o2.id = 2; o2.symbol = "AAPL"; o2.side = Side::BUY; | |
| o2.type = OrderType::LIMIT; o2.price = to_price(184.0); o2.qty = 200; | |
| o3.id = 3; o3.symbol = "AAPL"; o3.side = Side::SELL; | |
| o3.type = OrderType::LIMIT; o3.price = to_price(186.0); o3.qty = 150; | |
| book.add_order(o1); | |
| book.add_order(o2); | |
| book.add_order(o3); | |
| ASSERT_EQ(book.bid_levels(), 2u); | |
| ASSERT_EQ(book.ask_levels(), 1u); | |
| ASSERT_EQ(book.best_bid(), to_price(185.0)); | |
| ASSERT_EQ(book.best_ask(), to_price(186.0)); | |
| PASS(); | |
| } | |
| // βββ Order Book: Matching ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TEST(test_limit_buy_matches_ask) { | |
| OrderBook book("AAPL"); | |
| Order sell; | |
| sell.id = 1; sell.symbol = "AAPL"; sell.side = Side::SELL; | |
| sell.type = OrderType::LIMIT; sell.price = to_price(185.0); sell.qty = 100; | |
| book.add_order(sell); | |
| Order buy; | |
| buy.id = 2; buy.symbol = "AAPL"; buy.side = Side::BUY; | |
| buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 100; | |
| auto trades = book.add_order(buy); | |
| ASSERT_EQ(trades.size(), 1u); | |
| ASSERT_EQ(trades[0].qty, 100); | |
| ASSERT_EQ(trades[0].price, to_price(185.0)); | |
| ASSERT_EQ(book.order_count(), 0u); | |
| PASS(); | |
| } | |
| TEST(test_limit_sell_matches_bid) { | |
| OrderBook book("AAPL"); | |
| Order buy; | |
| buy.id = 1; buy.symbol = "AAPL"; buy.side = Side::BUY; | |
| buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 100; | |
| book.add_order(buy); | |
| Order sell; | |
| sell.id = 2; sell.symbol = "AAPL"; sell.side = Side::SELL; | |
| sell.type = OrderType::LIMIT; sell.price = to_price(185.0); sell.qty = 100; | |
| auto trades = book.add_order(sell); | |
| ASSERT_EQ(trades.size(), 1u); | |
| ASSERT_EQ(trades[0].qty, 100); | |
| ASSERT_EQ(book.order_count(), 0u); | |
| PASS(); | |
| } | |
| TEST(test_partial_fill) { | |
| OrderBook book("AAPL"); | |
| Order sell; | |
| sell.id = 1; sell.symbol = "AAPL"; sell.side = Side::SELL; | |
| sell.type = OrderType::LIMIT; sell.price = to_price(185.0); sell.qty = 200; | |
| book.add_order(sell); | |
| Order buy; | |
| buy.id = 2; buy.symbol = "AAPL"; buy.side = Side::BUY; | |
| buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 50; | |
| auto trades = book.add_order(buy); | |
| ASSERT_EQ(trades.size(), 1u); | |
| ASSERT_EQ(trades[0].qty, 50); | |
| ASSERT_EQ(book.order_count(), 1u); // resting sell still has 150 | |
| ASSERT_EQ(book.ask_levels(), 1u); | |
| PASS(); | |
| } | |
| TEST(test_multi_level_sweep) { | |
| OrderBook book("AAPL"); | |
| Order s1, s2; | |
| s1.id = 1; s1.symbol = "AAPL"; s1.side = Side::SELL; | |
| s1.type = OrderType::LIMIT; s1.price = to_price(185.0); s1.qty = 100; | |
| s2.id = 2; s2.symbol = "AAPL"; s2.side = Side::SELL; | |
| s2.type = OrderType::LIMIT; s2.price = to_price(186.0); s2.qty = 100; | |
| book.add_order(s1); | |
| book.add_order(s2); | |
| // Aggressive buy sweeps both levels | |
| Order buy; | |
| buy.id = 3; buy.symbol = "AAPL"; buy.side = Side::BUY; | |
| buy.type = OrderType::LIMIT; buy.price = to_price(187.0); buy.qty = 200; | |
| auto trades = book.add_order(buy); | |
| ASSERT_EQ(trades.size(), 2u); | |
| ASSERT_EQ(trades[0].price, to_price(185.0)); // best ask first | |
| ASSERT_EQ(trades[1].price, to_price(186.0)); | |
| ASSERT_EQ(book.order_count(), 0u); | |
| ASSERT_EQ(book.ask_levels(), 0u); | |
| PASS(); | |
| } | |
| TEST(test_sell_side_multi_level_sweep) { | |
| // This specifically tests the sell-side iterator fix (Bug #4) | |
| OrderBook book("AAPL"); | |
| Order b1, b2, b3; | |
| b1.id = 1; b1.symbol = "AAPL"; b1.side = Side::BUY; | |
| b1.type = OrderType::LIMIT; b1.price = to_price(185.0); b1.qty = 100; | |
| b2.id = 2; b2.symbol = "AAPL"; b2.side = Side::BUY; | |
| b2.type = OrderType::LIMIT; b2.price = to_price(184.0); b2.qty = 100; | |
| b3.id = 3; b3.symbol = "AAPL"; b3.side = Side::BUY; | |
| b3.type = OrderType::LIMIT; b3.price = to_price(183.0); b3.qty = 100; | |
| book.add_order(b1); | |
| book.add_order(b2); | |
| book.add_order(b3); | |
| // Aggressive sell sweeps all 3 bid levels | |
| Order sell; | |
| sell.id = 4; sell.symbol = "AAPL"; sell.side = Side::SELL; | |
| sell.type = OrderType::MARKET; sell.price = 0; sell.qty = 300; | |
| auto trades = book.add_order(sell); | |
| ASSERT_EQ(trades.size(), 3u); | |
| ASSERT_EQ(trades[0].price, to_price(185.0)); // best bid first | |
| ASSERT_EQ(trades[1].price, to_price(184.0)); | |
| ASSERT_EQ(trades[2].price, to_price(183.0)); | |
| ASSERT_EQ(book.bid_levels(), 0u); | |
| ASSERT_EQ(book.order_count(), 0u); | |
| PASS(); | |
| } | |
| // βββ Order Types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TEST(test_market_order_no_counterparty) { | |
| OrderBook book("AAPL"); | |
| Order buy; | |
| buy.id = 1; buy.symbol = "AAPL"; buy.side = Side::BUY; | |
| buy.type = OrderType::MARKET; buy.price = 0; buy.qty = 100; | |
| auto trades = book.add_order(buy); | |
| ASSERT_EQ(trades.size(), 0u); | |
| ASSERT_EQ(buy.status, OrderStatus::CANCELLED); | |
| ASSERT_EQ(book.order_count(), 0u); | |
| PASS(); | |
| } | |
| TEST(test_ioc_partial_fill) { | |
| OrderBook book("AAPL"); | |
| Order sell; | |
| sell.id = 1; sell.symbol = "AAPL"; sell.side = Side::SELL; | |
| sell.type = OrderType::LIMIT; sell.price = to_price(185.0); sell.qty = 50; | |
| book.add_order(sell); | |
| Order buy; | |
| buy.id = 2; buy.symbol = "AAPL"; buy.side = Side::BUY; | |
| buy.type = OrderType::IOC; buy.price = to_price(185.0); buy.qty = 100; | |
| auto trades = book.add_order(buy); | |
| ASSERT_EQ(trades.size(), 1u); | |
| ASSERT_EQ(trades[0].qty, 50); | |
| ASSERT_EQ(buy.status, OrderStatus::EXPIRED); // unfilled remainder expired | |
| ASSERT_EQ(book.order_count(), 0u); // IOC not added to book | |
| PASS(); | |
| } | |
| TEST(test_fok_rejected_insufficient_liquidity) { | |
| OrderBook book("AAPL"); | |
| Order sell; | |
| sell.id = 1; sell.symbol = "AAPL"; sell.side = Side::SELL; | |
| sell.type = OrderType::LIMIT; sell.price = to_price(185.0); sell.qty = 50; | |
| book.add_order(sell); | |
| Order buy; | |
| buy.id = 2; buy.symbol = "AAPL"; buy.side = Side::BUY; | |
| buy.type = OrderType::FOK; buy.price = to_price(185.0); buy.qty = 100; | |
| auto trades = book.add_order(buy); | |
| ASSERT_EQ(trades.size(), 0u); | |
| ASSERT_EQ(buy.status, OrderStatus::EXPIRED); | |
| ASSERT_EQ(book.order_count(), 1u); // resting sell untouched | |
| PASS(); | |
| } | |
| TEST(test_fok_filled_sufficient_liquidity) { | |
| OrderBook book("AAPL"); | |
| Order sell; | |
| sell.id = 1; sell.symbol = "AAPL"; sell.side = Side::SELL; | |
| sell.type = OrderType::LIMIT; sell.price = to_price(185.0); sell.qty = 100; | |
| book.add_order(sell); | |
| Order buy; | |
| buy.id = 2; buy.symbol = "AAPL"; buy.side = Side::BUY; | |
| buy.type = OrderType::FOK; buy.price = to_price(185.0); buy.qty = 100; | |
| auto trades = book.add_order(buy); | |
| ASSERT_EQ(trades.size(), 1u); | |
| ASSERT_EQ(trades[0].qty, 100); | |
| ASSERT_EQ(book.order_count(), 0u); | |
| PASS(); | |
| } | |
| // βββ Cancel & Modify βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TEST(test_cancel_order) { | |
| OrderBook book("AAPL"); | |
| Order buy; | |
| buy.id = 1; buy.symbol = "AAPL"; buy.side = Side::BUY; | |
| buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 100; | |
| book.add_order(buy); | |
| ASSERT_TRUE(book.cancel_order(1)); | |
| ASSERT_EQ(book.order_count(), 0u); | |
| ASSERT_EQ(book.bid_levels(), 0u); | |
| ASSERT_EQ(buy.status, OrderStatus::CANCELLED); | |
| PASS(); | |
| } | |
| TEST(test_cancel_nonexistent) { | |
| OrderBook book("AAPL"); | |
| ASSERT_TRUE(!book.cancel_order(999)); | |
| PASS(); | |
| } | |
| TEST(test_modify_order) { | |
| OrderBook book("AAPL"); | |
| Order buy; | |
| buy.id = 1; buy.symbol = "AAPL"; buy.side = Side::BUY; | |
| buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 100; | |
| book.add_order(buy); | |
| ASSERT_TRUE(book.modify_order(1, to_price(186.0), 200)); | |
| ASSERT_EQ(book.best_bid(), to_price(186.0)); | |
| PASS(); | |
| } | |
| // βββ Snapshot ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TEST(test_snapshot_depth) { | |
| OrderBook book("AAPL"); | |
| for (int i = 1; i <= 20; ++i) { | |
| Order o; | |
| o.id = i; o.symbol = "AAPL"; o.side = Side::BUY; | |
| o.type = OrderType::LIMIT; o.price = to_price(185.0 - i * 0.01); o.qty = 100; | |
| book.add_order(o); | |
| } | |
| auto snap = book.snapshot(5); | |
| ASSERT_EQ((int)snap.bids.size(), 5); // only top 5 | |
| ASSERT_TRUE(snap.bids[0].price > snap.bids[1].price); // sorted descending | |
| PASS(); | |
| } | |
| TEST(test_snapshot_spread) { | |
| OrderBook book("AAPL"); | |
| Order buy, sell; | |
| buy.id = 1; buy.symbol = "AAPL"; buy.side = Side::BUY; | |
| buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 100; | |
| sell.id = 2; sell.symbol = "AAPL"; sell.side = Side::SELL; | |
| sell.type = OrderType::LIMIT; sell.price = to_price(185.10); sell.qty = 100; | |
| book.add_order(buy); | |
| book.add_order(sell); | |
| auto snap = book.snapshot(); | |
| ASSERT_TRUE(snap.spread() > 0); | |
| ASSERT_TRUE(std::abs(snap.spread() - 0.10) < 0.001); | |
| PASS(); | |
| } | |
| TEST(test_empty_book_spread) { | |
| OrderBookSnapshot empty; | |
| ASSERT_TRUE(empty.spread() == 0.0); | |
| ASSERT_TRUE(empty.mid() == 0); | |
| PASS(); | |
| } | |
| // βββ FIFO Priority ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TEST(test_fifo_priority) { | |
| OrderBook book("AAPL"); | |
| // Two sells at same price β first should fill first | |
| Order s1, s2; | |
| s1.id = 1; s1.symbol = "AAPL"; s1.side = Side::SELL; | |
| s1.type = OrderType::LIMIT; s1.price = to_price(185.0); s1.qty = 100; | |
| s2.id = 2; s2.symbol = "AAPL"; s2.side = Side::SELL; | |
| s2.type = OrderType::LIMIT; s2.price = to_price(185.0); s2.qty = 100; | |
| book.add_order(s1); | |
| book.add_order(s2); | |
| Order buy; | |
| buy.id = 3; buy.symbol = "AAPL"; buy.side = Side::BUY; | |
| buy.type = OrderType::LIMIT; buy.price = to_price(185.0); buy.qty = 100; | |
| auto trades = book.add_order(buy); | |
| ASSERT_EQ(trades.size(), 1u); | |
| ASSERT_EQ(trades[0].sell_order_id, 1u); // s1 filled first (FIFO) | |
| ASSERT_EQ(book.order_count(), 1u); // s2 still resting | |
| PASS(); | |
| } | |
| // βββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| int main() { | |
| std::cout << "\n=== Orderflow Replay Engine β Unit Tests ===\n\n"; | |
| // Tests run automatically via static initialization | |
| std::cout << "\nβββββββββββββββββββββββββββββββββββββββββββββ\n"; | |
| std::cout << " Passed: " << tests_passed << "\n"; | |
| std::cout << " Failed: " << tests_failed << "\n"; | |
| std::cout << "βββββββββββββββββββββββββββββββββββββββββββββ\n\n"; | |
| return tests_failed > 0 ? 1 : 0; | |
| } | |