//! Wave Comparison Backtesting Module //! //! Validates performance improvements across Wave A, Wave B, and Wave C: //! - Wave A: 26 features (7 technical indicators + 3 microstructure) //! - Wave B: 26 features + alternative bars (tick, volume, dollar, imbalance, run) //! - Wave C: 201 features (comprehensive feature extraction pipeline) //! - Wave D: 225 features (regime detection + adaptive strategies) //! //! This module provides systematic backtesting to measure: //! - Win rate improvements //! - Sharpe ratio gains //! - Sortino ratio enhancements //! - Maximum drawdown reduction //! - Total PnL improvements use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tracing::info; use crate::repositories::BacktestingRepositories; use crate::strategy_engine::MarketData; /// Wave comparison backtest results #[derive(Debug, Serialize, Deserialize)] pub struct WaveComparisonResults { /// Symbol backtested pub symbol: String, /// Date range used pub date_range: DateRange, /// Wave A performance (26 features, baseline) pub wave_a: WavePerformanceMetrics, /// Wave B performance (26 features + alternative bars) pub wave_b: WavePerformanceMetrics, /// Wave C performance (201 features) pub wave_c: WavePerformanceMetrics, /// Wave D performance (225 features, regime detection + adaptive strategies) pub wave_d: WavePerformanceMetrics, /// Improvement matrix (percentage gains) pub improvements: ImprovementMatrix, /// Execution metadata pub metadata: BacktestMetadata, } /// Date range for backtesting #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DateRange { /// Start date pub start: DateTime, /// End date pub end: DateTime, } /// Performance metrics for a specific wave #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WavePerformanceMetrics { /// Wave identifier (A, B, C) pub wave_id: String, /// Feature count used pub feature_count: usize, /// Win rate (0.0-1.0) pub win_rate: f64, /// Sharpe ratio pub sharpe_ratio: f64, /// Sortino ratio pub sortino_ratio: f64, /// Maximum drawdown (0.0-1.0) pub max_drawdown: f64, /// Total number of trades pub total_trades: usize, /// Average PnL per trade pub avg_pnl: f64, /// Total PnL pub total_pnl: f64, /// Volatility (annualized) pub volatility: f64, /// Profit factor (total wins / total losses) pub profit_factor: f64, /// Average trade duration (seconds) pub avg_trade_duration_secs: f64, /// Best trade PnL pub best_trade: f64, /// Worst trade PnL pub worst_trade: f64, } /// Improvement matrix comparing waves #[derive(Debug, Serialize, Deserialize)] pub struct ImprovementMatrix { // --- Wave A to Wave B improvements --- /// Win rate: A to B (percentage improvement) pub a_to_b_win_rate: f64, /// Win rate: A to C (percentage improvement) pub a_to_c_win_rate: f64, /// Win rate: B to C (percentage improvement) pub b_to_c_win_rate: f64, /// Sharpe: A to B (absolute improvement) pub a_to_b_sharpe: f64, /// Sharpe: A to C (absolute improvement) pub a_to_c_sharpe: f64, /// Sharpe: B to C (absolute improvement) pub b_to_c_sharpe: f64, /// Sortino: A to B (absolute improvement) pub a_to_b_sortino: f64, /// Sortino: A to C (absolute improvement) pub a_to_c_sortino: f64, /// Sortino: B to C (absolute improvement) pub b_to_c_sortino: f64, /// Max Drawdown: A to B (percentage reduction, positive = better) pub a_to_b_drawdown: f64, /// Max Drawdown: A to C (percentage reduction, positive = better) pub a_to_c_drawdown: f64, /// Max Drawdown: B to C (percentage reduction, positive = better) pub b_to_c_drawdown: f64, // --- Wave D improvements --- /// Win rate: A to D (percentage improvement) pub a_to_d_win_rate: f64, /// Win rate: C to D (percentage improvement) pub c_to_d_win_rate: f64, /// Sharpe: A to D (absolute improvement) pub a_to_d_sharpe: f64, /// Sharpe: C to D (absolute improvement) pub c_to_d_sharpe: f64, /// Sortino: A to D (absolute improvement) pub a_to_d_sortino: f64, /// Sortino: C to D (absolute improvement) pub c_to_d_sortino: f64, /// Max Drawdown: A to D (percentage reduction, positive = better) pub a_to_d_drawdown: f64, /// Max Drawdown: C to D (percentage reduction, positive = better) pub c_to_d_drawdown: f64, // --- PnL improvements --- /// Total PnL: A to B (percentage improvement) pub a_to_b_pnl: f64, /// Total PnL: A to C (percentage improvement) pub a_to_c_pnl: f64, /// Total PnL: B to C (percentage improvement) pub b_to_c_pnl: f64, /// Total PnL: A to D (percentage improvement) pub a_to_d_pnl: f64, /// Total PnL: C to D (percentage improvement) pub c_to_d_pnl: f64, } /// Backtest execution metadata #[derive(Debug, Serialize, Deserialize)] pub struct BacktestMetadata { /// Execution timestamp pub execution_time: DateTime, /// Total backtest duration (milliseconds) pub duration_ms: u64, /// Number of bars processed pub bars_processed: usize, /// Initial capital pub initial_capital: f64, /// Strategy configuration used pub strategy_config: String, } /// Wave comparison backtest engine pub struct WaveComparisonBacktest { /// Repository access _repositories: Arc, /// Initial capital for backtesting initial_capital: f64, } impl WaveComparisonBacktest { /// Create new wave comparison backtest engine pub fn new(repositories: Arc, initial_capital: f64) -> Self { Self { _repositories: repositories, initial_capital, } } /// Run comprehensive wave comparison backtest pub async fn run_comparison( &self, symbol: &str, date_range: DateRange, ) -> Result { info!("🔬 Starting Wave Comparison Backtest (Wave A/B/C/D)"); info!(" Symbol: {}", symbol); info!(" Period: {} to {}", date_range.start, date_range.end); info!(" Initial Capital: ${:.2}", self.initial_capital); let start_time = std::time::Instant::now(); // Step 1: Load market data info!("\n📊 Loading market data..."); let market_data = self.load_market_data(symbol, &date_range).await?; info!(" Loaded {} bars", market_data.len()); // Step 2: Run Wave A backtest (26 features, baseline) info!("\n📊 Testing Wave A (26 features - baseline)..."); let wave_a = self .run_wave_backtest(symbol, &market_data, "A", 26) .await?; // Step 3: Run Wave B backtest (36 features: 26 base + 10 alternative bars) info!("\n📊 Testing Wave B (26 features + alternative bars)..."); let wave_b = self .run_wave_backtest( symbol, &market_data, "B", 36, // Wave B: 26 base + 10 alternative bars ) .await?; // Step 4: Run Wave C backtest (201 features) info!("\n📊 Testing Wave C (201 features)..."); let wave_c = self .run_wave_backtest( symbol, &market_data, "C", 201, // Wave C: 201 features ) .await?; // Step 5: Run Wave D backtest (225 features: 201 Wave C + 24 regime detection) info!("\n📊 Testing Wave D (225 features: 201 Wave C + 24 regime detection)..."); let wave_d = self .run_wave_backtest( symbol, &market_data, "D", 225, // Wave D: 201 Wave C + 24 regime detection ) .await?; // Step 6: Calculate improvements let improvements = self.calculate_improvements(&wave_a, &wave_b, &wave_c, &wave_d); let duration_ms = start_time.elapsed().as_millis() as u64; let metadata = BacktestMetadata { execution_time: Utc::now(), duration_ms, bars_processed: market_data.len(), initial_capital: self.initial_capital, strategy_config: "wave_comparison_v1".to_string(), }; Ok(WaveComparisonResults { symbol: symbol.to_string(), date_range, wave_a, wave_b, wave_c, wave_d, improvements, metadata, }) } /// Load market data for backtesting. /// /// Wiring plan: To connect to real data, `WaveComparisonBacktest` needs a /// `DbnDataSource` field (or access via `BacktestingRepositories`). The caller /// would construct a `DbnDataSource` with symbol-to-file mappings and pass it in. /// Example integration: /// /// ```rust,ignore /// // Add field: dbn_source: Arc /// let bars = self.dbn_source.load_ohlcv_bars(symbol).await?; /// // Then filter bars by date_range.start..=date_range.end /// ``` /// /// Currently returns an empty vec so callers can exercise the improvement-matrix /// logic without a live data source. async fn load_market_data( &self, _symbol: &str, _date_range: &DateRange, ) -> Result> { info!("load_market_data: no DbnDataSource wired yet; returning empty dataset"); Ok(vec![]) } /// Run backtest for a specific wave. /// /// Wiring plan: To connect to the real strategy engine, add a `StrategyEngine` /// field (constructed with `BacktestingRepositories` + `BacktestingStrategyConfig`). /// Per-wave execution would configure the feature extractor for the appropriate /// feature count, then call `engine.run_backtest(symbol, market_data, config)`. /// Example integration: /// /// ```rust,ignore /// // Add field: strategy_engine: Arc /// let config = wave_config_for(wave_id, feature_count); /// let result = self.strategy_engine.run_backtest(symbol, market_data, &config).await?; /// // Convert StrategyEngine::BacktestResult -> WavePerformanceMetrics /// ``` /// /// Currently returns design-target placeholder metrics so the comparison and /// export logic can be exercised without a live strategy engine. async fn run_wave_backtest( &self, _symbol: &str, _market_data: &[MarketData], wave_id: &str, feature_count: usize, ) -> Result { let (win_rate, sharpe, sortino, max_dd, pnl) = match wave_id { "A" => { // Wave A baseline (from investigation reports) (0.418, -6.52, -5.5, 0.25, -5000.0) }, "B" => { // Wave B target: +15-25% win rate, +1.5 Sharpe (conservative) (0.48, -5.0, -4.2, 0.22, 1000.0) }, "C" => { // Wave C target: +10-15% win rate, +50% Sharpe (201 features) (0.55, 1.5, 2.0, 0.18, 5000.0) }, "D" => { // Wave D target: +25-50% Sharpe improvement via regime detection // Expected metrics: win rate 60%, Sharpe 2.0, Sortino 2.5 // Based on Wave D Phase 6 production targets (CLAUDE.md) (0.60, 2.0, 2.5, 0.15, 7500.0) }, _ => (0.418, -6.52, -5.5, 0.25, -5000.0), }; let total_trades = match wave_id { "A" => 100, "B" => 120, // More trades with alternative bars "C" => 150, // Even more trades with 201 features "D" => 180, // Most trades with 225 features + regime detection _ => 100, }; let avg_pnl = pnl / total_trades as f64; let profit_factor = if pnl > 0.0 { 1.5 } else { 0.8 }; Ok(WavePerformanceMetrics { wave_id: wave_id.to_string(), feature_count, win_rate, sharpe_ratio: sharpe, sortino_ratio: sortino, max_drawdown: max_dd, total_trades, avg_pnl, total_pnl: pnl, volatility: 0.25, // 25% annualized profit_factor, avg_trade_duration_secs: 3600.0, // 1 hour average best_trade: pnl.abs() * 0.1, // 10% of total as best trade worst_trade: -pnl.abs() * 0.08, // 8% of total as worst trade }) } /// Calculate improvement matrix fn calculate_improvements( &self, wave_a: &WavePerformanceMetrics, wave_b: &WavePerformanceMetrics, wave_c: &WavePerformanceMetrics, wave_d: &WavePerformanceMetrics, ) -> ImprovementMatrix { ImprovementMatrix { // --- Win rate improvements (percentage) --- a_to_b_win_rate: ((wave_b.win_rate - wave_a.win_rate) / wave_a.win_rate) * 100.0, a_to_c_win_rate: ((wave_c.win_rate - wave_a.win_rate) / wave_a.win_rate) * 100.0, b_to_c_win_rate: ((wave_c.win_rate - wave_b.win_rate) / wave_b.win_rate) * 100.0, a_to_d_win_rate: ((wave_d.win_rate - wave_a.win_rate) / wave_a.win_rate) * 100.0, c_to_d_win_rate: ((wave_d.win_rate - wave_c.win_rate) / wave_c.win_rate) * 100.0, // --- Sharpe improvements (absolute) --- a_to_b_sharpe: wave_b.sharpe_ratio - wave_a.sharpe_ratio, a_to_c_sharpe: wave_c.sharpe_ratio - wave_a.sharpe_ratio, b_to_c_sharpe: wave_c.sharpe_ratio - wave_b.sharpe_ratio, a_to_d_sharpe: wave_d.sharpe_ratio - wave_a.sharpe_ratio, c_to_d_sharpe: wave_d.sharpe_ratio - wave_c.sharpe_ratio, // --- Sortino improvements (absolute) --- a_to_b_sortino: wave_b.sortino_ratio - wave_a.sortino_ratio, a_to_c_sortino: wave_c.sortino_ratio - wave_a.sortino_ratio, b_to_c_sortino: wave_c.sortino_ratio - wave_b.sortino_ratio, a_to_d_sortino: wave_d.sortino_ratio - wave_a.sortino_ratio, c_to_d_sortino: wave_d.sortino_ratio - wave_c.sortino_ratio, // --- Drawdown improvements (percentage reduction, positive = better) --- a_to_b_drawdown: ((wave_a.max_drawdown - wave_b.max_drawdown) / wave_a.max_drawdown) * 100.0, a_to_c_drawdown: ((wave_a.max_drawdown - wave_c.max_drawdown) / wave_a.max_drawdown) * 100.0, b_to_c_drawdown: ((wave_b.max_drawdown - wave_c.max_drawdown) / wave_b.max_drawdown) * 100.0, a_to_d_drawdown: ((wave_a.max_drawdown - wave_d.max_drawdown) / wave_a.max_drawdown) * 100.0, c_to_d_drawdown: ((wave_c.max_drawdown - wave_d.max_drawdown) / wave_c.max_drawdown) * 100.0, // --- PnL improvements (percentage) --- a_to_b_pnl: if wave_a.total_pnl != 0.0 { ((wave_b.total_pnl - wave_a.total_pnl) / wave_a.total_pnl.abs()) * 100.0 } else { 0.0 }, a_to_c_pnl: if wave_a.total_pnl != 0.0 { ((wave_c.total_pnl - wave_a.total_pnl) / wave_a.total_pnl.abs()) * 100.0 } else { 0.0 }, b_to_c_pnl: if wave_b.total_pnl != 0.0 { ((wave_c.total_pnl - wave_b.total_pnl) / wave_b.total_pnl.abs()) * 100.0 } else { 0.0 }, a_to_d_pnl: if wave_a.total_pnl != 0.0 { ((wave_d.total_pnl - wave_a.total_pnl) / wave_a.total_pnl.abs()) * 100.0 } else { 0.0 }, c_to_d_pnl: if wave_c.total_pnl != 0.0 { ((wave_d.total_pnl - wave_c.total_pnl) / wave_c.total_pnl.abs()) * 100.0 } else { 0.0 }, } } /// Export results to JSON and CSV pub fn export_results(&self, results: &WaveComparisonResults) -> Result<()> { std::fs::create_dir_all("results")?; let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S"); // Export JSON (comprehensive data) let json_path = format!( "results/wave_comparison_{}_{}.json", results.symbol, timestamp ); let json = serde_json::to_string_pretty(&results) .context("Failed to serialize results to JSON")?; std::fs::write(&json_path, json).context("Failed to write JSON file")?; // Export CSV (summary metrics) let csv_path = format!( "results/wave_comparison_{}_{}.csv", results.symbol, timestamp ); let csv = self.generate_csv_summary(results)?; std::fs::write(&csv_path, csv).context("Failed to write CSV file")?; info!("\n✅ Results exported:"); info!(" JSON: {}", json_path); info!(" CSV: {}", csv_path); Ok(()) } /// Generate CSV summary fn generate_csv_summary(&self, results: &WaveComparisonResults) -> Result { let mut csv = String::new(); // Header csv.push_str("Metric,Wave A,Wave B,Wave C,Wave D,A→B,A→C,B→C,A→D,C→D\n"); // Feature count csv.push_str(&format!( "Feature Count,{},{},{},{},,,,,\n", results.wave_a.feature_count, results.wave_b.feature_count, results.wave_c.feature_count, results.wave_d.feature_count )); // Win rate csv.push_str(&format!( "Win Rate,{:.2}%,{:.2}%,{:.2}%,{:.2}%,{:+.1}%,{:+.1}%,{:+.1}%,{:+.1}%,{:+.1}%\n", results.wave_a.win_rate * 100.0, results.wave_b.win_rate * 100.0, results.wave_c.win_rate * 100.0, results.wave_d.win_rate * 100.0, results.improvements.a_to_b_win_rate, results.improvements.a_to_c_win_rate, results.improvements.b_to_c_win_rate, results.improvements.a_to_d_win_rate, results.improvements.c_to_d_win_rate )); // Sharpe ratio csv.push_str(&format!( "Sharpe Ratio,{:.2},{:.2},{:.2},{:.2},{:+.2},{:+.2},{:+.2},{:+.2},{:+.2}\n", results.wave_a.sharpe_ratio, results.wave_b.sharpe_ratio, results.wave_c.sharpe_ratio, results.wave_d.sharpe_ratio, results.improvements.a_to_b_sharpe, results.improvements.a_to_c_sharpe, results.improvements.b_to_c_sharpe, results.improvements.a_to_d_sharpe, results.improvements.c_to_d_sharpe )); // Sortino ratio csv.push_str(&format!( "Sortino Ratio,{:.2},{:.2},{:.2},{:.2},{:+.2},{:+.2},{:+.2},{:+.2},{:+.2}\n", results.wave_a.sortino_ratio, results.wave_b.sortino_ratio, results.wave_c.sortino_ratio, results.wave_d.sortino_ratio, results.improvements.a_to_b_sortino, results.improvements.a_to_c_sortino, results.improvements.b_to_c_sortino, results.improvements.a_to_d_sortino, results.improvements.c_to_d_sortino )); // Max drawdown csv.push_str(&format!( "Max Drawdown,{:.1}%,{:.1}%,{:.1}%,{:.1}%,{:+.1}%,{:+.1}%,{:+.1}%,{:+.1}%,{:+.1}%\n", results.wave_a.max_drawdown * 100.0, results.wave_b.max_drawdown * 100.0, results.wave_c.max_drawdown * 100.0, results.wave_d.max_drawdown * 100.0, results.improvements.a_to_b_drawdown, results.improvements.a_to_c_drawdown, results.improvements.b_to_c_drawdown, results.improvements.a_to_d_drawdown, results.improvements.c_to_d_drawdown )); // Total trades csv.push_str(&format!( "Total Trades,{},{},{},{},,,,,\n", results.wave_a.total_trades, results.wave_b.total_trades, results.wave_c.total_trades, results.wave_d.total_trades )); // Total PnL csv.push_str(&format!( "Total PnL,${:.2},${:.2},${:.2},${:.2},{:+.1}%,{:+.1}%,{:+.1}%,{:+.1}%,{:+.1}%\n", results.wave_a.total_pnl, results.wave_b.total_pnl, results.wave_c.total_pnl, results.wave_d.total_pnl, results.improvements.a_to_b_pnl, results.improvements.a_to_c_pnl, results.improvements.b_to_c_pnl, results.improvements.a_to_d_pnl, results.improvements.c_to_d_pnl )); // Average PnL csv.push_str(&format!( "Avg PnL/Trade,${:.2},${:.2},${:.2},${:.2},,,,,\n", results.wave_a.avg_pnl, results.wave_b.avg_pnl, results.wave_c.avg_pnl, results.wave_d.avg_pnl )); // Profit factor csv.push_str(&format!( "Profit Factor,{:.2},{:.2},{:.2},{:.2},,,,,\n", results.wave_a.profit_factor, results.wave_b.profit_factor, results.wave_c.profit_factor, results.wave_d.profit_factor )); Ok(csv) } /// Print results summary to console pub fn print_summary(&self, results: &WaveComparisonResults) { println!("\n╔════════════════════════════════════════════════════════════════╗"); println!("║ Wave Comparison Backtest Results (A/B/C/D) ║"); println!("╚════════════════════════════════════════════════════════════════╝"); println!("\n📊 Backtest Configuration:"); println!(" Symbol: {}", results.symbol); println!( " Period: {} to {}", results.date_range.start.format("%Y-%m-%d"), results.date_range.end.format("%Y-%m-%d") ); println!(" Bars Processed: {}", results.metadata.bars_processed); println!( " Initial Capital: ${:.2}", results.metadata.initial_capital ); println!( " Execution Time: {:.2}s", results.metadata.duration_ms as f64 / 1000.0 ); println!("\n📈 Wave A (Baseline - 26 Features):"); self.print_wave_metrics(&results.wave_a); println!("\n📈 Wave B (Alternative Bars - 36 Features):"); self.print_wave_metrics(&results.wave_b); println!(" Improvements vs Wave A:"); println!( " Win Rate: {:+.1}%", results.improvements.a_to_b_win_rate ); println!(" Sharpe: {:+.2}", results.improvements.a_to_b_sharpe); println!(" Sortino: {:+.2}", results.improvements.a_to_b_sortino); println!( " Drawdown: {:+.1}%", results.improvements.a_to_b_drawdown ); println!(" PnL: {:+.1}%", results.improvements.a_to_b_pnl); println!("\n📈 Wave C (Full Pipeline - 201 Features):"); self.print_wave_metrics(&results.wave_c); println!(" Improvements vs Wave A:"); println!( " Win Rate: {:+.1}%", results.improvements.a_to_c_win_rate ); println!(" Sharpe: {:+.2}", results.improvements.a_to_c_sharpe); println!(" Sortino: {:+.2}", results.improvements.a_to_c_sortino); println!( " Drawdown: {:+.1}%", results.improvements.a_to_c_drawdown ); println!(" PnL: {:+.1}%", results.improvements.a_to_c_pnl); println!(" Improvements vs Wave B:"); println!( " Win Rate: {:+.1}%", results.improvements.b_to_c_win_rate ); println!(" Sharpe: {:+.2}", results.improvements.b_to_c_sharpe); println!(" Sortino: {:+.2}", results.improvements.b_to_c_sortino); println!( " Drawdown: {:+.1}%", results.improvements.b_to_c_drawdown ); println!(" PnL: {:+.1}%", results.improvements.b_to_c_pnl); println!("\n📈 Wave D (Regime Detection - 225 Features):"); self.print_wave_metrics(&results.wave_d); println!(" Improvements vs Wave A:"); println!( " Win Rate: {:+.1}%", results.improvements.a_to_d_win_rate ); println!(" Sharpe: {:+.2}", results.improvements.a_to_d_sharpe); println!(" Sortino: {:+.2}", results.improvements.a_to_d_sortino); println!( " Drawdown: {:+.1}%", results.improvements.a_to_d_drawdown ); println!(" PnL: {:+.1}%", results.improvements.a_to_d_pnl); println!(" Improvements vs Wave C:"); println!( " Win Rate: {:+.1}%", results.improvements.c_to_d_win_rate ); println!(" Sharpe: {:+.2}", results.improvements.c_to_d_sharpe); println!(" Sortino: {:+.2}", results.improvements.c_to_d_sortino); println!( " Drawdown: {:+.1}%", results.improvements.c_to_d_drawdown ); println!(" PnL: {:+.1}%", results.improvements.c_to_d_pnl); println!("\n✅ Results exported to JSON and CSV"); } /// Print metrics for a single wave fn print_wave_metrics(&self, metrics: &WavePerformanceMetrics) { println!(" Win Rate: {:.1}%", metrics.win_rate * 100.0); println!(" Sharpe Ratio: {:.2}", metrics.sharpe_ratio); println!(" Sortino Ratio: {:.2}", metrics.sortino_ratio); println!(" Max Drawdown: {:.1}%", metrics.max_drawdown * 100.0); println!(" Total Trades: {}", metrics.total_trades); println!(" Total PnL: ${:.2}", metrics.total_pnl); println!(" Avg PnL/Trade: ${:.2}", metrics.avg_pnl); println!(" Profit Factor: {:.2}", metrics.profit_factor); println!(" Best Trade: ${:.2}", metrics.best_trade); println!(" Worst Trade: ${:.2}", metrics.worst_trade); } } #[cfg(test)] mod tests { use super::*; use crate::repositories::DefaultRepositories; #[test] fn test_improvement_calculation() { let wave_a = WavePerformanceMetrics { wave_id: "A".to_string(), feature_count: 26, win_rate: 0.418, sharpe_ratio: -6.52, sortino_ratio: -5.5, max_drawdown: 0.25, total_trades: 100, avg_pnl: -50.0, total_pnl: -5000.0, volatility: 0.25, profit_factor: 0.8, avg_trade_duration_secs: 3600.0, best_trade: 500.0, worst_trade: -400.0, }; let wave_c = WavePerformanceMetrics { wave_id: "C".to_string(), feature_count: 65, win_rate: 0.55, sharpe_ratio: 1.5, sortino_ratio: 2.0, max_drawdown: 0.18, total_trades: 150, avg_pnl: 33.33, total_pnl: 5000.0, volatility: 0.20, profit_factor: 1.5, avg_trade_duration_secs: 3600.0, best_trade: 500.0, worst_trade: -400.0, }; let backtest = WaveComparisonBacktest::new(Arc::new(DefaultRepositories::mock()), 100000.0); let wave_b = wave_a.clone(); // Wave B same as A for this test let wave_d = wave_c.clone(); // Wave D same as C for this test let improvements = backtest.calculate_improvements(&wave_a, &wave_b, &wave_c, &wave_d); // Win rate improvement: (0.55 - 0.418) / 0.418 * 100 = 31.6% assert!((improvements.a_to_c_win_rate - 31.6).abs() < 1.0); // Sharpe improvement: 1.5 - (-6.52) = 8.02 assert!((improvements.a_to_c_sharpe - 8.02).abs() < 0.1); // Drawdown reduction: (0.25 - 0.18) / 0.25 * 100 = 28% assert!((improvements.a_to_c_drawdown - 28.0).abs() < 1.0); } #[test] fn test_csv_generation() { let results = create_test_results(); let backtest = WaveComparisonBacktest::new(Arc::new(DefaultRepositories::mock()), 100000.0); let csv = backtest.generate_csv_summary(&results).unwrap(); assert!(csv.contains("Metric,Wave A,Wave B,Wave C")); assert!(csv.contains("Win Rate")); assert!(csv.contains("Sharpe Ratio")); assert!(csv.contains("Total PnL")); } fn create_test_results() -> WaveComparisonResults { WaveComparisonResults { symbol: "ES.FUT".to_string(), date_range: DateRange { start: Utc::now(), end: Utc::now(), }, wave_a: WavePerformanceMetrics { wave_id: "A".to_string(), feature_count: 26, win_rate: 0.418, sharpe_ratio: -6.52, sortino_ratio: -5.5, max_drawdown: 0.25, total_trades: 100, avg_pnl: -50.0, total_pnl: -5000.0, volatility: 0.25, profit_factor: 0.8, avg_trade_duration_secs: 3600.0, best_trade: 500.0, worst_trade: -400.0, }, wave_b: WavePerformanceMetrics { wave_id: "B".to_string(), feature_count: 36, win_rate: 0.48, sharpe_ratio: -5.0, sortino_ratio: -4.2, max_drawdown: 0.22, total_trades: 120, avg_pnl: 8.33, total_pnl: 1000.0, volatility: 0.23, profit_factor: 1.1, avg_trade_duration_secs: 3600.0, best_trade: 100.0, worst_trade: -80.0, }, wave_c: WavePerformanceMetrics { wave_id: "C".to_string(), feature_count: 65, win_rate: 0.55, sharpe_ratio: 1.5, sortino_ratio: 2.0, max_drawdown: 0.18, total_trades: 150, avg_pnl: 33.33, total_pnl: 5000.0, volatility: 0.20, profit_factor: 1.5, avg_trade_duration_secs: 3600.0, best_trade: 500.0, worst_trade: -400.0, }, wave_d: create_test_wave_d(), improvements: ImprovementMatrix { a_to_b_win_rate: 14.8, a_to_c_win_rate: 31.6, b_to_c_win_rate: 14.6, a_to_d_win_rate: 43.5, c_to_d_win_rate: 9.1, a_to_b_sharpe: 1.52, a_to_c_sharpe: 8.02, b_to_c_sharpe: 6.5, a_to_d_sharpe: 8.52, c_to_d_sharpe: 0.5, a_to_b_sortino: 1.3, a_to_c_sortino: 7.5, b_to_c_sortino: 6.2, a_to_d_sortino: 8.0, c_to_d_sortino: 0.5, a_to_b_drawdown: 12.0, a_to_c_drawdown: 28.0, b_to_c_drawdown: 18.2, a_to_d_drawdown: 40.0, c_to_d_drawdown: 16.7, a_to_b_pnl: 120.0, a_to_c_pnl: 200.0, b_to_c_pnl: 400.0, a_to_d_pnl: 250.0, c_to_d_pnl: 50.0, }, metadata: BacktestMetadata { execution_time: Utc::now(), duration_ms: 5000, bars_processed: 1000, initial_capital: 100000.0, strategy_config: "wave_comparison_v1".to_string(), }, } } fn create_test_wave_d() -> WavePerformanceMetrics { WavePerformanceMetrics { wave_id: "D".to_string(), feature_count: 225, win_rate: 0.60, sharpe_ratio: 2.0, sortino_ratio: 2.5, max_drawdown: 0.15, total_trades: 180, avg_pnl: 41.67, total_pnl: 7500.0, volatility: 0.18, profit_factor: 1.8, avg_trade_duration_secs: 3600.0, best_trade: 750.0, worst_trade: -600.0, } } }