#![allow(clippy::inconsistent_digit_grouping, dead_code, unused_variables, unexpected_cfgs)] //! DBN Multi-Symbol Integration Tests //! //! Tests for multi-symbol loading with diverse asset classes: //! - ES.FUT (E-mini S&P 500) - Equity index futures //! - NQ.FUT (E-mini NASDAQ) - Tech index futures //! - GC (Gold) - Commodity futures //! - ZN.FUT (10-Year Treasury) - Fixed income futures //! - 6E.FUT (Euro FX) - Currency futures use anyhow::Result; use num_traits::ToPrimitive; use std::collections::HashMap; mod mock_repositories; use backtesting_service::dbn_data_source::DbnDataSource; use backtesting_service::dbn_repository::DbnMarketDataRepository; use backtesting_service::repositories::MarketDataRepository; /// Helper to get multi-symbol file mappings fn get_multi_symbol_file_mapping() -> HashMap { let root = mock_repositories::get_project_root(); let mut mapping = HashMap::new(); // Equity indices mapping.insert( "ES.FUT".to_string(), format!( "{}/test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn", root ), ); mapping.insert( "NQ.FUT".to_string(), format!( "{}/test_data/real/databento/NQ.FUT_ohlcv-1m_2024-01-02.dbn", root ), ); // Commodities (Gold) - uncompressed version mapping.insert( "GC".to_string(), format!("{}/test_data/real/databento/GC_continuous_ohlcv-1m_2024-01-02_to_2024-01-31.uncompressed.dbn", root), ); // Fixed income (Treasuries) - uncompressed version mapping.insert( "ZN.FUT".to_string(), format!( "{}/test_data/real/databento/ZN.FUT_ohlcv-1m_2024-01-02_to_2024-01-31.uncompressed.dbn", root ), ); // Currencies (Euro FX) - uncompressed version mapping.insert( "6E.FUT".to_string(), format!( "{}/test_data/real/databento/6E.FUT_ohlcv-1m_2024-01-02_to_2024-01-31.uncompressed.dbn", root ), ); mapping } #[tokio::test] async fn test_load_all_symbols() -> Result<()> { let file_mapping = get_multi_symbol_file_mapping(); let data_source = DbnDataSource::new(file_mapping).await?; // Test each symbol individually let symbols = vec!["ES.FUT", "NQ.FUT", "GC", "ZN.FUT", "6E.FUT"]; for symbol in symbols { let bars = data_source.load_ohlcv_bars(symbol).await?; assert!(!bars.is_empty(), "{} should have data", symbol); println!("✅ {}: {} bars loaded", symbol, bars.len()); // Validate all bars have correct symbol for bar in &bars { assert_eq!(bar.symbol, symbol, "Bar symbol mismatch"); } } println!("✅ All 5 symbols loaded successfully"); Ok(()) } #[tokio::test] async fn test_multi_symbol_loading() -> Result<()> { let file_mapping = get_multi_symbol_file_mapping(); let data_source = DbnDataSource::new(file_mapping).await?; // Load multiple symbols together let symbols = vec![ "ES.FUT".to_string(), "ZN.FUT".to_string(), "6E.FUT".to_string(), ]; let bars = data_source.load_multi_symbol_bars(&symbols).await?; assert!(!bars.is_empty(), "Should load multi-symbol data"); // Count bars per symbol let mut counts: HashMap = HashMap::new(); for bar in &bars { *counts.entry(bar.symbol.clone()).or_insert(0) += 1; } println!("✅ Multi-symbol loading results:"); for (symbol, count) in &counts { println!(" {}: {} bars", symbol, count); } // Verify all requested symbols are present for symbol in &symbols { assert!( counts.contains_key(symbol), "Symbol {} should be present", symbol ); assert!( *counts.get(symbol).expect("INVARIANT: Key should exist in map") > 0, "Symbol {} should have bars", symbol ); } Ok(()) } #[tokio::test] async fn test_asset_class_price_ranges() -> Result<()> { let file_mapping = get_multi_symbol_file_mapping(); let data_source = DbnDataSource::new(file_mapping).await?; // Expected price ranges for each asset class let price_validations = vec![ ("ES.FUT", 3000.0, 6000.0, "S&P 500 E-mini"), ("NQ.FUT", 10000.0, 20000.0, "NASDAQ E-mini"), ("GC", 1800.0, 2200.0, "Gold"), ("ZN.FUT", 100.0, 130.0, "10-Year Treasury"), ("6E.FUT", 1.00, 1.20, "Euro FX"), ]; for (symbol, min_price, max_price, name) in price_validations { let bars = data_source.load_ohlcv_bars(symbol).await?; assert!(!bars.is_empty(), "{} should have data", symbol); for (i, bar) in bars.iter().enumerate() { let close_f64 = bar.close.to_f64().unwrap_or(0.0); assert!( close_f64 >= min_price && close_f64 <= max_price, "{} bar {}: price {} outside expected range [{}, {}]", name, i, close_f64, min_price, max_price ); } println!( "✅ {}: Price range validation passed ({} bars)", name, bars.len() ); } Ok(()) } #[tokio::test] async fn test_repository_multi_symbol() -> Result<()> { let file_mapping = get_multi_symbol_file_mapping(); let repo = DbnMarketDataRepository::new(file_mapping).await?; // Test with 3 symbols let symbols = vec![ "ES.FUT".to_string(), "ZN.FUT".to_string(), "6E.FUT".to_string(), ]; // 2024-01-02 00:00:00 to 2024-01-03 00:00:00 let start_time = 1704153600_000_000_000i64; let end_time = 1704240000_000_000_000i64; let data = repo .load_historical_data(&symbols, start_time, end_time) .await?; assert!(!data.is_empty(), "Repository should load multi-symbol data"); // Verify symbol distribution let mut symbol_counts: HashMap = HashMap::new(); for bar in &data { *symbol_counts.entry(bar.symbol.clone()).or_insert(0) += 1; } println!("✅ Repository multi-symbol loading:"); for (symbol, count) in &symbol_counts { println!(" {}: {} bars", symbol, count); } // All symbols should have data assert_eq!(symbol_counts.len(), 3, "Should have 3 symbols"); Ok(()) } // Gated: check_data_availability method was removed from DbnMarketDataRepository #[cfg(feature = "__backtesting_integration")] #[tokio::test] async fn test_data_availability_multi_symbol() -> Result<()> { let file_mapping = get_multi_symbol_file_mapping(); let repo = DbnMarketDataRepository::new(file_mapping).await?; let symbols = vec![ "ES.FUT".to_string(), "NQ.FUT".to_string(), "GC".to_string(), "ZN.FUT".to_string(), "6E.FUT".to_string(), "NONEXISTENT".to_string(), ]; let start_time = 1704153600_000_000_000i64; let end_time = 1704240000_000_000_000i64; let availability = repo .check_data_availability(&symbols, start_time, end_time) .await?; // Verify availability assert_eq!( availability.get("ES.FUT"), Some(&true), "ES.FUT should be available" ); assert_eq!( availability.get("NQ.FUT"), Some(&true), "NQ.FUT should be available" ); assert_eq!( availability.get("GC"), Some(&true), "GC should be available" ); assert_eq!( availability.get("ZN.FUT"), Some(&true), "ZN.FUT should be available" ); assert_eq!( availability.get("6E.FUT"), Some(&true), "6E.FUT should be available" ); assert_eq!( availability.get("NONEXISTENT"), Some(&false), "NONEXISTENT should not be available" ); println!("✅ Data availability check:"); for (symbol, available) in &availability { println!(" {}: {}", symbol, if *available { "✅" } else { "❌" }); } Ok(()) } #[tokio::test] async fn test_multi_symbol_quality() -> Result<()> { let file_mapping = get_multi_symbol_file_mapping(); let data_source = DbnDataSource::new(file_mapping).await?; let symbols = vec!["ES.FUT", "ZN.FUT", "6E.FUT"]; for symbol in symbols { let bars = data_source.load_ohlcv_bars(symbol).await?; assert!(!bars.is_empty(), "{} should have data", symbol); let mut violations = 0; for (i, bar) in bars.iter().enumerate() { // Check OHLCV relationships if !(bar.high >= bar.low && bar.high >= bar.open && bar.high >= bar.close && bar.low <= bar.open && bar.low <= bar.close) { violations += 1; if violations <= 3 { eprintln!( "{} bar {}: OHLCV violation (O={}, H={}, L={}, C={})", symbol, i, bar.open, bar.high, bar.low, bar.close ); } } // Check positive values assert!( bar.open > rust_decimal::Decimal::ZERO, "{} bar {}: open must be positive", symbol, i ); assert!( bar.close > rust_decimal::Decimal::ZERO, "{} bar {}: close must be positive", symbol, i ); assert!( bar.volume >= rust_decimal::Decimal::ZERO, "{} bar {}: volume must be non-negative", symbol, i ); } println!( "✅ {}: {} bars validated, {} violations", symbol, bars.len(), violations ); // Allow minimal violations (<1% of data) assert!( violations < bars.len() / 100, "{}: too many violations ({}/{})", symbol, violations, bars.len() ); } Ok(()) } #[tokio::test] async fn test_cross_asset_correlation() -> Result<()> { let file_mapping = get_multi_symbol_file_mapping(); let data_source = DbnDataSource::new(file_mapping).await?; // Load equity indices (ES and NQ should be correlated) let es_bars = data_source.load_ohlcv_bars("ES.FUT").await?; let nq_bars = data_source.load_ohlcv_bars("NQ.FUT").await?; assert!(!es_bars.is_empty(), "ES.FUT should have data"); assert!(!nq_bars.is_empty(), "NQ.FUT should have data"); println!("✅ Cross-asset data loading:"); println!(" ES.FUT: {} bars", es_bars.len()); println!(" NQ.FUT: {} bars", nq_bars.len()); // Both should have similar bar counts (same trading session) let ratio = es_bars.len() as f64 / nq_bars.len() as f64; assert!( ratio > 0.8 && ratio < 1.2, "ES and NQ bar counts should be similar (ratio: {:.2})", ratio ); println!(" Ratio: {:.2} (within expected range 0.8-1.2)", ratio); Ok(()) } #[tokio::test] async fn test_multi_symbol_performance() -> Result<()> { use std::time::Instant; let file_mapping = get_multi_symbol_file_mapping(); let data_source = DbnDataSource::new(file_mapping).await?; // Warm-up let _ = data_source.load_ohlcv_bars("ES.FUT").await?; // Time multi-symbol loading let symbols = vec![ "ES.FUT".to_string(), "ZN.FUT".to_string(), "6E.FUT".to_string(), ]; let start = Instant::now(); let bars = data_source.load_multi_symbol_bars(&symbols).await?; let duration = start.elapsed(); println!("✅ Multi-symbol performance:"); println!(" Total bars: {}", bars.len()); println!(" Duration: {:?}", duration); println!( " Throughput: {:.0} bars/sec", bars.len() as f64 / duration.as_secs_f64() ); // Should be reasonably fast (<1 second for ~60K bars) assert!( duration.as_secs() < 1, "Multi-symbol loading should be fast (<1s), got {:?}", duration ); Ok(()) }