//! Backtesting Report Generation Module //! //! Provides comprehensive markdown report generation for comparing DQN model performance //! against baseline models. Includes deployment recommendations based on production criteria. //! //! # Features //! //! - **Markdown Report Generation**: Professional formatted reports with tables //! - **Production Criteria Validation**: Automated APPROVE/REJECT/REVIEW recommendations //! - **Baseline Comparison**: Side-by-side comparison with reference model (Trial #35) //! - **Comprehensive Metrics**: Returns, Sharpe, drawdown, win rate, alpha, trade stats //! //! # Usage //! //! ```rust //! use ml_backtesting::report::{BacktestReport, PerformanceMetrics}; //! //! let new_results = PerformanceMetrics { //! total_return_pct: 15.2, //! sharpe_ratio: 2.3, //! max_drawdown_pct: 12.5, //! win_rate: 0.58, //! alpha: 3.2, //! total_trades: 145, //! avg_trade_return_pct: 0.105, //! }; //! //! let baseline = Some(PerformanceMetrics { //! total_return_pct: 12.1, //! sharpe_ratio: 1.8, //! max_drawdown_pct: 18.3, //! win_rate: 0.52, //! alpha: 1.5, //! total_trades: 138, //! avg_trade_return_pct: 0.088, //! }); //! //! let report = BacktestReport { //! model_name: "DQN-Wave3-Entropy".to_owned(), //! baseline_name: "DQN-Trial35-Baseline".to_owned(), //! new_results, //! baseline_results: baseline, //! }; //! //! let markdown = report.generate_markdown(); //! std::fs::write("backtest_report.md", markdown).unwrap(); //! ``` use chrono::Utc; use serde::{Deserialize, Serialize}; use std::fmt::Write; /// Performance metrics for backtesting evaluation /// /// Simplified metrics struct focused on production criteria validation. /// Matches the key metrics from `backtesting/src/metrics.rs` but optimized /// for report generation and comparison purposes. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PerformanceMetrics { /// Total return as percentage (e.g., 15.2 = 15.2%) pub total_return_pct: f64, /// Sharpe ratio (risk-adjusted return) pub sharpe_ratio: f64, /// Maximum drawdown as percentage (e.g., 12.5 = 12.5%) pub max_drawdown_pct: f64, /// Win rate (0.0-1.0, e.g., 0.58 = 58%) pub win_rate: f64, /// Alpha (excess return vs benchmark, percentage) pub alpha: f64, /// Total number of trades executed pub total_trades: usize, /// Average trade return as percentage pub avg_trade_return_pct: f64, } /// Deployment recommendation structure #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Recommendation { /// Status string (✅ APPROVE, ⚠️ REVIEW, ❌ REJECT) pub status: String, /// Human-readable reasoning for the recommendation pub reasoning: String, } /// Complete backtesting report structure /// /// Compares a new model against an optional baseline and generates /// deployment recommendations based on production criteria. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BacktestReport { /// Name of the new model being evaluated pub model_name: String, /// Name of the baseline model for comparison pub baseline_name: String, /// Performance metrics for the new model pub new_results: PerformanceMetrics, /// Optional baseline performance metrics (e.g., Trial #35) pub baseline_results: Option, } impl BacktestReport { /// Generate comprehensive markdown report /// /// Creates a professional formatted report with: /// - Header with model names and timestamp /// - Performance summary table with production criteria /// - Baseline comparison table (if baseline provided) /// - Deployment recommendation with reasoning /// - Trade statistics summary /// /// # Returns /// /// A formatted markdown string ready to write to a file pub fn generate_markdown(&self) -> String { let mut report = String::new(); // Header report.push_str("# DQN Backtesting Report\n\n"); let _w = writeln!(report, "**Model**: {}", self.model_name); if self.baseline_results.is_some() { let _w = writeln!(report, "**Baseline**: {}", self.baseline_name); } let _w = write!(report, "**Generated**: {}\n\n", Utc::now().format("%Y-%m-%d %H:%M:%S UTC")); report.push_str("---\n\n"); // Performance Summary report.push_str("## Performance Summary\n\n"); report.push_str("| Metric | Value | Target | Status |\n"); report.push_str("|--------|-------|--------|--------|\n"); let m = &self.new_results; // Total Return let _w = writeln!( report, "| Total Return | {:.2}% | >0% | {} |", m.total_return_pct, if m.total_return_pct > 0.0 { "\u{2705}" } else { "\u{274c}" } ); // Sharpe Ratio let _w = writeln!( report, "| Sharpe Ratio | {:.2} | >1.5 | {} |", m.sharpe_ratio, if m.sharpe_ratio > 1.5 { "\u{2705}" } else { "\u{274c}" } ); // Max Drawdown let _w = writeln!( report, "| Max Drawdown | {:.2}% | <20% | {} |", m.max_drawdown_pct, if m.max_drawdown_pct < 20.0 { "\u{2705}" } else { "\u{274c}" } ); // Win Rate let _w = writeln!( report, "| Win Rate | {:.1}% | >50% | {} |", m.win_rate * 100.0, if m.win_rate > 0.50 { "\u{2705}" } else { "\u{274c}" } ); // Alpha let _w = writeln!( report, "| Alpha vs B&H | {:.2}% | >0% | {} |", m.alpha, if m.alpha > 0.0 { "\u{2705}" } else { "\u{274c}" } ); report.push('\n'); // Comparison to baseline (if provided) if let Some(baseline) = &self.baseline_results { report.push_str("## Comparison to Baseline\n\n"); report.push_str("| Metric | Baseline | New Model | Change | Direction |\n"); report.push_str("|--------|----------|-----------|--------|----------|\n"); // Returns comparison let return_change = m.total_return_pct - baseline.total_return_pct; let return_arrow = if return_change > 0.0 { "\u{2197}\u{fe0f}" } else if return_change < 0.0 { "\u{2198}\u{fe0f}" } else { "\u{2192}" }; let _w = writeln!( report, "| Returns | {:.2}% | {:.2}% | {:+.2}% | {} |", baseline.total_return_pct, m.total_return_pct, return_change, return_arrow ); // Sharpe comparison let sharpe_change = m.sharpe_ratio - baseline.sharpe_ratio; let sharpe_arrow = if sharpe_change > 0.0 { "\u{2197}\u{fe0f}" } else if sharpe_change < 0.0 { "\u{2198}\u{fe0f}" } else { "\u{2192}" }; let _w = writeln!( report, "| Sharpe | {:.2} | {:.2} | {:+.2} | {} |", baseline.sharpe_ratio, m.sharpe_ratio, sharpe_change, sharpe_arrow ); // Drawdown comparison (lower is better) let dd_change = m.max_drawdown_pct - baseline.max_drawdown_pct; let dd_arrow = if dd_change < 0.0 { "\u{2197}\u{fe0f}" } else if dd_change > 0.0 { "\u{2198}\u{fe0f}" } else { "\u{2192}" }; let _w = writeln!( report, "| Drawdown | {:.2}% | {:.2}% | {:+.2}% | {} |", baseline.max_drawdown_pct, m.max_drawdown_pct, dd_change, dd_arrow ); // Win Rate comparison let wr_change = (m.win_rate - baseline.win_rate) * 100.0; let wr_arrow = if wr_change > 0.0 { "\u{2197}\u{fe0f}" } else if wr_change < 0.0 { "\u{2198}\u{fe0f}" } else { "\u{2192}" }; let _w = writeln!( report, "| Win Rate | {:.1}% | {:.1}% | {:+.1}% | {} |", baseline.win_rate * 100.0, m.win_rate * 100.0, wr_change, wr_arrow ); // Alpha comparison let alpha_change = m.alpha - baseline.alpha; let alpha_arrow = if alpha_change > 0.0 { "\u{2197}\u{fe0f}" } else if alpha_change < 0.0 { "\u{2198}\u{fe0f}" } else { "\u{2192}" }; let _w = writeln!( report, "| Alpha | {:.2}% | {:.2}% | {:+.2}% | {} |", baseline.alpha, m.alpha, alpha_change, alpha_arrow ); report.push('\n'); } // Trade Statistics report.push_str("## Trade Statistics\n\n"); report.push_str("| Metric | Value |\n"); report.push_str("|--------|-------|\n"); let _w = writeln!(report, "| Total Trades | {} |", m.total_trades); let _w = writeln!(report, "| Avg Trade Return | {:.3}% |", m.avg_trade_return_pct); let _w = writeln!(report, "| Win Rate | {:.2}% |", m.win_rate * 100.0); if let Some(baseline) = &self.baseline_results { let trade_diff = m.total_trades as i64 - baseline.total_trades as i64; let _w = writeln!(report, "| Trades vs Baseline | {:+} |", trade_diff); } report.push('\n'); // Deployment Recommendation report.push_str("## Deployment Recommendation\n\n"); let recommendation = self.get_recommendation(); let _w = write!(report, "**Status**: {}\n\n", recommendation.status); let _w = write!(report, "{}\n\n", recommendation.reasoning); // Production Criteria Summary report.push_str("### Production Criteria Checklist\n\n"); let criteria_passed = self.count_criteria_passed(); let _w = writeln!(report, "- **Criteria Passed**: {}/5", criteria_passed); let _w = writeln!(report, "- **Total Return**: {} ({:.2}% > 0%)", if m.total_return_pct > 0.0 { "\u{2705} PASS" } else { "\u{274c} FAIL" }, m.total_return_pct ); let _w = writeln!(report, "- **Sharpe Ratio**: {} ({:.2} > 1.5)", if m.sharpe_ratio > 1.5 { "\u{2705} PASS" } else { "\u{274c} FAIL" }, m.sharpe_ratio ); let _w = writeln!(report, "- **Max Drawdown**: {} ({:.2}% < 20%)", if m.max_drawdown_pct < 20.0 { "\u{2705} PASS" } else { "\u{274c} FAIL" }, m.max_drawdown_pct ); let _w = writeln!(report, "- **Win Rate**: {} ({:.1}% > 50%)", if m.win_rate > 0.50 { "\u{2705} PASS" } else { "\u{274c} FAIL" }, m.win_rate * 100.0 ); let _w = writeln!(report, "- **Alpha vs B&H**: {} ({:.2}% > 0%)", if m.alpha > 0.0 { "\u{2705} PASS" } else { "\u{274c} FAIL" }, m.alpha ); report.push('\n'); // Footer report.push_str("---\n\n"); report.push_str("*Report generated automatically by Foxhunt ML Evaluation Framework*\n"); report } /// Count how many production criteria are passed /// /// # Returns /// /// Number of criteria passed (0-5) fn count_criteria_passed(&self) -> usize { let m = &self.new_results; [ m.total_return_pct > 0.0, m.sharpe_ratio > 1.5, m.max_drawdown_pct < 20.0, m.win_rate > 0.50, m.alpha > 0.0, ] .iter() .filter(|&&x| x) .count() } /// Generate deployment recommendation based on production criteria /// /// # Recommendation Logic /// /// - **APPROVE (4-5 criteria)**: Model is production-ready /// - **REVIEW (2-3 criteria)**: Marginal performance, needs review /// - **REJECT (0-1 criteria)**: Not production-ready /// /// # Returns /// /// A `Recommendation` with status and reasoning pub fn get_recommendation(&self) -> Recommendation { let m = &self.new_results; let passes = self.count_criteria_passed(); if passes >= 4 { Recommendation { status: "\u{2705} APPROVE - Ready for Production".to_owned(), reasoning: format!( "Model passes {}/5 production criteria. Strong performance with {:.2}% return, {:.2} Sharpe ratio, and {:.1}% win rate. \ Risk is acceptable with {:.2}% max drawdown. Model demonstrates profitability with {:.2}% alpha vs buy-and-hold. \ \n\n**Action**: Proceed with production deployment after final validation.", passes, m.total_return_pct, m.sharpe_ratio, m.win_rate * 100.0, m.max_drawdown_pct, m.alpha ), } } else if passes >= 2 { Recommendation { status: "\u{26a0}\u{fe0f} REVIEW - Marginal Performance".to_owned(), reasoning: format!( "Model passes {}/5 production criteria. Performance is marginal and requires careful review. \ \n\n**Concerns**:\n{} \n**Action**: Conduct detailed risk assessment and consider additional testing before deployment.", passes, self.generate_concerns_list() ), } } else { Recommendation { status: "\u{274c} REJECT - Not Production Ready".to_owned(), reasoning: format!( "Model only passes {}/5 production criteria. Performance is insufficient for production deployment. \ \n\n**Critical Issues**:\n{} \n**Action**: Do not deploy. Retrain model with improved hyperparameters or different architecture.", passes, self.generate_concerns_list() ), } } } /// Generate list of concerns for models not passing all criteria /// /// # Returns /// /// Markdown-formatted list of failed criteria fn generate_concerns_list(&self) -> String { let m = &self.new_results; let mut concerns = Vec::new(); if m.total_return_pct <= 0.0 { concerns.push(format!("- \u{274c} Negative total return ({:.2}%)", m.total_return_pct)); } if m.sharpe_ratio <= 1.5 { concerns.push(format!("- \u{274c} Low Sharpe ratio ({:.2} < 1.5)", m.sharpe_ratio)); } if m.max_drawdown_pct >= 20.0 { concerns.push(format!("- \u{274c} Excessive drawdown ({:.2}% > 20%)", m.max_drawdown_pct)); } if m.win_rate <= 0.50 { concerns.push(format!("- \u{274c} Poor win rate ({:.1}% < 50%)", m.win_rate * 100.0)); } if m.alpha <= 0.0 { concerns.push(format!("- \u{274c} Negative alpha ({:.2}%)", m.alpha)); } if concerns.is_empty() { "- No critical issues identified".to_owned() } else { concerns.join("\n") } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_report_generation_approve() { let report = BacktestReport { model_name: "DQN-Test-Model".to_owned(), baseline_name: "DQN-Baseline".to_owned(), new_results: PerformanceMetrics { total_return_pct: 15.2, sharpe_ratio: 2.3, max_drawdown_pct: 12.5, win_rate: 0.58, alpha: 3.2, total_trades: 145, avg_trade_return_pct: 0.105, }, baseline_results: None, }; let markdown = report.generate_markdown(); assert!(markdown.contains("# DQN Backtesting Report")); assert!(markdown.contains("DQN-Test-Model")); assert!(markdown.contains("✅ APPROVE")); assert!(markdown.contains("5/5")); } #[test] fn test_report_generation_reject() { let report = BacktestReport { model_name: "DQN-Poor-Model".to_owned(), baseline_name: "DQN-Baseline".to_owned(), new_results: PerformanceMetrics { total_return_pct: -5.2, sharpe_ratio: 0.8, max_drawdown_pct: 35.0, win_rate: 0.42, alpha: -2.1, total_trades: 120, avg_trade_return_pct: -0.043, }, baseline_results: None, }; let markdown = report.generate_markdown(); assert!(markdown.contains("❌ REJECT")); assert!(markdown.contains("0/5")); } #[test] fn test_baseline_comparison() { let report = BacktestReport { model_name: "DQN-New".to_owned(), baseline_name: "DQN-Trial35".to_owned(), new_results: PerformanceMetrics { total_return_pct: 18.5, sharpe_ratio: 2.5, max_drawdown_pct: 10.2, win_rate: 0.62, alpha: 4.5, total_trades: 150, avg_trade_return_pct: 0.123, }, baseline_results: Some(PerformanceMetrics { total_return_pct: 12.1, sharpe_ratio: 1.8, max_drawdown_pct: 18.3, win_rate: 0.52, alpha: 1.5, total_trades: 138, avg_trade_return_pct: 0.088, }), }; let markdown = report.generate_markdown(); assert!(markdown.contains("Comparison to Baseline")); assert!(markdown.contains("DQN-Trial35")); assert!(markdown.contains("+6.40%")); // Return improvement (18.5 - 12.1 = 6.4) } }