orderbook

Documentation

orderbook is a production-grade Go library for running a central limit order book (CLOB) and matching engine — the core of an exchange, a trading simulator, or any system where buyers and sellers meet at a price. It is embeddable, uses integer-exact pricing (int64 ticks and lots, never floats), has a zero-allocation hot path and a lock-free single-writer core, is deterministic and replayable, and ships both the order types real venues use and an opt-in layer of pre-trade risk & anti-manipulation controls.

go get github.com/intrepidkarthi/orderbook/pkg/matching

New to order books? Start with the from-scratch guide. For the full API, see pkg.go.dev; for release history, the changelog (latest: v0.6.0, the market-integrity release).

Architecture

The project follows one rule: strict downward layering. The matching core depends on nothing above it; persistence, surveillance, the gateway, auctions, research, and the web demo build on top. This keeps the core lean and importable on its own — most consumers use only types, orderbook, and matching.

apps / web / examples          (consume everything below)
gateway     (enforcing edge: rate gate, speed bump, audit)
surveillance · backtest · strategy · sim · signals
wal · auction · marketdata     (persistence, call auctions, feeds)
──────────────── CORE ────────────────
matching   (engine · Runner · events · snapshots · pre-trade risk)
orderbook  (book · price ladder · depth · L2/L3 · snapshot)
types      (Order, Trade, iceberg, errors)

Dependencies flow one direction only — downward. Credit, identity, fees, and wire protocols stay outside the core, the same boundary production venues draw (see the core-vs-layer boundary).

Order book internals

Data structure

Each side of the book is a map[price]→level for O(1) level lookup, plus a price-sorted slice maintained by binary-search insertion. Each level is a FIFO queue of orders (time priority). Cancels are O(1): an orderID→node index into intrusive doubly-linked lists unlinks a resting order without a scan. Book nodes and price levels are pooled, so steady-state add/cancel/match allocate nothing.

Price–time priority

An incoming order matches against the best price first; among orders at the same price, first-come-first-served. Trades always print at the resting (maker) order's price.

Integer-exact pricing

Prices are int64 ticks and quantities int64 lots — no floating point on the money path, so there is no silent rounding drift. A per-symbol Instrument converts decimals to ticks/lots only at the API boundary. This is both a correctness decision and a performance one: the hot path is pure integer arithmetic and allocation-free.

Determinism

Given the same ordered input stream, the engine produces byte-identical trades and book state. There is no wall clock or RNG in the matching path; anything that must be reproducible (timestamps, ids) is injected through Config.Clock. This is what makes record→replay, WAL recovery, and golden-file tests possible — a recorded session replays to an identical trade digest.

Concurrency — single writer, lock-free hot path

One matching goroutine owns the book with no lock on the hot path (the LMAX model). A Runner fronts it with an MPSC command queue so many producers submit concurrently while the matcher stays sequential and deterministic. A single book is inherently sequential at the match point, so you scale out across symbols (one book per symbol, via a shard router), not by parallelizing one book. Overload is handled by bounded backpressure: new orders are shed with ErrQueueFull while cancels keep flowing.

Order types

TypeBehavior
MarketTakes liquidity immediately; may sweep several levels.
LimitRests at a price or better; can be a maker.
Stop / Stop-limitDormant until the market reaches a trigger price, then fires as a market/limit order.
IcebergShows only a display slice; the hidden reserve auto-refills as it fills (with optional randomized peaks to defeat sniffing).
Post-onlyMaker-only — rejected if it would cross the spread and take.
PeggedPrice derived from a reference (bid / ask / mid) plus a signed offset.
OCO / bracketTwo linked orders; one completing cancels the other.
Trailing stopA stop that ratchets with the market by a fixed trail and never retreats.

Time-in-force: GTC (rest until cancelled), IOC (fill now, cancel the rest), FOK (fill entirely now or cancel all). Any advanced family can be feature-flagged off (Config.DisabledClasses) so one buggy exotic type is disabled without downing the venue — the ASX / Binance lesson.

Matching

An incoming order crosses the resting book by price–time priority until it is filled or the book stops crossing. The resting side is the maker; the crossing order is the taker. Trades print at the maker's price, so a large taker "walks the book" and pays a worse average price (slippage).

Self-trade prevention

When an order would match the same user's (or trade group's) resting order, one of five modes applies: cancel-newest (default), cancel-oldest, cancel-both, decrement (reduce both by the overlap, no trade — the modern default), or allow. A privileged liquidation order can be exempted so it is not self-blocked.

Pro-rata

An optional per-engine mode. At each price level, an incoming order's quantity is allocated in proportion to resting size instead of strictly FIFO, with the rounding remainder distributed so allocations sum exactly.

Auctions

A call-auction uncross computes the single clearing price that maximizes matched volume across resting bids and asks. auction.AuctionSession runs one for the open, the close, or halt recovery; its RandomizedClose jitters the uncross time deterministically (replay-safe) so nobody can aim aggression at the print — the structural defense against marking the close.

Market data

Three granularities of book view:

  • L1 — top of book (best bid/ask and sizes).
  • L2 — aggregated size per price level.
  • L3 / MBO — every resting order individually, price-then-time ordered.

Snapshots carry a monotonic sequence number for gap detection, and executions are available as a trade tape (time & sales). A typed, sequence-numbered EventSink stream (accepted / trade / canceled / rejected / halted) is the seam every market-data, drop-copy, and audit adapter sits on.

Security & market integrity

A first-class concern, not an afterthought. Every control here is grounded in a real regulatory enforcement action or market incident — spoofing convictions, the Knight Capital blow-up, oracle-manipulation hacks, integer-overflow exploits. The full catalogue (27 attacks → detection signal → defense → whether it belongs in the core or a layer above) is in docs/THREAT-MODEL.md; every knob below is documented in docs/CONFIG.md.

In-core pre-trade risk controls

Opt-in (zero value = off), enforced on the cold reject path so the zero-alloc hot path is untouched; liquidation/ADL orders are exempt from the caps, and the time-based checks are bypassed on deterministic replay.

ControlDefends againstGrounded in
MaxOrderQty / MaxOrderNotionalFat-finger / fat-order blowupsKnight Capital ($440M)
MinOrderQty / MinOrderNotionalDust that bloats the bookResource exhaustion
MaxOrdersPerAccountPer-account order-floodingQuote stuffing
MinRestingTimeSpoofing (post size, pull before it fills)JPMorgan ($920M), Coscia
DedupClientOrderIDsReplayed / duplicated orders double-bookingFIX PossDup abuse
MaxMarkStep + MinMarkDepthOracle / mark-price manipulation (jump & patient drag)Mango ($110M), Hyperliquid JELLY
MaxForceTradeQtyBook-clearing liquidation cascadesBitMEX, Hyperliquid HLP
BandBreachPauseVolatility spikes (timed pause + auto-resume)LULD / the 2010 Flash Crash
Guardrail (self-output)Runaway / repurposed-path order sprayingKnight Capital
IcebergPeakJitterHidden-reserve sniffing / order anticipationIceberg detection research
int64 + notional overflow guardInteger overflow / precision leaksBitcoin CVE-2010-5139 (184B BTC)

Surveillance detectors (pkg/surveillance)

Fed off the event stream, these detect and alert (they never touch the match path): spoofing / layering, order-to-trade ratio (the strongest standing spoofing signal), marking-the-close aggressor dominance, ramping / momentum ignition, pinging (tiny rapid-cancel bursts probing hidden liquidity), and a cross-book correlator that flags a user manipulating several correlated products at once.

Circuit breakers & degraded states

  • Price band — a collar around a risk-clamped mark/index price (not just the last trade) rejects limit orders priced beyond ±band; a breach can escalate to a timed pause.
  • Degraded statesOpen / CancelOnly (accept cancels, reject new liquidity — the venue wind-down path) / Halted, with HALTED / RESUMED emitted on the event stream so operators can page.
  • Stop-cascade protection — triggered stops chain in a bounded cascade, never an unbounded loop.

Enforcing gateway (pkg/gateway)

At the edge, above the core: a per-account token-bucket rate gate that rejects an over-quota order (cancels are never gated — keep-cancels-flowing under load) and an asymmetric speed bump on liquidity-taking orders (the IEX latency-arbitrage defense), plus a CAT-style audit export off the sequenced event stream.

