Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Python Limit Order Book

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.

What Is a Limit Order Book?

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: BUY or SELL
  • a price
  • a quantity
  • a timestamp
  • an order type: LIMIT or MARKET

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.

Features

  • 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

How the Matching Engine Works

The order book has two sides:

  • bids for buy orders
  • asks for 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:

  1. The best price is matched first.
  2. 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.

Project Structure

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

Architecture

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

Data Structures Used

The implementation uses a few simple data structures rather than hiding the logic behind too much abstraction.

deque for FIFO queues

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()

Dictionary for price levels

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.

Sorted price list

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.

Active order dictionary

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.

Example Demo

From the project root, run:

python src/main.py

Example 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.

Running the Tests

The project uses pytest.

From the project root:

python -m pytest

The 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.

Running the Benchmark

From the project root:

python benchmark.py

The 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.

Benchmark Results

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.

Design Decisions

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_id to 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.

Limitations

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.

What I Found Challenging

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.

Future Improvements

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.

Summary

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

About

A Python implementation of a limit order book and matching engine supporting limit orders, market orders, modifications, cancellations, testing, and benchmarking.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages