Repository navigation
Expand file tree
/
Copy pathengine.py
More file actions
170 lines (139 loc) · 5.38 KB
/
Copy pathengine.py
File metadata and controls
170 lines (139 loc) · 5.38 KB
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
# File: engine.py
from __future__ import annotations
from dataclasses import dataclass
from typing import List
from models import Order, SYMBOL, Side, Trade
from orderbook import OrderBook
@dataclass(slots=True)
class OrderExecutionResult:
trades: list[Trade]
resting_order_added: bool
class MatchingEngine:
"""
Deterministic limit-order matching engine for a single symbol.
Determinism assumptions:
- Single-threaded access by server orchestration.
- Monotonic sequence IDs for strict ordering decisions.
- Ordered price indexes in the order book (no unordered iteration for matching).
"""
def __init__(self, order_book: OrderBook | None = None, debug: bool = False) -> None:
self._order_book = order_book or OrderBook(debug=debug)
self._next_order_id = 1
self._next_trade_id = 1
self._next_sequence = 1
self._debug = debug
def execute_limit_order(
self,
trader_id: str,
side: Side,
price: int,
quantity: int,
symbol: str = SYMBOL,
) -> OrderExecutionResult:
order = Order(
order_id=self._allocate_order_id(),
trader_id=trader_id,
symbol=symbol,
side=side,
price=price,
quantity=quantity,
remaining_quantity=quantity,
sequence=self._allocate_sequence(),
)
trades: list[Trade] = []
while order.remaining_quantity > 0:
maker = self._order_book.find_next_matchable_opposite(
incoming_side=order.side,
limit_price=order.price,
taker_trader_id=order.trader_id,
)
if maker is None:
break
# Self-match prevention guarantee:
# find_next_matchable_opposite() only returns non-self candidates.
# A defensive guard is kept here to preserve safety if that contract changes.
if maker.trader_id == order.trader_id:
continue
fill_quantity = min(order.remaining_quantity, maker.remaining_quantity)
maker.remaining_quantity -= fill_quantity
order.remaining_quantity -= fill_quantity
trades.append(
Trade(
trade_id=self._allocate_trade_id(),
symbol=symbol,
price=maker.price,
quantity=fill_quantity,
maker_order_id=maker.order_id,
taker_order_id=order.order_id,
maker_trader_id=maker.trader_id,
taker_trader_id=order.trader_id,
aggressor_side=order.side,
sequence=self._allocate_sequence(),
)
)
if maker.remaining_quantity == 0:
self._order_book.remove_order(maker)
if self._debug:
self._order_book.validate_book_state()
# Ensure no stale zero-qty levels remain before best-price checks/snapshots.
self._order_book.compact()
resting_added = False
if order.remaining_quantity > 0:
# If opposite-side crossing liquidity still exists here, it means
# matching was blocked by SMP-only candidates. Resting this remainder
# would cross the book, so we intentionally do not rest it.
if not self._order_book.has_crossing_opposite(order.side, order.price):
self._order_book.add_resting(order)
resting_added = True
self._order_book.compact()
self._assert_uncrossed_book()
if self._debug:
self._order_book.validate_book_state()
return OrderExecutionResult(trades=trades, resting_order_added=resting_added)
def place_limit_order(
self,
trader_id: str,
side: Side,
price: int,
quantity: int,
symbol: str = SYMBOL,
) -> List[Trade]:
return self.execute_limit_order(
trader_id=trader_id,
side=side,
price=price,
quantity=quantity,
symbol=symbol,
).trades
def get_book_snapshot(self, depth: int = 5) -> dict[str, list[tuple[int, int]]]:
return self._order_book.get_snapshot(depth=depth)
def best_bid(self) -> int | None:
return self._order_book.best_bid()
def best_ask(self) -> int | None:
return self._order_book.best_ask()
def clear_order_book(self) -> None:
self._order_book.clear()
def reset_state(self) -> None:
self._order_book.clear()
self._next_order_id = 1
self._next_trade_id = 1
self._next_sequence = 1
def _assert_uncrossed_book(self) -> None:
best_bid = self._order_book.best_bid()
best_ask = self._order_book.best_ask()
if best_bid is not None and best_ask is not None:
assert best_bid < best_ask, (
f"crossed book invariant violated: best_bid={best_bid}, best_ask={best_ask}"
)
def _allocate_order_id(self) -> int:
current = self._next_order_id
self._next_order_id += 1
return current
def _allocate_trade_id(self) -> int:
current = self._next_trade_id
self._next_trade_id += 1
return current
def _allocate_sequence(self) -> int:
current = self._next_sequence
self._next_sequence += 1
return current