▶ Live demo — the real engine, compiled to WebAssembly, running in your browser · ▶ Live console — a running market with signals and surveillance, every panel titled by the call that produces it.

An embeddable limit order book and matching engine in Go. Integer-exact int64

pricing with no floating point on the money path, a match path that allocates

0 B/op, a single-writer core with no lock contention on the match path,

deterministic crash recovery gated in CI against a 2,000-command tape, and

cmd/obgw — a reference TCP gateway speaking a frozen binary protocol on both

edges, order entry and public market data. go get it into an exchange, a

simulator, or a backtester: the core owns the book, the matching algorithm,

order lifecycle, sequencing and market-data snapshots, while pkg/wal (durable

persistence), pkg/surveillance (market-abuse detection), pkg/gateway

(pre-trade admission) and pkg/auction (uniform-price call auction) cover the

layers around it. It has never run a live market, and

what that costs you is written down rather

than implied.

go get github.com/intrepidkarthi/orderbook/pkg/matchingRequires Go 1.23 or later.

Before evaluating a change or trusting a claim, run the local gate:

go vet ./...

go test ./...

go test -race ./...

make benchThe benchmarks are regression checks for the core, not an end-to-end venue capacity test. See docs/BENCHMARKS.md for the measurement boundary and docs/PRODUCTION-READINESS.md before using the gateway with anything valuable.

The engine works in integer ticks and lots. Pass them directly, or use an

Instrument to convert from decimals at the boundary. The snippets below are

API excerpts; examples/basic is the complete program

with imports and error handling.

eng := matching.NewEngine(matching.DefaultConfig("BTC-USD"))

// A resting sell, then a crossing buy that trades against it at the maker price.

sell, _ := types.NewOrder("mm", "BTC-USD", types.SideSell, types.OrderTypeLimit, 100, 5, types.TIFGoodTillCancel)

eng.Process(sell)

buy, _ := types.NewOrder("taker", "BTC-USD", types.SideBuy, types.OrderTypeLimit, 101, 3, types.TIFGoodTillCancel)

res := eng.Process(buy) // res.Trades, res.Status, res.RejectionReason

bid, qty, ok := eng.BestBid()Decimals at the boundary, concurrent submission, and the zero-allocation path:

// Decimals in, int64 ticks/lots out.

inst := types.NewInstrument("BTC-USD", decimal.RequireFromString("0.01"), decimal.RequireFromString("0.001"))

order, _ := inst.NewOrder("alice", types.SideBuy, types.OrderTypeLimit,

decimal.RequireFromString("30000.50"), decimal.RequireFromString("0.25"), types.TIFGoodTillCancel)

// Many producers, one matching goroutine.

r := matching.NewRunner(matching.RunnerConfig{Engine: matching.DefaultConfig("BTC-USD")})

defer r.Close()

r.SubmitAsync(order) // enqueue without blocking; result arrives on the returned channel

// Zero-allocation hot path: reuse the trade buffer across calls.

buf := make([]types.Trade, 0, 8)

buf, status, _ := eng.Match(order, buf[:0])Or run the reference gateway and talk to it over a socket. Use a protected

accounts file; credentials passed with -accounts are visible in the host's

process list:

go run ./cmd/obgw -addr 127.0.0.1:9000 -symbol BTC-USD \

-accounts-file /path/to/accounts -tls-cert server.crt -tls-key server.keygo run ./cmd/obgw -h gives the account-file format and every other flag;

docs/PROTOCOL.md says what the transport does and does not

protect. Without -tls-cert and -tls-key the venue speaks plaintext, which is

a loopback-development mode and nothing else. cmd/obgw's tests are a

working client — login, enter, cancel, reduce, query, resume — and are the most

useful reference for writing another one. The protocol helpers live in

server_test.go. The gateway is a reference edge, not a claim that the

repository is a complete production venue.

Runnable, testable examples render on pkg.go.dev.

