diff --git a/risk/src/var_calculator/parametric.rs b/risk/src/var_calculator/parametric.rs index 042d624f9..dc0abef45 100644 --- a/risk/src/var_calculator/parametric.rs +++ b/risk/src/var_calculator/parametric.rs @@ -146,24 +146,26 @@ impl ParametricVaR { .ok_or_else(|| anyhow::anyhow!("Failed to convert VaR amount to Decimal: {var_amount}")) } - /// Get z-score for given confidence level + /// Get z-score for given confidence level using Abramowitz-Stegun + /// rational approximation for the inverse normal CDF. + /// Accurate to ~4.5e-4 across the full range. fn get_z_score(confidence_level: f64) -> f64 { - // Approximate z-scores for common confidence levels - match (confidence_level * 100.0) as u32 { - 90 => 1.282, - 95 => 1.645, - 99 => 2.326, - _ => { - // Linear interpolation for other values - if confidence_level <= 0.90 { - 1.282 * confidence_level / 0.90 - } else if confidence_level <= 0.95 { - 1.282 + (1.645 - 1.282) * (confidence_level - 0.90) / 0.05 - } else { - 1.645 + (2.326 - 1.645) * (confidence_level - 0.95) / 0.04 - } - }, + let p = 1.0 - confidence_level; + if p <= 0.0 || p >= 1.0 { + return 2.326; // fallback to 99% z-score for invalid inputs } + + let t = (-2.0_f64 * p.ln()).sqrt(); + + // Coefficients from Abramowitz-Stegun formula 26.2.23 + let c0 = 2.515517; + let c1 = 0.802853; + let c2 = 0.010328; + let d1 = 1.432788; + let d2 = 0.189269; + let d3 = 0.001308; + + t - (c0 + c1 * t + c2 * t * t) / (1.0 + d1 * t + d2 * t * t + d3 * t * t * t) } /// Calculate component `VaR` (marginal contribution to `VaR`) @@ -228,10 +230,48 @@ mod tests { } #[test] - fn test_z_score_calculation() { - assert!((ParametricVaR::get_z_score(0.90) - 1.282).abs() < 0.01); - assert!((ParametricVaR::get_z_score(0.95) - 1.645).abs() < 0.01); - assert!((ParametricVaR::get_z_score(0.99) - 2.326).abs() < 0.01); + fn test_z_score_accuracy() { + assert!( + (ParametricVaR::get_z_score(0.90) - 1.282).abs() < 0.01, + "90% z-score: got {}, expected ~1.282", + ParametricVaR::get_z_score(0.90) + ); + assert!( + (ParametricVaR::get_z_score(0.95) - 1.645).abs() < 0.01, + "95% z-score: got {}, expected ~1.645", + ParametricVaR::get_z_score(0.95) + ); + assert!( + (ParametricVaR::get_z_score(0.99) - 2.326).abs() < 0.01, + "99% z-score: got {}, expected ~2.326", + ParametricVaR::get_z_score(0.99) + ); + assert!( + (ParametricVaR::get_z_score(0.999) - 3.090).abs() < 0.05, + "99.9% z-score should be ~3.09, not ~2.48; got {}", + ParametricVaR::get_z_score(0.999) + ); + } + + #[test] + fn test_z_score_edge_cases() { + // 50% confidence should give z near 0 + let z = ParametricVaR::get_z_score(0.5); + assert!( + z.abs() < 0.1, + "50% z-score should be ~0.0, got {}", + z + ); + // Very high confidence should give large z + let z_high = ParametricVaR::get_z_score(0.9999); + assert!( + z_high > 3.0, + "99.99% z-score should be >3.0, got {}", + z_high + ); + // Boundary: p=0 or p=1 should return fallback + assert!((ParametricVaR::get_z_score(1.0) - 2.326).abs() < 1e-6); + assert!((ParametricVaR::get_z_score(0.0) - 2.326).abs() < 1e-6); } #[test]