Chapter 25
NASDAQ TotalView-ITCH: Order Book Data Parsing
NASDAQ TotalView-ITCH: Order Book Data Parsing
Chapter 3: Market Microstructure
Docker image: ml4t
Purpose
This notebook demonstrates how to parse NASDAQ's TotalView-ITCH binary protocol. Understanding MBO (message-by-order) data is foundational for microstructure-based ML features.
Learning Objectives
After completing this notebook, you will be able to:
- Understand the ITCH 5.0 binary message format
- Parse ITCH messages using Python's
structmodule - Load pre-parsed ITCH data for analysis
- Choose between Python (educational) and Rust (production) parsers
Cross-References
- Downstream:
02_itch_lob_reconstruction(builds order book from these messages) - Related:
09_databento_mbo_analysis(alternative MBO data source)
Data Requirements
ITCH sample data can be downloaded using:
python data/equities/market/microstructure/nasdaq_itch_download.py --list # List available files
python data/equities/market/microstructure/nasdaq_itch_download.py # Download default sampleFiles are ~5GB compressed from: https://emi.nasdaq.com/ITCH/
ITCH Message Types
The ITCH v5.0 specification defines 20+ message types:
| Type | Name | Description |
|---|---|---|
| S | System Event | Market open/close events |
| R | Stock Directory | Ticker information and characteristics |
| H | Trading Action | Trading halts, pauses, and resumptions |
| Y | Reg SHO Restriction | Short sale price test restrictions |
| L | Market Participant | Market maker positions |
| V | MWCB Decline Level | Market-wide circuit breaker levels |
| W | MWCB Status | Circuit breaker breach status |
| A | Add Order | New limit order enters the book |
| F | Add Order (MPID) | Same as A, with market participant ID |
| E | Order Executed | Partial/full execution against standing order |
| C | Order Executed w/Price | Execution at different price (hidden orders) |
| X | Order Cancel | Partial cancellation |
| D | Order Delete | Full removal from book |
| U | Order Replace | Modify price/size (cancel + add) |
| P | Trade | Non-displayed execution |
| Q | Cross Trade | Opening/closing cross |
| B | Broken Trade | Trade cancellation |
| I | NOII | Net Order Imbalance Indicator (auction) |
| J | LULD Auction Collar | Limit up-limit down price bands |
| K | IPO Quoting Period | IPO quotation timing |
By combining these messages chronologically, we can reconstruct the order book at any point in time.
"""NASDAQ TotalView-ITCH: Order Book Data Parsing — parse ITCH binary protocol into structured messages."""
import gzip
import os
import shutil
import struct
import warnings
from collections import Counter, defaultdict
from datetime import date, datetime
from pathlib import Path
from time import time
warnings.filterwarnings("ignore")
import polars as pl
from itch_message_specs import (
FMT_DICT,
MESSAGE_SPECS,
NT_DICT,
flush_to_parquet,
parse_price4,
parse_timestamp,
print_message_formats,
)
from tqdm.auto import tqdm
from data import load_nasdaq_itchSKIP_PARSING = False# Data paths
# Canonical parsed messages location (both read and write target)
MESSAGE_DIR = load_nasdaq_itch(get_base_path=True)
MESSAGE_DIR.mkdir(parents=True, exist_ok=True)
# Raw ITCH binary input (from download script)
ITCH_RAW_DIR = MESSAGE_DIR.parent / "raw"
print(f"Raw ITCH data (input): {ITCH_RAW_DIR}")
print(f"Parsed messages (output): {MESSAGE_DIR}")
_raw_present = ITCH_RAW_DIR.exists() and next(ITCH_RAW_DIR.iterdir(), None) is not None
print(f"Raw data exists: {_raw_present}")
if not _raw_present:
print(" (run NB00_itch_download.py first to fetch the ITCH binary)")Output
Raw ITCH data (input): data/equities/market/microstructure/nasdaq_itch/raw Parsed messages (output): data/equities/market/microstructure/nasdaq_itch/messages Raw data exists: True
1. Message Specifications
Each ITCH message has a fixed binary structure. The format is defined in itch_message_specs.py
using Python's struct module. Format codes:
H= unsigned short (2 bytes)I= unsigned int (4 bytes)Q= unsigned long long (8 bytes)s= char (1 byte),Ns= N chars>= big-endian byte order
# Show message formats (loaded from utils/itch_message_specs.py)
print_message_formats()Output
ITCH Message Formats: S (System Event ): 11 bytes - >HH6ss R (Stock Directory ): 38 bytes - >HH6s8sssIss2ssssssIs A (Add Order ): 35 bytes - >HH6sQsI8sI F (Add Order MPID ): 39 bytes - >HH6sQsI8sI4s E (Order Executed ): 30 bytes - >HH6sQIQ C (Order Executed with Price): 35 bytes - >HH6sQIQsI X (Order Cancel ): 22 bytes - >HH6sQI D (Order Delete ): 18 bytes - >HH6sQ U (Order Replace ): 34 bytes - >HH6sQQII P (Trade ): 43 bytes - >HH6sQsI8sIQ Q (Cross Trade ): 39 bytes - >HH6sQ8sIQs H (Trading Action ): 24 bytes - >HH6s8sss4s Y (Reg SHO Restriction ): 19 bytes - >HH6s8ss L (Market Participant Position): 25 bytes - >HH6s4s8ssss V (MWCB Decline Level ): 34 bytes - >HH6sQQQ W (MWCB Status ): 11 bytes - >HH6ss J (LULD Auction Collar ): 34 bytes - >HH6s8sIIII K (IPO Quoting Period ): 27 bytes - >HH6s8sIsI B (Broken Trade ): 18 bytes - >HH6sQ I (NOII ): 49 bytes - >HH6sQQs8sIIIss
2. Binary Parsing Example
Let's demonstrate how binary parsing works by creating and parsing a sample Add Order message.
# Create a sample Add Order message to demonstrate parsing
# Format: >HH6sQsI8sI (big-endian)
sample_add_order = struct.pack(
">HH6sQsI8sI",
1234, # stock_locate
5678, # tracking_number
b"\x00\x00\x00\x00\x00\x01", # timestamp (1 nanosecond)
9876543210, # order_reference_number
b"B", # buy_sell_indicator
100, # shares
b"AAPL ", # stock (padded to 8 chars)
1500000, # price (150.0000 in price4 format)
)
print(f"Raw Add Order message ({len(sample_add_order)} bytes):")
print(f" Hex: {sample_add_order.hex()}")Output
Raw Add Order message (35 bytes): Hex: 04d2162e000000000001000000024cb016ea42000000644141504c202020200016e360
# Parse the binary data using our struct format
parsed = struct.unpack(FMT_DICT["A"], sample_add_order)
add_order = NT_DICT["A"]._make(parsed)
print("Parsed Add Order Message:")
print("-" * 40)
for field, value in add_order._asdict().items():
if isinstance(value, bytes):
value = value.decode("ascii").strip()
print(f" {field:25}: {value}")Output
Parsed Add Order Message: ---------------------------------------- stock_locate : 1234 tracking_number : 5678 timestamp : order_reference_number : 9876543210 buy_sell_indicator : B shares : 100 stock : AAPL price : 1500000
# Apply conversions using helper functions from utils.itch_message_specs
ts_ns = parse_timestamp(add_order.timestamp)
price = parse_price4(add_order.price)
print(f"Timestamp: {ts_ns:,} nanoseconds = {ts_ns / 1e9:.9f} seconds after midnight")
print(f"Price: ${price:.4f}")Output
Timestamp: 1 nanoseconds = 0.000000001 seconds after midnight Price: $150.0000
3. Loading Pre-Parsed ITCH Data
If you've already parsed ITCH data (using the Rust parser or Python parser), you can load the pre-parsed messages directly. This is the recommended approach for analysis.
# Check what data is available locally
print("ITCH Data Pipeline Status:")
print("-" * 50)
# Step 1: Raw binary from download
raw_files = []
if ITCH_RAW_DIR.exists():
raw_files = list(ITCH_RAW_DIR.glob("*.gz")) + list(ITCH_RAW_DIR.glob("*.bin"))
print(f"Raw binary files: {len(raw_files)}")
for f in raw_files:
print(f" {f.name} ({f.stat().st_size / 1e9:.2f} GB)")
# Step 2: Parsed messages (single uppercase letter = message type)
parsed_types = (
[
d
for d in sorted(MESSAGE_DIR.iterdir())
if d.is_dir() and len(d.name) == 1 and d.name.isupper()
]
if MESSAGE_DIR.exists()
else []
)
parsed_with_data = [d for d in parsed_types if list(d.glob("*.parquet"))]
print(f"Parsed message types: {len(parsed_with_data)}")
for msg_dir in parsed_with_data:
name = MESSAGE_SPECS.get(msg_dir.name, {}).get("name", "Unknown")
n_files = len(list(msg_dir.glob("*.parquet")))
print(f" {msg_dir.name} ({name}): {n_files} files")Output
ITCH Data Pipeline Status: -------------------------------------------------- Raw binary files: 2 01302020.NASDAQ_ITCH50.gz (5.60 GB) 01302020.NASDAQ_ITCH50.bin (12.95 GB) Parsed message types: 18 A (Add Order): 43 files C (Order Executed with Price): 43 files D (Order Delete): 43 files E (Order Executed): 43 files F (Add Order MPID): 43 files H (Trading Action): 11 files I (NOII): 18 files J (LULD Auction Collar): 4 files K (IPO Quoting Period): 2 files L (Market Participant Position): 21 files P (Trade): 43 files Q (Cross Trade): 7 files R (Stock Directory): 1 files S (System Event): 4 files U (Order Replace): 43 files V (MWCB Decline Level): 1 files X (Order Cancel): 43 files Y (Reg SHO Restriction): 38 files
# Validate: at minimum we need parsed data to continue
trade_dir = MESSAGE_DIR / "P"
assert trade_dir.exists() and list(trade_dir.glob("*.parquet")), (
f"No parsed ITCH data at {MESSAGE_DIR}.\n"
"To set up the data pipeline:\n"
" 1. Download raw data: uv run python data/equities/market/microstructure/nasdaq_itch_download.py\n"
" 2. Parse (this notebook, Section 4) or use Rust parser (Section 6)\n"
" 3. Parsed messages go to: data/equities/market/microstructure/nasdaq_itch/messages/"
)
trades = pl.read_parquet(trade_dir / "*.parquet")
print(f"\nLoaded {len(trades):,} trade messages")
print(f"Columns: {trades.columns}")Output
Loaded 1,779,727 trade messages Columns: ['stock_locate', 'tracking_number', 'timestamp', 'order_reference_number', 'buy_sell_indicator', 'shares', 'stock', 'price', 'match_number']
4. Full Parser Implementation
This Python parser is for educational purposes. For production use with large files, use the Rust parser (see Section 6) which provides order-of-magnitude speedups.
Parser Helpers
We split the parser into three functions: _read_frame reads one binary message
frame, _decode_message unpacks and converts it, and parse_itch_file orchestrates
the loop with buffered Parquet writes.
def _read_frame(f, pbar) -> tuple[str, bytes] | None:
"""Read one ITCH message frame: 2-byte length + 1-byte type + payload.
Returns (msg_type, payload) on success, or None on EOF/truncation.
"""
# 2-byte big-endian length prefix (message size including type byte)
length_bytes = f.read(2)
if len(length_bytes) < 2:
return None
pbar.update(2)
msg_size = int.from_bytes(length_bytes, "big")
# 1-byte message type
msg_type_byte = f.read(1)
if len(msg_type_byte) < 1:
print(f"\nWarning: Truncated message at byte {f.tell()}, expected type byte")
return None
pbar.update(1)
msg_type = msg_type_byte.decode("ascii")
# Payload (msg_size includes type byte, so payload is msg_size - 1)
payload = f.read(msg_size - 1)
if len(payload) < msg_size - 1:
print(f"\nWarning: Truncated payload for message type {msg_type}")
return None
pbar.update(msg_size - 1)
return msg_type, payloadDecode binary payload into a Python dict, converting raw timestamp bytes to nanosecond integers and byte strings to stripped ASCII.
def _decode_message(msg_type: str, payload: bytes) -> dict | None:
"""Unpack binary payload into a dict, converting timestamps and strings.
Returns parsed message dict, or None on struct error.
"""
try:
parsed = struct.unpack(FMT_DICT[msg_type], payload)
msg = NT_DICT[msg_type]._make(parsed)._asdict()
except struct.error:
return None
# Convert timestamp: nanoseconds since midnight
if "timestamp" in msg:
msg["timestamp"] = int.from_bytes(msg["timestamp"], "big")
# Decode string fields
for field, value in msg.items():
if isinstance(value, bytes):
msg[field] = value.decode("ascii").strip()
return msgThe main parser reads the binary file sequentially, buffering decoded messages and flushing to Parquet periodically to bound memory usage.
def parse_itch_file(
itch_file: Path,
trading_day: date,
output_dir: Path,
max_buffered_messages: int = 10_000_000,
max_messages: int | None = None,
) -> dict[str, int]:
"""Parse ITCH binary file and store messages as Parquet.
Args:
itch_file: Path to binary ITCH file (.bin, not .gz).
trading_day: Trading date for timestamp construction.
output_dir: Directory for Parquet output (one subdir per message type).
max_buffered_messages: Flush threshold (total buffered messages).
max_messages: Optional limit for testing.
Returns:
Dictionary with message type counts.
"""
# Midnight timestamp for the trading day (ITCH timestamps are nanoseconds offset)
base_ts = datetime(trading_day.year, trading_day.month, trading_day.day)
file_counters: dict[str, int] = defaultdict(int)
buffers = defaultdict(list)
counts = Counter()
file_size = itch_file.stat().st_size
start_time = time()
with (
itch_file.open("rb") as f,
tqdm(total=file_size, desc="Parsing ITCH", unit="B", unit_scale=True) as pbar,
):
while True:
if max_messages and sum(counts.values()) >= max_messages:
print(f"\nLimit reached: {max_messages:,} messages")
break
frame = _read_frame(f, pbar)
if frame is None:
break
msg_type, payload = frame
counts[msg_type] += 1
if msg_type not in FMT_DICT:
continue
msg = _decode_message(msg_type, payload)
if msg is None:
continue
# Check for end of messages
if msg_type == "S" and msg.get("event_code") == "C":
print("\nEnd of Messages")
flush_to_parquet(buffers, output_dir, base_ts, file_counters)
break
buffers[msg_type].append(msg)
# Periodic flush
if sum(len(v) for v in buffers.values()) >= max_buffered_messages:
flush_to_parquet(buffers, output_dir, base_ts, file_counters)
# Final flush
if any(buffers.values()):
flush_to_parquet(buffers, output_dir, base_ts, file_counters)
elapsed = time() - start_time
total = sum(counts.values())
print(f"Parsed {total:,} messages in {elapsed:.1f}s ({total / elapsed:,.0f} msg/s)")
return dict(counts)# Locate ITCH data file (skip if SKIP_PARSING is set)
if SKIP_PARSING:
print("SKIP_PARSING=True: skipping ITCH binary parsing (uses pre-parsed data)")
itch_file = None
counts = {}
else:
# Clear any existing parsed data to avoid schema conflicts
# (Different parser versions may produce different schemas)
# Set ITCH_KEEP_EXISTING=1 to skip cleanup and use existing data
clear_existing = os.environ.get("ITCH_KEEP_EXISTING", "0") != "1"
if clear_existing and MESSAGE_DIR.exists() and list(MESSAGE_DIR.glob("*/part-*.parquet")):
print(f"Clearing existing parsed data in {MESSAGE_DIR}")
print(" (Set ITCH_KEEP_EXISTING=1 to keep existing data)")
shutil.rmtree(MESSAGE_DIR)
MESSAGE_DIR.mkdir(parents=True, exist_ok=True)
# Find ITCH file (compressed or uncompressed)
gz_files = list(ITCH_RAW_DIR.glob("*.gz")) if ITCH_RAW_DIR.exists() else []
bin_files = list(ITCH_RAW_DIR.glob("*.bin")) if ITCH_RAW_DIR.exists() else []
if not gz_files and not bin_files:
raise FileNotFoundError(
f"No raw ITCH binary found at {ITCH_RAW_DIR}.\n"
"Download first:\n"
" uv run python data/equities/market/microstructure/nasdaq_itch_download.py"
)Output
Clearing existing parsed data in data/equities/market/microstructure/nasdaq_itch/messages (Set ITCH_KEEP_EXISTING=1 to keep existing data)
# Decompress if needed and extract trading date
if not SKIP_PARSING and (gz_files or bin_files):
# Prefer uncompressed, otherwise decompress
if bin_files:
itch_file = bin_files[0]
print(f"Found uncompressed: {itch_file.name}")
else:
gz_file = gz_files[0]
itch_file = gz_file.with_suffix(".bin")
if not itch_file.exists():
print(f"Decompressing {gz_file.name}...")
with gzip.open(gz_file, "rb") as f_in, open(itch_file, "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
print(f"Created: {itch_file.name} ({itch_file.stat().st_size / 1e9:.1f} GB)")
else:
print(f"Found: {itch_file.name}")
# Extract trading date from filename (format: MMDDYYYY.NASDAQ_ITCH50.bin)
date_str = itch_file.stem.split(".")[0]
if len(date_str) == 8 and date_str.isdigit():
trading_day = date(int(date_str[4:8]), int(date_str[:2]), int(date_str[2:4]))
else:
trading_day = date(2020, 1, 30) # Fallback
print(f"Trading day: {trading_day}")
# Parse ITCH file (full-day parse: ~22 min on the reference machine)
if itch_file and itch_file.exists():
counts = parse_itch_file(
itch_file=itch_file,
trading_day=trading_day,
output_dir=MESSAGE_DIR,
# max_messages=1_000_000, # Remove this line for full parse
)
print("\nMessage counts:")
for msg_type, count in sorted(counts.items(), key=lambda x: -x[1]):
name = MESSAGE_SPECS.get(msg_type, {}).get("name", "Unknown")
print(f" {msg_type} ({name:25}): {count:>10,}")Output
Found uncompressed: 01302020.NASDAQ_ITCH50.bin Trading day: 2020-01-30
Parsing ITCH: 0%| | 0.00/13.0G [00:00<?, ?B/s]
End of Messages
Parsed 423,285,709 messages in 1318.7s (320,982 msg/s) Message counts: A (Add Order ): 184,735,355 D (Order Delete ): 180,285,101 U (Order Replace ): 36,777,372 E (Order Executed ): 8,415,610 X (Order Cancel ): 4,990,972 I (NOII ): 4,025,192 F (Add Order MPID ): 1,875,350 P (Trade ): 1,779,727 L (Market Participant Position): 216,802 C (Order Executed with Price): 139,474 Q (Cross Trade ): 17,835 Y (Reg SHO Restriction ): 9,068 H (Trading Action ): 8,921 R (Stock Directory ): 8,916 S (System Event ): 6 J (LULD Auction Collar ): 5 K (IPO Quoting Period ): 2 V (MWCB Decline Level ): 1
5. Message Type Analysis
After parsing, we can analyze message distributions.
# Message type distribution — use lazy scan to count without loading all data
print("Parsed Message Types:")
print("-" * 50)
for msg_dir in sorted(MESSAGE_DIR.iterdir()):
if (
msg_dir.is_dir()
and len(msg_dir.name) == 1
and msg_dir.name.isupper()
and list(msg_dir.glob("*.parquet"))
):
count = pl.scan_parquet(msg_dir / "*.parquet").select(pl.len()).collect().item()
name = MESSAGE_SPECS.get(msg_dir.name, {}).get("name", "Unknown")
print(f" {msg_dir.name} ({name:25}): {count:>12,} messages")Output
Parsed Message Types: -------------------------------------------------- A (Add Order ): 184,735,355 messages C (Order Executed with Price): 139,474 messages D (Order Delete ): 180,285,101 messages E (Order Executed ): 8,415,610 messages F (Add Order MPID ): 1,875,350 messages H (Trading Action ): 8,921 messages I (NOII ): 4,025,192 messages J (LULD Auction Collar ): 5 messages K (IPO Quoting Period ): 2 messages L (Market Participant Position): 216,802 messages P (Trade ): 1,779,727 messages Q (Cross Trade ): 17,835 messages R (Stock Directory ): 8,916 messages S (System Event ): 5 messages U (Order Replace ): 36,777,372 messages V (MWCB Decline Level ): 1 messages X (Order Cancel ): 4,990,972 messages Y (Reg SHO Restriction ): 9,068 messages
# Schema compatibility check — verify we can read each message type
print("Schema Compatibility Check:")
print("-" * 50)
for msg_dir in sorted(MESSAGE_DIR.iterdir()):
if (
msg_dir.is_dir()
and len(msg_dir.name) == 1
and msg_dir.name.isupper()
and list(msg_dir.glob("*.parquet"))
):
try:
sample = pl.scan_parquet(msg_dir / "*.parquet").head(5).collect()
name = MESSAGE_SPECS.get(msg_dir.name, {}).get("name", "Unknown")
print(f" [OK] {msg_dir.name} ({name:25}): cols={list(sample.columns)[:4]}...")
except Exception as e:
print(f" [FAIL] {msg_dir.name}: {e}")Output
Schema Compatibility Check: -------------------------------------------------- [OK] A (Add Order ): cols=['stock_locate', 'tracking_number', 'timestamp', 'order_reference_number']... [OK] C (Order Executed with Price): cols=['stock_locate', 'tracking_number', 'timestamp', 'order_reference_number']... [OK] D (Order Delete ): cols=['stock_locate', 'tracking_number', 'timestamp', 'order_reference_number']... [OK] E (Order Executed ): cols=['stock_locate', 'tracking_number', 'timestamp', 'order_reference_number']... [OK] F (Add Order MPID ): cols=['stock_locate', 'tracking_number', 'timestamp', 'order_reference_number']... [OK] H (Trading Action ): cols=['stock_locate', 'tracking_number', 'timestamp', 'stock']... [OK] I (NOII ): cols=['stock_locate', 'tracking_number', 'timestamp', 'paired_shares']... [OK] J (LULD Auction Collar ): cols=['stock_locate', 'tracking_number', 'timestamp', 'stock']... [OK] K (IPO Quoting Period ): cols=['stock_locate', 'tracking_number', 'timestamp', 'stock']... [OK] L (Market Participant Position): cols=['stock_locate', 'tracking_number', 'timestamp', 'mpid']... [OK] P (Trade ): cols=['stock_locate', 'tracking_number', 'timestamp', 'order_reference_number']... [OK] Q (Cross Trade ): cols=['stock_locate', 'tracking_number', 'timestamp', 'shares']... [OK] R (Stock Directory ): cols=['stock_locate', 'tracking_number', 'timestamp', 'stock']... [OK] S (System Event ): cols=['stock_locate', 'tracking_number', 'timestamp', 'event_code']... [OK] U (Order Replace ): cols=['stock_locate', 'tracking_number', 'timestamp', 'original_order_reference_number']... [OK] V (MWCB Decline Level ): cols=['stock_locate', 'tracking_number', 'timestamp', 'level_1']... [OK] X (Order Cancel ): cols=['stock_locate', 'tracking_number', 'timestamp', 'order_reference_number']... [OK] Y (Reg SHO Restriction ): cols=['stock_locate', 'tracking_number', 'timestamp', 'stock']...
6. Production Parsing with Rust
The Python parser above is educational but slow for full-day files. For production use, we provide a Rust parser that is an order of magnitude faster.
Repository: github.com/ml4t/itch-parser
Performance Characteristics
| Aspect | Python | Rust |
|---|---|---|
| Speed | Baseline | 10-20× faster |
| Memory | High (buffers in RAM) | Low (streaming) |
| Use case | Learning, debugging | Production pipelines |
Actual speedups depend on disk I/O and CPU. The Rust parser uses memory-mapped I/O and zero-copy parsing, which provides substantial gains on modern hardware.
Installation
# Clone the repository
git clone https://github.com/ml4t/itch-parser.git
cd itch-parser
# Build release binary
cargo build --releaseUsage
# Parse ITCH file (works with .gz or uncompressed)
./target/release/itch_parser <input_file> <output_dir> <MMDDYYYY>
# Example
./target/release/itch_parser data/01302020.NASDAQ_ITCH50.gz ./messages 01302020Output is identical Parquet files partitioned by message type, compatible with the Python code in this notebook and downstream analysis.
When to Use Which
| Use Case | Recommendation |
|---|---|
| Learning the protocol | Python (this notebook) |
| Debugging parse issues | Python |
| Processing a single day | Either |
| Multi-day backtesting | Rust |
| Production pipeline | Rust |
Key Takeaways
- ITCH Protocol: Binary message-by-order format with nanosecond precision
- Message Types: A/F (add), E/C (execute), X (cancel), D (delete), U (replace)
- Price Format: Integers with 4 implied decimals (150.0000 → 1500000)
- Parser Choice: Python for learning, Rust for production (20× faster)
Next Steps
- Order Book Reconstruction:
02_itch_lob_reconstruction - Trading Activity Overview:
05_itch_trading_activity(includes E/C enrichment)
Reference
Bouchaud, J.-P., Bonart, J., Donier, J., & Gould, M. (2018). Trades, Quotes and Prices: Financial Markets Under the Microscope. Cambridge University Press. https://doi.org/10.1017/9781009028943
