File size: 8,219 Bytes
10150dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
"""
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.")