The core-vs-layer boundary

A matching core earns trust by being small, deterministic, and neutral. It defends market-microstructure mechanics and its own arithmetic; it deliberately does not own beneficial-ownership clustering, credit / margin, oracle price sourcing, ingress ordering / MEV, or authentication — those are identity / clearing / oracle / gateway concerns by design. The core refuses to act on an unbacked mark; it cannot manufacture a good one. See THREAT-MODEL §6 for why that line is drawn where it is.

Recovery & persistence

Because matching is deterministic, durability is just a log of commands. pkg/wal is an append-only, length-prefixed write-ahead log: submits and cancels are recorded write-ahead (before the engine applies them) and Sync is the group-commit durability point. A crash mid-write leaves a torn tail that the reader stops at cleanly. Recovery replays the log into a fresh engine to reach byte-identical book state — the same journal-plus-snapshot model LMAX and Binance use.

Snapshots (TakeSnapshot / RestoreEngine) are keyed to the engine sequence, so recovery is bounded to O(recent): load a snapshot, replay only the WAL tail after it. Operational guidance is in docs/INTEGRATION.md.

Performance

Core microbenchmarks (Apple M-series, Go 1.23, single-threaded). CI runs them on every push; full methodology in docs/BENCHMARKS.md.

Operationns/opallocs/op
Best bid/ask read6.30
Cancel (drain)2530
Cancel / replace (MM churn)1800
Match round-trip (Match)3520
Match round-trip (Process wrapper)4914

Tail latency — a ~90%-cancel / 10%-new mix against a warm book: p50 83 ns · p99 167 ns · p999 292 ns, 0 allocs/op. The zero-alloc Match(order, buf) path appends value trades into a caller buffer; Process is the ergonomic wrapper (4 allocs) for when convenience beats the last nanosecond.

Full documentation

The deep documentation lives as versioned markdown in the repository (and renders on GitHub); the API reference is on pkg.go.dev.

DocumentWhat's inside
THREAT-MODEL.mdOrder-book attacks & defenses: 27-attack catalogue with real enforcement cases, top-ten deep dives, what the engine defends, and the core-vs-layer boundary.
CONFIG.mdEvery configuration knob — instrument grid, engine policy, and all pre-trade risk controls — with defaults and validation.
INTEGRATION.mdEmbedding & operating the engine: reference architecture, WAL/recovery, market integrity, backpressure, scaling, and a production checklist.
SPEC.mdArchitecture, the order model, and the core design decisions with their rationale.
EXCHANGE-ARCHITECTURE.mdHow real venues (MetaTrader, Binance, Coinbase, Nasdaq/LMAX/CME/IEX, dYdX/Hyperliquid) implement matching, and the incidents that shaped this design.
BENCHMARKS.mdPerformance results, methodology, and how to reproduce.
LEARN.mdOrder books and market making from first principles.
CHANGELOG.mdRelease history (v0.1.0 → v0.6.0) with breaking-change notes.
pkg.go.devGenerated API reference for every package, with runnable examples.

Getting started

go get github.com/intrepidkarthi/orderbook/pkg/matching
eng := matching.NewEngine(matching.DefaultConfig("BTC-USD"))

// A resting sell (maker) — integer ticks & lots
ask, _ := types.NewOrder("alice", "BTC-USD", types.SideSell,
    types.OrderTypeLimit, 30000, 5, types.TIFGoodTillCancel)
eng.Process(ask)

// A crossing buy (taker) — trades against it at the maker price
bid, _ := types.NewOrder("bob", "BTC-USD", types.SideBuy,
    types.OrderTypeLimit, 30000, 3, types.TIFGoodTillCancel)
res := eng.Process(bid)

for _, t := range res.Trades {
    fmt.Printf("%d @ %d\n", t.Quantity, t.Price)   // 3 @ 30000
}

Turn on the market-integrity controls that fit your venue via matching.Config (see Security), wrap the engine in a Runner for concurrency, and enable pkg/wal for durability. Runnable examples: go run ./examples/basic, ./examples/eventfeed, ./examples/gateway. Full API on pkg.go.dev.