//! Market Data Event Processing Latency Benchmarks //! //! Measures ultra-low latency market data processing: //! - Event parsing: <1μs target //! - Event processing: <10μs target //! - Order book update: <5μs target //! - Feature extraction: <20μs target //! - Full pipeline: <50μs target //! //! Critical for HFT signal generation and strategy execution use criterion::{black_box, criterion_group, criterion_main, Criterion, Throughput, BenchmarkId}; use hdrhistogram::Histogram; use std::time::{Duration, Instant}; use rust_decimal::Decimal; use std::collections::VecDeque; /// Latency metrics struct LatencyMetrics { histogram: Histogram, samples: Vec, } impl LatencyMetrics { fn new() -> Self { Self { histogram: Histogram::::new(5).unwrap(), samples: Vec::new(), } } fn record_nanos(&mut self, nanos: u64) { self.histogram.record(nanos / 1000).ok(); self.samples.push(nanos); } fn report(&self, label: &str, target_us: f64) { let p50 = self.histogram.value_at_percentile(50.0) as f64; let p95 = self.histogram.value_at_percentile(95.0) as f64; let p99 = self.histogram.value_at_percentile(99.0) as f64; println!("\n=== {} ===", label); println!("P50: {:.3}μs", p50); println!("P95: {:.3}μs", p95); println!("P99: {:.3}μs", p99); if p99 < target_us { println!("✅ P99 {:.3}μs < {:.1}μs", p99, target_us); } else { println!("❌ P99 {:.3}μs >= {:.1}μs", p99, target_us); } } } /// Market data event types #[derive(Clone)] enum MarketEvent { Trade { symbol: String, price: Decimal, quantity: Decimal, timestamp_ns: u64, }, Quote { symbol: String, bid: Decimal, ask: Decimal, bid_size: Decimal, ask_size: Decimal, timestamp_ns: u64, }, Level2 { symbol: String, side: Side, price: Decimal, quantity: Decimal, timestamp_ns: u64, }, } #[derive(Clone)] enum Side { Bid, Ask, } /// Order book level #[derive(Clone)] struct Level { price: Decimal, quantity: Decimal, } /// High-performance order book struct OrderBook { bids: VecDeque, asks: VecDeque, last_update_ns: u64, } impl OrderBook { fn new() -> Self { let mut bids = VecDeque::new(); let mut asks = VecDeque::new(); // Pre-populate with 10 levels for i in 0..10 { bids.push_back(Level { price: Decimal::new(65000 - i * 10, 0), quantity: Decimal::new(1, 1), }); asks.push_back(Level { price: Decimal::new(65100 + i * 10, 0), quantity: Decimal::new(1, 1), }); } Self { bids, asks, last_update_ns: 0, } } fn update_level(&mut self, side: Side, price: Decimal, quantity: Decimal, timestamp_ns: u64) { let levels = match side { Side::Bid => &mut self.bids, Side::Ask => &mut self.asks, }; // Find and update level if let Some(level) = levels.iter_mut().find(|l| l.price == price) { if quantity == Decimal::ZERO { // Remove level levels.retain(|l| l.price != price); } else { level.quantity = quantity; } } else if quantity > Decimal::ZERO { // Add new level levels.push_back(Level { price, quantity }); } self.last_update_ns = timestamp_ns; } fn get_mid_price(&self) -> Option { let bid = self.bids.front()?.price; let ask = self.asks.front()?.price; Some((bid + ask) / Decimal::TWO) } fn get_spread(&self) -> Option { let bid = self.bids.front()?.price; let ask = self.asks.front()?.price; Some(ask - bid) } } /// Feature extractor for ML struct FeatureExtractor { price_history: VecDeque, volume_history: VecDeque, } impl FeatureExtractor { fn new() -> Self { Self { price_history: VecDeque::with_capacity(100), volume_history: VecDeque::with_capacity(100), } } fn extract_features(&mut self, price: Decimal, volume: Decimal) -> Vec { // Update history self.price_history.push_back(price); self.volume_history.push_back(volume); if self.price_history.len() > 100 { self.price_history.pop_front(); self.volume_history.pop_front(); } // Calculate features let mut features = Vec::with_capacity(5); // Price momentum if self.price_history.len() >= 2 { let current = self.price_history.back().unwrap(); let prev = self.price_history.front().unwrap(); features.push(((current - prev) / prev).to_string().parse::().unwrap_or(0.0)); } // Volume ratio if self.volume_history.len() >= 10 { let recent_vol: Decimal = self.volume_history.iter().rev().take(10).sum(); let avg_vol: Decimal = self.volume_history.iter().sum::() / Decimal::new(self.volume_history.len() as i64, 0); features.push((recent_vol / avg_vol / Decimal::new(10, 0)).to_string().parse::().unwrap_or(0.0)); } // Volatility (simplified) if self.price_history.len() >= 20 { let prices: Vec<_> = self.price_history.iter().collect(); let mean = prices.iter().map(|&&p| p).sum::() / Decimal::new(prices.len() as i64, 0); let variance: Decimal = prices.iter() .map(|&&p| (p - mean) * (p - mean)) .sum::() / Decimal::new(prices.len() as i64, 0); // rust_decimal doesn't have sqrt, convert to f64 first let variance_f64 = variance.to_string().parse::().unwrap_or(0.0); let volatility = variance_f64.sqrt(); features.push(volatility); } features } } // // ==================== BENCHMARK 1: Event Parsing (<1μs) ==================== // fn bench_event_parsing(c: &mut Criterion) { c.bench_function("market_event_parsing", |b| { b.iter_custom(|iters| { let mut metrics = LatencyMetrics::new(); for i in 0..iters { let raw_data = ( "BTC-USD".to_string(), 65000 + (i % 100) as i64, 100 + (i % 10) as i64, 1234567890000u64 + i, ); let start = Instant::now(); let event = MarketEvent::Trade { symbol: raw_data.0, price: Decimal::new(raw_data.1, 0), quantity: Decimal::new(raw_data.2, 2), timestamp_ns: raw_data.3, }; black_box(event); metrics.record_nanos(start.elapsed().as_nanos() as u64); } metrics.report("Market Event Parsing", 1.0); Duration::from_nanos((metrics.samples.iter().sum::() / iters.max(1)) as u64) }); }); } // // ==================== BENCHMARK 2: Order Book Update (<5μs) ==================== // fn bench_orderbook_update(c: &mut Criterion) { c.bench_function("orderbook_level_update", |b| { b.iter_custom(|iters| { let mut metrics = LatencyMetrics::new(); let mut ob = OrderBook::new(); for i in 0..iters { let price = Decimal::new(65000 + (i % 100) as i64, 0); let quantity = Decimal::new(1 + (i % 10) as i64, 1); let side = if i % 2 == 0 { Side::Bid } else { Side::Ask }; let start = Instant::now(); ob.update_level(side, price, quantity, i); metrics.record_nanos(start.elapsed().as_nanos() as u64); } metrics.report("Order Book Level Update", 5.0); Duration::from_nanos((metrics.samples.iter().sum::() / iters.max(1)) as u64) }); }); } // // ==================== BENCHMARK 3: Mid-Price Calculation (<1μs) ==================== // fn bench_mid_price_calc(c: &mut Criterion) { let ob = OrderBook::new(); c.bench_function("mid_price_calculation", |b| { b.iter_custom(|iters| { let mut metrics = LatencyMetrics::new(); for _ in 0..iters { let start = Instant::now(); black_box(ob.get_mid_price()); metrics.record_nanos(start.elapsed().as_nanos() as u64); } metrics.report("Mid-Price Calculation", 1.0); Duration::from_nanos((metrics.samples.iter().sum::() / iters.max(1)) as u64) }); }); } // // ==================== BENCHMARK 4: Feature Extraction (<20μs) ==================== // fn bench_feature_extraction(c: &mut Criterion) { c.bench_function("feature_extraction", |b| { b.iter_custom(|iters| { let mut metrics = LatencyMetrics::new(); let mut extractor = FeatureExtractor::new(); for i in 0..iters { let price = Decimal::new(65000 + (i % 100) as i64, 0); let volume = Decimal::new(100 + (i % 50) as i64, 2); let start = Instant::now(); black_box(extractor.extract_features(price, volume)); metrics.record_nanos(start.elapsed().as_nanos() as u64); } metrics.report("Feature Extraction", 20.0); Duration::from_nanos((metrics.samples.iter().sum::() / iters.max(1)) as u64) }); }); } // // ==================== BENCHMARK 5: Full Pipeline (<50μs) ==================== // fn bench_full_pipeline(c: &mut Criterion) { c.bench_function("full_market_data_pipeline", |b| { b.iter_custom(|iters| { let mut metrics = LatencyMetrics::new(); let mut ob = OrderBook::new(); let mut extractor = FeatureExtractor::new(); for i in 0..iters { let start = Instant::now(); // Step 1: Parse event let price = Decimal::new(65000 + (i % 100) as i64, 0); let quantity = Decimal::new(1 + (i % 10) as i64, 1); // Step 2: Update order book ob.update_level(Side::Bid, price, quantity, i); // Step 3: Calculate mid-price let mid_price = ob.get_mid_price().unwrap_or(Decimal::ZERO); // Step 4: Extract features let features = extractor.extract_features(mid_price, quantity); black_box(features); metrics.record_nanos(start.elapsed().as_nanos() as u64); } metrics.report("Full Market Data Pipeline", 50.0); Duration::from_nanos((metrics.samples.iter().sum::() / iters.max(1)) as u64) }); }); } // // ==================== BENCHMARK 6: Throughput Test ==================== // fn bench_event_throughput(c: &mut Criterion) { let mut group = c.benchmark_group("market_data_throughput"); for events_per_sec in &[1000, 10000, 100000] { group.throughput(Throughput::Elements(*events_per_sec as u64)); group.bench_with_input( BenchmarkId::from_parameter(events_per_sec), events_per_sec, |b, &n| { b.iter_custom(|_iters| { let mut ob = OrderBook::new(); let start = Instant::now(); for i in 0..n { let price = Decimal::new(65000 + (i % 100) as i64, 0); let quantity = Decimal::new(1, 1); ob.update_level(Side::Bid, price, quantity, i as u64); } start.elapsed() }); }, ); } group.finish(); } criterion_group!( market_data_benches, bench_event_parsing, bench_orderbook_update, bench_mid_price_calc, bench_feature_extraction, bench_full_pipeline, bench_event_throughput, ); criterion_main!(market_data_benches);