This project is a Python implementation of a simple limit order book and matching engine. It supports limit orders, market orders, cancellations, modifications, trade generation, testing, and benchmarking.
I built it to understand how matching engines work beneath the surface: not just how orders trade, but how the book keeps its internal state consistent after fills, cancellations, and price changes. It is not intended to be production trading software. The goal is to make the core mechanics clear, testable, and easy to reason about.
A limit order book stores buy and sell orders for a market.
Buy orders are called bids and sell orders are called asks. Each order has:
- an order ID
- a side:
BUYorSELL - a price
- a quantity
- a timestamp
- an order type:
LIMITorMARKET
The best bid is the highest buy price currently available. The best ask is the lowest sell price currently available.
A trade happens when the best bid is greater than or equal to the best ask. For example, a buy order at 101.0 can trade with a sell order at 100.0.
- Limit orders
- Market orders
- Price-time priority
- FIFO queues at each price level
- Multi-level matching
- Trade generation
- Order cancellation
- Order modification
- Active order tracking
- Duplicate active order ID rejection
- Pytest test suite
- Benchmark script
The order book has two sides:
bidsfor buy ordersasksfor sell orders
Limit orders can rest in the book. When a new limit order is added, the engine checks whether the book is crossed. If it is, the engine matches orders until there are no more possible trades.
Matching follows price-time priority:
- The best price is matched first.
- If several orders exist at the same price, the oldest order is matched first.
This means:
- buy orders match against the lowest ask first
- sell orders match against the highest bid first
- orders at the same price are processed in FIFO order
Market orders work slightly differently. They do not rest in the book. A market buy immediately trades against the best available asks, while a market sell immediately trades against the best available bids. If there is not enough liquidity to fully fill the market order, the unfilled quantity is discarded rather than stored.
Trades are represented as Trade objects containing:
- buy order ID
- sell order ID
- trade price
- trade quantity
- timestamp
The trade price is based on the order that was already resting in the book, which is how matching engines usually behave.
lob_project/
├── benchmark.py
├── README.md
├── src/
│ ├── book_side.py
│ ├── constants.py
│ ├── main.py
│ ├── order.py
│ ├── order_book.py
│ ├── price_level.py
│ └── trade.py
└── tests/
└── test_order_book.py
Order
↓
PriceLevel
- stores orders at one price
- uses deque for FIFO priority
BookSide
- manages either all bids or all asks
- tracks price levels and best price
OrderBook
- coordinates matching
- handles cancellations and modifications
- tracks active orders
Trade
- records each completed match
The implementation uses a few simple data structures rather than hiding the logic behind too much abstraction.
Each PriceLevel uses a deque to store orders at the same price. This makes it natural to process the oldest order first.
self.queue = deque()Each side of the book stores price levels in a dictionary:
self.levels = {}This makes it easy to find the PriceLevel for a specific price.
Prices are also stored in a sorted list. This allows the book to identify:
- the highest price for bids
- the lowest price for asks
The project uses bisect when inserting a new price level.
The OrderBook maintains an orders dictionary so active resting orders can be found by order_id.
This is used for cancellation, modification, and duplicate active ID checks.
From the project root, run:
python src/main.pyExample output:
Trades:
Trade(buy_order_id=3, sell_order_id=1, price=100.0, quantity=5, timestamp=3)
Trade(buy_order_id=3, sell_order_id=2, price=101.0, quantity=1, timestamp=3)
{'bids': [], 'asks': [(101.0, 2)]}
In this example, a market buy order consumes liquidity from the ask side:
- 5 units at
100.0 - 1 unit at
101.0
The remaining ask quantity at 101.0 stays in the book.
The project uses pytest.
From the project root:
python -m pytestThe tests cover:
- adding unmatched limit orders
- best bid and best ask behaviour
- buy limit order matching
- sell limit order matching
- resting order price logic
- partial fills
- multi-level matching
- FIFO priority
- cancellations
- duplicate active order IDs
- invalid orders
- market buy orders
- market sell orders
- market orders not resting
- order modification by quantity
- order modification by price
- modification preserving FIFO priority when only quantity changes
- modification that causes an order to execute
Testing was one of the most useful parts of the project. A few bugs only became obvious once I wrote tests for edge cases, especially around resting price logic and price modifications that crossed the book.
From the project root:
python benchmark.pyThe benchmark generates random limit orders and measures how many orders per second the book can process.
This is a basic benchmark, not a full performance study. It is mainly useful for checking that the engine still runs efficiently after changes.
The benchmark used randomly generated limit orders with prices between 90 and 110 and quantities between 1 and 20.
These results were measured on my local machine, so they will vary depending on hardware, Python version, and background processes.
| Orders Processed | Trades Generated | Time (s) | Orders/sec | Final Active Orders |
|---|---|---|---|---|
| 1,000 | 708 | 0.0065 | 154,880 | 244 |
| 10,000 | 7,635 | 0.0589 | 169,666 | 1,993 |
| 100,000 | 76,191 | 0.5975 | 167,375 | 19,795 |
The results show that the engine can process a large number of simple randomly generated orders quickly. The benchmark is intentionally simple, but it gives a useful baseline for comparing future changes.
The project deliberately uses understandable data structures.
For example, cancellation currently searches through a queue at the relevant price level. This is not optimal, but it makes the code easier to follow while still showing the core matching-engine logic.
A production-grade order book would use more advanced structures, such as:
- a doubly linked list at each price level
- an
order_idto node mapping - a tree-based structure for price levels
That would allow faster cancellation and price-level management, but it would also make the code harder to understand. For this project, clarity was more important than building the fastest possible implementation.
This project is intentionally limited in scope.
Current limitations include:
- It is not production-grade.
- It does not handle networking, persistence, concurrency, or exchange protocols.
- Duplicate order ID rejection applies to active resting orders.
- Market orders do not support options such as fill-or-kill or immediate-or-cancel.
- Cancellation is
O(n)within a price level because it searches the queue by order ID. - Removing a price from the sorted price list is also
O(n). - The benchmark uses a simple random workload rather than real market data.
The hardest part was not creating the individual classes. The harder part was keeping the book internally consistent after every operation.
For example, when a trade happens, the engine may need to:
- reduce the incoming order quantity
- reduce the resting order quantity
- remove fully filled orders
- delete empty price levels
- update the active order dictionary
- return the generated trades
Small mistakes in one of those steps can leave the order book in an inconsistent state.
Another challenge was trade pricing. At first, it was tempting to use timestamps to infer which order was resting. That worked for basic examples, but it was not reliable enough. The matching logic now explicitly tracks the incoming order so trades are priced using the true resting order.
Possible future improvements include:
- Use a doubly linked list and order ID lookup table for
O(1)cancellation. - Use a balanced tree or sorted container for price levels.
- Add global order ID tracking if ID reuse should be fully prevented.
- Separate user timestamps from internal priority sequence numbers.
- Add order types such as immediate-or-cancel and fill-or-kill.
- Add benchmark cases for market orders, cancellations, and modifications.
- Package the project so tests do not need manual import path setup.
- Add a simple ASCII visualisation of the bid/ask ladder.
This project implements the core mechanics of a limit order book in Python. It focuses on correctness, clear data structures, and testable matching behaviour rather than production-level performance.
The main things it demonstrates are:
- how price-time priority works
- how orders can be grouped by price level
- how FIFO queues preserve time priority
- how trades are generated
- how cancellations and modifications affect book state
- how tests and benchmarks can be used to check correctness and performance