Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ocaml-trading-primitives

Functional limit order book and trading primitives in OCaml — pure transformations, pluggable matching priority, no mutable state in core logic.

Build and Test


Overview

A complete exchange-style matching engine in idiomatic OCaml:

  • Fixed-point pricesOrder.price = int ($0.0001 units). No float, no phantom price levels.
  • int64 order IDs — single machine-word comparisons on every cancel and index lookup.
  • Separated instruction typesCancel/Replace are Order.Instruction.t, not order_type. Order.t can only carry Market | 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_totalMake(P : PRIORITY) passes ~level_total to P.allocate at 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 replaceReplace distinguishes: 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 dataMarket_data.of_match_result converts every match result to a typed event stream (Trade | Quote | BookSnapshot). Previously market_data.ml was dead code.
  • 11 QCheck property tests — including proportional ProRata fill test and time-priority preservation test.

Architecture

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

Design decisions

1. float prices → Order.price = int

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.

2. Cancel/Replace are instructions, not order types

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.

3. Functional deque for level storage

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.

4. level_total passed at call time, not baked into the functor

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.

5. Time-priority preservation on reduce-only replace

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.

6. Market data wired, not declared

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.


Properties tested (11 total)

# 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

Build and run

opam install . --deps-only --with-test -y
dune build
dune test
dune exec bin/main.exe

Jane Street idioms demonstrated

  • Illegal states unrepresentableorder_type cannot carry Cancel; the compiler enforces it
  • Fixed-point arithmeticOrder.price = int throughout; no float in any matching path
  • Functors with correct semanticsMake(PRIORITY) passes ~level_total at call time; Make(BOOK) abstracts the matching engine over any LOB
  • Abstract output typesLOB.book is sealed; callers cannot depend on the IntMap representation
  • 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

Related repos


Author

Nishant Gemawat · github.com/nisgemML
12+ years financial services engineering · Morgan Stanley · State Street Alpha Frontier

About

Functional limit order book and trading primitives in OCaml — ADTs, functors, QCheck property tests

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages