Ondřej Kutil

projects / order-book

Order Book Engine

High-throughput system modelling market liquidity and minimising execution slippage.

C++Pythonpybind11
github ↗

The Problem

Understanding market microstructure — how orders interact, how liquidity forms, and how prices move — requires more than reading about it. This project builds a working limit order book engine from scratch as a hands-on way to learn the core mechanics before layering any analysis on top.

The current scope is intentionally narrow: get the matching engine right. Agents, simulations, and performance analysis are planned next steps.

Technical Approach

The core data structure is a std::map<Price, std::vector<Order>> — a sorted map of price levels, each holding a FIFO queue of resting orders. This gives $O(\log n)$ insertion and $O(1)$ best-bid/ask lookups while enforcing price-time priority automatically.

// Incoming buy limit order — match against resting sells (price-time priority)
while (working_order.quantity > 0 && !sell_orders.empty() &&
       working_order.price >= sell_orders.begin()->first) {

    Order& resting = sell_orders.begin()->second.front(); // earliest order at best ask
    Quantity trade_qty = std::min(working_order.quantity, resting.quantity);

    // Execution always at the resting (maker) price, not the aggressor's
    trade_logs.push_back({ next_trade_id++, working_order.order_id, resting.order_id,
                           resting.price, trade_qty });

    working_order.quantity -= trade_qty;
    resting.quantity       -= trade_qty;

    if (resting.quantity == 0) {
        sell_orders.begin()->second.erase(sell_orders.begin()->second.begin());
        if (sell_orders.begin()->second.empty())
            sell_orders.erase(sell_orders.begin()); // remove empty price level
    }
}
// Any unfilled remainder rests in the book
if (working_order.quantity > 0)
    buy_orders[working_order.price].push_back(working_order);

The pybind11 layer wires each C++ struct into a proper Python class — constructor, readonly fields, and a to_dict() helper for analysis:

py::class_<PendingOrder>(m, "PendingOrder")
    .def(py::init<OrderID, TraderID, Price, Quantity, OrderSide>(),
         py::arg("order_id"), py::arg("trader_id"),
         py::arg("price"), py::arg("quantity"), py::arg("side"))
    .def_readonly("order_id",  &PendingOrder::order_id)
    .def_readonly("trader_id", &PendingOrder::trader_id)
    .def_readonly("price",     &PendingOrder::price)
    .def_readonly("quantity",  &PendingOrder::quantity)
    .def_readonly("side",      &PendingOrder::side)
    .def("to_dict", [](const PendingOrder& x) {
        py::dict d;
        d["order_id"]  = x.order_id;
        d["trader_id"] = x.trader_id;
        d["price"]     = x.price;
        d["quantity"]  = x.quantity;
        d["side"]      = x.side;
        return d;
    });

Every other type in the module — Order, TradeLog, Level1Data, OrderBookSnapshot — is bound the same way. The same engine is then accessible from Python:

import market_simulator as ms

sim = ms.Simulator(start_time=0)

# Stage orders (agents will do this programmatically)
sim.place_limit_order(ms.PendingOrder(
    order_id=1, trader_id=101, price=99.0, quantity=10, side=ms.OrderSide.BUY))
sim.place_limit_order(ms.PendingOrder(
    order_id=2, trader_id=102, price=101.0, quantity=5, side=ms.OrderSide.SELL))

sim.submit_pending_orders()
sim.advance_time(1000)  # advance simulation clock by 1 second

l1 = sim.get_current_level1_data()
print(l1.bid_price, l1.ask_price, l1.spread)  # 99.0  101.0  2.0

A separate Simulator class wraps the order book and manages a simulation clock, staging orders from multiple callers before submitting them together — the hook for eventually plugging in agents.

The engine supports:

  • Limit orders — placed at a specific price, rest in the book if not immediately matched
  • Market orders — consume available liquidity at the best available prices
  • Cancel / modify — O(1) lookup via a dedicated order_index map
  • Market data — Level 1 (best bid/ask), Level 2 (full depth snapshot), and full order/trade logs

Current Status

The C++ engine and Python bindings are complete. No agents or simulation scenarios have been written yet — that is the next phase of the project.

ComponentStatus
C++ matching engineDone
Python bindings (pybind11)Done
Trading agentsPlanned
Simulation scenariosPlanned
Performance benchmarksPlanned

Methodology

  1. Motivation — I want to understand quant trading and market microstructure from the inside, not just the outside. Building the engine myself forces precision about details that are easy to gloss over when just reading.
  2. Architecture — C++ for the matching hot-path; pybind11 to expose it to Python for future research/analysis. The two layers stay cleanly separated.
  3. Implementation — Built incrementally: limit order matching first, then market orders, then cancel/modify, then the simulator wrapper and Python bindings. Each step was validated against hand-traced order sequences.
  4. Next steps — Write agent classes that call into the Simulator API, run scenarios, and start producing data worth analysing.

Code & Artifacts