#![allow(clippy::inconsistent_digit_grouping, dead_code, unused_variables)] //! Performance tests for DBN data loading //! //! These tests measure throughput, memory usage, and compare against targets //! to ensure the DBN loading infrastructure meets HFT requirements. use anyhow::Result; use backtesting_service::dbn_repository::DbnMarketDataRepository; use backtesting_service::repositories::MarketDataRepository; use rust_decimal::Decimal; use std::collections::HashMap; use std::time::{Duration, Instant}; /// Helper to create a DBN repository with ES.FUT test data async fn create_dbn_repository() -> Result { // Find workspace root to get absolute path to test file let current_dir = std::env::current_dir()?; let workspace_root = current_dir .ancestors() .find(|p| p.join("Cargo.toml").exists() && p.join("test_data").exists()) .ok_or_else(|| anyhow::anyhow!("Could not find workspace root"))?; let test_file = workspace_root.join("test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn"); if !test_file.exists() { anyhow::bail!("Test file not found: {}", test_file.display()); } let mut file_mapping = HashMap::new(); file_mapping.insert( "ES.FUT".to_string(), test_file.to_string_lossy().to_string(), ); DbnMarketDataRepository::new(file_mapping).await } #[tokio::test] async fn test_throughput() -> Result<()> { let repo = create_dbn_repository().await?; // Load multiple times to get average let iterations = 10; let mut durations = Vec::new(); for _ in 0..iterations { let start = Instant::now(); let symbols = vec!["ES.FUT".to_string()]; let data = repo .load_historical_data( &symbols, 1704153600_000_000_000i64, // 2024-01-02 00:00:00 1704240000_000_000_000i64, // 2024-01-03 00:00:00 ) .await?; let duration = start.elapsed(); durations.push(duration); assert!(!data.is_empty(), "Should load data"); } // Calculate statistics let avg_duration = durations.iter().sum::() / iterations as u32; let min_duration = durations.iter().min().unwrap(); let max_duration = durations.iter().max().unwrap(); let bars_loaded = 390; // ES.FUT has ~390 bars on 2024-01-02 let bars_per_sec = (bars_loaded as f64 / avg_duration.as_secs_f64()) as u64; println!("\n=== Throughput Analysis ({} iterations) ===", iterations); println!(" Min: {:?}", min_duration); println!(" Avg: {:?}", avg_duration); println!(" Max: {:?}", max_duration); println!(" Throughput: {} bars/sec", bars_per_sec); // Targets: // - Average: <10ms // - Throughput: >10,000 bars/sec assert!( avg_duration.as_millis() < 10, "Average load time should be <10ms, got {:?}", avg_duration ); assert!( bars_per_sec > 10_000, "Throughput should be >10K bars/sec, got {}", bars_per_sec ); Ok(()) } #[tokio::test] async fn test_load_time_consistency() -> Result<()> { let repo = create_dbn_repository().await?; let iterations = 20; let mut durations = Vec::new(); // Warm up for _ in 0..5 { let symbols = vec!["ES.FUT".to_string()]; let _ = repo .load_historical_data( &symbols, 1704153600_000_000_000i64, 1704240000_000_000_000i64, ) .await?; } // Measure for _ in 0..iterations { let start = Instant::now(); let symbols = vec!["ES.FUT".to_string()]; let _ = repo .load_historical_data( &symbols, 1704153600_000_000_000i64, 1704240000_000_000_000i64, ) .await?; durations.push(start.elapsed().as_micros()); } // Calculate statistics let avg: u128 = durations.iter().sum::() / iterations; let variance: u128 = durations .iter() .map(|d| { let diff = (*d as i128) - (avg as i128); (diff * diff) as u128 }) .sum::() / iterations; let stddev = (variance as f64).sqrt(); let cv = stddev / (avg as f64); // Coefficient of variation println!( "\n=== Load Time Consistency ({} iterations) ===", iterations ); println!(" Avg: {} μs", avg); println!(" StdDev: {:.2} μs", stddev); println!(" CV: {:.2}% (lower is better)", cv * 100.0); // Target: CV < 20% (consistent performance) assert!( cv < 0.20, "Coefficient of variation should be <20%, got {:.2}%", cv * 100.0 ); Ok(()) } #[tokio::test] async fn test_partial_day_load() -> Result<()> { let repo = create_dbn_repository().await?; // Test loading different time windows let test_cases = vec![ ( "First hour", 1704203400_000_000_000i64, // 2024-01-02 09:30:00 ET 1704207000_000_000_000i64, // 2024-01-02 10:30:00 ET ), ( "Morning session", 1704203400_000_000_000i64, // 2024-01-02 09:30:00 ET 1704218400_000_000_000i64, // 2024-01-02 14:00:00 ET ), ( "Full day", 1704153600_000_000_000i64, // 2024-01-02 00:00:00 1704240000_000_000_000i64, // 2024-01-03 00:00:00 ), ]; println!("\n=== Partial Day Load Performance ==="); for (name, start_time, end_time) in test_cases { let start = Instant::now(); let symbols = vec!["ES.FUT".to_string()]; let data = repo .load_historical_data(&symbols, start_time, end_time) .await?; let duration = start.elapsed(); let bars_per_sec = (data.len() as f64 / duration.as_secs_f64()) as u64; println!( " {:<16} | Bars: {:>4} | Time: {:>7.2}ms | Throughput: {:>8} bars/s", name, data.len(), duration.as_secs_f64() * 1000.0, bars_per_sec ); // All loads should be fast assert!( duration.as_millis() < 10, "{} load took too long: {:?}", name, duration ); } Ok(()) } #[tokio::test] async fn test_cold_start_vs_warm() -> Result<()> { // Cold start let cold_start = Instant::now(); let repo = create_dbn_repository().await?; let cold_init_duration = cold_start.elapsed(); let symbols = vec!["ES.FUT".to_string()]; let cold_load_start = Instant::now(); let data = repo .load_historical_data( &symbols, 1704153600_000_000_000i64, 1704240000_000_000_000i64, ) .await?; let cold_load_duration = cold_load_start.elapsed(); let bars_loaded = data.len(); // Warm loads (reuse repository) let mut warm_durations = Vec::new(); for _ in 0..5 { let warm_start = Instant::now(); let _ = repo .load_historical_data( &symbols, 1704153600_000_000_000i64, 1704240000_000_000_000i64, ) .await?; warm_durations.push(warm_start.elapsed()); } let avg_warm = warm_durations.iter().sum::() / warm_durations.len() as u32; println!("\n=== Cold Start vs Warm Performance ==="); println!(" Repository init: {:?}", cold_init_duration); println!(" Cold load: {:?}", cold_load_duration); println!(" Warm load (avg): {:?}", avg_warm); println!( " Warm speedup: {:.2}x", cold_load_duration.as_secs_f64() / avg_warm.as_secs_f64() ); println!(" Bars loaded: {}", bars_loaded); // Warm loads should be faster or similar assert!( avg_warm <= cold_load_duration, "Warm loads should not be slower than cold" ); Ok(()) } #[tokio::test] async fn test_data_correctness_at_speed() -> Result<()> { let repo = create_dbn_repository().await?; let start = Instant::now(); let symbols = vec!["ES.FUT".to_string()]; let data = repo .load_historical_data( &symbols, 1704153600_000_000_000i64, 1704240000_000_000_000i64, ) .await?; let duration = start.elapsed(); println!("\n=== Data Correctness at Speed ==="); println!(" Load time: {:?}", duration); println!(" Bars loaded: {}", data.len()); // Verify data quality assert!(!data.is_empty(), "Should load data"); assert!(data.len() > 100, "Should load substantial data"); // Check that bars are properly sorted for i in 1..data.len() { assert!( data[i].timestamp >= data[i - 1].timestamp, "Bars should be sorted by timestamp" ); } // Verify all bars have valid data for (i, bar) in data.iter().enumerate() { assert!( bar.open > Decimal::ZERO, "Bar {} should have positive open price", i ); assert!(bar.high >= bar.low, "Bar {} high should be >= low", i); assert!( bar.high >= bar.open && bar.high >= bar.close, "Bar {} high should be highest price", i ); assert!( bar.low <= bar.open && bar.low <= bar.close, "Bar {} low should be lowest price", i ); assert!( bar.volume >= Decimal::ZERO, "Bar {} should have non-negative volume", i ); } println!(" ✓ All {} bars passed validation", data.len()); Ok(()) }