When to Use
Use this skill for market microstructure research, high-frequency backtesting, and queue-position simulation, when the input is a market-by-order (L3) message log rather than pre-aggregated depth snapshots. Feeds such as Nasdaq TotalView-ITCH 5.0, LOBSTER message files, and CME MDP 3.0 Market by Order publish individual order lifecycle events; the aggregated L2 book that a strategy actually reasons about has to be rebuilt from them, event by event.
The engine maintains the L3 order map and the L2 price-level aggregation together, so taking a snapshot after every message — the normal way to avoid lookahead bias in an event-driven backtest — stays affordable.
The second job this skill does is telling you when the reconstruction is wrong. A message log with a gap in it still replays without error; it just produces a book that quietly disagrees with the real one. Every anomaly that implies divergence is counted and surfaced rather than absorbed.
When NOT to Use
- When your feed already publishes aggregated depth. For L2 snapshot+delta feeds, reconcile rather than reconstruct — see
market-data-snapshot-plus-delta-reconciliation. - As a gap-detection mechanism for a live feed. Book-integrity violations here are a consequence of dropped messages, detected after the fact. Sequence-number gaps are detected at the transport layer (MoldUDP64/SoupBinTCP) before the book is touched — see
sequence-number-gap-detection-for-feeds. - For hidden liquidity, cross/auction prints, or halts. This reconstructs the displayed book only. LOBSTER event types 5 (hidden execution), 6 (cross trade) and 7 (halt), and Nasdaq Trade
P/ Cross TradeQmessages, do not decrement a resting displayed order. Feeding them in asEXECUTEcorrupts the book. - For true queue-position simulation on its own. This tracks displayed size and order count per level, not per-order queue rank. A
REPLACEre-enters at the back of the queue; modelling that requiresqueue-position-modeling-for-passive-orders. - For multi-symbol replay in one instance. One engine holds one symbol's book and does not filter by symbol; shard by symbol upstream.
Prerequisites
- A chronologically ordered L3 message stream. Ordering is the correctness precondition — the engine flags a timestamp regression but cannot repair one.
- Per message:
order_id,msg_type(ADD,CANCEL,DELETE,EXECUTE,REPLACE),timestamp_nanos, plus the fields that message type carries (see the table inL3OrderMessage). - The feed's price precision.
price_scalemust match it:10_000for Nasdaq ITCHPrice (4)and LOBSTER,100for a cent-quoted feed. Optionally setmax_priceto reject implausible prices caused by a scaling mistake. - Target depth level count for snapshots (e.g. top 5 or 10).
Workflow
-
Map the source feed's message types onto the five canonical types.
- Decision point —
CANCELandDELETEare not the same message. Nasdaq ITCHX(Order Cancel) carries a Canceled Shares count and is a partial reduction;D(Order Delete) carries no share count and removes all remaining shares. LOBSTER splits them the same way (type 2 vs type 3). Mapping a total deletion ontoCANCELrequires inventing a share count, which is exactly the fabrication this engine refuses. - Route hidden executions, cross prints and halts away from the engine.
- Decision point —
-
Replay messages in order, one at a time.
ADDinserts a new resting order at a price level.CANCEL/EXECUTEdeduct shares; effects are cumulative and an order that reaches zero shares is removed.DELETEremoves the whole order.REPLACEremoves the original and inserts the replacement.- Decision point — a
REPLACEchanges the order's identity. ITCHUcarries an Original and a New Order Reference Number, and "the NASDAQ system will use this new order reference number for all subsequent updates". Keying the replacement under the old id makes every later message for that order look like a gap. - Decision point — a
REPLACEdoes not carry the side. It cannot change the side, so the message omits it; the side must be inherited from the originalADD. Taking it from the replace message lets a caller flip a bid onto the ask book. - Decision point —
REPLACEquantity is absolute, not a delta. ITCH calls the field "the new total displayed quantity", unlike the deduction semantics ofCANCEL/EXECUTE.
-
Classify every anomaly before trusting the book. Malformed input (unknown message type, non-positive quantity, non-finite price, unrecognised side) raises immediately. A well-formed message that cannot be applied to a consistent book is an integrity violation, counted in
violations_by_kind:UNKNOWN_ORDER— cancel/delete/execute/replace for an order never added.DUPLICATE_ORDER_ID— anADDfor an already-live id (reference numbers are day-unique).OVER_CANCEL/OVER_EXECUTE— more shares removed than were resting.TIMESTAMP_REGRESSION— the log went backwards.- Decision point — decide the policy before the run, not after.
strict=Trueraises on the first violation (right for a validated production pipeline); the default records and continues (right for exploratory replay of an imperfect archive). Either way, a non-zero violation count invalidates any microstructure statistic computed from that replay — do not report it as clean.
-
Snapshot the L2 book. Aggregate into bids (descending) and asks (ascending), take the top N levels, and compute BBO, mid-price and spread.
- Decision point — check
is_crossed_book/is_locked_bookbefore using the mid. A crossed book yields a negative spread and a meaningless mid; both fields are still populated, so an unguarded consumer will silently ingest them.
- Decision point — check
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Silently absorbing a cancel for an order you never saw. The natural implementation is
if order_id in book:— which turns a droppedADDinto a no-op and leaves a book that is wrong for the rest of the session with nothing in the output saying so. Every unmatched message is a divergence and must be counted. - Fabricating an order out of a
REPLACE. Creating the replacement when the original is absent invents depth that never rested on the book — and the side cannot even be known, because the replace message does not carry it. A reported gap is strictly better than invented liquidity. - Treating
CANCELas a full delete. Conflating ITCHXwithDremoves an order that still had displayed size, deleting real depth at the touch. - Getting the price scale wrong in either direction. A price divided by the scale twice becomes sub-tick and is rejected; a raw wire integer passed straight through is not —
1000000is the ITCHPrice (4)encoding of $100.00 but reads as a perfectly plausible $1,000,000.00 book. Only an explicitmax_pricecatches that direction. - Using floats as price-level keys.
102.4 + 0.7is103.10000000000001, not103.1. Two orders genuinely quoted at the same tick then land in different dictionary buckets and split one price level in two — in a direct test, that reports 2 bid levels and a best-bid size of 10 where the correct answer is 1 level of 15, a 33% understatement of depth at the BBO. Prices are integers on the wire (Price (4), "dollar price times 10000"); keep them integers internally. - Accepting an unrecognised side.
bid_map if side == "BUY" else ask_maproutes"B","Buy ",""and every typo onto the ask book, manufacturing a crossed book out of a single well-formed order. - Carrying an unused timestamp field. L3 replay is order-dependent; a
timestamp_nanosfield that nothing ever reads gives a false impression that ordering is being checked. Note that equal timestamps are legal — multiple ITCH messages share a nanosecond — so only a strict regression is an error. - Rebuilding the whole book on every snapshot. Re-aggregating all live orders per snapshot is O(N) per message, so tick-by-tick replay degrades to O(N·M) — the same complexity blow-up that an unindexed order lookup causes, just moved to the snapshot path. Measured on a 20,000-order book with a snapshot after every message: 52.7s rebuilding versus 0.68s maintaining the aggregation incrementally.
- Reading
mid_pricewithout checking the crossed flag. A crossed book produces a negative spread and a mid that sits between two prices that cannot both be real. - Treating a clean replay as a validated one. No exception raised means only that no message was malformed. Check
integrity_violation_count.
Verification
- Instantiate
HistoricalOrderBookReconstructEngine(symbol="AAPL"). ReplayADD BUY ID_1 @ 100.0 x10,ADD BUY ID_2 @ 100.0 x5,ADD SELL ID_3 @ 101.0 x8,CANCEL ID_1 x4. Verify Best Bid $= 100.0$ (Qty $11$, order count $2$), Best Ask $= 101.0$ (Qty $8$), Mid $= 100.50$, Spread $= 1.00$, andintegrity_violation_count == 0. - Semantics: a
REPLACEfromOLDtoNEWmust leaveNEWon the book andOLDoff it, must set the size to the replace message's absolute quantity, and must keep the original side even when the message carries the opposite one. - Integrity: a cancel, delete, execute or replace for an unknown order must increment
UNKNOWN_ORDERand add no depth; over-cancelling must incrementOVER_CANCEL; a duplicateADDmust incrementDUPLICATE_ORDER_IDand leave exactly one order; a backwards timestamp must incrementTIMESTAMP_REGRESSIONwhile equal timestamps must not.strict=Truemust raiseBookIntegrityErroron the first of these. - Numerics: orders at
102.4 + 0.7and103.1must aggregate into one level of size 15; a locked book (bid == ask) must reportis_locked_bookand notis_crossed_book. - Consistency: after a mixed replay of adds, cancels, executes, deletes and replaces, the incrementally maintained level aggregation must equal a from-scratch rebuild off the order map.
- Run
python -m unittest discover -s skills/historical-order-book-reconstruction-from-message-logs/scriptsand confirm 43/43 pass.