//! Value at Risk (`VaR`) calculation methods //! //! Implements Historical, Parametric, and Monte Carlo `VaR` methods. use crate::error::{Result, RiskAnalyzerError}; use rand::SeedableRng; use rand_distr::{Distribution, Normal}; /// Calculate historical `VaR` from a sample of returns /// /// # Arguments /// * `returns` - Historical returns (must be non-empty) /// * `confidence` - Confidence level (e.g., 0.95 for 95%) /// /// # Returns /// `VaR` as a positive value (represents potential loss) pub fn historical_var(returns: &[f64], confidence: f64) -> Result { if returns.is_empty() { return Err(RiskAnalyzerError::InsufficientData( "Returns array is empty".to_string(), )); } if !(0.0..=1.0).contains(&confidence) { return Err(RiskAnalyzerError::InvalidConfig(format!( "Confidence level must be between 0 and 1, got {confidence}" ))); } let mut sorted = returns.to_vec(); sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); let index = ((1.0 - confidence) * sorted.len() as f64).floor() as usize; let index = index.min(sorted.len() - 1); Ok(-sorted[index]) } /// Calculate parametric `VaR` assuming normal distribution /// /// # Arguments /// * `mean` - Mean return /// * `std_dev` - Standard deviation of returns /// * `confidence` - Confidence level (e.g., 0.95 for 95%) /// /// # Returns /// `VaR` as a positive value (represents potential loss) pub fn parametric_var(mean: f64, std_dev: f64, confidence: f64) -> Result { if std_dev < 0.0 { return Err(RiskAnalyzerError::InvalidConfig(format!( "Standard deviation must be non-negative, got {std_dev}" ))); } if !(0.0..=1.0).contains(&confidence) { return Err(RiskAnalyzerError::InvalidConfig(format!( "Confidence level must be between 0 and 1, got {confidence}" ))); } let z_score = normal_quantile(1.0 - confidence)?; Ok(-(mean + z_score * std_dev)) } /// Calculate Monte Carlo `VaR` by simulating future returns /// /// # Arguments /// * `mean` - Mean return per period /// * `std_dev` - Standard deviation per period /// * `horizon` - Time horizon in periods /// * `num_simulations` - Number of Monte Carlo paths /// * `confidence` - Confidence level (e.g., 0.95 for 95%) /// /// # Returns /// `VaR` as a positive value (represents potential loss) pub fn monte_carlo_var( mean: f64, std_dev: f64, horizon: u32, num_simulations: u32, confidence: f64, ) -> Result { if num_simulations == 0 { return Err(RiskAnalyzerError::InvalidConfig( "Number of simulations must be positive".to_string(), )); } if horizon == 0 { return Err(RiskAnalyzerError::InvalidConfig( "Time horizon must be positive".to_string(), )); } if std_dev < 0.0 { return Err(RiskAnalyzerError::InvalidConfig(format!( "Standard deviation must be non-negative, got {std_dev}" ))); } if !(0.0..=1.0).contains(&confidence) { return Err(RiskAnalyzerError::InvalidConfig(format!( "Confidence level must be between 0 and 1, got {confidence}" ))); } let mut rng = rand::rngs::StdRng::seed_from_u64(42); let normal = Normal::new(mean, std_dev).map_err(|e| { RiskAnalyzerError::CalculationError(format!("Failed to create normal distribution: {e}")) })?; let mut final_returns = Vec::with_capacity(num_simulations as usize); for _ in 0..num_simulations { let mut cumulative_return = 0.0; for _ in 0..horizon { cumulative_return += normal.sample(&mut rng); } final_returns.push(cumulative_return); } historical_var(&final_returns, confidence) } /// Calculate the quantile of a standard normal distribution /// /// Uses Beasley-Springer-Moro algorithm for inverse normal CDF fn normal_quantile(p: f64) -> Result { if !(0.0..=1.0).contains(&p) { return Err(RiskAnalyzerError::InvalidConfig(format!( "Probability must be between 0 and 1, got {p}" ))); } if p == 0.0 { return Ok(f64::NEG_INFINITY); } if p == 1.0 { return Ok(f64::INFINITY); } let a = [ -3.969_683_028_665_376e1, 2.209_460_984_245_205e2, -2.759_285_104_469_687e2, 1.383_577_518_672_69e2, -3.066_479_806_614_716e1, 2.506_628_277_459_239, ]; let b = [ -5.447_609_879_822_406e1, 1.615_858_368_580_409e2, -1.556_989_798_598_866e2, 6.680_131_188_771_972e1, -1.328_068_155_288_572e1, ]; let c = [ -7.784_894_002_430_293e-3, -3.223_964_580_411_365e-1, -2.400_758_277_161_838, -2.549_732_539_343_734, 4.374_664_141_464_968, 2.938_163_982_698_783, ]; let d = [ 7.784_695_709_041_462e-3, 3.224_671_290_700_398e-1, 2.445_134_137_142_996, 3.754_408_661_907_416, ]; let p_low = 0.02425; let p_high = 1.0 - p_low; let q = if p < p_low { let q = (-2.0 * p.ln()).sqrt(); (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0) } else if p <= p_high { let q = p - 0.5; let r = q * q; (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q / (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1.0) } else { let q = (-2.0 * (1.0 - p).ln()).sqrt(); -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0) }; Ok(q) } #[cfg(test)] mod tests { use super::*; #[test] fn test_historical_var_empty_returns() { let result = historical_var(&[], 0.95); assert!(result.is_err()); assert!(matches!( result.unwrap_err(), RiskAnalyzerError::InsufficientData(_) )); } #[test] fn test_historical_var_invalid_confidence() { let returns = vec![0.01, -0.02, 0.03]; let result = historical_var(&returns, 1.5); assert!(result.is_err()); } #[test] fn test_historical_var_95_confidence() { let returns = vec![ 0.01, -0.02, 0.03, -0.01, 0.02, -0.03, 0.015, -0.025, 0.005, -0.015, ]; let var = historical_var(&returns, 0.95).unwrap(); assert!(var > 0.0); assert!(var <= 0.03); } #[test] fn test_historical_var_99_confidence() { let returns = vec![ 0.01, -0.02, 0.03, -0.01, 0.02, -0.03, 0.015, -0.025, 0.005, -0.015, -0.04, 0.025, -0.035, 0.018, -0.028, 0.012, -0.022, 0.008, -0.018, 0.002, ]; let var = historical_var(&returns, 0.99).unwrap(); assert!(var > 0.0); assert!(var >= 0.035); } #[test] fn test_parametric_var_negative_std() { let result = parametric_var(0.01, -0.1, 0.95); assert!(result.is_err()); } #[test] fn test_parametric_var_invalid_confidence() { let result = parametric_var(0.01, 0.1, 2.0); assert!(result.is_err()); } #[test] fn test_parametric_var_positive_mean() { let var = parametric_var(0.001, 0.02, 0.95).unwrap(); assert!(var > 0.0); } #[test] fn test_parametric_var_negative_mean() { let var = parametric_var(-0.001, 0.02, 0.95).unwrap(); assert!(var > 0.0); } #[test] fn test_parametric_var_99_vs_95() { let mean = 0.001; let std = 0.02; let var_95 = parametric_var(mean, std, 0.95).unwrap(); let var_99 = parametric_var(mean, std, 0.99).unwrap(); assert!(var_99 > var_95); } #[test] fn test_monte_carlo_var_zero_simulations() { let result = monte_carlo_var(0.001, 0.02, 1, 0, 0.95); assert!(result.is_err()); } #[test] fn test_monte_carlo_var_zero_horizon() { let result = monte_carlo_var(0.001, 0.02, 0, 1000, 0.95); assert!(result.is_err()); } #[test] fn test_monte_carlo_var_negative_std() { let result = monte_carlo_var(0.001, -0.02, 1, 1000, 0.95); assert!(result.is_err()); } #[test] fn test_monte_carlo_var_basic() { let var = monte_carlo_var(0.001, 0.02, 1, 10_000, 0.95).unwrap(); assert!(var > 0.0); assert!(var < 0.1); } #[test] fn test_monte_carlo_var_horizon_scaling() { let var_1 = monte_carlo_var(0.001, 0.02, 1, 10_000, 0.95).unwrap(); let var_10 = monte_carlo_var(0.001, 0.02, 10, 10_000, 0.95).unwrap(); assert!(var_10 > var_1); } #[test] fn test_monte_carlo_var_confidence_scaling() { let var_95 = monte_carlo_var(0.001, 0.02, 1, 10_000, 0.95).unwrap(); let var_99 = monte_carlo_var(0.001, 0.02, 1, 10_000, 0.99).unwrap(); assert!(var_99 > var_95); } #[test] fn test_normal_quantile_invalid() { let result = normal_quantile(1.5); assert!(result.is_err()); } #[test] fn test_normal_quantile_0_5() { let q = normal_quantile(0.5).unwrap(); assert!((q.abs()) < 1e-10); } #[test] fn test_normal_quantile_0_05() { let q = normal_quantile(0.05).unwrap(); assert!((q - (-1.6449)).abs() < 0.01); } #[test] fn test_normal_quantile_0_95() { let q = normal_quantile(0.95).unwrap(); assert!((q - 1.6449).abs() < 0.01); } #[test] fn test_normal_quantile_0_01() { let q = normal_quantile(0.01).unwrap(); assert!((q - (-2.3263)).abs() < 0.01); } #[test] fn test_normal_quantile_0_99() { let q = normal_quantile(0.99).unwrap(); assert!((q - 2.3263).abs() < 0.01); } }