A high-speed financial exchange engine. OCaml-Limit pairs buyers and sellers at sub-millisecond speeds, featuring a live Bloomberg-style trading terminal.
At modern stock exchanges, millions of buy and sell orders arrive every second. OCaml-Limit is the core engine responsible for matching buyers and sellers instantly and fairly. It is built to run continuously with zero micro-pauses or lag, paired with an interactive market dashboard.
The matching hot path is allocation-free per submit (bench-validated), achieves ~18 million orders/sec in clean performance benchmarks, and holds p99 latency under 1 microsecond.
π’ Live demo: ocaml-lob.vercel.app. The page is static on Vercel and the engine runs in a container behind a Tailscale Funnel. Each price row shows its queue of resting orders, and orders you send join the same book. Press 3 for a short explanation.
A working implementation of an exchange-grade matching engine. The engine matches trades based on price and time priority, supporting standard professional order types:
- Limit and Market Orders: Standard trades at specified or market prices.
- Iceberg Orders: Large orders that hide total volume to avoid moving the market.
- Immediate-or-Cancel (IOC) and Fill-or-Kill (FOK): Time-in-force conditions that prevent partial or lingering fills.
- Pre-trade Risk Checks: Automated guardrails blocking invalid or oversized orders.
The core API is compact:
open Ocaml_lob.Types
module Engine = Ocaml_lob.Engine
let engine = Engine.create default_config in
let order = make_order ~id:1 ~side:Buy ~price:1_502_500 ~qty:100
~order_type:Limit ~timestamp:0 in
match Engine.submit engine order (fun passive_id active_id price qty _ts ->
Printf.printf "fill: %dΓ%d @ %d (passive=%d active=%d)\n"
qty 1 price passive_id active_id)
with
| Ok () -> () (* order accepted *)
| Error _ -> () (* pre-trade risk rejected *)Six layered optimizations eliminate memory allocation overhead on the matching hot path:
- Reusing pre-allocated result constants
- Direct exception handling to avoid memory wrapper allocations
- Top-level execution loops that avoid runtime closures
- Compact doubly linked lists for price levels
- Reusing inactive price levels rather than rebuilding tree structures
- Fast custom hash tables replacing general-purpose map allocations
Full architectural details are in ONBOARDING.md.
The browser loads the static dashboard from Vercel. Its JavaScript opens an EventSource on /events and sends orders with POST /order, pointing both at the Funnel host when the page is served from a Vercel hostname. Tailscale terminates TLS and proxies both routes to the Dream server in bin/server.ml. Every 500 ms the server sends a BOOK event with each level's FIFO queue (order id and remaining size, oldest first) and a STATS event with counters, then any TRADE events since the last frame. POST /order takes {"side": "BUY" | "SELL", "price": <ticks>, "qty": <int>} and answers with the order id and how much filled on arrival; later fills follow over SSE.
At startup the server times the perf_test workload on its own host and reports that throughput and allocation figure to the page. A demo bot keeps the book moving on a 50-tick ($0.0050) grid. Visitor orders must sit on that grid within 1,000 ticks of the mid, and the bot cancels the oldest order at the farthest level once a side holds more than 120 orders, so a long-running demo can't fill the price index or the node pool.
The Docker image runs the server by default and also ships ./perf_test. SSE replaced an earlier WebSocket transport after gluten_lwt's WS close path was found to wedge the event loop in a tight retry-on-failed-write cycle (see commit 0c62bc0).
Demo videos below were recorded with Playwright + the project's local E2E suite. Each one shows a single user flow end-to-end so you can see the dashboard behaving live.
The live book, drawn as queues
The dashboard streams the book every 500 ms over Server-Sent Events. Each row of the price ladder holds the orders resting at that price, oldest next to the price, and fills from the demo bot eat them from the front.
Joining the queue at the best bid
Clicking a price loads it into the ticket, and Send posts the order into the running engine. Your order is outlined in its queue, and the ticket counts its place in line as orders ahead of it fill.
How it works
Pressing 3, or choosing it from the numbered menu, opens a short explanation of price-time priority and integer ticks.
For maintainers:
npm --prefix e2e run demorecords mp4s at 1440Γ900 intoe2e/demo-output/, thennpm --prefix e2e run gifsconverts them to 1280px-wide GIFs inassets/demos/. The page is dark-only, so each flow has one GIF. See e2e/README.md for recording from a machine inside the tailnet.
Prerequisites: opam 2.x with an OCaml 5.x switch active.
opam install --deps-only --with-test -y . # alcotest + transitive
opam install -y dream yojson lwt_ppx # server-only deps
dune build # compile everything
dune runtest # 11 tests across matching + perf
dune exec bin/server.exe # serves on localhost:8080Open http://localhost:8080/. The demo bot starts automatically.
For deploying to your own VM (Oracle Cloud Always-Free walkthrough), see DEPLOY.md.
.github/workflows/deploy.yml wires up CI + CD on every push to main.
Both image builds wait for the test job, so a failed test publishes nothing.
A push to main runs dune build and dune runtest. Passing tests start native amd64 and arm64 image builds in parallel. Their tags merge into one GHCR manifest tagged latest and with the commit SHA, and the deploy job pulls latest and runs it on the host. Editable HTML source.
Sequential gating: both architecture builds have needs: test, so a failed test prevents either image from being published. merge-manifest waits for both builds, and deploy-vm waits for the manifest. The deploy-prod concurrency group keeps releases from racing on the host.
Caching: ocaml/setup-ocaml@v3 caches the opam switch. Both native Docker builds use the GitHub Actions layer cache.
Secrets (SSH_HOST, SSH_USER, SSH_PRIVATE_KEY) are used by the deploy job's SSH command. Setup walkthrough in DEPLOY.md.
Two Alcotest files under test/, 11 cases total. Runs in well under a second.
| Group | Test | What it pins |
|---|---|---|
matching |
Basic matching | Same-price Buy/Ask filling each other |
matching |
Iceberg reload | Visible-portion exhausts, hidden portion reloads at the queue tail; total qty conserved |
matching |
Post-only rejection | Crossing post-only returnsReject_price_band |
matching |
Cancel preserves FIFO | Canceling a mid-queue order leaves FIFO linkage intact across the gap |
matching |
Sweep across levels | Aggressive order eating partial fills across 3 price levels with correctpl_total_qty |
matching |
Level resurrection | Empty level + new order at same price re-promotesbest_level |
messaging |
MPSC FIFO (1 producer) | Single-producer/consumer round-trip preserves order |
messaging |
MPSC FIFO (4 domains) | 4 OCaml Domains push concurrently; per-producer FIFO holds across 1000 messages |
regression |
Zero per-order allocation | β€ 0.20 bytes/order over 100k submits (measured: ~0.06) |
regression |
Throughput floor | β₯ 0.5 M orders/sec (measured: ~30+) |
regression |
p99 latency ceiling | β€ 100 ΞΌs (measured: ~1 ΞΌs) |
The two matching cases that originally surfaced as test failures (Sweep across levelsandLevel resurrection) caught a real production bug β an OCaml scope gotcha where else let β¦ in β¦; stmt; stmt;silently extended into following statements, droppingpl_total_qtyand the resurrection check on thetail_idx = -1 code path. Fix shipped; the tests stay as regression sentinels.
ocaml_lob/
βββ lib/ engine + types + custom data structures
β βββ types.ml price_level, book_side, sentinel_level
β βββ engine.ml submit, fill_loop, link_level
β βββ price_index.ml open-addressing hashtable replacing stdlib Map
β βββ pool.ml pre-allocated order pool
β βββ risk.ml pre-trade gates (variant returns, no exns)
β βββ stats.ml nanosecond latency tracker
β βββ messaging.ml OCaml 5 atomic MPSC queue
βββ bin/
β βββ server.ml Dream HTTP: GET /events (SSE), POST /order, demo bot
βββ test/
β βββ test_engine.ml matching + messaging cases
β βββ perf_test.ml regression guards
βββ front/ dashboard (one HTML file and one script, no build step)
βββ assets/ repo-only banner artwork
βββ Dockerfile multi-stage: ocaml/opam β ubuntu:26.04
βββ ONBOARDING.md architecture deep-dive for new contributors
βββ .github/workflows/deploy.yml CI (test) + CD (build + ship)
- ONBOARDING.md β full architecture, the six perf fixes explained, known gotchas, hang runbook
MIT. See ocaml_lob.opam for full package metadata.