Files
foxhunt/services/backtesting_service/tests/ma_crossover_multi_symbol_tests.rs
jgrusewski e8a68ee39f Download 360 DBN files (36.3 MB) using Rust databento client
- Created data/examples/download_ml_training_data.rs using reqwest + Databento HTTP API
- Downloaded 90 days × 4 symbols (ES.FUT, NQ.FUT, ZN.FUT, 6E.FUT)
- Files saved to test_data/real/databento/ml_training/
- Total: 360 files, 15 MB compressed DBN format
- Used existing Rust pattern from download_nq_fut.rs
- API key loaded from .env file
- 100% success rate (360/360 files)
- Ready for ML training benchmarks

Next: Create simplified training benchmark for RTX 3050 Ti GPU measurements
2025-10-13 13:30:02 +02:00

485 lines
19 KiB
Rust

//! Moving Average Crossover Strategy Multi-Symbol Backtests
//!
//! Comprehensive backtesting of MA crossover strategy across 5 diverse symbols:
//! - 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
//!
//! Strategy Parameters:
//! - Fast MA: 10 periods
//! - Slow MA: 50 periods
//! - Signal: Buy when fast > slow, Sell when fast < slow
use anyhow::Result;
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal::prelude::ToPrimitive;
use std::collections::HashMap;
use std::sync::Arc;
mod mock_repositories;
use backtesting_service::dbn_repository::DbnMarketDataRepository;
use backtesting_service::repositories::{BacktestingRepositories, MarketDataRepository, NewsRepository, TradingRepository};
use backtesting_service::service::BacktestContext;
use backtesting_service::strategy_engine::{MarketData, StrategyEngine, TimeFrame, TradeSide, StrategyExecutor, TradeSignal};
use backtesting_service::performance::{PerformanceMetrics, PerformanceAnalyzer};
use config::structures::BacktestingPerformanceConfig;
use config::structures::BacktestingStrategyConfig;
use mock_repositories::*;
/// Helper to get multi-symbol file mappings
fn get_multi_symbol_file_mapping() -> HashMap<String, String> {
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)
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)
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)
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
}
/// Real Moving Average Crossover Strategy with proper technical analysis
#[derive(Debug)]
struct RealMaCrossoverStrategy {
fast_period: usize,
slow_period: usize,
price_history: std::sync::RwLock<HashMap<String, Vec<f64>>>,
}
impl RealMaCrossoverStrategy {
fn new(fast_period: usize, slow_period: usize) -> Self {
Self {
fast_period,
slow_period,
price_history: std::sync::RwLock::new(HashMap::new()),
}
}
fn calculate_sma(prices: &[f64], period: usize) -> Option<f64> {
if prices.len() < period {
return None;
}
let sum: f64 = prices.iter().rev().take(period).sum();
Some(sum / period as f64)
}
}
impl StrategyExecutor for RealMaCrossoverStrategy {
fn execute(
&self,
market_data: &MarketData,
portfolio: &backtesting_service::strategy_engine::Portfolio,
_parameters: &HashMap<String, String>,
) -> Result<Vec<TradeSignal>> {
let mut signals = Vec::new();
// Update price history
let current_price = market_data.close.to_f64().unwrap_or(0.0);
{
let mut history = self.price_history.write().unwrap();
let prices = history.entry(market_data.symbol.clone()).or_insert_with(Vec::new);
prices.push(current_price);
}
// Read price history
let history = self.price_history.read().unwrap();
let prices = history.get(&market_data.symbol);
if let Some(prices) = prices {
// Need enough data for slow MA
if prices.len() < self.slow_period {
return Ok(signals);
}
// Calculate MAs
let fast_ma = Self::calculate_sma(prices, self.fast_period);
let slow_ma = Self::calculate_sma(prices, self.slow_period);
if let (Some(fast), Some(slow)) = (fast_ma, slow_ma) {
// Get previous MAs for crossover detection
if prices.len() > self.slow_period {
let prev_prices = &prices[..prices.len() - 1];
let prev_fast_ma = Self::calculate_sma(prev_prices, self.fast_period);
let prev_slow_ma = Self::calculate_sma(prev_prices, self.slow_period);
if let (Some(prev_fast), Some(prev_slow)) = (prev_fast_ma, prev_slow_ma) {
let position = portfolio.get_position(&market_data.symbol);
// Bullish crossover: fast crosses above slow
if prev_fast <= prev_slow && fast > slow && position.is_none() {
// Calculate position size (10% of capital per position)
let allocation = 0.10;
let cash = portfolio.cash().to_f64().unwrap_or(0.0);
let position_value = cash * allocation;
let quantity = Decimal::from_f64_retain(position_value / current_price)
.unwrap_or(Decimal::ZERO);
if quantity > Decimal::ZERO {
signals.push(TradeSignal {
symbol: market_data.symbol.clone(),
side: TradeSide::Buy,
quantity,
strength: Decimal::from_f64_retain(0.85).unwrap_or(Decimal::ZERO),
reason: format!(
"MA Crossover BUY: fast={:.2} > slow={:.2}",
fast, slow
),
features: None,
news_events: None,
});
}
}
// Bearish crossover: fast crosses below slow
else if prev_fast >= prev_slow && fast < slow && position.is_some() {
if let Some(pos) = position {
signals.push(TradeSignal {
symbol: market_data.symbol.clone(),
side: TradeSide::Sell,
quantity: pos.quantity,
strength: Decimal::from_f64_retain(0.85).unwrap_or(Decimal::ZERO),
reason: format!(
"MA Crossover SELL: fast={:.2} < slow={:.2}",
fast, slow
),
features: None,
news_events: None,
});
}
}
}
}
}
}
Ok(signals)
}
fn name(&self) -> &str {
"ma_crossover_10_50"
}
}
/// Create repositories with DBN data source
async fn create_repositories() -> Result<Arc<dyn BacktestingRepositories>> {
let file_mapping = get_multi_symbol_file_mapping();
let market_data_repo = Box::new(DbnMarketDataRepository::new(file_mapping).await?) as Box<dyn MarketDataRepository>;
let trading_repo = Box::new(MockTradingRepository::new()) as Box<dyn TradingRepository>;
let news_repo = Box::new(MockNewsRepository::new()) as Box<dyn NewsRepository>;
Ok(Arc::new(MockBacktestingRepositories::new(
market_data_repo,
trading_repo,
news_repo,
)))
}
/// Helper to run backtest for a symbol
async fn run_backtest_for_symbol(
symbol: &str,
initial_capital: f64,
) -> Result<(Vec<backtesting_service::strategy_engine::BacktestTrade>, PerformanceMetrics)> {
let repositories = create_repositories().await?;
// Create custom strategy engine with MA crossover
let config = BacktestingStrategyConfig::default();
let mut engine = StrategyEngine::new(&config, repositories.clone()).await?;
// Register custom MA strategy
let ma_strategy = Box::new(RealMaCrossoverStrategy::new(10, 50));
engine.register_strategy("ma_crossover_10_50".to_string(), ma_strategy);
// Backtest period: Jan 2-3, 2024 (using available data)
let start_time = DateTime::parse_from_rfc3339("2024-01-02T00:00:00Z")?.with_timezone(&Utc);
let end_time = DateTime::parse_from_rfc3339("2024-01-03T00:00:00Z")?.with_timezone(&Utc);
let context = BacktestContext {
id: format!("ma_crossover_{}", symbol.replace(".", "_")),
status: backtesting_service::foxhunt::tli::BacktestStatus::Running,
progress: 0.0,
current_date: start_time.format("%Y-%m-%d").to_string(),
trades_executed: 0,
current_pnl: 0.0,
started_at: start_time.timestamp_nanos_opt().unwrap_or(0),
completed_at: Some(end_time.timestamp_nanos_opt().unwrap_or(0)),
error_message: None,
strategy_name: "ma_crossover_10_50".to_string(),
symbols: vec![symbol.to_string()],
initial_capital,
parameters: HashMap::new(),
};
let trades = engine.execute_backtest(&context).await?;
// Calculate performance metrics
let perf_config = BacktestingPerformanceConfig::default();
let analyzer = PerformanceAnalyzer::new(&perf_config)?;
let metrics = analyzer.calculate_metrics(&trades, initial_capital);
Ok((trades, metrics))
}
/// Helper to print metrics summary
fn print_metrics_summary(symbol: &str, trades: &[backtesting_service::strategy_engine::BacktestTrade], metrics: &PerformanceMetrics) {
println!("\n============================================================");
println!(" {} - MA Crossover (10/50) Results", symbol);
println!("============================================================");
println!(" Total Trades: {}", trades.len());
println!(" Total Return: {:.2}%", metrics.total_return * 100.0);
println!(" Sharpe Ratio: {:.3}", metrics.sharpe_ratio);
println!(" Max Drawdown: {:.2}%", metrics.max_drawdown * 100.0);
println!(" Win Rate: {:.2}%", metrics.win_rate * 100.0);
println!(" Profit Factor: {:.3}", metrics.profit_factor);
if trades.len() > 0 {
let winning_trades = trades.iter().filter(|t| t.pnl > Decimal::ZERO).count();
let losing_trades = trades.iter().filter(|t| t.pnl < Decimal::ZERO).count();
let avg_win = if winning_trades > 0 {
trades.iter()
.filter(|t| t.pnl > Decimal::ZERO)
.map(|t| t.pnl.to_f64().unwrap_or(0.0))
.sum::<f64>() / winning_trades as f64
} else {
0.0
};
let avg_loss = if losing_trades > 0 {
trades.iter()
.filter(|t| t.pnl < Decimal::ZERO)
.map(|t| t.pnl.to_f64().unwrap_or(0.0))
.sum::<f64>() / losing_trades as f64
} else {
0.0
};
println!(" Winning Trades: {}", winning_trades);
println!(" Losing Trades: {}", losing_trades);
println!(" Avg Win: ${:.2}", avg_win);
println!(" Avg Loss: ${:.2}", avg_loss);
}
println!("============================================================\n");
}
// ============================================================================
// INDIVIDUAL SYMBOL TESTS
// ============================================================================
#[tokio::test]
async fn test_ma_crossover_es_fut() -> Result<()> {
let (trades, metrics) = run_backtest_for_symbol("ES.FUT", 100000.0).await?;
print_metrics_summary("ES.FUT", &trades, &metrics);
// Basic validation
assert!(trades.len() >= 0, "Should execute some trades or none");
assert!(metrics.sharpe_ratio.is_finite(), "Sharpe ratio should be finite");
Ok(())
}
#[tokio::test]
async fn test_ma_crossover_nq_fut() -> Result<()> {
let (trades, metrics) = run_backtest_for_symbol("NQ.FUT", 100000.0).await?;
print_metrics_summary("NQ.FUT", &trades, &metrics);
assert!(trades.len() >= 0, "Should execute some trades or none");
assert!(metrics.sharpe_ratio.is_finite(), "Sharpe ratio should be finite");
Ok(())
}
#[tokio::test]
async fn test_ma_crossover_zn_fut() -> Result<()> {
let (trades, metrics) = run_backtest_for_symbol("ZN.FUT", 100000.0).await?;
print_metrics_summary("ZN.FUT", &trades, &metrics);
assert!(trades.len() >= 0, "Should execute some trades or none");
assert!(metrics.sharpe_ratio.is_finite(), "Sharpe ratio should be finite");
Ok(())
}
#[tokio::test]
async fn test_ma_crossover_6e_fut() -> Result<()> {
let (trades, metrics) = run_backtest_for_symbol("6E.FUT", 100000.0).await?;
print_metrics_summary("6E.FUT", &trades, &metrics);
assert!(trades.len() >= 0, "Should execute some trades or none");
assert!(metrics.sharpe_ratio.is_finite(), "Sharpe ratio should be finite");
Ok(())
}
#[tokio::test]
async fn test_ma_crossover_gc() -> Result<()> {
let (trades, metrics) = run_backtest_for_symbol("GC", 100000.0).await?;
print_metrics_summary("GC", &trades, &metrics);
assert!(trades.len() >= 0, "Should execute some trades or none");
assert!(metrics.sharpe_ratio.is_finite(), "Sharpe ratio should be finite");
Ok(())
}
// ============================================================================
// MULTI-SYMBOL TESTS
// ============================================================================
#[tokio::test]
async fn test_ma_crossover_multi_symbol() -> Result<()> {
let symbols = vec!["ES.FUT", "NQ.FUT", "ZN.FUT"];
let initial_capital = 300000.0; // $100k per symbol
let repositories = create_repositories().await?;
let config = BacktestingStrategyConfig::default();
let mut engine = StrategyEngine::new(&config, repositories.clone()).await?;
let ma_strategy = Box::new(RealMaCrossoverStrategy::new(10, 50));
engine.register_strategy("ma_crossover_10_50".to_string(), ma_strategy);
let start_time = DateTime::parse_from_rfc3339("2024-01-02T00:00:00Z")?.with_timezone(&Utc);
let end_time = DateTime::parse_from_rfc3339("2024-01-03T00:00:00Z")?.with_timezone(&Utc);
let context = BacktestContext {
id: "ma_crossover_multi_symbol".to_string(),
status: backtesting_service::foxhunt::tli::BacktestStatus::Running,
progress: 0.0,
current_date: start_time.format("%Y-%m-%d").to_string(),
trades_executed: 0,
current_pnl: 0.0,
started_at: start_time.timestamp_nanos_opt().unwrap_or(0),
completed_at: Some(end_time.timestamp_nanos_opt().unwrap_or(0)),
error_message: None,
strategy_name: "ma_crossover_10_50".to_string(),
symbols: symbols.iter().map(|s| s.to_string()).collect(),
initial_capital,
parameters: HashMap::new(),
};
let trades = engine.execute_backtest(&context).await?;
let perf_config = BacktestingPerformanceConfig::default();
let analyzer = PerformanceAnalyzer::new(&perf_config)?;
let metrics = analyzer.calculate_metrics(&trades, initial_capital);
println!("\n============================================================");
println!(" MULTI-SYMBOL PORTFOLIO - MA Crossover (10/50)");
println!("============================================================");
println!(" Symbols: {:?}", symbols);
println!(" Initial Capital: ${:.2}", initial_capital);
println!(" Total Trades: {}", trades.len());
println!(" Total Return: {:.2}%", metrics.total_return * 100.0);
println!(" Sharpe Ratio: {:.3}", metrics.sharpe_ratio);
println!(" Max Drawdown: {:.2}%", metrics.max_drawdown * 100.0);
println!(" Win Rate: {:.2}%", metrics.win_rate * 100.0);
// Trades per symbol
for symbol in &symbols {
let symbol_trades = trades.iter().filter(|t| t.symbol == *symbol).count();
println!(" {} trades: {}", symbol, symbol_trades);
}
println!("============================================================\n");
assert!(trades.len() >= 0, "Should execute trades across multiple symbols");
Ok(())
}
#[tokio::test]
async fn test_ma_crossover_performance_comparison() -> Result<()> {
println!("\n============================================================");
println!(" PERFORMANCE COMPARISON ACROSS ASSET CLASSES");
println!("============================================================\n");
let symbols = vec![
("ES.FUT", "Equity Futures"),
("NQ.FUT", "Tech Futures"),
("GC", "Gold Commodity"),
("ZN.FUT", "Treasury Futures"),
("6E.FUT", "FX Futures"),
];
let initial_capital = 100000.0;
let mut results = Vec::new();
for (symbol, description) in &symbols {
match run_backtest_for_symbol(symbol, initial_capital).await {
Ok((trades, metrics)) => {
results.push((symbol.to_string(), description.to_string(), trades.len(), metrics));
}
Err(e) => {
eprintln!("Warning: Failed to backtest {}: {}", symbol, e);
}
}
}
// Sort by Sharpe ratio
results.sort_by(|a, b| b.3.sharpe_ratio.partial_cmp(&a.3.sharpe_ratio).unwrap_or(std::cmp::Ordering::Equal));
println!(" Ranking by Sharpe Ratio:");
println!(" --------------------------------------------------------");
println!(" Rank Symbol Asset Class Trades Return% Sharpe");
println!(" --------------------------------------------------------");
for (i, (symbol, desc, trades, metrics)) in results.iter().enumerate() {
println!(
" {:2} {:8} {:16} {:4} {:6.2}% {:6.3}",
i + 1,
symbol,
desc,
trades,
metrics.total_return * 100.0,
metrics.sharpe_ratio
);
}
println!(" --------------------------------------------------------\n");
// Find best and worst performers
if results.len() > 0 {
let best = &results[0];
let worst = &results[results.len() - 1];
println!(" Best Performer: {} ({}) - Sharpe: {:.3}", best.0, best.1, best.3.sharpe_ratio);
println!(" Worst Performer: {} ({}) - Sharpe: {:.3}", worst.0, worst.1, worst.3.sharpe_ratio);
println!("\n============================================================\n");
}
assert!(results.len() > 0, "Should have at least one successful backtest");
Ok(())
}