Files
foxhunt/services/backtesting_service/tests/mock_repositories.rs
jgrusewski e05189d904 Multi-Symbol Integration Complete - 5 Asset Classes, 8/8 Tests Passing
**Summary**: Expanded real data coverage from 2 to 5 diverse symbols across equity, commodity, fixed income, and currency markets. All integration tests passing with zero data quality violations.

**Symbols Added**:
- GC (Gold Futures): 781 bars, 30 days, $0.00
- ZN.FUT (10-Year Treasury): 28,935 bars, 30 days, $0.11
- 6E.FUT (Euro FX): 29,937 bars, 30 days, $0.11

**Existing Symbols**:
- ES.FUT (S&P 500 E-mini): 1,674 bars, 1 day
- NQ.FUT (NASDAQ E-mini): 1,593 bars, 1 day

**Test Results**: 8/8 passing (100%)
- test_load_all_symbols
- test_multi_symbol_loading
- test_asset_class_price_ranges
- test_repository_multi_symbol
- test_data_availability_multi_symbol
- test_multi_symbol_quality
- test_cross_asset_correlation
- test_multi_symbol_performance

**Data Quality**: 62,920 bars validated, 0 OHLCV violations
**Performance**: <100ms for all symbols, 1,514 bars/ms throughput
**Production Ready**: 4/5 symbols (80%) - ES, NQ, ZN, 6E approved

**Budget Tracking**:
- Total spent: $0.62 of $125.00 (0.5%)
- Remaining: $124.38 (99.5%)

**Files Modified**:
- services/backtesting_service/tests/dbn_multi_symbol_tests.rs (+315 lines)
- services/backtesting_service/tests/mock_repositories.rs (+12 lines)
- MULTI_SYMBOL_INTEGRATION_COMPLETE.md (+415 lines)
- CLAUDE.md (updated with multi-symbol status)

**Next Steps**: Moving Average Crossover backtesting with multi-symbol data

🎯 Foxhunt Real Data Integration - Agent 24 Multi-Symbol Expansion
2025-10-13 11:08:09 +02:00

433 lines
13 KiB
Rust

