Marketmind / tests /test_order_book.py
ARKAISW's picture
Initial MarketMind commit (Corrected Author)
10150dc
"""
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
# Add parent to path so we can import marketmind
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 at 99, Ask at 101 — no cross
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)
# Resting ask at 100
ask = Order(agent_id="seller", side=Side.SELL, price=100.0, quantity=5, timestamp=1)
book.submit_order(ask)
# Incoming buy at 100 — crosses the 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
# Both sides fully filled — book should be empty
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)
# Resting ask: 3 units at 100
ask = Order(agent_id="seller", side=Side.SELL, price=100.0, quantity=3, timestamp=1)
book.submit_order(ask)
# Incoming buy: 5 units at 100 — should fill 3, rest 2
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 # ask fully consumed
assert len(book.bids) == 1 # residual buy rests
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)
# Two bids at different prices
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}"
# Incoming sell at 99 — should match the 100 bid (better price), not the 98 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 # fills at passive order's price
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)
# Two asks at same price, different timestamps
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 3 at 100 — should match the earlier ask
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)
# Ask book: 3 @ 100, 5 @ 101, 2 @ 102
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 7 @ 102 — should eat through 100 and 101 levels
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 # first level
assert trades[1].price == 101.0 and trades[1].quantity == 4 # partial second level
# s2 should have 1 unit remaining at 101
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
# No residual buy (7 filled: 3 + 4)
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 # no trades yet
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))
# Self-trade is allowed in this simulation
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.")