⚠ This is an experiment, not a product. It has never run a live market. It has had no independent review. Its Go API and its wire protocol have each broken more than once in a single week, and the compatibility promise that now governs them (docs/COMPATIBILITY.md) is days old. Known problems are documented rather than fixed — a venue left running with

-wal-retainunset still becomes progressively harder to restart, because the command log then keeps every byte it has ever written and recovery reads all of it.If you run this and something goes wrong — lost orders, a wrong book, money — that is yours, not mine. The MIT licence's "without warranty of any kind" is the legal form of that sentence; this is the plain one. I am not responsible for what happens in your production, and nobody has run it in theirs.

What this repository does offer is unusual specificity about its own limits. Every claim names the test or the measurement behind it, several sections exist only to correct earlier claims that were wrong, and docs/PRODUCTION-READINESS.md is a list of what would stop you. Read those before trusting any of this, not after.

Scope. Embeddable concedes the venue is not here — and no adjective in this README claims what only a deployment can prove. Production readiness is a property of a deployment, not of code: docs/PRODUCTION-READINESS.md is the honest account of what is solid, what is partial, and what is yours. Nobody runs this in production today, and this paragraph will say so until someone does.

What ships: the matching core, durable recovery, an event stream proven to

reconstruct the book, an operator kill switch, a frozen binary protocol

(docs/PROTOCOL.md), and cmd/obgw — a reference TCP gateway

serving both edges: order entry with authentication, per-account outbound streams

and gap-free resume across a disconnect, and a public market-data feed with

snapshot-plus-delta recovery. Bounded backpressure at every stage. Around it:

EngineSnapshot.Digest (the book fingerprint replication and recovery agree on),

