//! Test Fixtures for Real DBN Market Data //! //! This module provides efficient, cached access to real DBN market data for tests. //! All data is loaded once and cached in static memory, shared across test threads. //! //! # Performance //! //! - **First access**: ~5-10ms (DBN file I/O) //! - **Subsequent access**: ~0.1μs (static memory read) //! - **Thread-safe**: Uses `once_cell::sync::Lazy` for safe concurrent access //! //! # Supported Symbols //! //! - **ES.FUT**: E-mini S&P 500 futures (2024-01-02, ~390 bars) //! - **NQ.FUT**: E-mini NASDAQ-100 futures (2024-01-02, ~390 bars) //! - **CL.FUT**: WTI Crude Oil futures (2024-01-02, ~1440 bars) //! //! # Usage Examples //! //! ```rust //! use fixtures::{get_es_fut_bars, get_nq_fut_bars, get_regime_sample}; //! //! #[tokio::test] //! async fn test_strategy_with_real_data() { //! // Fast: data cached after first call //! let bars = get_es_fut_bars().await.unwrap(); //! assert!(!bars.is_empty()); //! } //! //! #[tokio::test] //! async fn test_trending_regime() { //! let bars = get_regime_sample(RegimeType::Trending).await.unwrap(); //! // bars filtered for trending market conditions //! } //! ``` use anyhow::Result; use backtesting_service::dbn_data_source::DbnDataSource; use num_traits::ToPrimitive; use backtesting_service::strategy_engine::MarketData; use chrono::{DateTime, Utc}; use once_cell::sync::Lazy; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; // ============================================================================ // Static Cached Data (Singleton Pattern) // ============================================================================ /// Cached ES.FUT bars (loaded once per test run) static ES_FUT_CACHE: Lazy>>>> = Lazy::new(|| Arc::new(RwLock::new(None))); /// Cached NQ.FUT bars (loaded once per test run) static NQ_FUT_CACHE: Lazy>>>> = Lazy::new(|| Arc::new(RwLock::new(None))); /// Cached CL.FUT bars (loaded once per test run) static CL_FUT_CACHE: Lazy>>>> = Lazy::new(|| Arc::new(RwLock::new(None))); // ============================================================================ // Path Resolution // ============================================================================ /// Get absolute path to project root fn get_project_root() -> std::path::PathBuf { let mut current = std::env::current_dir().expect("INVARIANT: Current directory should be accessible"); // Navigate up until we find the workspace root while !current.join("Cargo.toml").exists() || !current.join("test_data").exists() { if !current.pop() { panic!("Could not find project root"); } } current } /// Get absolute path to a DBN test file fn get_dbn_file_path(filename: &str) -> String { get_project_root() .join("test_data/real/databento") .join(filename) .to_string_lossy() .to_string() } // ============================================================================ // Core Data Loading Functions // ============================================================================ /// Get cached ES.FUT bars (E-mini S&P 500 futures) /// /// # Data Details /// /// - **Symbol**: ES.FUT /// - **Date**: 2024-01-02 /// - **Timeframe**: 1-minute OHLCV /// - **Bar count**: ~390 bars (typical trading day) /// - **Price range**: ~4700-4750 (typical for 2024) /// /// # Performance /// /// - First call: ~5-10ms (DBN file load) /// - Subsequent: ~0.1μs (cache hit) /// /// # Returns /// /// `Vec` with chronologically sorted bars /// /// # Example /// /// ```rust /// let bars = get_es_fut_bars().await?; /// assert_eq!(bars[0].symbol, "ES.FUT"); /// assert!(bars.len() > 350); /// ``` pub async fn get_es_fut_bars() -> Result> { // Check cache first { let cache = ES_FUT_CACHE.read().await; if let Some(ref bars) = *cache { return Ok(bars.clone()); } } // Cache miss: load from DBN file let mut file_mapping = HashMap::new(); file_mapping.insert( "ES.FUT".to_string(), get_dbn_file_path("ES.FUT_ohlcv-1m_2024-01-02.dbn"), ); let data_source = DbnDataSource::new(file_mapping).await?; let bars = data_source.load_ohlcv_bars("ES.FUT").await?; // Cache for future calls { let mut cache = ES_FUT_CACHE.write().await; *cache = Some(bars.clone()); } Ok(bars) } /// Get cached NQ.FUT bars (E-mini NASDAQ-100 futures) /// /// # Data Details /// /// - **Symbol**: NQ.FUT /// - **Date**: 2024-01-02 /// - **Timeframe**: 1-minute OHLCV /// - **Bar count**: ~390 bars /// - **Price range**: ~16500-16700 (typical for 2024) /// /// # Performance /// /// - First call: ~5-10ms /// - Subsequent: ~0.1μs /// /// # Example /// /// ```rust /// let bars = get_nq_fut_bars().await?; /// assert_eq!(bars[0].symbol, "NQ.FUT"); /// ``` pub async fn get_nq_fut_bars() -> Result> { // Check cache first { let cache = NQ_FUT_CACHE.read().await; if let Some(ref bars) = *cache { return Ok(bars.clone()); } } // Cache miss: load from DBN file let mut file_mapping = HashMap::new(); file_mapping.insert( "NQ.FUT".to_string(), get_dbn_file_path("NQ.FUT_ohlcv-1m_2024-01-02.dbn"), ); let data_source = DbnDataSource::new(file_mapping).await?; let bars = data_source.load_ohlcv_bars("NQ.FUT").await?; // Cache for future calls { let mut cache = NQ_FUT_CACHE.write().await; *cache = Some(bars.clone()); } Ok(bars) } /// Get cached CL.FUT bars (WTI Crude Oil futures) /// /// # Data Details /// /// - **Symbol**: CL.FUT /// - **Date**: 2024-01-02 /// - **Timeframe**: 1-minute OHLCV /// - **Bar count**: ~1440 bars (24-hour trading) /// - **Price range**: ~71-73 USD/barrel (typical for 2024) /// /// # Performance /// /// - First call: ~10-15ms (larger file) /// - Subsequent: ~0.2μs /// /// # Example /// /// ```rust /// let bars = get_cl_fut_bars().await?; /// assert!(bars.len() > 1400); /// ``` pub async fn get_cl_fut_bars() -> Result> { // Check cache first { let cache = CL_FUT_CACHE.read().await; if let Some(ref bars) = *cache { return Ok(bars.clone()); } } // Cache miss: load from DBN file let mut file_mapping = HashMap::new(); file_mapping.insert( "CL.FUT".to_string(), get_dbn_file_path("CL.FUT_ohlcv-1m_2024-01-02.dbn"), ); let data_source = DbnDataSource::new(file_mapping).await?; let bars = data_source.load_ohlcv_bars("CL.FUT").await?; // Cache for future calls { let mut cache = CL_FUT_CACHE.write().await; *cache = Some(bars.clone()); } Ok(bars) } // ============================================================================ // Filtered Data Access // ============================================================================ /// Get bars for a specific date and symbol /// /// # Arguments /// /// * `symbol` - Symbol name (e.g., "ES.FUT", "NQ.FUT", "CL.FUT") /// * `date` - UTC date (only date component used, time ignored) /// /// # Returns /// /// Bars matching the specified date, or empty vec if no data available /// /// # Example /// /// ```rust /// use chrono::NaiveDate; /// let date = NaiveDate::from_ymd_opt(2024, 1, 2).unwrap().and_hms_opt(0, 0, 0).expect("INVARIANT: Valid time parameters"); /// let bars = get_bars_for_date("ES.FUT", date).await?; /// ``` pub async fn get_bars_for_date(symbol: &str, date: DateTime) -> Result> { let all_bars = match symbol { "ES.FUT" => get_es_fut_bars().await?, "NQ.FUT" => get_nq_fut_bars().await?, "CL.FUT" => get_cl_fut_bars().await?, _ => return Ok(Vec::new()), }; // Filter by date (compare only the date component) let target_date = date.date_naive(); let filtered: Vec = all_bars .into_iter() .filter(|bar| bar.timestamp.date_naive() == target_date) .collect(); Ok(filtered) } /// Market regime types for filtered data access #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RegimeType { /// Trending market (strong directional movement) Trending, /// Ranging/sideways market (bounded oscillation) Ranging, /// High volatility period Volatile, /// Low volatility period Stable, } /// Get a sample of bars matching a specific market regime /// /// This function analyzes cached data and returns a subset that exhibits /// the requested market characteristics. /// /// # Arguments /// /// * `regime_type` - The type of market regime to sample /// /// # Returns /// /// Vec of 50-100 bars exhibiting the requested regime characteristics /// /// # Regime Detection Logic /// /// - **Trending**: Price movement > 1.5% over 60 bars /// - **Ranging**: Price range < 0.8% over 60 bars /// - **Volatile**: Price standard deviation > 0.5% /// - **Stable**: Price standard deviation < 0.3% /// /// # Example /// /// ```rust /// let trending_bars = get_regime_sample(RegimeType::Trending).await?; /// // Use these bars to test trend-following strategies /// ``` pub async fn get_regime_sample(regime_type: RegimeType) -> Result> { // Use ES.FUT as default data source (most liquid) let all_bars = get_es_fut_bars().await?; if all_bars.len() < 100 { return Ok(all_bars); } // Analyze bars to find regime match let window_size = 60; // 1 hour window for regime detection let mut best_match_idx = 0; let mut best_score = 0.0; for i in 0..(all_bars.len() - window_size) { let window = &all_bars[i..i + window_size]; let score = calculate_regime_score(window, regime_type); if score > best_score { best_score = score; best_match_idx = i; } } // Return 100 bars starting from best match let end_idx = (best_match_idx + 100).min(all_bars.len()); Ok(all_bars[best_match_idx..end_idx].to_vec()) } /// Calculate how well a window matches a regime type fn calculate_regime_score(window: &[MarketData], regime_type: RegimeType) -> f64 { if window.is_empty() { return 0.0; } let prices: Vec = window .iter() .map(|bar| bar.close.to_f64().unwrap_or(0.0)) .collect(); let first_price = prices[0]; let last_price = *prices.last().expect("INVARIANT: Collection should be non-empty"); let mean = prices.iter().sum::() / prices.len() as f64; // Calculate standard deviation let variance: f64 = prices.iter().map(|p| (p - mean).powi(2)).sum::() / prices.len() as f64; let std_dev = variance.sqrt(); let std_dev_pct = (std_dev / mean) * 100.0; // Calculate price change percentage let price_change_pct = ((last_price - first_price) / first_price).abs() * 100.0; // Calculate price range let max_price = prices.iter().cloned().fold(f64::NEG_INFINITY, f64::max); let min_price = prices.iter().cloned().fold(f64::INFINITY, f64::min); let range_pct = ((max_price - min_price) / mean) * 100.0; match regime_type { RegimeType::Trending => { // High price change, moderate to high volatility price_change_pct * 2.0 + std_dev_pct }, RegimeType::Ranging => { // Low price change, low to moderate range let range_score = if range_pct < 1.0 { 100.0 - range_pct * 20.0 } else { 0.0 }; let change_score = if price_change_pct < 0.5 { 50.0 } else { 0.0 }; range_score + change_score }, RegimeType::Volatile => { // High standard deviation std_dev_pct * 10.0 }, RegimeType::Stable => { // Low standard deviation if std_dev_pct < 0.5 { 100.0 - std_dev_pct * 50.0 } else { 0.0 } }, } } // ============================================================================ // Multi-Symbol Access // ============================================================================ /// Get bars for multiple symbols /// /// Loads data for multiple symbols in parallel for efficiency. /// /// # Arguments /// /// * `symbols` - Slice of symbol names /// /// # Returns /// /// HashMap mapping symbol name to bars /// /// # Example /// /// ```rust /// let symbols = vec!["ES.FUT", "NQ.FUT"]; /// let data = get_multi_symbol_bars(&symbols).await?; /// assert!(data.contains_key("ES.FUT")); /// ``` pub async fn get_multi_symbol_bars(symbols: &[&str]) -> Result>> { let mut result = HashMap::new(); // Load all symbols in parallel let mut handles = vec![]; for &symbol in symbols { let symbol = symbol.to_string(); handles.push(tokio::spawn(async move { let bars = match symbol.as_str() { "ES.FUT" => get_es_fut_bars().await, "NQ.FUT" => get_nq_fut_bars().await, "CL.FUT" => get_cl_fut_bars().await, _ => Ok(Vec::new()), }; (symbol, bars) })); } // Collect results for handle in handles { let (symbol, bars) = handle.await?; if let Ok(bars) = bars { result.insert(symbol, bars); } } Ok(result) } // ============================================================================ // Unit Tests // ============================================================================ #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_es_fut_cache() -> Result<()> { use std::time::Instant; // First call (cold cache) let start = Instant::now(); let bars1 = get_es_fut_bars().await?; let cold_duration = start.elapsed(); assert!(!bars1.is_empty(), "Should load ES.FUT bars"); assert!(bars1.len() > 350 && bars1.len() < 450, "Expected ~390 bars"); // Second call (warm cache) let start = Instant::now(); let bars2 = get_es_fut_bars().await?; let warm_duration = start.elapsed(); assert_eq!(bars1.len(), bars2.len(), "Cache should return same data"); println!("Cache performance:"); println!(" Cold: {:?}", cold_duration); println!(" Warm: {:?}", warm_duration); // Warm should be significantly faster assert!( warm_duration < cold_duration / 10, "Cached access should be >10x faster" ); Ok(()) } #[tokio::test] async fn test_nq_fut_cache() -> Result<()> { let bars = get_nq_fut_bars().await?; assert!(!bars.is_empty(), "Should load NQ.FUT bars"); assert_eq!(bars[0].symbol, "NQ.FUT"); Ok(()) } #[tokio::test] async fn test_cl_fut_cache() -> Result<()> { let bars = get_cl_fut_bars().await?; assert!(!bars.is_empty(), "Should load CL.FUT bars"); assert!(bars.len() > 1400, "CL.FUT has 24-hour trading"); assert_eq!(bars[0].symbol, "CL.FUT"); Ok(()) } #[tokio::test] async fn test_bars_for_date() -> Result<()> { use chrono::NaiveDate; let date = NaiveDate::from_ymd_opt(2024, 1, 2) .unwrap() .and_hms_opt(0, 0, 0) .unwrap() .and_utc(); let bars = get_bars_for_date("ES.FUT", date).await?; assert!(!bars.is_empty(), "Should find bars for 2024-01-02"); // All bars should be from requested date for bar in &bars { assert_eq!(bar.timestamp.date_naive(), date.date_naive()); } Ok(()) } #[tokio::test] async fn test_regime_samples() -> Result<()> { // Test trending regime let trending = get_regime_sample(RegimeType::Trending).await?; assert!(!trending.is_empty(), "Should find trending sample"); // Test ranging regime let ranging = get_regime_sample(RegimeType::Ranging).await?; assert!(!ranging.is_empty(), "Should find ranging sample"); // Test volatile regime let volatile = get_regime_sample(RegimeType::Volatile).await?; assert!(!volatile.is_empty(), "Should find volatile sample"); // Test stable regime let stable = get_regime_sample(RegimeType::Stable).await?; assert!(!stable.is_empty(), "Should find stable sample"); Ok(()) } #[tokio::test] async fn test_multi_symbol_bars() -> Result<()> { let symbols = vec!["ES.FUT", "NQ.FUT", "CL.FUT"]; let data = get_multi_symbol_bars(&symbols).await?; assert_eq!(data.len(), 3, "Should load all 3 symbols"); assert!(data.contains_key("ES.FUT")); assert!(data.contains_key("NQ.FUT")); assert!(data.contains_key("CL.FUT")); // Verify data quality for (symbol, bars) in &data { assert!(!bars.is_empty(), "Symbol {} should have bars", symbol); assert_eq!(bars[0].symbol, *symbol); } Ok(()) } #[tokio::test] async fn test_cache_thread_safety() -> Result<()> { // Spawn multiple concurrent reads let mut handles = vec![]; for _ in 0..10 { handles.push(tokio::spawn(async { get_es_fut_bars().await })); } // All should succeed for handle in handles { let bars = handle.await??; assert!(!bars.is_empty()); } Ok(()) } }