//! Performance benchmarks for market data replay engine //! //! Measures throughput and latency characteristics of the backtesting system //! under various data loads and configurations. // Explicit alias to avoid core crate shadowing std::core for async_trait extern crate std as stdlib; use async_trait::async_trait; use chrono::{TimeDelta, Utc}; use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use std::io::Write; use tempfile::NamedTempFile; use tokio::runtime::Runtime; use rust_decimal::Decimal; use rust_decimal_macros::dec; use backtesting::{ replay_engine::{DataFormat, DataSource, MarketReplay, ReplayConfig, SourceType}, BacktestConfig, BacktestEngine, }; use common::{Order, Position}; use trading_engine::types::events::MarketEvent; /// Benchmark market data replay throughput fn bench_replay_throughput(c: &mut Criterion) { let rt = Runtime::new().unwrap(); let mut group = c.benchmark_group("replay_throughput"); for event_count in &[1_000, 10_000, 100_000] { group.throughput(Throughput::Elements(*event_count)); group.bench_with_input( BenchmarkId::new("events", event_count), event_count, |b, &event_count| { b.iter(|| { rt.block_on(async { let data_file = create_benchmark_data(event_count as usize).await.unwrap(); let config = ReplayConfig { data_sources: vec![DataSource { source_type: SourceType::CsvFile, path: data_file.clone(), format: DataFormat::OhlcvTicks, priority: 1, }], speed_multiplier: 0.0, // Maximum speed tick_by_tick: true, buffer_size: 50000, ..Default::default() }; let replay = MarketReplay::new(config); let mut receiver = replay.take_receiver().await.unwrap(); // Start replay let replay_handle = tokio::spawn(async move { replay.start_replay().await }); // Count events let mut count = 0; let start = std::time::Instant::now(); while let Some(_event) = receiver.recv().await { count += 1; black_box(count); } replay_handle.await.unwrap().unwrap(); // Clean up std::fs::remove_file(&data_file).ok(); let duration = start.elapsed(); black_box((count, duration)); }) }); }, ); } group.finish(); } /// Benchmark event processing latency fn bench_event_latency(c: &mut Criterion) { let rt = Runtime::new().unwrap(); c.bench_function("event_latency", |b| { b.iter(|| { rt.block_on(async { let data_file = create_benchmark_data(1000).await.unwrap(); let config = ReplayConfig { data_sources: vec![DataSource { source_type: SourceType::CsvFile, path: data_file.clone(), format: DataFormat::OhlcvTicks, priority: 1, }], speed_multiplier: 0.0, tick_by_tick: true, buffer_size: 10000, ..Default::default() }; let replay = MarketReplay::new(config); let mut receiver = replay.take_receiver().await.unwrap(); let replay_handle = tokio::spawn(async move { replay.start_replay().await }); // Measure latency of first 100 events let mut latencies = Vec::new(); for _ in 0..100 { let start = std::time::Instant::now(); if let Some(_event) = receiver.recv().await { let latency = start.elapsed(); latencies.push(latency); } } // Drain remaining events while receiver.recv().await.is_some() {} replay_handle.await.unwrap().unwrap(); std::fs::remove_file(&data_file).ok(); black_box(latencies); }) }); }); } /// Benchmark memory usage under load fn bench_memory_usage(c: &mut Criterion) { let rt = Runtime::new().unwrap(); let mut group = c.benchmark_group("memory_usage"); for buffer_size in &[1_000, 10_000, 50_000] { group.bench_with_input( BenchmarkId::new("buffer_size", buffer_size), buffer_size, |b, &buffer_size| { b.iter(|| { rt.block_on(async { let data_file = create_benchmark_data(50000).await.unwrap(); let config = ReplayConfig { data_sources: vec![DataSource { source_type: SourceType::CsvFile, path: data_file.clone(), format: DataFormat::OhlcvTicks, priority: 1, }], speed_multiplier: 0.0, tick_by_tick: true, buffer_size, ..Default::default() }; let replay = MarketReplay::new(config); let mut receiver = replay.take_receiver().await.unwrap(); let replay_handle = tokio::spawn(async move { replay.start_replay().await }); // Process all events let mut count = 0; while let Some(_event) = receiver.recv().await { count += 1; // Simulate some processing work if count % 1000 == 0 { tokio::task::yield_now().await; } } replay_handle.await.unwrap().unwrap(); std::fs::remove_file(&data_file).ok(); black_box(count); }) }); }, ); } group.finish(); } /// Benchmark complete backtesting engine fn bench_full_backtest(c: &mut Criterion) { let rt = Runtime::new().unwrap(); c.bench_function("full_backtest", |b| { b.iter(|| { rt.block_on(async { let data_file = create_benchmark_data(10000).await.unwrap(); let config = BacktestConfig { initial_capital: dec!(100000), replay_config: ReplayConfig { data_sources: vec![DataSource { source_type: SourceType::CsvFile, path: data_file.clone(), format: DataFormat::OhlcvTicks, priority: 1, }], speed_multiplier: 0.0, tick_by_tick: true, buffer_size: 20000, ..Default::default() }, enable_logging: false, // Disable logging for benchmarks snapshot_interval: 3600, max_memory_usage: 256 * 1024 * 1024, ..Default::default() }; let mut engine = BacktestEngine::new(config).await.unwrap(); // Use a simple buy-and-hold strategy for benchmarking let strategy = Box::new(BenchmarkStrategy::new()); engine.set_strategy(strategy).await.unwrap(); let result = engine.run().await.unwrap(); std::fs::remove_file(&data_file).ok(); black_box(result); }) }); }); } /// Benchmark strategy execution overhead fn bench_strategy_execution(c: &mut Criterion) { let rt = Runtime::new().unwrap(); let mut group = c.benchmark_group("strategy_execution"); for complexity in &["simple", "medium", "complex"] { group.bench_with_input( BenchmarkId::new("strategy", complexity), complexity, |b, &complexity| { b.iter(|| { rt.block_on(async { let data_file = create_benchmark_data(5000).await.unwrap(); let config = BacktestConfig { initial_capital: dec!(100000), replay_config: ReplayConfig { data_sources: vec![DataSource { source_type: SourceType::CsvFile, path: data_file.clone(), format: DataFormat::OhlcvTicks, priority: 1, }], speed_multiplier: 0.0, tick_by_tick: true, buffer_size: 10000, ..Default::default() }, enable_logging: false, snapshot_interval: 3600, max_memory_usage: 128 * 1024 * 1024, ..Default::default() }; let mut engine = BacktestEngine::new(config).await.unwrap(); let strategy: Box = match complexity { "simple" => Box::new(SimpleStrategy::new()), "medium" => Box::new(MediumStrategy::new()), "complex" => Box::new(ComplexStrategy::new()), _ => Box::new(BenchmarkStrategy::new()), }; engine.set_strategy(strategy).await.unwrap(); let result = engine.run().await.unwrap(); std::fs::remove_file(&data_file).ok(); black_box(result); }) }); }, ); } group.finish(); } /// Create benchmark data file async fn create_benchmark_data(event_count: usize) -> Result> { let mut temp_file = NamedTempFile::new()?; writeln!(temp_file, "timestamp,symbol,open,high,low,close,volume")?; let base_time = Utc::now() - TimeDelta::days(1); let mut price = dec!(50000.0); for i in 0..event_count { let timestamp = base_time + TimeDelta::seconds(i as i64); // Simple price movement price += Decimal::from_f64_retain((i as f64 * 0.01).sin() * 10.0).unwrap_or_default(); let open = price; let high = price + dec!(50); let low = price - dec!(50); let close = price + Decimal::from_f64_retain((i as f64 * 0.1).cos() * 25.0).unwrap_or_default(); let volume = dec!(1000); writeln!( temp_file, "{},BTCUSD,{},{},{},{},{}", timestamp.timestamp_millis(), open, high, low, close, volume )?; price = close; } let path = temp_file.path().to_string_lossy().to_string(); temp_file.keep()?; Ok(path) } // Benchmark strategies with different complexity levels /// Simple strategy for benchmarking struct BenchmarkStrategy; impl BenchmarkStrategy { fn new() -> Self { Self } } #[async_trait(?Send)] impl backtesting::Strategy for BenchmarkStrategy { fn name(&self) -> &str { "benchmark_strategy" } async fn initialize( &mut self, _initial_capital: Decimal, _config: backtesting::StrategyConfig, ) -> anyhow::Result<()> { Ok(()) } async fn on_market_event( &mut self, _event: &MarketEvent, _context: &backtesting::StrategyContext, ) -> anyhow::Result> { Ok(vec![]) } async fn on_order_update( &mut self, _order: &Order, _context: &backtesting::StrategyContext, ) -> anyhow::Result<()> { Ok(()) } async fn on_position_update( &mut self, _position: &Position, _context: &backtesting::StrategyContext, ) -> anyhow::Result<()> { Ok(()) } async fn finalize( &mut self, _context: &backtesting::StrategyContext, ) -> anyhow::Result { Ok(backtesting::StrategyResult { strategy_name: "benchmark_strategy".to_string(), total_return: dec!(0.05), annualized_return: dec!(0.05), max_drawdown: dec!(0.02), sharpe_ratio: dec!(1.0), total_trades: 10, win_rate: dec!(0.6), avg_trade_return: dec!(0.005), final_value: dec!(105000), trades: vec![], performance_timeline: vec![], }) } async fn get_state(&self) -> anyhow::Result { Ok(serde_json::json!({"name": "benchmark_strategy"})) } } /// Simple strategy with minimal computation struct SimpleStrategy; impl SimpleStrategy { fn new() -> Self { Self } } #[async_trait(?Send)] impl backtesting::Strategy for SimpleStrategy { fn name(&self) -> &str { "simple_strategy" } async fn initialize( &mut self, _initial_capital: Decimal, _config: backtesting::StrategyConfig, ) -> anyhow::Result<()> { Ok(()) } async fn on_market_event( &mut self, event: &MarketEvent, _context: &backtesting::StrategyContext, ) -> anyhow::Result> { // Simple logic: check if price changed if let MarketEvent::Trade { price, .. } = event { if price.to_decimal().unwrap_or_default() > dec!(50000) { // Some minimal computation let _ = price.to_decimal().unwrap_or_default() * dec!(1.01); } } Ok(vec![]) } async fn on_order_update( &mut self, _order: &Order, _context: &backtesting::StrategyContext, ) -> anyhow::Result<()> { Ok(()) } async fn on_position_update( &mut self, _position: &Position, _context: &backtesting::StrategyContext, ) -> anyhow::Result<()> { Ok(()) } async fn finalize( &mut self, _context: &backtesting::StrategyContext, ) -> anyhow::Result { Ok(backtesting::StrategyResult { strategy_name: "simple_strategy".to_string(), total_return: dec!(0.03), annualized_return: dec!(0.03), max_drawdown: dec!(0.01), sharpe_ratio: dec!(0.8), total_trades: 5, win_rate: dec!(0.6), avg_trade_return: dec!(0.006), final_value: dec!(103000), trades: vec![], performance_timeline: vec![], }) } async fn get_state(&self) -> anyhow::Result { Ok(serde_json::json!({"name": "simple_strategy"})) } } /// Medium complexity strategy struct MediumStrategy { price_history: std::collections::VecDeque, } impl MediumStrategy { fn new() -> Self { Self { price_history: std::collections::VecDeque::new(), } } } #[async_trait(?Send)] impl backtesting::Strategy for MediumStrategy { fn name(&self) -> &str { "medium_strategy" } async fn initialize( &mut self, _initial_capital: Decimal, _config: backtesting::StrategyConfig, ) -> anyhow::Result<()> { self.price_history.clear(); Ok(()) } async fn on_market_event( &mut self, event: &MarketEvent, _context: &backtesting::StrategyContext, ) -> anyhow::Result> { if let MarketEvent::Trade { price, .. } = event { self.price_history .push_back(price.to_decimal().unwrap_or_default()); if self.price_history.len() > 20 { self.price_history.pop_front(); } // Calculate simple moving average if self.price_history.len() >= 10 { let sum: Decimal = self.price_history.iter().rev().take(10).sum(); let _avg = sum / dec!(10); // Some medium computation } } Ok(vec![]) } async fn on_order_update( &mut self, _order: &Order, _context: &backtesting::StrategyContext, ) -> anyhow::Result<()> { Ok(()) } async fn on_position_update( &mut self, _position: &Position, _context: &backtesting::StrategyContext, ) -> anyhow::Result<()> { Ok(()) } async fn finalize( &mut self, _context: &backtesting::StrategyContext, ) -> anyhow::Result { Ok(backtesting::StrategyResult { strategy_name: "medium_strategy".to_string(), total_return: dec!(0.07), annualized_return: dec!(0.07), max_drawdown: dec!(0.03), sharpe_ratio: dec!(1.2), total_trades: 15, win_rate: dec!(0.65), avg_trade_return: dec!(0.0047), final_value: dec!(107000), trades: vec![], performance_timeline: vec![], }) } async fn get_state(&self) -> anyhow::Result { Ok(serde_json::json!({ "name": "medium_strategy", "price_history_length": self.price_history.len() })) } } /// Complex strategy with heavy computation struct ComplexStrategy { price_history: std::collections::VecDeque, indicators: std::collections::HashMap, } impl ComplexStrategy { fn new() -> Self { Self { price_history: std::collections::VecDeque::new(), indicators: std::collections::HashMap::new(), } } } #[async_trait(?Send)] impl backtesting::Strategy for ComplexStrategy { fn name(&self) -> &str { "complex_strategy" } async fn initialize( &mut self, _initial_capital: Decimal, _config: backtesting::StrategyConfig, ) -> anyhow::Result<()> { self.price_history.clear(); self.indicators.clear(); Ok(()) } async fn on_market_event( &mut self, event: &MarketEvent, _context: &backtesting::StrategyContext, ) -> anyhow::Result> { if let MarketEvent::Trade { price, .. } = event { self.price_history .push_back(price.to_decimal().unwrap_or_default()); if self.price_history.len() > 100 { self.price_history.pop_front(); } // Calculate multiple indicators (complex computation) if self.price_history.len() >= 20 { // SMA 20 let sma20: Decimal = self.price_history.iter().rev().take(20).sum::() / dec!(20); self.indicators.insert("sma20".to_string(), sma20); // SMA 50 if self.price_history.len() >= 50 { let sma50: Decimal = self.price_history.iter().rev().take(50).sum::() / dec!(50); self.indicators.insert("sma50".to_string(), sma50); } // Standard deviation calculation let prices: Vec = self.price_history.iter().rev().take(20).cloned().collect(); let mean = sma20; let variance: Decimal = prices .iter() .map(|p| (*p - mean) * (*p - mean)) .sum::() / dec!(20); self.indicators.insert("std_dev".to_string(), variance); } } Ok(vec![]) } async fn on_order_update( &mut self, _order: &Order, _context: &backtesting::StrategyContext, ) -> anyhow::Result<()> { Ok(()) } async fn on_position_update( &mut self, _position: &Position, _context: &backtesting::StrategyContext, ) -> anyhow::Result<()> { Ok(()) } async fn finalize( &mut self, _context: &backtesting::StrategyContext, ) -> anyhow::Result { Ok(backtesting::StrategyResult { strategy_name: "complex_strategy".to_string(), total_return: dec!(0.10), annualized_return: dec!(0.10), max_drawdown: dec!(0.04), sharpe_ratio: dec!(1.5), total_trades: 25, win_rate: dec!(0.70), avg_trade_return: dec!(0.004), final_value: dec!(110000), trades: vec![], performance_timeline: vec![], }) } async fn get_state(&self) -> anyhow::Result { Ok(serde_json::json!({ "name": "complex_strategy", "price_history_length": self.price_history.len(), "indicators": self.indicators })) } } criterion_group!( benches, bench_replay_throughput, bench_event_latency, bench_memory_usage, bench_full_backtest, bench_strategy_execution ); criterion_main!(benches);