Implementation of a limit order book with price-time priority, together with a synthetic order flow generator so there is something to run it against.
- Limit and market orders, cancels
- Matching best price first, FIFO within a price, trades printed at the resting order's price
- An aggressive order walks as many price levels as it needs and rests the remainder
- Aggregated depth snapshots and top of book
- A simulator that generates random order flow around a drifting fair price
pip install -e ".[dev]"
python -m orderbook.cli --steps 2000 --seed 3 --levels 5
bids | asks
1,700 99.95 | 99.96 300
1,100 99.92 | 99.97 1,300
200 99.91 | 99.98 1,700
900 99.90 | 99.99 2,200
400 99.89 | 100.00 1,100
spread 0.01 mid 99.955
last 99.96
48 resting orders
last trades
99.97 x 300 (buy 1720 / sell 1718, aggressor B)
99.97 x 200 (buy 1720 / sell 1721, aggressor S)
99.95 x 200 (buy 1712 / sell 1722, aggressor S)
99.96 x 500 (buy 1726 / sell 1721, aggressor B)
orders 1,726 cancels 274 trades 1,186 volume 248,900 vwap 99.88
from orderbook import OrderBook, Side
book = OrderBook("AAPL")
book.limit(Side.SELL, 10_050, 300) # 300 @ 100.50
report = book.limit(Side.BUY, 10_100, 500) # buys 300, rests 200
report.trades # [Trade(price=10050, qty=300, ...)]
report.resting_qty # 200
book.best_bid # 10100
book.depth(5) # ([(10100, 200)], [])
book.cancel(report.order_id)Prices are integers in ticks (cents), so 100.50 is 10050. Floats would put
orders that should share a price onto two different levels.
OrderBook keeps a dict of price -> PriceLevel per side, and a sorted list of
the live prices on that side, maintained with bisect. The list gives the best
bid and ask in O(1) and depth queries in order; the dict gives the level for a
price in O(1).
A PriceLevel holds its orders in a dict keyed by order id. Dicts keep
insertion order, so iterating a level is FIFO — which is what time priority
means — while removing a cancelled order in the middle stays O(1). It also
tracks the aggregate size at that price so depth queries never sum a queue.
A separate order_id -> Order map makes cancels O(1): a cancel arrives with an
id and nothing else, so without it every cancel would be a scan.
| add / cancel | O(1), plus a bisect insert or removal when a price level appears or empties |
| best bid / ask | O(1) |
| depth(k) | O(k) |
| match | O(number of orders filled) |
pytest
The suite covers the matching rules (price priority, time priority, partial fills, walking levels, an aggressor stopping at its limit), and runs random sessions through the simulator checking after every batch of messages that the book is not crossed, that level sizes equal the sum of their orders, and that emptied levels are removed rather than left at zero.