Functional limit order book and trading primitives in OCaml — pure transformations, pluggable matching priority, no mutable state in core logic.
A complete exchange-style matching engine in idiomatic OCaml:
- Fixed-point prices —
Order.price = int($0.0001 units). Nofloat, no phantom price levels. - int64 order IDs — single machine-word comparisons on every cancel and index lookup.
- Separated instruction types —
Cancel/ReplaceareOrder.Instruction.t, notorder_type.Order.tcan only carryMarket | Limit— illegal states are unrepresentable at compile time. - Functional deque level storage — O(1) amortised enqueue/dequeue per level. The previous
existing @ [o]append was O(n) in level depth — quadratic on busy levels. - Functor with dynamic
level_total—Make(P : PRIORITY)passes~level_totaltoP.allocateat match time, not at functor creation. Pro-rata fills are always proportional to the current level composition, not a stale constant. - Time-priority preservation on reduce-only replace —
Replacedistinguishes: price-change or qty-increase → cancel/resubmit (time priority lost); qty-reduce at same price → in-place update (time priority preserved). This follows CME Globex rules. - Wired market data —
Market_data.of_match_resultconverts every match result to a typed event stream (Trade | Quote | BookSnapshot). Previouslymarket_data.mlwas dead code. - 11 QCheck property tests — including proportional ProRata fill test and time-priority preservation test.
lib/
order.ml – Fixed-point price (int), int64 order IDs,
order_type (Market | Limit only),
Order.Instruction (New | Cancel | Replace — separate type)
order_book.ml – Functional LOB: Map.Make(Int) + functional deque levels,
PRIORITY signature with ~level_total at call time,
Make(P) functor, PriceTime + ProRata (no baked constant)
matching_engine.ml – Pure dispatcher: Make(BOOK) functor,
replace with CME time-priority rules
execution_report.ml – Fill/Cancel/Rejected reports (fixed-point prices)
position_tracker.ml – Cost-basis, realised/unrealised PnL (integer arithmetic)
market_data.ml – Quote/Trade/BookSnapshot events; of_match_result wires
matching engine output to the feed
test/
test_lob.ml – 11 QCheck properties; up to 2000 random cases each
bin/
main.ml – Demo: PriceTime, ProRata, replace priority, market data
Map.Make(Float) has a soundness bug: 0.1 +. 0.2 <> 0.3 in IEEE 754. Two orders at nominally the same price can land in different map buckets if one price was computed and the other was parsed. Order.price = int (units of $0.0001) with price_of_float rounding on entry makes all comparisons exact. Jane Street's internal Price module uses the same fixed-point representation.
order_type = Market | Limit | Cancel | Replace (the original design) meant an Order.t with order_type = Cancel "id" had a side, qty, and symbol — all semantically meaningless — and add_order had to guard at runtime. Fix: order_type only contains types that can rest in a book; Order.Instruction.t carries New | Cancel | Replace. The compiler enforces this. Passing a cancel to add_order is a type error.
The previous version stored each price level as Order.t list and appended new orders with existing @ [o] — O(n) in level depth. Under high-frequency quoting on a busy level this is quadratic. The fix is a two-list functional deque: O(1) amortised enqueue and O(1) dequeue. The deque is internal to order_book.ml; the public API still exposes level.orders : Order.t list (materialised on demand) so callers aren't affected.
The previous ProRata(struct let total = 400 end) required passing level_total at module creation — stale the moment any order at that level was cancelled. PRIORITY.allocate now takes ~level_total:int as a call-time argument, computed from the actual deque before each sweep. This is the difference between a structurally correct functor and a semantically correct one.
Exchange rules (CME Globex, ICE, Eurex) preserve queue position when an order reduces its qty at the same price. The previous implementation always cancelled and resubmitted, moving the order to the back of the queue — incorrectly penalising participants managing risk by reducing size. process_replace now distinguishes three cases: price change or qty increase → resubmit (priority lost); qty reduce at same price → in-place update (priority preserved). Property 11 tests this directly.
Market_data.of_match_result converts any match_result to a Trade list @ [BookSnapshot]. quote_of_book snapshots the current BBO. The module is now a genuine part of the data flow, not dead code attached to a diagram.
| # | Property |
|---|---|
| 1 | spread >= 0 for any resting order set |
| 2 | fill_qty <= order qty |
| 3 | Market orders reduce or maintain opposite-side depth |
| 4 | Cancel removes exactly one order's qty |
| 5 | Crossing orders produce ≥ 1 fill |
| 6 | Position qty = sum of signed fill qtys |
| 7 | Empty book: depth=0, qty=0, spread=None |
| 8 | Replace: new price visible in book |
| 9 | Make(ProRata) satisfies fill-qty invariant |
| 10 | ProRata fills proportional to resting size |
| 11 | Replace qty-reduce preserves time priority |
opam install . --deps-only --with-test -y
dune build
dune test
dune exec bin/main.exe- Illegal states unrepresentable —
order_typecannot carryCancel; the compiler enforces it - Fixed-point arithmetic —
Order.price = intthroughout; no float in any matching path - Functors with correct semantics —
Make(PRIORITY)passes~level_totalat call time;Make(BOOK)abstracts the matching engine over any LOB - Abstract output types —
LOB.bookis sealed; callers cannot depend on theIntMaprepresentation - Exchange rule fidelity — CME time-priority rules for replace implemented and property-tested
- Property-based testing — QCheck; invariants hold for all inputs, not just chosen examples
Low-Latency-Trading-Engine— C++20 engine, 6M+ msgs/sec, lock-free queues, Kyle's λ adverse selectionavellaneda-stoikov— Optimal market making, inventory risk, multi-assetoptions-market-maker— Delta/gamma/vega hedging, skew-aware quoting
Nishant Gemawat · github.com/nisgemML
12+ years financial services engineering · Morgan Stanley · State Street Alpha Frontier