A correctness-first Nasdaq TotalView-ITCH 5.0 replay and limit-order-book engine in C++20.
nanobook replays a full trading day of real Nasdaq market data, reconstructing the limit order book for every symbol, and it does that through two independently designed implementations that were proven to agree exactly, order by order, over the first 100 million messages of the day. It also measures where the time goes and produces reproducible aggregate statistics about the day. It is a single-threaded engine built to be checked, not a trading system.
New here? Quickstart builds and runs it on synthetic data in about a minute, and demo.md walks through the same commands with their real output.
| Measure | Result | Source |
|---|---|---|
| Framed messages in the pinned day | 282,229,684, every byte accounted for | day1_scan.md |
| Real messages decoded and re-encoded byte for byte | 276,798,644, zero mismatches | day2_decode.md |
| Dual-engine exact equivalence | 100,000,000 real frames, 8,840 books, zero mismatches | day4_fastbook.md |
| FastBook over MapBook throughput | 1.8456x, provisional Apple M2 macOS benchmark | 2026-07-26-final.json |
| Independent checks over the committed analysis | 174,323, all agreeing, mostly per-row assertions over the 87,098 committed series rows | day6_findings.md |
The speedup is a provisional measurement on one Apple M2 macOS development machine, not a platform-independent result. Performance explains exactly what was and was not controlled.
- Opens a historical Nasdaq ITCH 5.0 daily file and maps it read only.
- Walks the length-prefixed frames, checking bounds before every read.
- Decodes nine message types into typed structs, and encodes them back.
- Applies each message to the order book of its symbol.
- Runs a second, independently designed book over the same messages.
- Compares the two books exactly, level by level and order by order.
- Measures throughput and per-operation latency under a fixed protocol.
- Produces aggregate market statistics with an independent reconciliation.
The engine mirrors the exchange's outbound events. It reports what Nasdaq said happened; it never decides which orders should trade. There is no matching logic anywhere in it, by design.
One replay path serves all five commands: scan, book, validate, bench,
and analyze. Benchmarking and analysis attach to it through template seams
whose default instantiations compile to nothing, so the code that is measured
and the code that is analyzed is the code that ships; validation instead drives
two engines from a single decode and compares them. Full details in
design.md.
| Layer | Evidence |
|---|---|
| Framing | The full 8,661,679,413-byte file consumed with exact byte accounting, zero unknown types |
| Codec | 276,798,644 supported messages decoded and re-encoded byte for byte, zero mismatches |
| MapBook | 50,000,000-frame semantic replay, 48,452,372 mutations, zero errors, invariants green |
| FastBook | Same frozen contract, independent internals, randomized equivalence against MapBook |
| Dual engine | 100,000,000 real frames, 8,840 books, final exact state identical |
| Analysis | 57 statistical invariants, then 174,323 independent checks over the committed outputs |
| Synthetic suite | 474 tests in three build presets, including hand-transcribed golden decodes, randomized MapBook against FastBook equivalence, a million-operation fuzz of the order index against std::unordered_map, and deliberate fault injection |
Digests exist to find divergence quickly. They never decide the outcome: every validation run ends with an exact element-by-element comparison of every price level and every queue position, and only that pass can call a run green. correctness.md is the full account, including what these checks do not cover.
MapBook is the reference. std::map price levels so the best level is
begin(), a std::list FIFO per level, and a std::unordered_map of live
orders holding the stored list iterator. It is written to be read and audited,
not to be fast.
FastBook is the optimized engine. Orders live in a chunked slab pool
addressed by 32-bit indices, found through a hand-written open-addressing hash
of order references. Price levels live in their own pool, so a level handle is
stable for the level's whole life, and each side keeps a sorted vector of
{price, handle} pairs with the best level first. Orders within a level form
an intrusive doubly linked FIFO through the pool. The order slot is exactly 32
bytes, statically asserted.
The two share a public surface and no implementation. BookContract, a C++20
concept both books statically assert, freezes the mutations and the queries;
both also provide the canonical read-only iteration that validation walks. No
mutation logic, no data structure, and no helper is common to them. That is
what makes their agreement over 100 million real messages meaningful: the same
inputs reach the same observable state through two designs that were written
separately.
Provisional controlled measurements on an Apple M2 macOS development machine. The environment capture recorded the machine on battery power at 26 percent and discharging, with low-power mode off, so the benchmark gate refused to certify the results as final. The machine was also shared with other applications and its swap was heavily used throughout, which widens run-to-run spread more than any other factor here. CPU pinning, Linux governor control, and hardware performance counters do not exist on this platform, so no counter values are reported rather than estimated. These are not Linux reference numbers, and the run-to-run spread is wide enough that the before-and-after comparison in the generated tables cannot separate a code change from drift.
Whole pinned day, one warmup pass and three measured passes, median reported, fresh engine per pass:
| Mode | Median wall time (s) | Median frames/s | Median book updates/s | Max peak RSS (MiB) |
|---|---|---|---|---|
| Framing only (scan) | 37.082 | 7,610,869 | n/a (no book) | 1,010.3 |
| MapBook replay | 129.667 | 2,176,568 | 2,134,615 | 1,125.2 |
| FastBook replay | 70.258 | 4,017,032 | 3,939,605 | 1,609.0 |
The resident-set figures are the maximum across the measured passes, and they are dominated by resident pages of the memory-mapped input rather than by engine state, which is why the framing scan, which builds no book at all, still reports 1,010.3 MiB. FastBook's engine state is what lifts it above the others, and its own passes vary: 1,479.0 MiB at the median against 1,609.0 MiB at the maximum. The generated tables carry both.
FastBook over MapBook: 1.846x on frames per second and on book updates per second. Isolating book cost against the framing scan on identical input: 33.176 seconds for FastBook against 92.585 seconds for MapBook. Those two are differences of medians whose own passes span up to 14 percent on this machine, so read them as approximate: the ordering is solid, the third decimal is not.
Per-operation latency, one in four operations sampled per group, measurement overhead included and never subtracted (the empty two-read probe is p50 0 ns, p90 42 ns on this platform's 41.67 ns clock tick):
| Operation | FastBook p50 (ns) | MapBook p50 (ns) |
|---|---|---|
| add | 125.0 | 291.0 |
| execute | 84.0 | 250.0 |
| cancel | 42.0 | 84.0 |
| delete | 83.0 | 291.0 |
| replace | 208.0 | 541.0 |
The medians above are the informative part. The tails are not: the empty probe's own maximum, 14,458 ns, is larger than every p99.9 in the generated table, so nothing in the tail columns can be attributed to book work rather than to the machine.
The one tuning experiment of the project failed and was reverted. Changing how the order index chooses between a same-capacity purge and a doubling made three of the component microbenchmarks 15 to 36 percent faster, while the rest moved by about 1 percent. Paired alternating replays at 50 and 100 million frames then landed within about 1 percent of each other at the real working set, and peak anonymous memory rose about 14 percent. The acceptance rule had been written before the measurement, so the change came out and the negative result stayed in (D-047, D-048).
Every number above is generated from bench/results/2026-07-26-final.json into docs/generated/benchmark_tables.md, which also carries the latency tails, the memory table, the profiling summary, and the before-and-after tuning comparison. Methodology: day5_benchmarks.md.
Three findings, generated from the committed analysis outputs into docs/generated/finding_tables.md. Scope for all three: Nasdaq TotalView-ITCH 5.0, 2019-07-30, 282,229,684 frames, one venue, one day.
1. Book activity is adds and deletes, not trades. Of 276,789,789 book mutations, 45.327 percent were order additions and 43.354 percent were deletions, against 2.788 percent executions: 16.26 added orders for every execution message. Limitation: this counts messages, not intent. "Added orders" is the A and F messages; the 21,253,951 replacements also place an order and are counted separately. "Execution message" is E and C, which report one resting order's fill each, so this is not an order-to-trade ratio, and the day's 1,461,010 non-cross and 17,700 cross trade messages are counted but not decoded.
2. Flow is extremely uneven, and the closing second is the extreme. Across the 56,971 seconds carrying at least one message the mean was 4,953.9 messages per second, while the single busiest second, 16:00:00, carried 982,255 messages, 198 times that mean. Of those, 916,875 were order deletions: the close is a book being torn down, not a burst of trading. At millisecond resolution the picture changes: the busiest millisecond of the day, 2,004 messages at 15:59:25.001, is 35 seconds before the close, and the closing second itself averaged 982 messages per millisecond across all thousand of its milliseconds. The close is sustained rather than spiky. Limitations: these are feed-event counts, not latency measurements, and no cause is established. The mean is taken over every active second, including the long, quiet pre-market and post-market stretches, so it is a whole-file baseline rather than a regular-session one.
3. Spreads over the whole day are far wider than inside the session. Time weighted by how long each spread was in force, all three of the most active symbols rested at a one-cent median spread inside regular market hours, while over the whole observed day, which includes the pre-market and post-market quoting either side of it, the time-weighted mean spread was 2.14 to 3.40 times its regular-session value. The comparison is full day against regular session, and the full day contains the regular session, so it understates how different the extended hours alone were. Each held a two-sided book for the entire session, with zero nanoseconds locked and zero crossed. Limitations: this venue's own book, not the national best bid and offer, and one day. One cent is the minimum quoting increment for securities at these prices, so a one-cent median is the spread sitting on its floor rather than a measured degree of tightness. And a single venue's own displayed book essentially cannot lock or cross itself, because such an order would have matched, so zero locked and zero crossed time is closer to structural than empirical; it is reported here because the classifier measured it, and it is why those code paths are exercised only by synthetic tests.
| Symbol | Median spread, regular | Median spread, full day | Full-day mean over regular mean |
|---|---|---|---|
| SPY | 0.0100 | 0.0200 | 2.64x |
| QQQ | 0.0100 | 0.0200 | 2.14x |
| IWM | 0.0100 | 0.0300 | 3.40x |
SPY, QQQ, and IWM were selected mechanically as the three symbols with the most book mutations among the 8,841 eligible, not chosen by preference. The remaining charts, the per-second activity, the per-symbol totals for all 8,850 locates, and the eleven generated finding candidates are in analysis/, and the methodology is in day6_findings.md.
On the spread statistics: they are time weighted and computed in C++ from every top-of-book state change, 1,248,035 of them for SPY alone. The committed per-symbol CSV files are a documented one-second reduction of that series, kept small enough to inspect and to chart. A full-resolution rerun produced identical statistics and only different row counts, and the independent verifier rebuilt every duration and weighted quantile from that complete series (D-072).
No market data required. Everything here runs on synthetic bytes this repository generates.
cmake --preset release
cmake --build --preset release
ctest --preset release --output-on-failure
./build/release/itch_synth basic /tmp/nanobook-basic.itch
./build/release/nanobook scan /tmp/nanobook-basic.itch
./build/release/nanobook scan /tmp/nanobook-basic.itch --decode-check
./build/release/nanobook book /tmp/nanobook-basic.itch --engine=map
./build/release/nanobook book /tmp/nanobook-basic.itch --engine=fast
./build/release/nanobook validate /tmp/nanobook-basic.itchRequirements: GCC 13+ or Clang 17+, CMake 3.24+, Ninja. Other presets: debug
and asan-ubsan, which run the same 474 tests under AddressSanitizer and
UndefinedBehaviorSanitizer.
demo.md walks through the same commands with their real output, adds the analysis step, and takes about five minutes.
Nasdaq market data is never redistributed here. The file is public, and the exact one used is pinned by checksum:
| Property | Value |
|---|---|
| Basename | 07302019.NASDAQ_ITCH50 (July 30, 2019) |
| Decompressed size | 8,661,679,413 bytes |
| Decompressed SHA-256 | 9f8634a048b8195ccdcbe618e5833a759e35e04486d74e36b79176d35172654a |
data.md covers obtaining and verifying it, including the fact that Nasdaq's published MD5 file returned 404 and what was checked instead. Once the file is local:
./build/release/nanobook scan /path/to/07302019.NASDAQ_ITCH50
./build/release/nanobook scan /path/to/07302019.NASDAQ_ITCH50 --decode-check
./build/release/nanobook book /path/to/07302019.NASDAQ_ITCH50 --engine=fast
./build/release/nanobook validate /path/to/07302019.NASDAQ_ITCH50 \
--messages=100000000 --checkpoint=1000000
./scripts/run_bench.sh /path/to/07302019.NASDAQ_ITCH50 --label=my-run
./scripts/run_analysis.sh /path/to/07302019.NASDAQ_ITCH50 --series-interval-ns=1000000000
python3 scripts/verify_analysis.py analysis/resultsBoth scripts refuse to run unless the file's size and SHA-256 match the pinned values, the working tree is clean, the release build is current, and the tests are green.
include/nanobook/ core engine headers, header only
src/ the CLI: scan, book, validate, bench, analyze
tools/ itch_synth, the synthetic feed generator
tests/ 474 synthetic tests, no real data
bench/ microbenchmarks and committed benchmark results
analysis/ committed aggregate analysis results and charts
scripts/ reproduction, verification, and rendering scripts
docs/ design, decisions, milestone evidence, generated tables
A few that shaped the result, each recorded with its alternatives and evidence in decisions.md:
memcpyfield readers, not packed struct overlays. Unaligned reads through casted pointers are undefined behavior and bake in host endianness; the compiler folds the safe form into one load and a byte swap anyway (D-001).- A reference book before an optimized one. Optimizing without an oracle means optimizing without knowing you are still correct. MapBook exists to be the oracle (D-016, D-020).
- Indices, never pointers, into pooled storage. 32-bit slot indices survive pool growth, halve the link size, and make a use-after-release detectable rather than undefined (D-021, D-022).
- An exact final comparison, not digests alone. A digest can only say two books differ somewhere; the exact pass says where, and it is what decides every run (D-027).
- Fixed-point integers everywhere, never floating point. Prices stay raw ticks and the midpoint is carried doubled, so a half tick is exact (D-052, D-053).
- A microbenchmark win that was reverted. It did not survive the real working set and cost memory, and the acceptance rule had been written first (D-047, D-048).
- Independent reconciliation of the analysis. A reducer that is wrong twice in the same way proves nothing, so the verifier shares no code with the analyzer (D-067).
- Feed-derived text is untrusted at every output boundary. A ticker becomes a filename, a CSV field, and a JSON string; each needs its own validation. An adversarial review found all three missing, and each now has a regression test (D-075).
- Historical file replay, not a live feed handler. No UDP, no MoldUDP64 sequencing, no gap recovery.
- Single-threaded by design. Nothing here is lock-free or concurrent.
- No order matching: the engine mirrors the exchange's reported events.
- One trading day, one venue. Nasdaq TotalView shows this venue's own book, not the consolidated national market.
- Non-displayed liquidity never appears in the reconstructed book, because it is never quoted. Its executions are reported by the feed's trade messages, which this project counts but does not decode, so no part of it is reconstructed here.
- Benchmark numbers are provisional, from one macOS machine, with no CPU pinning, no governor control, and no hardware counters.
- Measurement overhead is included in every reported latency.
- Nothing here is production-ready or exchange-certified, and none of it is investment advice or a trading strategy.
| Document | What it covers |
|---|---|
| product_spec.md | The full specification this was built against |
| design.md | How each component works and why |
| decisions.md | Every decision record, with alternatives and evidence |
| correctness.md | How correctness was established, and its limits |
| demo.md | Five-minute synthetic walkthrough |
| data.md | Obtaining and verifying the real data |
| day1_scan.md | Framing over the full file |
| day2_decode.md | Byte-exact codec verification |
| day3_mapbook.md | Reference book replay |
| day4_fastbook.md | Optimized book and dual-engine validation |
| day5_benchmarks.md | Benchmark methodology and results |
| day6_findings.md | Analysis methodology and the three findings |
| findings.md | Index of the findings and the data behind them |
| analysis.json | The analysis manifest, with a checksum per output |
| 2026-07-26-final.json | The authoritative benchmark result |
v1 is complete: the engine, the validation, the benchmark protocol, the
analysis, and the evidence behind all of them. Post-v1 ideas, including
external-oracle reconciliation and a MoldUDP64-style feed handler, are
described in product_spec.md section 15 and are not implemented.
MIT. See LICENSE.


