1use crate::core::utils::{norm_pdf, inv_norm_cdf};
10
11fn quantile(sorted: &[f64], p: f64) -> f64 {
13 let n = sorted.len();
14 assert!(n > 0);
15 let h = (n as f64 - 1.0) * p.clamp(0.0, 1.0);
16 let lo = h.floor() as usize;
17 let hi = (lo + 1).min(n - 1);
18 sorted[lo] + (h - lo as f64) * (sorted[hi] - sorted[lo])
19}
20
21pub fn historical_var(pnl: &[f64], confidence: f64) -> f64 {
23 assert!(!pnl.is_empty() && confidence > 0.5 && confidence < 1.0);
24 let mut losses: Vec<f64> = pnl.iter().map(|x| -x).collect();
25 losses.sort_by(f64::total_cmp);
26 quantile(&losses, confidence).max(0.0)
27}
28
29pub fn historical_expected_shortfall(pnl: &[f64], confidence: f64) -> f64 {
32 assert!(!pnl.is_empty() && confidence > 0.5 && confidence < 1.0);
33 let mut losses: Vec<f64> = pnl.iter().map(|x| -x).collect();
34 losses.sort_by(f64::total_cmp);
35 let var = quantile(&losses, confidence);
36 let tail: Vec<f64> = losses.iter().copied().filter(|&l| l >= var).collect();
37 if tail.is_empty() {
38 return var.max(0.0);
39 }
40 (tail.iter().sum::<f64>() / tail.len() as f64).max(0.0)
41}
42
43pub fn parametric_var(mean: f64, std: f64, confidence: f64) -> f64 {
46 assert!(std >= 0.0 && confidence > 0.5 && confidence < 1.0);
47 (-mean + std * inv_norm_cdf(confidence)).max(0.0)
48}
49
50pub fn parametric_expected_shortfall(mean: f64, std: f64, confidence: f64) -> f64 {
53 assert!(std >= 0.0 && confidence > 0.5 && confidence < 1.0);
54 let z = inv_norm_cdf(confidence);
55 (-mean + std * norm_pdf(z) / (1.0 - confidence)).max(0.0)
56}
57
58pub fn cornish_fisher_var(
63 mean: f64,
64 std: f64,
65 skewness: f64,
66 excess_kurtosis: f64,
67 confidence: f64,
68) -> f64 {
69 let zl = -inv_norm_cdf(confidence);
74 let z2 = zl * zl;
75 let q = zl
76 + (z2 - 1.0) * skewness / 6.0
77 + zl * (z2 - 3.0) * excess_kurtosis / 24.0
78 - zl * (2.0 * z2 - 5.0) * skewness * skewness / 36.0;
79 (-(mean + std * q)).max(0.0)
80}
81
82#[derive(Debug, Clone)]
85pub struct DeltaNormalVar {
86 pub var: f64,
88 pub std: f64,
90 pub component_var: Vec<f64>,
92 pub marginal_var: Vec<f64>,
94}
95
96pub fn delta_normal_var(
100 exposures: &[f64],
101 covariance: &[Vec<f64>],
102 confidence: f64,
103) -> DeltaNormalVar {
104 let n = exposures.len();
105 assert!(n > 0 && covariance.len() == n);
106 assert!(covariance.iter().all(|row| row.len() == n));
107 let cw: Vec<f64> = (0..n)
109 .map(|i| (0..n).map(|j| covariance[i][j] * exposures[j]).sum())
110 .collect();
111 let variance: f64 = exposures.iter().zip(&cw).map(|(w, c)| w * c).sum();
112 assert!(variance >= -1e-12, "covariance matrix is not PSD on this exposure");
113 let std = variance.max(0.0).sqrt();
114 let z = inv_norm_cdf(confidence);
115 let var = std * z;
116 let (component_var, marginal_var) = if std > 0.0 {
118 (
119 exposures.iter().zip(&cw).map(|(w, c)| w * c / std * z).collect(),
120 cw.iter().map(|c| c / std * z).collect(),
121 )
122 } else {
123 (vec![0.0; n], vec![0.0; n])
124 };
125 DeltaNormalVar { var, std, component_var, marginal_var }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::core::montecarlo::path_rng;
132 use rand::Rng;
133
134 #[test]
135 fn historical_measures_on_a_hand_checked_sample() {
136 let pnl = [-10.0, -8.0, -6.0, -4.0, -2.0, 1.0, 2.0, 3.0, 4.0, 5.0];
138 let var90 = historical_var(&pnl, 0.9);
139 assert!((var90 - 8.2).abs() < 1e-12, "{var90}");
141 let es90 = historical_expected_shortfall(&pnl, 0.9);
142 assert!(es90 >= var90 && es90 <= 10.0, "{es90}");
143 assert!(historical_var(&pnl, 0.95) >= var90);
145 }
146
147 #[test]
148 fn historical_converges_to_parametric_on_normal_pnl() {
149 let n = 60_000;
150 let (mu, sd) = (0.5, 10.0);
151 let mut rng = path_rng(7, 0);
152 let pnl: Vec<f64> =
153 (0..n).map(|_| mu + sd * rng.sample::<f64, _>(rand_distr::StandardNormal)).collect();
154 for conf in [0.95, 0.99] {
155 let hist = historical_var(&pnl, conf);
156 let para = parametric_var(mu, sd, conf);
157 assert!((hist - para).abs() < 0.4, "VaR {conf}: {hist} vs {para}");
158 let hist_es = historical_expected_shortfall(&pnl, conf);
159 let para_es = parametric_expected_shortfall(mu, sd, conf);
160 assert!((hist_es - para_es).abs() < 0.5, "ES {conf}: {hist_es} vs {para_es}");
161 assert!(hist_es > hist && para_es > para, "ES dominates VaR");
162 }
163 }
164
165 #[test]
166 fn parametric_es_matches_numerical_tail_integration() {
167 let (mu, sd, conf) = (0.0, 1.0, 0.975);
169 let es = parametric_expected_shortfall(mu, sd, conf);
170 let z = inv_norm_cdf(conf);
171 let steps = 400_000;
172 let (a, b) = (z, 10.0);
173 let h = (b - a) / steps as f64;
174 let mut num = 0.0;
175 for i in 0..=steps {
176 let x = a + i as f64 * h;
177 let w = if i == 0 || i == steps { 0.5 } else { 1.0 };
178 num += w * x * norm_pdf(x) * h;
179 }
180 let tail_mean = num / (1.0 - conf);
181 assert!((es - tail_mean).abs() < 1e-4, "{es} vs {tail_mean}");
182 }
183
184 #[test]
185 fn cornish_fisher_reduces_to_normal_and_penalizes_left_skew() {
186 let base = parametric_var(0.0, 5.0, 0.99);
187 assert!((cornish_fisher_var(0.0, 5.0, 0.0, 0.0, 0.99) - base).abs() < 1e-12);
188 let skewed = cornish_fisher_var(0.0, 5.0, -0.8, 0.0, 0.99);
191 assert!(skewed > base, "{skewed} vs {base}");
192 let fat = cornish_fisher_var(0.0, 5.0, 0.0, 3.0, 0.99);
193 assert!(fat > base, "excess kurtosis must raise tail risk");
194 }
195
196 #[test]
197 fn delta_normal_var_and_euler_decomposition() {
198 let cov = vec![
201 vec![0.02 * 0.02, 0.5 * 0.02 * 0.03],
202 vec![0.5 * 0.02 * 0.03, 0.03 * 0.03],
203 ];
204 let w = [1_000_000.0, -200_000.0];
205 let out = delta_normal_var(&w, &cov, 0.99);
206 let variance = w[0] * w[0] * cov[0][0]
207 + 2.0 * w[0] * w[1] * cov[0][1]
208 + w[1] * w[1] * cov[1][1];
209 assert!((out.std - variance.sqrt()).abs() < 1e-6);
210 assert!((out.var - variance.sqrt() * inv_norm_cdf(0.99)).abs() < 1e-6);
211 let sum: f64 = out.component_var.iter().sum();
213 assert!((sum - out.var).abs() < 1e-6, "{sum} vs {}", out.var);
214 assert!(out.component_var[1] < 0.0, "{:?}", out.component_var);
218 let big_short = delta_normal_var(&[1_000_000.0, -500_000.0], &cov, 0.99);
221 assert!(big_short.component_var[1] > 0.0);
222 let sum2: f64 = big_short.component_var.iter().sum();
223 assert!((sum2 - big_short.var).abs() < 1e-6);
224 }
225}