| """ |
| Unit tests for the CDA Order Book engine. |
| |
| Tests per Phase 1 spec: |
| - Crossing orders execute correctly |
| - Non-crossing orders rest in book |
| - Price-time priority |
| - Partial fills |
| - Trade log accuracy |
| """ |
|
|
| import sys |
| import os |
|
|
| |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) |
|
|
| from engine.order_book import OrderBook, Order, Side, Trade |
|
|
|
|
| def test_non_crossing_orders_rest(): |
| """Non-crossing orders should rest in the book without matching.""" |
| book = OrderBook() |
| book.set_tick(1) |
|
|
| |
| bid = Order(agent_id="agent_a", side=Side.BUY, price=99.0, quantity=5, timestamp=1) |
| ask = Order(agent_id="agent_b", side=Side.SELL, price=101.0, quantity=5, timestamp=1) |
|
|
| trades_bid = book.submit_order(bid) |
| trades_ask = book.submit_order(ask) |
|
|
| assert trades_bid == [], "Non-crossing bid should not produce trades" |
| assert trades_ask == [], "Non-crossing ask should not produce trades" |
| assert book.best_bid == 99.0 |
| assert book.best_ask == 101.0 |
| assert book.mid_price == 100.0 |
| assert book.spread == 2.0 |
| assert len(book.bids) == 1 |
| assert len(book.asks) == 1 |
| print("✓ test_non_crossing_orders_rest") |
|
|
|
|
| def test_crossing_orders_execute(): |
| """When a buy crosses the ask, a trade should execute.""" |
| book = OrderBook() |
| book.set_tick(1) |
|
|
| |
| ask = Order(agent_id="seller", side=Side.SELL, price=100.0, quantity=5, timestamp=1) |
| book.submit_order(ask) |
|
|
| |
| buy = Order(agent_id="buyer", side=Side.BUY, price=100.0, quantity=5, timestamp=2) |
| trades = book.submit_order(buy) |
|
|
| assert len(trades) == 1 |
| t = trades[0] |
| assert t.price == 100.0 |
| assert t.quantity == 5 |
| assert t.buyer_id == "buyer" |
| assert t.seller_id == "seller" |
| assert t.aggressor_side == Side.BUY |
| |
| assert len(book.bids) == 0 |
| assert len(book.asks) == 0 |
| print("✓ test_crossing_orders_execute") |
|
|
|
|
| def test_partial_fill(): |
| """A larger buy should partially fill against a smaller ask, with residual resting.""" |
| book = OrderBook() |
| book.set_tick(1) |
|
|
| |
| ask = Order(agent_id="seller", side=Side.SELL, price=100.0, quantity=3, timestamp=1) |
| book.submit_order(ask) |
|
|
| |
| buy = Order(agent_id="buyer", side=Side.BUY, price=100.0, quantity=5, timestamp=2) |
| trades = book.submit_order(buy) |
|
|
| assert len(trades) == 1 |
| assert trades[0].quantity == 3 |
| assert len(book.asks) == 0 |
| assert len(book.bids) == 1 |
| assert book.bids[0].quantity == 2 |
| assert book.bids[0].agent_id == "buyer" |
| print("✓ test_partial_fill") |
|
|
|
|
| def test_price_priority(): |
| """Best price should match first (highest bid, lowest ask).""" |
| book = OrderBook() |
| book.set_tick(1) |
|
|
| |
| bid_low = Order(agent_id="bidder_low", side=Side.BUY, price=98.0, quantity=5, timestamp=1) |
| bid_high = Order(agent_id="bidder_high", side=Side.BUY, price=100.0, quantity=5, timestamp=2) |
| book.submit_order(bid_low) |
| book.submit_order(bid_high) |
|
|
| assert book.best_bid == 100.0, f"Best bid should be 100, got {book.best_bid}" |
|
|
| |
| sell = Order(agent_id="seller", side=Side.SELL, price=99.0, quantity=3, timestamp=3) |
| trades = book.submit_order(sell) |
|
|
| assert len(trades) == 1 |
| assert trades[0].buyer_id == "bidder_high" |
| assert trades[0].price == 100.0 |
| assert trades[0].quantity == 3 |
| print("✓ test_price_priority") |
|
|
|
|
| def test_time_priority(): |
| """At the same price level, earlier orders should fill first (FIFO).""" |
| book = OrderBook() |
| book.set_tick(1) |
|
|
| |
| ask_early = Order(agent_id="early_seller", side=Side.SELL, price=100.0, quantity=5, timestamp=1) |
| ask_late = Order(agent_id="late_seller", side=Side.SELL, price=100.0, quantity=5, timestamp=2) |
| book.submit_order(ask_early) |
| book.submit_order(ask_late) |
|
|
| |
| buy = Order(agent_id="buyer", side=Side.BUY, price=100.0, quantity=3, timestamp=3) |
| trades = book.submit_order(buy) |
|
|
| assert len(trades) == 1 |
| assert trades[0].seller_id == "early_seller" |
| print("✓ test_time_priority") |
|
|
|
|
| def test_multi_level_fill(): |
| """A large aggressive order should sweep through multiple price levels.""" |
| book = OrderBook() |
| book.set_tick(1) |
|
|
| |
| book.submit_order(Order("s1", Side.SELL, 100.0, 3, 1)) |
| book.submit_order(Order("s2", Side.SELL, 101.0, 5, 2)) |
| book.submit_order(Order("s3", Side.SELL, 102.0, 2, 3)) |
|
|
| |
| buy = Order(agent_id="buyer", side=Side.BUY, price=102.0, quantity=7, timestamp=4) |
| trades = book.submit_order(buy) |
|
|
| assert len(trades) == 2 |
| assert trades[0].price == 100.0 and trades[0].quantity == 3 |
| assert trades[1].price == 101.0 and trades[1].quantity == 4 |
|
|
| |
| assert len(book.asks) == 2 |
| assert book.asks[0].price == 101.0 |
| assert book.asks[0].quantity == 1 |
| assert book.asks[1].price == 102.0 |
| |
| assert len(book.bids) == 0 |
| print("✓ test_multi_level_fill") |
|
|
|
|
| def test_trade_log(): |
| """Trade log should accumulate all executed trades.""" |
| book = OrderBook() |
| book.set_tick(1) |
|
|
| book.submit_order(Order("s1", Side.SELL, 100.0, 5, 1)) |
| book.submit_order(Order("b1", Side.BUY, 100.0, 3, 2)) |
|
|
| book.set_tick(2) |
| book.submit_order(Order("s2", Side.SELL, 99.0, 2, 3)) |
| book.submit_order(Order("b2", Side.BUY, 99.0, 2, 4)) |
|
|
| assert len(book.trade_log) == 2 |
| assert book.trade_log[0].tick == 1 |
| assert book.trade_log[1].tick == 2 |
| print("✓ test_trade_log") |
|
|
|
|
| def test_snapshot(): |
| """Snapshot should return correct book state.""" |
| book = OrderBook() |
| book.set_tick(1) |
|
|
| book.submit_order(Order("b1", Side.BUY, 99.0, 10, 1)) |
| book.submit_order(Order("s1", Side.SELL, 101.0, 8, 2)) |
|
|
| snap = book.snapshot() |
| assert snap["best_bid"] == 99.0 |
| assert snap["best_ask"] == 101.0 |
| assert snap["mid_price"] == 100.0 |
| assert snap["spread"] == 2.0 |
| assert snap["bid_depth"] == 10 |
| assert snap["ask_depth"] == 8 |
| assert snap["last_trade_price"] is None |
| print("✓ test_snapshot") |
|
|
|
|
| def test_cancel_agent_orders(): |
| """Canceling an agent's orders should remove only their orders.""" |
| book = OrderBook() |
| book.set_tick(1) |
|
|
| book.submit_order(Order("a1", Side.BUY, 99.0, 5, 1)) |
| book.submit_order(Order("a2", Side.BUY, 98.0, 5, 2)) |
| book.submit_order(Order("a1", Side.SELL, 102.0, 5, 3)) |
|
|
| book.cancel_agent_orders("a1") |
|
|
| assert len(book.bids) == 1 |
| assert book.bids[0].agent_id == "a2" |
| assert len(book.asks) == 0 |
| print("✓ test_cancel_agent_orders") |
|
|
|
|
| def test_self_trade_prevention_not_required(): |
| """ |
| Note: The spec doesn't require self-trade prevention. |
| Documenting that an agent CAN match against itself (this is fine in a simulation). |
| """ |
| book = OrderBook() |
| book.set_tick(1) |
|
|
| book.submit_order(Order("same_agent", Side.SELL, 100.0, 5, 1)) |
| trades = book.submit_order(Order("same_agent", Side.BUY, 100.0, 3, 2)) |
|
|
| |
| assert len(trades) == 1 |
| assert trades[0].buyer_id == "same_agent" |
| assert trades[0].seller_id == "same_agent" |
| print("✓ test_self_trade (allowed in simulation)") |
|
|
|
|
| if __name__ == "__main__": |
| test_non_crossing_orders_rest() |
| test_crossing_orders_execute() |
| test_partial_fill() |
| test_price_priority() |
| test_time_priority() |
| test_multi_level_fill() |
| test_trade_log() |
| test_snapshot() |
| test_cancel_agent_orders() |
| test_self_trade_prevention_not_required() |
| print("\n✅ All order book tests passed.") |
|
|