//! Mock repository implementations for backtesting service tests
#![allow(dead_code)]
use anyhow::Result;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use backtesting_service::foxhunt::tli::BacktestStatus;
use backtesting_service::performance::PerformanceMetrics;
use backtesting_service::repositories::*;
use backtesting_service::storage::BacktestSummary;
use backtesting_service::strategy_engine::{BacktestTrade, MarketData, NewsEvent, TimeFrame};
/// Mock market data repository for testing
pub struct MockMarketDataRepository {
pub data: Arc<RwLock<Vec<MarketData>>>,
}
impl MockMarketDataRepository {
pub fn new() -> Self {
Self {
data: Arc::new(RwLock::new(Vec::new())),
}
}
pub fn with_data(data: Vec<MarketData>) -> 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<Vec<MarketData>> {
let data = self.data.read().await;
let filtered: Vec<MarketData> = 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)
}
async fn check_data_availability(
&self,
symbols: &[String],
_start_time: i64,
_end_time: i64,
) -> Result<HashMap<String, bool>> {
let mut availability = HashMap::new();
for symbol in symbols {
availability.insert(symbol.clone(), true);
}
Ok(availability)
}
}
/// Mock trading repository for testing
pub struct MockTradingRepository {
pub trades: Arc<RwLock<HashMap<String, Vec<BacktestTrade>>>>,
pub metrics: Arc<RwLock<HashMap<String, PerformanceMetrics>>>,
pub backtests: Arc<RwLock<Vec<BacktestSummary>>>,
}
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<BacktestTrade>, 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 create_backtest_record(
&self,
backtest_id: &str,
strategy_name: &str,
symbols: &[String],
start_date: DateTime<Utc>,
end_date: DateTime<Utc>,
initial_capital: f64,
_parameters: &HashMap<String, String>,
description: &str,
) -> Result<()> {
let summary = BacktestSummary {
backtest_id: backtest_id.to_string(),
strategy_name: strategy_name.to_string(),
symbols: symbols.to_vec(),
status: BacktestStatus::Queued,
total_return: 0.0,
sharpe_ratio: 0.0,
max_drawdown: 0.0,
created_at: Utc::now(),
start_date,
end_date,
description: description.to_string(),
};
self.backtests.write().await.push(summary);
Ok(())
}
async fn update_backtest_status(
&self,
backtest_id: &str,
status: BacktestStatus,
_error_message: Option<&str>,
) -> Result<()> {
let mut backtests = self.backtests.write().await;
if let Some(bt) = backtests.iter_mut().find(|b| b.backtest_id == backtest_id) {
bt.status = status;
}
Ok(())
}
async fn list_backtests(
&self,
limit: u32,
offset: u32,
strategy_name: Option<String>,
status_filter: Option<BacktestStatus>,
) -> Result<Vec<BacktestSummary>> {
let backtests = self.backtests.read().await;
let filtered: Vec<BacktestSummary> = 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)
}
async fn store_time_series_data(
&self,
_backtest_id: &str,
_timestamp: DateTime<Utc>,
_equity: f64,
_drawdown: f64,
) -> Result<()> {
Ok(())
}
}
/// Mock news repository for testing
pub struct MockNewsRepository {
pub events: Arc<RwLock<Vec<NewsEvent>>>,
}
impl MockNewsRepository {
pub fn new() -> Self {
Self {
events: Arc::new(RwLock::new(Vec::new())),
}
}
pub fn with_events(events: Vec<NewsEvent>) -> 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<Utc>,
end_time: DateTime<Utc>,
) -> Result<Vec<NewsEvent>> {
let events = self.events.read().await;
let filtered: Vec<NewsEvent> = events
.iter()
.filter(|e| {
e.symbols.iter().any(|s| symbols.contains(s))
&& e.timestamp >= start_time
&& e.timestamp <= end_time
})
.cloned()
.collect();
Ok(filtered)
}
async fn get_sentiment_data(
&self,
symbols: &[String],
timestamp: DateTime<Utc>,
lookback_hours: i32,
) -> Result<HashMap<String, f64>> {
let events = self.events.read().await;
let lookback_time = timestamp - chrono::Duration::hours(lookback_hours as i64);
let mut sentiment_map = HashMap::new();
for symbol in symbols {
let sentiment: f64 = events
.iter()
.filter(|e| {
e.symbols.contains(symbol)
&& e.timestamp >= lookback_time
&& e.timestamp <= timestamp
})
.map(|e| e.sentiment)
.sum::<f64>()
/ events.len().max(1) as f64;
sentiment_map.insert(symbol.clone(), sentiment);
}
Ok(sentiment_map)
}
}
/// Mock combined repositories for testing
pub struct MockBacktestingRepositories {
market_data: Box<dyn MarketDataRepository>,
trading: Box<dyn TradingRepository>,
news: Box<dyn NewsRepository>,
}
impl MockBacktestingRepositories {
pub fn new(
market_data: Box<dyn MarketDataRepository>,
trading: Box<dyn TradingRepository>,
news: Box<dyn NewsRepository>,
) -> 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<MarketData> {
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(2000000.0 + (i as f64 * 1000.0))
.unwrap_or(Decimal::ZERO);
data.push(MarketData {
symbol: symbol.to_string(),
timestamp,
open,
high,
low,
close,
volume,
timeframe: TimeFrame::Daily,
});
}
data
}
/// Helper function to generate sample news events
pub fn generate_sample_news_events(
symbols: &[String],
num_events: usize,
) -> Vec<NewsEvent> {
use rand::Rng;
let mut rng = rand::thread_rng();
let mut events = Vec::new();
let start_time = Utc::now() - chrono::Duration::days(30);
for i in 0..num_events {
let timestamp = start_time + chrono::Duration::hours(i as i64 * 24 / num_events as i64);
let symbol_idx = rng.gen_range(0..symbols.len());
let sentiment = rng.gen_range(-1.0..1.0);
let importance = rng.gen_range(0.0..1.0);
events.push(NewsEvent {
id: format!("news_{}", i),
timestamp,
symbols: vec![symbols[symbol_idx].clone()],
title: format!("News event {} for {}", i, symbols[symbol_idx]),
content: format!("Sample news content {}", i),
sentiment,
importance,
source: "mock_source".to_string(),
});
}
events
}
/// Get project root directory
///
/// Resolves the path from the project root, handling different working directories.
#[allow(dead_code)]
pub fn get_project_root() -> String {
// Try to find project root by looking for Cargo.toml
let mut current = std::env::current_dir().unwrap();
// 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.
#[allow(dead_code)]
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
#[allow(dead_code)]
pub async fn create_dbn_repository() -> Result<Box<dyn MarketDataRepository>, 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))
}