//! Simplified Wave Comparison Backtest (Feature Quality Assessment) //! //! This backtest compares Wave C (65 features) vs Wave D (54 features) //! using a simple momentum strategy to evaluate feature quality improvements. //! //! Strategy: Buy when momentum > threshold, sell when momentum < -threshold //! This isolates the impact of feature engineering from model complexity. //! //! Usage: //! cargo run -p ml --example wave_comparison_simple --release use anyhow::Result; use chrono::{DateTime, Utc}; use common::ml_strategy::MLFeatureExtractor; use data::providers::databento::dbn_parser::{DbnParser, ProcessedMessage}; use num_traits::ToPrimitive; use std::path::PathBuf; /// Performance metrics for backtest #[derive(Debug, Clone)] struct PerformanceMetrics { total_trades: usize, winning_trades: usize, win_rate: f64, total_pnl: f64, total_return: f64, sharpe_ratio: f64, max_drawdown: f64, calmar_ratio: f64, profit_factor: f64, } /// Trade record #[derive(Debug, Clone)] struct Trade { entry_time: DateTime, exit_time: DateTime, entry_price: f64, exit_price: f64, side: TradeSide, pnl: f64, } #[derive(Debug, Clone, Copy)] enum TradeSide { Long, Short, } /// Market data bar #[derive(Debug, Clone)] struct MarketBar { timestamp: DateTime, open: f64, high: f64, low: f64, close: f64, volume: f64, } /// Load market data from DBN file fn load_market_data(dbn_path: &PathBuf) -> Result> { println!("šŸ“– Loading market data from: {}", dbn_path.display()); let parser = DbnParser::new().map_err(|e| anyhow::anyhow!("Failed to create DBN parser: {}", e))?; let dbn_bytes = std::fs::read(dbn_path)?; let messages = parser .parse_batch(&dbn_bytes) .map_err(|e| anyhow::anyhow!("Failed to parse DBN file: {}", e))?; let mut bars = Vec::new(); for msg in messages { if let ProcessedMessage::Ohlcv { symbol: _, timestamp, open, high, low, close, volume, } = msg { let ts_secs = (timestamp.as_nanos() / 1_000_000_000) as i64; bars.push(MarketBar { timestamp: DateTime::from_timestamp(ts_secs, 0).unwrap_or_else(|| Utc::now()), open: open.to_f64(), high: high.to_f64(), low: low.to_f64(), close: close.to_f64(), volume: volume.to_f64().unwrap_or(0.0), }); } } bars.sort_by_key(|bar| bar.timestamp); println!("āœ… Loaded {} bars", bars.len()); Ok(bars) } /// Calculate maximum drawdown from equity curve fn calculate_max_drawdown(equity_curve: &[f64]) -> f64 { if equity_curve.is_empty() { return 0.0; } let mut max_drawdown = 0.0; let mut peak = equity_curve[0]; for &equity in equity_curve { if equity > peak { peak = equity; } let drawdown = (peak - equity) / peak; if drawdown > max_drawdown { max_drawdown = drawdown; } } max_drawdown } /// Extract momentum signal from feature vector /// Uses features 5-10 (technical indicators like RSI, MACD) for signal generation fn extract_momentum_signal(features: &[f64]) -> f64 { if features.len() < 10 { return 0.0; } // Combine multiple feature signals // Features 5-10 typically include RSI, MACD, momentum indicators let signal1 = features.get(5).cloned().unwrap_or(0.0); let signal2 = features.get(6).cloned().unwrap_or(0.0); let signal3 = features.get(7).cloned().unwrap_or(0.0); let signal4 = features.get(8).cloned().unwrap_or(0.0); let signal5 = features.get(9).cloned().unwrap_or(0.0); // Average and normalize let avg = (signal1 + signal2 + signal3 + signal4 + signal5) / 5.0; avg.clamp(-1.0, 1.0) } /// Run momentum-based backtest fn run_backtest( feature_extractor: &mut MLFeatureExtractor, market_data: &[MarketBar], initial_capital: f64, wave_name: &str, ) -> Result { println!("\nšŸ”„ Running {} backtest...", wave_name); let mut trades = Vec::new(); let mut position: Option<(TradeSide, f64, DateTime, f64)> = None; let mut equity_curve = vec![initial_capital]; let mut current_capital = initial_capital; // Simple momentum strategy parameters let signal_threshold = 0.15; // Lower threshold for more trades let holding_periods = 20; // Hold for ~20 bars let mut bars_in_position = 0; for bar in market_data.iter() { // Extract features let features = feature_extractor.extract_features(bar.close, bar.volume, bar.timestamp); // Get momentum signal from features let signal = extract_momentum_signal(&features); // Trading logic if position.is_none() && signal.abs() > signal_threshold { // Enter position let side = if signal > 0.0 { TradeSide::Long } else { TradeSide::Short }; let size = (current_capital * 0.1) / bar.close; // 10% of capital position = Some((side, size, bar.timestamp, bar.close)); bars_in_position = 0; } else if let Some((side, size, entry_time, entry_price)) = position { bars_in_position += 1; // Exit logic: signal reversal or holding period exceeded let should_exit = match side { TradeSide::Long => signal < -0.1 || bars_in_position >= holding_periods, TradeSide::Short => signal > 0.1 || bars_in_position >= holding_periods, }; if should_exit { let pnl = match side { TradeSide::Long => size * (bar.close - entry_price), TradeSide::Short => size * (entry_price - bar.close), }; current_capital += pnl; equity_curve.push(current_capital); trades.push(Trade { entry_time, exit_time: bar.timestamp, entry_price, exit_price: bar.close, side, pnl, }); position = None; bars_in_position = 0; } } } // Close any open position at the end if let Some((side, size, entry_time, entry_price)) = position { let last_bar = &market_data[market_data.len() - 1]; let pnl = match side { TradeSide::Long => size * (last_bar.close - entry_price), TradeSide::Short => size * (entry_price - last_bar.close), }; current_capital += pnl; equity_curve.push(current_capital); trades.push(Trade { entry_time, exit_time: last_bar.timestamp, entry_price, exit_price: last_bar.close, side, pnl, }); } // Calculate metrics let total_trades = trades.len(); let winning_trades = trades.iter().filter(|t| t.pnl > 0.0).count(); let win_rate = if total_trades > 0 { (winning_trades as f64 / total_trades as f64) * 100.0 } else { 0.0 }; let total_pnl: f64 = trades.iter().map(|t| t.pnl).sum(); let total_return = (current_capital - initial_capital) / initial_capital * 100.0; // Sharpe ratio (annualized) let returns: Vec = trades.iter().map(|t| t.pnl / initial_capital).collect(); let sharpe_ratio = if !returns.is_empty() { let mean_return = returns.iter().sum::() / returns.len() as f64; let variance = returns .iter() .map(|r| (r - mean_return).powi(2)) .sum::() / returns.len() as f64; let std_dev = variance.sqrt(); if std_dev > 0.0 { (mean_return / std_dev) * (252.0_f64).sqrt() } else { 0.0 } } else { 0.0 }; // Max drawdown let max_drawdown = calculate_max_drawdown(&equity_curve) * 100.0; // Calmar ratio let calmar_ratio = if max_drawdown > 0.0 { total_return / max_drawdown } else { 0.0 }; // Profit factor let gross_profit: f64 = trades.iter().filter(|t| t.pnl > 0.0).map(|t| t.pnl).sum(); let gross_loss: f64 = trades .iter() .filter(|t| t.pnl < 0.0) .map(|t| t.pnl.abs()) .sum(); let profit_factor = if gross_loss > 0.0 { gross_profit / gross_loss } else if gross_profit > 0.0 { f64::INFINITY } else { 0.0 }; Ok(PerformanceMetrics { total_trades, winning_trades, win_rate, total_pnl, total_return, sharpe_ratio, max_drawdown, calmar_ratio, profit_factor, }) } fn print_metrics(metrics: &PerformanceMetrics, wave_name: &str) { println!("\n{}", "=".repeat(70)); println!("šŸ“ˆ {} RESULTS", wave_name.to_uppercase()); println!("{}", "=".repeat(70)); println!("\nšŸ’° Performance Metrics:"); println!(" Total Trades: {}", metrics.total_trades); println!(" Winning Trades: {}", metrics.winning_trades); println!(" Win Rate: {:.2}%", metrics.win_rate); println!(" Total PnL: ${:.2}", metrics.total_pnl); println!(" Total Return: {:.2}%", metrics.total_return); println!(" Sharpe Ratio: {:.2}", metrics.sharpe_ratio); println!(" Max Drawdown: {:.2}%", metrics.max_drawdown); println!(" Calmar Ratio: {:.2}", metrics.calmar_ratio); println!(" Profit Factor: {:.2}", metrics.profit_factor); } fn print_comparison(wave_c: &PerformanceMetrics, wave_d: &PerformanceMetrics) { println!("\n{}", "=".repeat(70)); println!("šŸ“Š WAVE C vs WAVE D COMPARISON"); println!("{}", "=".repeat(70)); let sharpe_improvement = ((wave_d.sharpe_ratio - wave_c.sharpe_ratio) / wave_c.sharpe_ratio.max(0.01)) * 100.0; let win_rate_improvement = wave_d.win_rate - wave_c.win_rate; let drawdown_improvement = ((wave_c.max_drawdown - wave_d.max_drawdown) / wave_c.max_drawdown.max(0.01)) * 100.0; let return_improvement = wave_d.total_return - wave_c.total_return; println!("\nšŸŽÆ Key Improvements:"); println!( " Sharpe Ratio: {:.2} → {:.2} ({:+.1}%)", wave_c.sharpe_ratio, wave_d.sharpe_ratio, sharpe_improvement ); println!( " Win Rate: {:.2}% → {:.2}% ({:+.1}pp)", wave_c.win_rate, wave_d.win_rate, win_rate_improvement ); println!( " Max Drawdown: {:.2}% → {:.2}% ({:+.1}%)", wave_c.max_drawdown, wave_d.max_drawdown, drawdown_improvement ); println!( " Total Return: {:.2}% → {:.2}% ({:+.2}pp)", wave_c.total_return, wave_d.total_return, return_improvement ); println!("\nāœ… Target Validation:"); println!( " Sharpe ≄ 2.0: {} (actual: {:.2})", if wave_d.sharpe_ratio >= 2.0 { "āœ… PASS" } else { "āŒ FAIL" }, wave_d.sharpe_ratio ); println!( " Win Rate ≄ 60%: {} (actual: {:.2}%)", if wave_d.win_rate >= 60.0 { "āœ… PASS" } else { "āŒ FAIL" }, wave_d.win_rate ); println!( " Drawdown ≤ 15%: {} (actual: {:.2}%)", if wave_d.max_drawdown <= 15.0 { "āœ… PASS" } else { "āŒ FAIL" }, wave_d.max_drawdown ); let all_targets_met = wave_d.sharpe_ratio >= 2.0 && wave_d.win_rate >= 60.0 && wave_d.max_drawdown <= 15.0; println!( "\n{}", if all_targets_met { "šŸŽ‰ ALL TARGETS MET - PRODUCTION READY!" } else { "āš ļø Some targets not met - further optimization needed" } ); } fn main() -> Result<()> { println!("\n{}", "=".repeat(70)); println!("šŸš€ WAVE COMPARISON BACKTEST (Simplified Feature Quality Assessment)"); println!("{}\n", "=".repeat(70)); let data_path = PathBuf::from( "/home/jgrusewski/Work/foxhunt/test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn", ); let initial_capital = 100000.0; // Load market data once let market_data = load_market_data(&data_path)?; println!("šŸ“Š Backtest Configuration:"); println!(" Symbol: ES.FUT"); println!(" Bars: {}", market_data.len()); println!(" Initial Capital: ${:.2}", initial_capital); println!(" Strategy: Simple Momentum (Feature Quality Test)"); // Run Wave C backtest (65 features) let mut wave_c_extractor = MLFeatureExtractor::new_wave_c(20); let wave_c_metrics = run_backtest( &mut wave_c_extractor, &market_data, initial_capital, "Wave C (65 features)", )?; print_metrics(&wave_c_metrics, "Wave C (65 features)"); // Run Wave D backtest (54 features) let mut wave_d_extractor = MLFeatureExtractor::new_wave_d(20); let wave_d_metrics = run_backtest( &mut wave_d_extractor, &market_data, initial_capital, "Wave D (54 features)", )?; print_metrics(&wave_d_metrics, "Wave D (54 features)"); // Print comparison print_comparison(&wave_c_metrics, &wave_d_metrics); println!("\n{}", "=".repeat(70)); Ok(()) }