//! Mock repository implementations for backtesting service tests #![allow(dead_code, clippy::new_without_default)] use anyhow::Result; use async_trait::async_trait; use chrono::{DateTime, Utc}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use backtesting_service::performance::PerformanceMetrics; use backtesting_service::repositories::*; use backtesting_service::storage::BacktestSummary; use backtesting_service::strategy_engine::{BacktestTrade, MarketData, NewsEvent}; /// Mock market data repository for testing pub struct MockMarketDataRepository { pub data: Arc>>, } impl MockMarketDataRepository { pub fn new() -> Self { Self { data: Arc::new(RwLock::new(Vec::new())), } } pub fn with_data(data: Vec) -> Self { Self { data: Arc::new(RwLock::new(data)), } } } #[async_trait] impl MarketDataRepository for MockMarketDataRepository { async fn load_historical_data( &self, symbols: &[String], start_time: i64, end_time: i64, ) -> Result> { let data = self.data.read().await; let filtered: Vec = data .iter() .filter(|d| { symbols.contains(&d.symbol) && d.timestamp.timestamp_nanos_opt().unwrap_or(0) >= start_time && d.timestamp.timestamp_nanos_opt().unwrap_or(0) <= end_time }) .cloned() .collect(); Ok(filtered) } } /// Mock trading repository for testing pub struct MockTradingRepository { pub trades: Arc>>>, pub metrics: Arc>>, pub backtests: Arc>>, } impl MockTradingRepository { pub fn new() -> Self { Self { trades: Arc::new(RwLock::new(HashMap::new())), metrics: Arc::new(RwLock::new(HashMap::new())), backtests: Arc::new(RwLock::new(Vec::new())), } } } #[async_trait] impl TradingRepository for MockTradingRepository { async fn save_backtest_results( &self, backtest_id: &str, trades: &[BacktestTrade], metrics: &PerformanceMetrics, ) -> Result<()> { self.trades .write() .await .insert(backtest_id.to_string(), trades.to_vec()); self.metrics .write() .await .insert(backtest_id.to_string(), metrics.clone()); Ok(()) } async fn load_backtest_results( &self, backtest_id: &str, ) -> Result<(Vec, PerformanceMetrics)> { let trades = self .trades .read() .await .get(backtest_id) .cloned() .unwrap_or_default(); let metrics = self .metrics .read() .await .get(backtest_id) .cloned() .unwrap_or_default(); Ok((trades, metrics)) } async fn list_backtests( &self, limit: u32, offset: u32, strategy_name: Option, status_filter: Option, ) -> Result> { let backtests = self.backtests.read().await; let filtered: Vec = backtests .iter() .filter(|bt| { let name_match = strategy_name .as_ref() .map(|n| bt.strategy_name == *n) .unwrap_or(true); let status_match = status_filter.map(|s| bt.status == s).unwrap_or(true); name_match && status_match }) .skip(offset as usize) .take(limit as usize) .cloned() .collect(); Ok(filtered) } } /// Mock news repository for testing pub struct MockNewsRepository { pub events: Arc>>, } impl MockNewsRepository { pub fn new() -> Self { Self { events: Arc::new(RwLock::new(Vec::new())), } } pub fn with_events(events: Vec) -> Self { Self { events: Arc::new(RwLock::new(events)), } } } #[async_trait] impl NewsRepository for MockNewsRepository { async fn load_news_events( &self, _symbols: &[String], _start_time: DateTime, _end_time: DateTime, ) -> Result> { let events = self.events.read().await; Ok(events.clone()) } } /// Mock combined repositories for testing pub struct MockBacktestingRepositories { market_data: Box, trading: Box, news: Box, } impl MockBacktestingRepositories { pub fn new( market_data: Box, trading: Box, news: Box, ) -> Self { Self { market_data, trading, news, } } } #[async_trait] impl BacktestingRepositories for MockBacktestingRepositories { fn market_data(&self) -> &dyn MarketDataRepository { self.market_data.as_ref() } fn trading(&self) -> &dyn TradingRepository { self.trading.as_ref() } fn news(&self) -> &dyn NewsRepository { self.news.as_ref() } } /// Helper function to generate sample market data /// /// Uses deterministic price pattern to avoid flaky tests pub fn generate_sample_market_data( symbol: &str, num_points: usize, start_price: f64, volatility: f64, ) -> Vec { use rust_decimal::Decimal; let mut data = Vec::new(); let start_time = Utc::now() - chrono::Duration::days(num_points as i64); for i in 0..num_points { // Deterministic oscillation: price varies +/-volatility in a sine wave pattern // This ensures price crosses any reasonable trigger level multiple times let phase = (i as f64) / (num_points as f64) * 4.0 * std::f64::consts::PI; let price_multiplier = 1.0 + volatility * phase.sin(); let price = start_price * price_multiplier; let timestamp = start_time + chrono::Duration::days(i as i64); let open = Decimal::from_f64_retain(price * 0.99).unwrap_or(Decimal::ZERO); let high = Decimal::from_f64_retain(price * 1.02).unwrap_or(Decimal::ZERO); let low = Decimal::from_f64_retain(price * 0.98).unwrap_or(Decimal::ZERO); let close = Decimal::from_f64_retain(price).unwrap_or(Decimal::ZERO); // Deterministic volume based on index let volume = Decimal::from_f64_retain(2_000_000.0 + (i as f64 * 1000.0)).unwrap_or(Decimal::ZERO); data.push(MarketData { symbol: symbol.to_string(), timestamp, open, high, low, close, volume, }); } data } /// Helper function to generate sample news events pub fn generate_sample_news_events(_symbols: &[String], num_events: usize) -> Vec { let mut events = Vec::new(); for _ in 0..num_events { events.push(NewsEvent); } events } /// Get project root directory /// /// Resolves the path from the project root, handling different working directories. pub fn get_project_root() -> String { // Try to find project root by looking for Cargo.toml let mut current = std::env::current_dir().expect("INVARIANT: Current directory should be accessible"); // If we're in a subdirectory, go up until we find the workspace root while !current.join("Cargo.toml").exists() || !current.join("test_data").exists() { if !current.pop() { // Fallback to relative path if we can't find root return "../..".to_string(); } } current.to_string_lossy().to_string() } /// Get absolute path to the test DBN file /// /// Resolves the path from the project root, handling different working directories. pub fn get_dbn_test_file_path() -> String { let root = get_project_root(); format!( "{}/test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn", root ) } /// Create a DBN-based market data repository for testing with real data /// /// This function creates a repository that loads data from the real DBN file /// in test_data/real/databento/. /// /// # Returns /// /// DbnMarketDataRepository configured with ES.FUT data for 2024-01-02 pub async fn create_dbn_repository() -> Result, anyhow::Error> { use backtesting_service::dbn_repository::DbnMarketDataRepository; use std::collections::HashMap; let mut file_mapping = HashMap::new(); file_mapping.insert("ES.FUT".to_string(), get_dbn_test_file_path()); let repo = DbnMarketDataRepository::new(file_mapping).await?; Ok(Box::new(repo)) }