examples/replication (the drilled primary-backup reference), cmd/obdash (an

operator dashboard that is an ordinary subscriber of the venue's own feed), and a

live console running

the engine, signals and surveillance in the browser.

What does not, and is yours: continuous operation (the log rotates and a

covered prefix can be deleted, but retention is opt-in and unset by default, so a

venue left running without -wal-retain still becomes slower and hungrier to

restart every day it stays up — the first thing to set before running this for a

week), credential lifecycle (the reference speaks TLS and

holds secret digests, never plaintext — but rotation, revocation and expiry are

yours, and it says so), multi-symbol routing (order ids and

sequences are per-engine, so several symbols means several engines and a router

above them), clearing and settlement, and any HA topology — the library

ships the seams for primary-backup (deterministic apply, an ordered command log,

replay mode, snapshot bootstrap), proves them with a reference example and CI

drills (docs/REPLICATION.md), and deliberately not the

consensus, because bundling one forces a wrong answer on everybody. See

docs/EXCHANGE-ARCHITECTURE.md for why, including

the venues that lost quorum getting it wrong.

What is offered here is that the pieces you build on are correct, tested, and honest about their edges. docs/PRODUCTION-READINESS.md is the checklist: what ships, what is deliberately absent, what you would have to build — and which gaps no library work can close, because they are properties of your deployment.

-

Integer-exact pricing. The engine works in int64ticks and lots; a per-symbolInstrumentconverts decimals only at the API boundary. No floating-point on the money path.

-

Low latency, zero allocation. O(1) cancel, pooled book nodes and price levels, and a caller-buffer match path that allocates 0 B/op. A realistic cancel-heavy flow runs at p50 83 ns · p99 167 ns · p999 250 ns per operation.

-

Single-writer core. One matching goroutine owns the book, so nothing contends its mutex on the match path (the LMAX model). A Runnerfronts it with an MPSC command queue so many producers can submit concurrently. Queue order is the engine's order; deployments that need a venue-wide ingress policy must sequence commands before they reach the runner.

-

Deterministic and recoverable. The same ordered command stream produces byte-identical trades and book state — enabling command-log replay, durable WAL crash recovery ( pkg/wal: write-ahead log + snapshots), and reproducible backtests.Runner.Checkpointandwal.Recoverjoin the snapshot to its log position, and the property is gated in CI against a 2,000-command tape: checkpoint anywhere, recover, and the book, all three sequence counters, the duplicate guard and the conditional-order state match the uninterrupted run. What a snapshot bounds is the replay and the parse, not the read: recovery reads and checksum-verifies every retained byte however recent the snapshot. What bounds the read is retention — the log rotates into segments and a prefix of them is deleted once a verified snapshot covers it, so restart cost is O(retained log) and the retained size is a byte budget you set. That budget is not set by default, and a venue that leaves it unset gets slower to restart every day it stays up. Measured in docs/PRODUCTION-READINESS.md; the design and its costs are in docs/LOG-ROTATION.md and docs/BOUNDED-RECOVERY.md.

-

An event stream that reconstructs the book. Accepted/Trade/Canceled/Replacedreplay into an L3 book identical to the engine's, asserted on every commit across 28 scenarios covering every order class — including iceberg refill, all five self-trade-prevention modes, FOK reversal, FOK against a resting iceberg, cascade-fired stops and a bust.

-

Full order surface. Limit, market, stop / stop-limit, iceberg (hidden), post-only, pegged, OCO / bracket, and trailing-stop orders; GTC / IOC / FOK / DAY / GTD time-in-force, with the venue holding the deadline rather than the client remembering to cancel; self-trade prevention; a price-band circuit breaker; FIFO or pro-rata allocation.

-

A trading session. Pre-open accepts orders without matching them, so the book accumulates and may legitimately cross; opening resolves it at a single clearing price by auction, in price-time priority, so a venue never opens onto a crossed book. The engine holds no calendar — it knows what each phase permits, not when phases change, because that is the venue's business.

-

Market integrity & safety. Opt-in pre-trade risk controls — fat-finger and dust caps, per-account order limits, minimum resting time, client-order-id idempotency, mark-price step and depth bounds, a self-output guardrail, and timed band-breach pauses — plus a market-abuse surveillance suite (spoofing, order-to-trade ratio, marking-the-close, ramping, pinging, cross-book), an enforcing per-account rate gate, an operator kill switch, and a uniform-price call auction. Each maps to a real case in docs/THREAT-MODEL.md. The taker speed bump is an observation hook, not enforcement — it reports, it does not delay.

-

A network edge that works. cmd/obgwis a reference TCP order-entry gateway: authentication defaulting to deny, a frozen binary protocol (docs/PROTOCOL.md), per-account outbound streams that outlive a connection, and gap-free resume — reconnect with your cursor and receive the fills that landed while you were disconnected. A client can never name another account's order, because the wire has no field for it.The full client lifecycle is on the wire: six order types (limit, market, stop / stop-limit, OCO, iceberg, pegged, trailing), reduce in place keeping queue position, atomic cancel/replace, mass cancel, cancel-on-disconnect, and query your open orders when resume is not available. Orders that outlive a restart stay nameable and keep reporting their fills.

-

An admin edge, and a soak harness that used it. cmd/obgw -adminserves Prometheus metrics,/healthzand/readyzon a third port — counters off the engine's own event stream, so they count what the book saw rather than what the gateway believed it sent, and nothing a scrape touches goes through the command queue.cmd/obsoakdrives the venue at a sustained rate and reports what grows. The longest run is four hours over three books — 14.4M messages, goroutines and descriptors flat across 240 samples, no orphans (docs/SOAK.md §1e). The first hour of ever running it found a defect that the whole test suite, two fuzzers and the race detector had not: under load the venue refused cancels for orders live in its own book, and filled to its order ceiling while reporting itself healthy. Fixed, regression-tested, and written up in docs/SOAK.md.

-

A market-data edge. cmd/obgw -mdaddrpublishes the venue's public feed on its own listener: a snapshot naming the sequence it is consistent with, then incremental level changes, trade prints and venue-state changes in one dense, gap-free stream. A subscriber holding a cursor gets a gap-fill instead; one too far behind, or from another venue incarnation, is refused explicitly rather than quietly resynchronised. The guarantee — snapshot plus everything after its sequence equals the engine's book — is asserted both in-process and end to end over a socket.

-

Market data. L1 / L2 / L3 (market-by-order) snapshots with sequence numbers, plus marketdata.L2Feed— incremental aggregated depth derived from the event stream, coalesced per command, with absolute quantities so a subscriber that misses an update recovers on the next one. Its test asserts the derived levels equal the engine's own snapshot after every command, which is how a long-standing depth bug was found.

-

Tested and benchmarked. Race, fuzz, soak, and replay-recovery suites; microbenchmarks run in CI on every push.

The bare Engine has no internal mutex by design; calling its mutating methods

from multiple goroutines is a data race. Use a Runner, or serialize to one

goroutine. See docs/INTEGRATION.md.

Core-library microbenchmarks (Apple M4, go1.23.5 darwin/arm64, single-threaded):

Median of 5 runs on an idle machine; ±10% run to run. The fractional allocation

counts are deliberate: Go prints allocs/op as integer division, so "0" can mean

anything under 1.0. Measured against runtime.MemStats, cancel really does allocate

~0.0002 objects per operation — and Add into a growing book really does allocate

2.01, which is what "pooled" means rather than "allocation-free".

Book size is part of the result, so it is stated. Cancel is given at two depths because the benchmark's iteration count doubles as the book size, and the 4× spread between them is cache behaviour, not code. Any book-level benchmark quoted without its depth — including, until now, this one — is not comparable to anything.

What these numbers measure. In-process calls into the matching core, and

nothing else. *types.Order values are constructed before b.ResetTimer() and

passed directly to the engine, so the figures exclude order allocation, decoding,

validation at the API boundary, network I/O, and the session and order-entry

protocol layers. Those layers exist here (internal/wire, pkg/orderentry,

cmd/obgw) and none of them is measured on this page. They are a measure of the

matching algorithm and its data structures, not of end-to-end order latency in a

venue. Read them as a floor, and treat any real system built on this as strictly

slower.

Tail latency on a realistic ~90%-cancel / 10%-new flow (Match / Cancel):

p50 83 ns · p99 167 ns · p999 250 ns, 0 allocs/op — the p999 stays within ~3×

of the median. Absolute figures include time.Now overhead; the median-to-tail

shape is the signal.

These are the bare Engine. Most embedders use the Runner (the

concurrency-safe front), usually with a write-ahead log and an event sink. That

path allocates 3 allocs/op rather than 0, and adding group-committed durability

costs ~30× — dominated by fsync, and syncing on every command instead costs a

further ~210×. The zero-allocation claim above is about Match into a caller

buffer, not about the concurrent API.

See docs/BENCHMARKS.md for the comparison

and for why the durable figures are given as ratios rather than nanoseconds.

Scaling across cores. A book cannot be parallelised — it is a single writer by

design — so the only axis that scales is books. BenchmarkShards_Scaling drives N

shards through matching.Shards, one producer goroutine per shard, on a 70/20/10

rest / cancel / marketable mix holding ~2 K resting orders per book:

GOMAXPROCS=4 on the same M4 (4 performance cores), median of 5 × 10 s runs,

~5.7 minutes of measurement:

GOMAXPROCS=4 go test -run '^$' -bench BenchmarkShards_Scaling -benchtime=10s -count=5 ./pkg/matching/.

4 allocs/op, 663 B/op — order construction is inside

the timed loop here, unlike the table above, because a producer that builds its own

orders is what a shard actually faces.

It is not linear, and that is the finding. Four books on four cores returns 2.24×, not 4×, and books 5 through 8 return nothing further. Each shard is a pair of goroutines — a producer blocked on its reply, and the matching goroutine draining the queue — so past core count the machine is spent on the handoff rather than on matching. Adding books beyond the core count buys queue headroom, not throughput. The per-book figure is also not comparable to the single-threaded table above: it is a different workload, and both the queue handoff and order allocation are inside it.

Reproduce with make bench. CI runs the benchmarks on every push. Methodology

and full results: docs/BENCHMARKS.md.

The core is a strictly downward-layered set of packages. Research, simulation, and the web demo depend on the core; the core depends on nothing above it.

web/ (React + TS) ──▶ cmd/obwasm (Go → WASM) ─┐

│

backtest ▸ strategy ▸ sim ▸ signals ▸ marketdata

│

═══════════════ CORE LIBRARY ════════════▼═══════

surveillance ▸ matching ▸ orderbook ▸ types

Design records. Each of the following was written as a spec before the code existed, and each ends with a section recording what building it actually found — including where the spec turned out to be wrong about its own design. They are the working record rather than the reference manual, and they are listed here because a document nothing links to is a document nobody reads.

The same core powers two additional layers, kept strictly above the library:

- Research harness — order-flow-imbalance, price-impact, and delta/CVD signals, a deterministic exchange simulator, an Avellaneda–Stoikov market-making backtest, and three reproducible studies in docs/research/ that test the popular claims against ground truth — and mostly refute them. Market-abuse surveillance (spoofing/layering and rate limits) and call-auction uncrossing are included. The engine is its own data source — per-order (L3 / market-by-order) event streams plus simulator ground truth; see data and scope for what that does and doesn't cover.

- Interactive demo — the engine compiled to WebAssembly, running live in the browser to visualize matching and market making.

go run ./examples/basic # place two orders and watch them match

go run ./examples/eventfeed # consume the event stream as an exec-report + position feed

go run ./examples/gateway # edge controls: enforcing rate gate, speed bump, audit trail

go run ./examples/marketmaker # backtest an Avellaneda–Stoikov maker

go run ./cmd/obdemo # end-to-end matching demonstration

go run ./cmd/ofistudy # is order-flow imbalance predictive, or just contemporaneous?

go run ./cmd/lambdastudy # Kyle's λ: price impact, depth, and the cost of a block order

go run ./cmd/flowstudy # delta/CVD/absorption vs ground truth: what survives a control

go run ./cmd/l2capture # live order-flow imbalance on Coinbase dataContributions are welcome — bug fixes, order types, signals, strategies,

surveillance detectors, protocol codecs, or research write-ups. Start with

CONTRIBUTING.md (make check before a PR), and open an issue

or discussion if you want to talk through an idea first.

Good places to start (open an issue to claim one):

- Render the markdown docs to HTML in the Pages build so the hosted docs stay in sync with source automatically.

- Protocol codecs — a FIX / OUCH / SBE adapter package translating the wire ↔

types.Orderand theEventSinkstream ↔ execution reports.

- New EventSinkkinds — emit the reservedTriggered/BookDeltaevents.

- Tracing and structured logging — pkg/observabilitycovers metrics; spans across the gateway → queue → matcher → publisher path are not there.

- More surveillance — a quote-fading detector, or a wash-trade detector keyed on beneficial-ownership groups (the cross-account case the core can't see).

- More signals — micro-price, VPIN, or a queue-position model in pkg/signals.

- Performance work — shared Go/C++/Rust benchmark tapes, alternative price-level structures, and end-to-end latency measurements. See docs/PERFORMANCE-ROADMAP.md.

If the library is useful to you, a ⭐ helps other developers find it.

Actively developed. Releases follow semantic versioning; the public API uses

integer ticks/lots and a single-writer engine as of v0.3.0, with a

threat-model-driven market-integrity layer as of v0.6.0. Breaking changes are

gated behind minor-version bumps until a v1.0.0 API freeze. See the

CHANGELOG.

The design is informed by the author's prior production matching engine — price–time priority, the map-plus-ladder book structure, and the matching algorithm. This repository is a clean, independent re-implementation for research and education, not a copy of that stack.

MIT © Karthikeyan NG

Topics: order-book · matching-engine · limit-order-book · clob · market-making · avellaneda-stoikov · order-flow-imbalance · market-microstructure · backtesting · algorithmic-trading · quantitative-finance · webassembly · golang · exchange · price-time-priority