#![allow(unexpected_cfgs)] #![cfg(feature = "__backtesting_integration")] //! Tests for data replay functionality in backtesting service //! //! Target Coverage: 50%+ for historical data replay, timestamp handling, and data validation use anyhow::Result; use chrono::{Duration, Utc}; use rust_decimal::Decimal; use std::sync::Arc; mod mock_repositories; use backtesting_service::repositories::{MarketDataRepository, NewsRepository}; use mock_repositories::*; /// Test loading historical market data #[tokio::test] async fn test_load_historical_data() -> Result<()> { let market_data = generate_sample_market_data("AAPL", 100, 150.0, 0.02); let repo = MockMarketDataRepository::with_data(market_data.clone()); let start_time = market_data .first() .unwrap() .timestamp .timestamp_nanos_opt() .unwrap_or(0); let end_time = market_data .last() .unwrap() .timestamp .timestamp_nanos_opt() .unwrap_or(0); let loaded = repo .load_historical_data(&["AAPL".to_string()], start_time, end_time) .await?; assert_eq!(loaded.len(), 100, "Should load all 100 data points"); assert_eq!(loaded[0].symbol, "AAPL"); assert!(loaded[0].close > Decimal::ZERO); Ok(()) } /// Test data filtering by symbol #[tokio::test] async fn test_data_filtering_by_symbol() -> Result<()> { let mut all_data = Vec::new(); all_data.extend(generate_sample_market_data("AAPL", 50, 150.0, 0.02)); all_data.extend(generate_sample_market_data("MSFT", 50, 200.0, 0.015)); all_data.extend(generate_sample_market_data("GOOGL", 50, 120.0, 0.025)); let repo = MockMarketDataRepository::with_data(all_data.clone()); let start_time = all_data .first() .unwrap() .timestamp .timestamp_nanos_opt() .unwrap_or(0); let end_time = all_data .last() .unwrap() .timestamp .timestamp_nanos_opt() .unwrap_or(0); // Load only AAPL data let aapl_data = repo .load_historical_data(&["AAPL".to_string()], start_time, end_time) .await?; assert_eq!(aapl_data.len(), 50, "Should load only AAPL data"); assert!(aapl_data.iter().all(|d| d.symbol == "AAPL")); // Load multiple symbols let multi_data = repo .load_historical_data( &["AAPL".to_string(), "MSFT".to_string()], start_time, end_time, ) .await?; assert_eq!(multi_data.len(), 100, "Should load AAPL and MSFT data"); Ok(()) } /// Test timestamp range filtering #[tokio::test] async fn test_timestamp_range_filtering() -> Result<()> { let market_data = generate_sample_market_data("AAPL", 100, 150.0, 0.02); let repo = MockMarketDataRepository::with_data(market_data.clone()); // Get middle 50 days let start_time = market_data[25].timestamp.timestamp_nanos_opt().unwrap_or(0); let end_time = market_data[74].timestamp.timestamp_nanos_opt().unwrap_or(0); let filtered = repo .load_historical_data(&["AAPL".to_string()], start_time, end_time) .await?; assert_eq!(filtered.len(), 50, "Should load middle 50 data points"); assert!(filtered[0].timestamp >= market_data[25].timestamp); assert!(filtered.last().expect("INVARIANT: Collection should be non-empty").timestamp <= market_data[74].timestamp); Ok(()) } /// Test data availability check #[tokio::test] async fn test_data_availability_check() -> Result<()> { let market_data = generate_sample_market_data("AAPL", 50, 150.0, 0.02); let repo = MockMarketDataRepository::with_data(market_data.clone()); let start_time = market_data .first() .unwrap() .timestamp .timestamp_nanos_opt() .unwrap_or(0); let end_time = market_data .last() .unwrap() .timestamp .timestamp_nanos_opt() .unwrap_or(0); let availability = repo .check_data_availability( &["AAPL".to_string(), "MSFT".to_string()], start_time, end_time, ) .await?; assert_eq!(availability.len(), 2); assert_eq!(availability.get("AAPL"), Some(&true)); assert_eq!(availability.get("MSFT"), Some(&true)); Ok(()) } /// Test empty data range #[tokio::test] async fn test_empty_data_range() -> Result<()> { let market_data = generate_sample_market_data("AAPL", 50, 150.0, 0.02); let repo = MockMarketDataRepository::with_data(market_data.clone()); // Request data from future (no data available) let future_start = Utc::now().timestamp_nanos_opt().unwrap_or(0) + 1_000_000_000_000; let future_end = future_start + 1_000_000_000_000; let loaded = repo .load_historical_data(&["AAPL".to_string()], future_start, future_end) .await?; assert_eq!(loaded.len(), 0, "Future data should be empty"); Ok(()) } /// Test chronological order of replayed data #[tokio::test] async fn test_chronological_order() -> Result<()> { let market_data = generate_sample_market_data("AAPL", 100, 150.0, 0.02); let repo = MockMarketDataRepository::with_data(market_data.clone()); let start_time = market_data .first() .unwrap() .timestamp .timestamp_nanos_opt() .unwrap_or(0); let end_time = market_data .last() .unwrap() .timestamp .timestamp_nanos_opt() .unwrap_or(0); let loaded = repo .load_historical_data(&["AAPL".to_string()], start_time, end_time) .await?; // Verify data is in chronological order for i in 1..loaded.len() { assert!( loaded[i].timestamp >= loaded[i - 1].timestamp, "Data should be in chronological order" ); } Ok(()) } /// Test news event replay #[tokio::test] async fn test_news_event_replay() -> Result<()> { let symbols = vec!["AAPL".to_string()]; let news_events = generate_sample_news_events(&symbols, 50); let repo = MockNewsRepository::with_events(news_events.clone()); let start_time = news_events.first().expect("INVARIANT: Collection should be non-empty").timestamp; let end_time = news_events.last().expect("INVARIANT: Collection should be non-empty").timestamp; let loaded = repo .load_news_events(&symbols, start_time, end_time) .await?; assert_eq!(loaded.len(), 50, "Should load all news events"); assert!(loaded .iter() .all(|e| e.symbols.contains(&"AAPL".to_string()))); Ok(()) } /// Test news event filtering by time range #[tokio::test] async fn test_news_event_time_filtering() -> Result<()> { let symbols = vec!["AAPL".to_string()]; let news_events = generate_sample_news_events(&symbols, 100); let repo = MockNewsRepository::with_events(news_events.clone()); // Get middle portion let start_time = news_events[30].timestamp; let end_time = news_events[69].timestamp; let loaded = repo .load_news_events(&symbols, start_time, end_time) .await?; assert!( loaded.len() >= 30 && loaded.len() <= 50, "Should load middle portion of events" ); assert!(loaded .iter() .all(|e| e.timestamp >= start_time && e.timestamp <= end_time)); Ok(()) } /// Test sentiment data aggregation #[tokio::test] async fn test_sentiment_data_aggregation() -> Result<()> { let symbols = vec!["AAPL".to_string(), "MSFT".to_string()]; let news_events = generate_sample_news_events(&symbols, 50); let repo = MockNewsRepository::with_events(news_events.clone()); let timestamp = Utc::now(); let lookback_hours = 24; let sentiment = repo .get_sentiment_data(&symbols, timestamp, lookback_hours) .await?; assert!(sentiment.contains_key("AAPL")); assert!(sentiment.contains_key("MSFT")); // Sentiment should be in valid range for (_, value) in &sentiment { assert!( *value >= -1.0 && *value <= 1.0, "Sentiment should be between -1 and 1" ); } Ok(()) } /// Test mixed timeframe data replay #[tokio::test] async fn test_mixed_timeframe_data() -> Result<()> { use backtesting_service::strategy_engine::{MarketData, TimeFrame}; let mut market_data = Vec::new(); let base_time = Utc::now() - Duration::days(100); // Create data with different timeframes for i in 0..30 { market_data.push(MarketData { symbol: "AAPL".to_string(), timestamp: base_time + Duration::days(i), open: Decimal::from(150), high: Decimal::from(152), low: Decimal::from(148), close: Decimal::from(151), volume: Decimal::from(1000000), timeframe: TimeFrame::Daily, }); } for i in 0..24 { market_data.push(MarketData { symbol: "AAPL".to_string(), timestamp: base_time + Duration::hours(i), open: Decimal::from(150), high: Decimal::from(151), low: Decimal::from(149), close: Decimal::from(150), volume: Decimal::from(100000), timeframe: TimeFrame::Hour, }); } let repo = MockMarketDataRepository::with_data(market_data.clone()); let start_time = base_time.timestamp_nanos_opt().unwrap_or(0); let end_time = (base_time + Duration::days(50)) .timestamp_nanos_opt() .unwrap_or(0); let loaded = repo .load_historical_data(&["AAPL".to_string()], start_time, end_time) .await?; // Should load all data regardless of timeframe assert!(!loaded.is_empty(), "Should load mixed timeframe data"); Ok(()) } /// Test data integrity validation #[tokio::test] async fn test_data_integrity_validation() -> Result<()> { let market_data = generate_sample_market_data("AAPL", 50, 150.0, 0.02); let repo = MockMarketDataRepository::with_data(market_data.clone()); let start_time = market_data .first() .unwrap() .timestamp .timestamp_nanos_opt() .unwrap_or(0); let end_time = market_data .last() .unwrap() .timestamp .timestamp_nanos_opt() .unwrap_or(0); let loaded = repo .load_historical_data(&["AAPL".to_string()], start_time, end_time) .await?; // Validate data integrity for data_point in &loaded { // OHLC validation assert!(data_point.high >= data_point.open, "High should be >= open"); assert!( data_point.high >= data_point.close, "High should be >= close" ); assert!(data_point.low <= data_point.open, "Low should be <= open"); assert!(data_point.low <= data_point.close, "Low should be <= close"); assert!( data_point.volume >= Decimal::ZERO, "Volume should be non-negative" ); } Ok(()) } /// Test concurrent data loading #[tokio::test] async fn test_concurrent_data_loading() -> Result<()> { let market_data = generate_sample_market_data("AAPL", 100, 150.0, 0.02); let repo = Arc::new(MockMarketDataRepository::with_data(market_data.clone())); let start_time = market_data .first() .unwrap() .timestamp .timestamp_nanos_opt() .unwrap_or(0); let end_time = market_data .last() .unwrap() .timestamp .timestamp_nanos_opt() .unwrap_or(0); // Spawn multiple concurrent load tasks let mut handles = Vec::new(); for _ in 0..10 { let repo_clone = repo.clone(); let handle = tokio::spawn(async move { repo_clone .load_historical_data(&["AAPL".to_string()], start_time, end_time) .await }); handles.push(handle); } // Wait for all tasks for handle in handles { let result = handle.await??; assert_eq!( result.len(), 100, "Each concurrent load should return all data" ); } Ok(()) }