Skip to content

Repository files navigation

nanobook

A correctness-first Nasdaq TotalView-ITCH 5.0 replay and limit-order-book engine in C++20.

C++20 Tests License

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.

Evidence

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.

What it does

  1. Opens a historical Nasdaq ITCH 5.0 daily file and maps it read only.
  2. Walks the length-prefixed frames, checking bounds before every read.
  3. Decodes nine message types into typed structs, and encodes them back.
  4. Applies each message to the order book of its symbol.
  5. Runs a second, independently designed book over the same messages.
  6. Compares the two books exactly, level by level and order by order.
  7. Measures throughput and per-operation latency under a fixed protocol.
  8. 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.

Architecture

Architecture: a mapped ITCH file feeds a frame iterator, then a typed decoder, then an engine registry holding one book per stock locate, which drives MapBook and FastBook; three read-only observers hang off that path for validation, benchmarking, and analysis, writing JSON, CSV, and chart outputs

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.

Correctness

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

The correctness pipeline, from hand-transcribed golden bytes through synthetic fixtures, real-file framing, real-message round trips, MapBook semantics, randomized equivalence, and the 100 million frame dual-engine validation, to the full-day statistical reconciliation

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.

Two books, on purpose

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.

FastBook data layout: an order reference hashes to a pool index, the 32-byte order slot carries a stable level handle, levels live in their own pool and are listed by a sorted price vector, and orders link through intrusive previous and next indices

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.

Performance

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.

What the day looked like

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.

Message-type composition of the trading day

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.

Messages and book mutations per second across the trading day

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 top-of-book spread across the trading day

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).

Quickstart

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.itch

Requirements: 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.

Reproducing the real-data results

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/results

Both 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.

Repository map

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

Engineering decisions

A few that shaped the result, each recorded with its alternatives and evidence in decisions.md:

  • memcpy field 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).

Limitations

  • 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.

Documentation

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

Status

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.

License

MIT. See LICENSE.

About

C++20 Nasdaq TotalView-ITCH 5.0 replay and limit-order-book engineering project.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages