itch5
A fast, zero-copy parser for the Nasdaq TotalView-ITCH 5.0 binary protocol.
ITCH 5.0 is Nasdaq's real-time market data feed. It carries every order book event — adds, cancels, executes, replaces — plus market-wide events, trading halts, and cross-trade notifications, all encoded as compact fixed-length binary messages framed with a 2-byte big-endian length prefix.
Usage
Add the crate to your Cargo.toml:
[]
= "0.1"
# Optional: enable chrono for `messages::Timestamp::to_naive_time` method
# itch5 = {version = "0.1", features = ["chrono"]}
Implement [MessageHandler] for the message types you care about, then feed bytes to [Parser]:
use ControlFlow;
use ;
Return ControlFlow::Break(()) from any handler method to stop parsing early.
Core API
Parser
new
Iterates over every framed message in buf, dispatching each one to the corresponding method on handler. The buffer is borrowed for the lifetime of the parser; no heap allocation occurs.
parse_one
parse_one
Parses a single length-prefixed message. Returns (body, remainder) where body starts with the one-byte message-type tag. Useful when you need the raw bytes rather than typed structs — for example, to forward messages to another consumer.
MessageHandler
A visitor trait with one method per message type. Every method has a default no-op implementation, so you only override what you need:
ParseError
Message Types
All 23 ITCH 5.0 message types are covered:
| Tag | Struct | Description |
|---|---|---|
S |
SystemEventMessage |
Market/session lifecycle events |
R |
StockDirectory |
Daily instrument reference data |
H |
StockTradingAction |
Trading halt and resume |
Y |
RegSHORestriction |
Regulation SHO short-sale restriction |
L |
MarketParticipantPosition |
Market maker status and mode |
V |
MWCBDeclineLevelMessage |
Market-wide circuit breaker levels |
W |
MWCBStatusMessage |
Market-wide circuit breaker status |
K |
QuotingPeriodUpdate |
IPO quotation release |
J |
LULDAuctionCollar |
Limit Up/Limit Down auction collar |
h |
OperationalHalt |
Exchange-specific operational halt |
A |
AddOrderNoMPIDAttribution |
New order (anonymous) |
F |
AddOrderWithMPIDAttribution |
New order (attributed) |
E |
OrderExecutedMessage |
Order execution |
C |
OrderExecutedWithPriceMessage |
Order execution at non-display price |
X |
OrderCancelMessage |
Partial order cancellation |
D |
OrderDeleteMessage |
Full order removal |
U |
OrderReplaceMessage |
Cancel-and-replace |
P |
TradeMessage |
Non-displayed trade |
Q |
CrossTradeMessage |
Opening/closing/halt cross execution |
B |
BrokenTradeMessage |
Trade break |
I |
NetOrderImbalanceIndicatorMessage |
Cross imbalance data |
N |
RetailPriceImprovementIndicator |
Retail interest flags |
O |
DirectListingwithCapitalRaisePriceDiscoveryMessage |
Direct listing price discovery |
Price Types
Prices in the ITCH protocol are fixed-point integers. The crate provides two newtype wrappers:
| Type | Bytes | Precision |
|---|---|---|
Price4 |
4 | 4 decimal places |
Price8 |
8 | 8 decimal places |
Symbol
Symbol is an 8-byte ASCII stock ticker, space-padded on the right per the ITCH specification.
let sym: Symbol = msg.stock;
println!; // e.g. "AAPL " trimmed to "AAPL"
// Parse from a string
let sym: Symbol = "MSFT".parse.unwrap;
// Compact hash/unhash for use as a map key
let key: u64 = sym.hash;
let restored: Symbol = from_hash;
Timestamp
Timestamp is a u48 representing nanoseconds since midnight.
The crate provides a Timestamp newtype wrapper.
let ts: &Timestamp = ref_from_bytes.unwrap;
let dur: Duration = ts.to_dur;
// requires feature `chrono` to be enabled.
let naivetime: NaiveTime = ts.to_naive_time;
Wire Format
Each ITCH message is framed as:
[2 bytes: big-endian message length][message body]
The message body begins with a single ASCII tag byte that identifies the message type, followed by fixed-width fields in network byte order (big-endian). All message structs are #[repr(C, packed)] and implement zerocopy::FromBytes, so parsing is a bounds-checked pointer cast with no copying.
Examples
Count every trade in an ITCH binary file:
cargo run --example parse_file -- /path/to/feed.itch
The source is in examples/parse_file.rs. It memory-maps the file and counts TradeMessage events.
Benchmarks
# Default: uses the 1M-message sample at ../data/itch_1000_000
# Or override the sample location:
ITCH5_BENCH_FILE=/path/to/feed.itch
The suite (benches/itch_bench.rs) is structured around the questions an HFT consumer would actually ask:
itch5/framing/parse_one_only_100k— pure wire-format framing cost. Walks a synthetic stream with [parse_one] and only inspects the tag byte. This is the floor: length-prefix decode and slice splits, no dispatch, nocast.itch5/per_msg_type/{add_no_mpid,add_with_mpid,order_executed, order_cancel,order_delete,order_replace,trade}— fullparse_streamover a homogeneous synthetic stream of each hot message type, dispatched into a no-op handler. Surfaces dispatch + cast cost per type so a regression in one is isolatable.itch5/full_file/{noop_handler,counting_handler}— the recorded sample replayed end-to-end.noopreports the parser's intrinsic ceiling;countingreports what an order-book consumer pays after touching every accessor on the hot message types. Reported in messages/sec and bytes/sec.itch5/field_accessors/{price4_into_i64,timestamp_to_u64, symbol_to_u64}— confirms each zero-copy big-endian accessor folds to a single load (or load + bswap) at release optimization.
If the sample file is unavailable, the data-driven group prints a notice and skips; the synthetic and accessor groups still run.
License
Unlicense — public domain.