fix(risk): replace linear z-score interpolation with Abramowitz-Stegun
Linear extrapolation above 0.99 gave z=2.48 for 99.9% (correct: 3.09). VaR was understated by ~25% at high confidence levels. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user