Overview
A working limit order book engine built from scratch — a hands-on way to learn market microstructure (how orders interact, how liquidity forms, how prices move) 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_indexmap - 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.
| Component | Status |
|---|---|
| C++ matching engine | Done |
| Python bindings (pybind11) | Done |
| Trading agents | Planned |
| Simulation scenarios | Planned |
| Performance benchmarks | Planned |
Build Notes
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.