Domain Deep-Dive
ExpertMarket Data Processing in C++
High-frequency market data processing in C++: multicast feed handling, order book reconstruction, feed arbitrage, and FPGA/kernel-bypass feeds.
Market data architecture
cpp
Exchange matching engine
│
│ UDP multicast (ITCH, PITCH, OPRA, XDP...)
▼
[Market data feed]
├── Primary feed (primary port)
└── Backup feed (redundant port)
│
│ kernel-bypass NIC (DPDK / Solarflare ef_vi / Mellanox VMA)
▼
[Feed handler process] ← pinned to isolated CPU core
├── Sequence gap detection
├── Multicast recovery (via TCP retransmit channel)
└── Order book reconstruction
│
│ shared memory (huge pages, lock-free ring)
▼
[Strategy process] ← alpha generationITCH 5.0 parser
NASDAQ ITCH is a binary protocol — the dominant US equities feed:
cpp
#pragma pack(push, 1)
// ITCH Add Order (message type 'A')
struct ItchAddOrder {
char msg_type; // 'A'
uint16_t stock_locate;
uint16_t tracking_num;
uint8_t timestamp[6]; // 48-bit nanoseconds since midnight
uint64_t order_ref;
char side; // 'B' or 'S'
uint32_t shares;
char stock[8]; // space-padded
uint32_t price; // in 1/10000 dollars (4 implied decimals)
};
struct ItchDeleteOrder {
char msg_type; // 'D'
uint16_t stock_locate;
uint16_t tracking_num;
uint8_t timestamp[6];
uint64_t order_ref;
};
struct ItchExecuteOrder {
char msg_type; // 'E'
uint16_t stock_locate;
uint16_t tracking_num;
uint8_t timestamp[6];
uint64_t order_ref;
uint32_t executed_shares;
uint64_t match_num;
};
#pragma pack(pop)
class ItchFeedHandler {
OrderBook& m_book;
public:
// Parse one message from a UDP packet payload
void OnMessage(const uint8_t* data, int len) {
char type = static_cast<char>(data[0]);
switch (type) {
case 'A': return OnAddOrder(reinterpret_cast<const ItchAddOrder*>(data));
case 'D': return OnDeleteOrder(reinterpret_cast<const ItchDeleteOrder*>(data));
case 'E': return OnExecuteOrder(reinterpret_cast<const ItchExecuteOrder*>(data));
case 'U': return OnReplaceOrder(data);
case 'P': return OnTrade(data);
case 'H': return OnHalt(data);
}
}
private:
void OnAddOrder(const ItchAddOrder* msg) {
// Byte-swap (ITCH is big-endian)
uint64_t ref = __builtin_bswap64(msg->order_ref);
uint32_t qty = __builtin_bswap32(msg->shares);
uint32_t price = __builtin_bswap32(msg->price);
char side = msg->side;
m_book.Add(ref, side == 'B', qty, price);
}
};Sequence gap detection and recovery
UDP multicast can drop packets. You MUST detect and recover gaps:
cpp
class SequencedFeedHandler {
uint64_t m_expected_seq = 1;
struct GapRequest {
uint64_t start, end;
std::chrono::steady_clock::time_point requested_at;
};
std::vector<GapRequest> m_pending_gaps;
public:
void OnPacket(uint64_t seq, const uint8_t* data, int len) {
if (seq < m_expected_seq) return; // duplicate, ignore
if (seq > m_expected_seq) {
// Gap detected!
RequestRetransmit(m_expected_seq, seq - 1);
m_pending_gaps.push_back({ m_expected_seq, seq - 1,
std::chrono::steady_clock::now() });
// Queue this packet for later processing
m_reorder_buffer[seq] = std::vector<uint8_t>(data, data + len);
return;
}
// In-order packet
ProcessMessage(data, len);
m_expected_seq = seq + 1;
// Drain reorder buffer
while (true) {
auto it = m_reorder_buffer.find(m_expected_seq);
if (it == m_reorder_buffer.end()) break;
ProcessMessage(it->second.data(), it->second.size());
m_reorder_buffer.erase(it);
++m_expected_seq;
}
}
void RequestRetransmit(uint64_t start, uint64_t end) {
// Connect to exchange retransmit server (TCP) and request range
// Most exchanges provide this as a separate TCP service
}
};Dual-feed arbitrage
Most exchanges send redundant feeds (A+B) for reliability. Merge them:
cpp
class DualFeedMerger {
uint64_t m_last_seq_a = 0;
uint64_t m_last_seq_b = 0;
// Set of sequence numbers already processed
// Using a sliding window bitmask instead of unordered_set
static constexpr int WINDOW = 64;
uint64_t m_seen_bitmask = 0;
uint64_t m_window_base = 0;
public:
// Returns true if this message is new (should be processed)
bool ShouldProcess(uint64_t seq) {
if (seq < m_window_base) return false; // too old
if (seq >= m_window_base + WINDOW) {
// Advance window
uint64_t advance = seq - (m_window_base + WINDOW) + 1;
m_seen_bitmask >>= advance;
m_window_base += advance;
}
uint64_t bit = seq - m_window_base;
if (m_seen_bitmask & (1ULL << bit)) return false; // already seen from other feed
m_seen_bitmask |= (1ULL << bit);
return true;
}
};Feed latency measurement
Know your feed latency — it drives all alpha decisions:
cpp
// Hardware timestamp from NIC (SOLARFLARE, MELLANOX)
// ef_vi and DPDK support hardware timestamping
struct MarketDataPacket {
uint64_t exchange_timestamp_ns; // from ITCH header (ns since midnight)
uint64_t recv_timestamp_ns; // NIC hardware timestamp
uint64_t process_timestamp_ns; // TSC when handler ran
};
void MeasureLatency(const MarketDataPacket& pkt) {
uint64_t wire_latency = pkt.recv_timestamp_ns - pkt.exchange_timestamp_ns;
uint64_t processing = pkt.process_timestamp_ns - pkt.recv_timestamp_ns;
// Track percentiles (no allocations — reservoir sampling or HDR histogram)
m_wire_latency_hist.record(wire_latency);
m_processing_hist.record(processing);
}Typical latencies (co-located in exchange data center):
| Path | Latency |
|---|---|
| Exchange → NIC (wire) | 1–5 µs |
| NIC DMA → process (kernel) | 5–20 µs |
| NIC DMA → process (kernel-bypass) | 0.5–2 µs |
| NIC DMA → process (FPGA smart NIC) | 100–500 ns |
Lock-free market data ring buffer
Share parsed market data between feed handler and strategy without locking:
cpp
// SPSC (single-producer, single-consumer) ring buffer
// Feed handler writes, strategy reads
template<typename T, int N>
class MarketDataRing {
static_assert((N & (N-1)) == 0, "N must be power of 2");
alignas(64) T m_data[N];
alignas(64) std::atomic<uint64_t> m_write_idx{0};
alignas(64) std::atomic<uint64_t> m_read_idx{0};
public:
// Called by feed handler (producer)
bool TryWrite(const T& item) {
uint64_t w = m_write_idx.load(std::memory_order_relaxed);
uint64_t r = m_read_idx.load(std::memory_order_acquire);
if (w - r >= N) return false; // full
m_data[w & (N-1)] = item;
m_write_idx.store(w + 1, std::memory_order_release);
return true;
}
// Called by strategy (consumer)
bool TryRead(T& out) {
uint64_t r = m_read_idx.load(std::memory_order_relaxed);
uint64_t w = m_write_idx.load(std::memory_order_acquire);
if (r == w) return false; // empty
out = m_data[r & (N-1)];
m_read_idx.store(r + 1, std::memory_order_release);
return true;
}
};Edit on GitHubUpdated 2026-05-24T00:00:00.000Z