#![allow(unexpected_cfgs)] //! Comprehensive tests for performance metrics calculation //! //! Target Coverage: 70%+ for Sharpe ratio, drawdown, win rate, and all performance metrics //! //! Uses real DBN market data (ES.FUT 2024-01-02) for realistic metric calculations. use anyhow::Result; mod test_data_helpers; use backtesting_service::performance::PerformanceAnalyzer; use backtesting_service::strategy_engine::{BacktestTrade, TradeSide}; use config::structures::BacktestingPerformanceConfig; use test_data_helpers::*; /// Test basic performance metrics calculation with real data #[tokio::test] async fn test_basic_performance_metrics() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; // Generate trades from real ES.FUT data let trades = generate_real_trades(3).await?; let initial_capital = 100000.0; let metrics = analyzer.calculate_metrics(&trades, initial_capital); // Validate basic counts assert_eq!(metrics.total_trades, 3, "Should have 3 trades"); // Win rate should be between 0-100% assert!( metrics.win_rate >= 0.0 && metrics.win_rate <= 100.0, "Win rate should be valid percentage: {}", metrics.win_rate ); // Total winning + losing trades = total trades assert_eq!( metrics.winning_trades + metrics.losing_trades, metrics.total_trades, "Winning + losing should equal total trades" ); // Real data should have realistic returns (not guaranteed profit) assert!( metrics.total_return.is_finite(), "Total return should be finite" ); Ok(()) } /// Test Sharpe ratio calculation with real data #[tokio::test] async fn test_sharpe_ratio_calculation() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; // Generate trades from real data let trades = generate_mixed_trades().await?; if trades.is_empty() { return Ok(()); // Skip if no data available } let metrics = analyzer.calculate_metrics(&trades, 100000.0); // Get expected Sharpe range from real data analysis let (min_sharpe, max_sharpe) = get_real_sharpe_range().await?; // Sharpe ratio should be finite and within realistic bounds assert!( metrics.sharpe_ratio.is_finite(), "Sharpe ratio should be finite, got: {}", metrics.sharpe_ratio ); // Real intraday data can have negative Sharpe (choppy markets) assert!( metrics.sharpe_ratio >= min_sharpe && metrics.sharpe_ratio <= max_sharpe, "Sharpe ratio {} outside expected range [{}, {}]", metrics.sharpe_ratio, min_sharpe, max_sharpe ); Ok(()) } /// Test Sortino ratio calculation #[tokio::test] async fn test_sortino_ratio_calculation() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; // Mix of wins and losses let trades = vec![ create_trade(1, "AAPL", TradeSide::Buy, 100.0, 100.0, 110.0, 0, 5), // +10% create_trade(2, "AAPL", TradeSide::Buy, 100.0, 100.0, 95.0, 5, 10), // -5% create_trade(3, "AAPL", TradeSide::Buy, 100.0, 100.0, 108.0, 10, 15), // +8% create_trade(4, "AAPL", TradeSide::Buy, 100.0, 100.0, 92.0, 15, 20), // -8% create_trade(5, "AAPL", TradeSide::Buy, 100.0, 100.0, 112.0, 20, 25), // +12% ]; let metrics = analyzer.calculate_metrics(&trades, 100000.0); // Sortino ratio should be calculated (can be positive or negative depending on downside) assert!( metrics.sortino_ratio.is_finite(), "Sortino ratio should be finite" ); Ok(()) } /// Test maximum drawdown calculation with real data #[tokio::test] async fn test_maximum_drawdown() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; // Generate trades from real data (includes natural drawdown patterns) let trades = generate_mixed_trades().await?; if trades.is_empty() { return Ok(()); // Skip if no data available } let metrics = analyzer.calculate_metrics(&trades, 100000.0); // Get expected drawdown range from real data let (min_dd, max_dd) = get_real_drawdown_range().await?; // Max drawdown should be non-negative assert!( metrics.max_drawdown >= 0.0, "Max drawdown should be non-negative, got: {}", metrics.max_drawdown ); // Real data should have realistic drawdown assert!( metrics.max_drawdown >= min_dd && metrics.max_drawdown <= max_dd, "Max drawdown {} outside expected range [{}, {}]", metrics.max_drawdown, min_dd, max_dd ); Ok(()) } /// Test win rate calculation with real data #[tokio::test] async fn test_win_rate_calculation() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; // Generate 10 trades from real data let trades = generate_real_trades(10).await?; let metrics = analyzer.calculate_metrics(&trades, 100000.0); assert_eq!(metrics.total_trades, 10, "Should have 10 trades"); // Win + loss should equal total assert_eq!( metrics.winning_trades + metrics.losing_trades, metrics.total_trades, "Winning + losing should equal total" ); // Win rate should be valid percentage assert!( metrics.win_rate >= 0.0 && metrics.win_rate <= 100.0, "Win rate should be 0-100%, got: {}", metrics.win_rate ); // Win rate calculation should match trade counts let expected_win_rate = (metrics.winning_trades as f64 / metrics.total_trades as f64) * 100.0; assert!( (metrics.win_rate - expected_win_rate).abs() < 0.1, "Win rate calculation mismatch: expected {}, got {}", expected_win_rate, metrics.win_rate ); Ok(()) } /// Test profit factor calculation #[tokio::test] async fn test_profit_factor() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; // Gross profit: $1000, Gross loss: $300 -> Profit factor: 3.33 let trades = vec![ create_trade(1, "AAPL", TradeSide::Buy, 100.0, 100.0, 110.0, 0, 5), // +$1000 create_trade(2, "AAPL", TradeSide::Buy, 100.0, 100.0, 97.0, 5, 10), // -$300 ]; let metrics = analyzer.calculate_metrics(&trades, 100000.0); assert!( metrics.profit_factor > 3.0 && metrics.profit_factor < 3.5, "Profit factor should be ~3.33" ); Ok(()) } /// Test average win and loss calculation #[tokio::test] async fn test_average_win_loss() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; let trades = vec![ create_trade(1, "AAPL", TradeSide::Buy, 100.0, 100.0, 110.0, 0, 1), // +$1000 create_trade(2, "AAPL", TradeSide::Buy, 100.0, 100.0, 106.0, 1, 2), // +$600 create_trade(3, "AAPL", TradeSide::Buy, 100.0, 100.0, 95.0, 2, 3), // -$500 create_trade(4, "AAPL", TradeSide::Buy, 100.0, 100.0, 92.0, 3, 4), // -$800 ]; let metrics = analyzer.calculate_metrics(&trades, 100000.0); // Average win: ($1000 + $600) / 2 = $800 assert!( (metrics.avg_win - 800.0).abs() < 1.0, "Average win should be $800" ); // Average loss: -($500 + $800) / 2 = -$650 assert!( (metrics.avg_loss + 650.0).abs() < 1.0, "Average loss should be -$650" ); Ok(()) } /// Test largest win and loss tracking #[tokio::test] async fn test_largest_win_loss() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; let trades = vec![ create_trade(1, "AAPL", TradeSide::Buy, 100.0, 100.0, 105.0, 0, 1), // +$500 create_trade(2, "AAPL", TradeSide::Buy, 100.0, 100.0, 115.0, 1, 2), // +$1500 (largest win) create_trade(3, "AAPL", TradeSide::Buy, 100.0, 100.0, 95.0, 2, 3), // -$500 create_trade(4, "AAPL", TradeSide::Buy, 100.0, 100.0, 88.0, 3, 4), // -$1200 (largest loss) ]; let metrics = analyzer.calculate_metrics(&trades, 100000.0); assert!( (metrics.largest_win - 1500.0).abs() < 1.0, "Largest win should be $1500" ); assert!( (metrics.largest_loss + 1200.0).abs() < 1.0, "Largest loss should be -$1200" ); Ok(()) } /// Test Calmar ratio calculation #[tokio::test] async fn test_calmar_ratio() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; // Create trades over a year with known drawdown let trades = vec![ create_trade(1, "AAPL", TradeSide::Buy, 100.0, 100.0, 120.0, 0, 90), // +20% create_trade(2, "AAPL", TradeSide::Buy, 100.0, 120.0, 110.0, 90, 180), // -10% create_trade(3, "AAPL", TradeSide::Buy, 100.0, 110.0, 130.0, 180, 365), // +20% ]; let metrics = analyzer.calculate_metrics(&trades, 100000.0); // Calmar = Annualized Return / Max Drawdown assert!( metrics.calmar_ratio > 0.0, "Calmar ratio should be positive" ); Ok(()) } /// Test VaR (Value at Risk) calculation #[tokio::test] async fn test_var_calculation() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; // Mix of returns for VaR calculation let trades = vec![ create_trade(1, "AAPL", TradeSide::Buy, 100.0, 100.0, 105.0, 0, 1), create_trade(2, "AAPL", TradeSide::Buy, 100.0, 100.0, 103.0, 1, 2), create_trade(3, "AAPL", TradeSide::Buy, 100.0, 100.0, 98.0, 2, 3), create_trade(4, "AAPL", TradeSide::Buy, 100.0, 100.0, 107.0, 3, 4), create_trade(5, "AAPL", TradeSide::Buy, 100.0, 100.0, 95.0, 4, 5), ]; let metrics = analyzer.calculate_metrics(&trades, 100000.0); assert!(metrics.var_95.is_some(), "VaR should be calculated"); let var = metrics.var_95.unwrap(); assert!(var < 0.0, "VaR should be negative (potential loss)"); Ok(()) } /// Test expected shortfall calculation #[tokio::test] async fn test_expected_shortfall() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; let trades = vec![ create_trade(1, "AAPL", TradeSide::Buy, 100.0, 100.0, 105.0, 0, 1), create_trade(2, "AAPL", TradeSide::Buy, 100.0, 100.0, 92.0, 1, 2), create_trade(3, "AAPL", TradeSide::Buy, 100.0, 100.0, 108.0, 2, 3), create_trade(4, "AAPL", TradeSide::Buy, 100.0, 100.0, 90.0, 3, 4), ]; let metrics = analyzer.calculate_metrics(&trades, 100000.0); assert!( metrics.expected_shortfall.is_some(), "Expected shortfall should be calculated" ); Ok(()) } /// Test annualized return calculation #[tokio::test] async fn test_annualized_return() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; // Trades over 6 months with 10% total return let trades = vec![ create_trade(1, "AAPL", TradeSide::Buy, 100.0, 100.0, 110.0, 0, 180), // +10% over 6 months ]; let metrics = analyzer.calculate_metrics(&trades, 100000.0); // Annualized return should be higher than 10% (compound effect) assert!( metrics.annualized_return > 10.0, "Annualized return should be > 10%" ); Ok(()) } /// Test volatility calculation #[tokio::test] async fn test_volatility_calculation() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; // High volatility trades let trades = vec![ create_trade(1, "AAPL", TradeSide::Buy, 100.0, 100.0, 120.0, 0, 1), // +20% create_trade(2, "AAPL", TradeSide::Buy, 100.0, 100.0, 85.0, 1, 2), // -15% create_trade(3, "AAPL", TradeSide::Buy, 100.0, 100.0, 115.0, 2, 3), // +15% create_trade(4, "AAPL", TradeSide::Buy, 100.0, 100.0, 90.0, 3, 4), // -10% ]; let metrics = analyzer.calculate_metrics(&trades, 100000.0); assert!(metrics.volatility > 0.0, "Volatility should be positive"); Ok(()) } /// Test edge case: no trades #[tokio::test] async fn test_no_trades() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; let trades: Vec = vec![]; let metrics = analyzer.calculate_metrics(&trades, 100000.0); assert_eq!(metrics.total_trades, 0); assert_eq!(metrics.total_return, 0.0); assert_eq!(metrics.win_rate, 0.0); assert_eq!(metrics.sharpe_ratio, 0.0); Ok(()) } /// Test edge case: all winning trades #[tokio::test] async fn test_all_winning_trades() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; let trades = vec![ create_trade(1, "AAPL", TradeSide::Buy, 100.0, 100.0, 105.0, 0, 1), create_trade(2, "AAPL", TradeSide::Buy, 100.0, 100.0, 103.0, 1, 2), create_trade(3, "AAPL", TradeSide::Buy, 100.0, 100.0, 107.0, 2, 3), ]; let metrics = analyzer.calculate_metrics(&trades, 100000.0); assert_eq!(metrics.win_rate, 100.0); assert_eq!(metrics.winning_trades, 3); assert_eq!(metrics.losing_trades, 0); assert!( metrics.profit_factor.is_infinite(), "Profit factor should be infinite with no losses" ); Ok(()) } /// Test edge case: all losing trades #[tokio::test] async fn test_all_losing_trades() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; let trades = vec![ create_trade(1, "AAPL", TradeSide::Buy, 100.0, 100.0, 95.0, 0, 1), create_trade(2, "AAPL", TradeSide::Buy, 100.0, 100.0, 93.0, 1, 2), create_trade(3, "AAPL", TradeSide::Buy, 100.0, 100.0, 92.0, 2, 3), ]; let metrics = analyzer.calculate_metrics(&trades, 100000.0); assert_eq!(metrics.win_rate, 0.0); assert_eq!(metrics.winning_trades, 0); assert_eq!(metrics.losing_trades, 3); assert_eq!( metrics.profit_factor, 0.0, "Profit factor should be 0 with no wins" ); Ok(()) } /// Test equity curve generation #[tokio::test] async fn test_equity_curve_generation() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; let trades = vec![ create_trade(1, "AAPL", TradeSide::Buy, 100.0, 100.0, 105.0, 0, 1), create_trade(2, "AAPL", TradeSide::Buy, 100.0, 100.0, 110.0, 1, 2), create_trade(3, "AAPL", TradeSide::Buy, 100.0, 100.0, 108.0, 2, 3), ]; let initial_capital = 100000.0; let equity_curve = analyzer.generate_equity_curve(&trades, initial_capital); // Should have points for each trade + initial assert!( equity_curve.len() >= 4, "Equity curve should have at least 4 points" ); // First point should be initial capital assert!((equity_curve[0].equity - initial_capital).abs() < 0.01); // Drawdown at start should be 0 assert_eq!(equity_curve[0].drawdown, 0.0); Ok(()) } /// Test rolling metrics calculation // Gated: calculate_rolling_metrics method does not exist on PerformanceAnalyzer #[cfg(feature = "__backtesting_integration")] #[tokio::test] async fn test_rolling_metrics() -> Result<()> { let config = BacktestingPerformanceConfig::default(); let analyzer = PerformanceAnalyzer::new(&config)?; let trades = vec![ create_trade(1, "AAPL", TradeSide::Buy, 100.0, 100.0, 105.0, 0, 5), create_trade(2, "AAPL", TradeSide::Buy, 100.0, 100.0, 103.0, 5, 10), create_trade(3, "AAPL", TradeSide::Buy, 100.0, 100.0, 107.0, 10, 15), create_trade(4, "AAPL", TradeSide::Buy, 100.0, 100.0, 102.0, 15, 20), create_trade(5, "AAPL", TradeSide::Buy, 100.0, 100.0, 108.0, 20, 25), ]; let rolling = analyzer.calculate_rolling_metrics(&trades, 10); assert!( !rolling.rolling_sharpe.is_empty(), "Rolling Sharpe should be calculated" ); assert!( !rolling.rolling_volatility.is_empty(), "Rolling volatility should be calculated" ); assert!( !rolling.rolling_returns.is_empty(), "Rolling returns should be calculated" ); Ok(()) }