1#[derive(Debug, Clone, PartialEq)]
7pub enum VarMethod {
8 Historical,
10 Parametric,
12 MonteCarlo {
14 n_simulations: usize,
16 seed: u64,
18 },
19 CornishFisher,
21}
22
23#[derive(Debug, Clone)]
25pub struct VaRResult {
26 pub confidence: f64,
28 pub horizon_days: u32,
30 pub var_usd: f64,
32 pub var_pct: f64,
34 pub method: VarMethod,
36}
37
38#[derive(Debug, Clone)]
40pub struct CVaRResult {
41 pub confidence: f64,
43 pub cvar_usd: f64,
45 pub var_result: VaRResult,
47}
48
49pub struct VaRCalculator;
51
52impl VaRCalculator {
53 fn probit(p: f64) -> f64 {
60 const A: [f64; 4] = [2.515517, 0.802853, 0.010328, 0.0];
62 const B: [f64; 3] = [1.432788, 0.189269, 0.001308];
63
64 let p = p.clamp(1e-10, 1.0 - 1e-10);
65 let sign = if p < 0.5 { -1.0_f64 } else { 1.0_f64 };
66 let t = if p < 0.5 {
67 (-2.0 * p.ln()).sqrt()
68 } else {
69 (-2.0 * (1.0 - p).ln()).sqrt()
70 };
71 let numerator = A[0] + A[1] * t + A[2] * t * t + A[3] * t * t * t;
72 let denominator = 1.0 + B[0] * t + B[1] * t * t + B[2] * t * t * t;
73 sign * (t - numerator / denominator)
74 }
75
76 fn mean(data: &[f64]) -> f64 {
78 if data.is_empty() {
79 return 0.0;
80 }
81 data.iter().sum::<f64>() / data.len() as f64
82 }
83
84 fn std_dev(data: &[f64]) -> f64 {
86 if data.len() < 2 {
87 return 0.0;
88 }
89 let m = Self::mean(data);
90 let var = data.iter().map(|x| (x - m).powi(2)).sum::<f64>() / (data.len() - 1) as f64;
91 var.sqrt()
92 }
93
94 fn skewness(data: &[f64]) -> f64 {
96 let n = data.len() as f64;
97 if n < 3.0 {
98 return 0.0;
99 }
100 let m = Self::mean(data);
101 let s = Self::std_dev(data);
102 if s == 0.0 {
103 return 0.0;
104 }
105 let sum3 = data.iter().map(|x| ((x - m) / s).powi(3)).sum::<f64>();
106 (n / ((n - 1.0) * (n - 2.0))) * sum3
107 }
108
109 fn excess_kurtosis(data: &[f64]) -> f64 {
111 let n = data.len() as f64;
112 if n < 4.0 {
113 return 0.0;
114 }
115 let m = Self::mean(data);
116 let s = Self::std_dev(data);
117 if s == 0.0 {
118 return 0.0;
119 }
120 let sum4 = data.iter().map(|x| ((x - m) / s).powi(4)).sum::<f64>();
121 let kurt = (n * (n + 1.0) / ((n - 1.0) * (n - 2.0) * (n - 3.0))) * sum4
122 - 3.0 * (n - 1.0).powi(2) / ((n - 2.0) * (n - 3.0));
123 kurt
124 }
125
126 fn lcg_random(seed: &mut u64) -> f64 {
128 *seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
129 let bits = 0x3FF0000000000000_u64 | (*seed >> 12);
130 f64::from_bits(bits) - 1.0
131 }
132
133 fn standard_normal(seed: &mut u64) -> f64 {
135 let u1 = Self::lcg_random(seed).max(1e-10);
136 let u2 = Self::lcg_random(seed);
137 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
138 }
139
140 pub fn historical_var(
146 returns: &[f64],
147 position_value: f64,
148 confidence: f64,
149 horizon_days: u32,
150 ) -> VaRResult {
151 if returns.is_empty() {
152 return VaRResult {
153 confidence,
154 horizon_days,
155 var_usd: 0.0,
156 var_pct: 0.0,
157 method: VarMethod::Historical,
158 };
159 }
160 let mut sorted = returns.to_vec();
161 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
162
163 let alpha = 1.0 - confidence;
164 let idx = ((alpha * sorted.len() as f64).floor() as usize).min(sorted.len() - 1);
165 let daily_var_pct = -sorted[idx]; let var_pct = daily_var_pct * (horizon_days as f64).sqrt();
169 let var_usd = var_pct * position_value;
170
171 VaRResult {
172 confidence,
173 horizon_days,
174 var_usd: var_usd.max(0.0),
175 var_pct: var_pct.max(0.0),
176 method: VarMethod::Historical,
177 }
178 }
179
180 pub fn parametric_var(
184 mu: f64,
185 sigma: f64,
186 position_value: f64,
187 confidence: f64,
188 horizon_days: u32,
189 ) -> VaRResult {
190 let z = -Self::probit(1.0 - confidence); let daily_var_pct = -(mu - z * sigma);
193 let var_pct = (daily_var_pct * (horizon_days as f64).sqrt()).max(0.0);
194 let var_usd = var_pct * position_value;
195
196 VaRResult {
197 confidence,
198 horizon_days,
199 var_usd,
200 var_pct,
201 method: VarMethod::Parametric,
202 }
203 }
204
205 pub fn monte_carlo_var(
210 mu: f64,
211 sigma: f64,
212 position_value: f64,
213 confidence: f64,
214 horizon_days: u32,
215 n: usize,
216 seed: u64,
217 ) -> VaRResult {
218 let mut rng_seed = seed;
219 let dt = 1.0; let mut terminal_returns: Vec<f64> = Vec::with_capacity(n);
221
222 for _ in 0..n {
223 let mut log_return = 0.0_f64;
224 for _ in 0..horizon_days {
225 let z = Self::standard_normal(&mut rng_seed);
226 log_return += (mu - 0.5 * sigma * sigma) * dt + sigma * dt.sqrt() * z;
227 }
228 terminal_returns.push(log_return.exp() - 1.0);
230 }
231
232 terminal_returns.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
233 let alpha = 1.0 - confidence;
234 let idx = ((alpha * n as f64).floor() as usize).min(n.saturating_sub(1));
235 let var_pct = (-terminal_returns[idx]).max(0.0);
236 let var_usd = var_pct * position_value;
237
238 VaRResult {
239 confidence,
240 horizon_days,
241 var_usd,
242 var_pct,
243 method: VarMethod::MonteCarlo {
244 n_simulations: n,
245 seed,
246 },
247 }
248 }
249
250 pub fn conditional_var(
252 returns: &[f64],
253 position_value: f64,
254 confidence: f64,
255 horizon_days: u32,
256 ) -> CVaRResult {
257 let var_result =
258 Self::historical_var(returns, position_value, confidence, horizon_days);
259
260 if returns.is_empty() {
261 return CVaRResult {
262 confidence,
263 cvar_usd: 0.0,
264 var_result,
265 };
266 }
267
268 let mut sorted = returns.to_vec();
269 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
270
271 let alpha = 1.0 - confidence;
272 let cutoff_idx = ((alpha * sorted.len() as f64).floor() as usize).min(sorted.len() - 1);
273
274 let tail: Vec<f64> = sorted[..=cutoff_idx].to_vec();
276 let cvar_pct = if tail.is_empty() {
277 0.0
278 } else {
279 -Self::mean(&tail) * (horizon_days as f64).sqrt()
280 };
281 let cvar_usd = (cvar_pct * position_value).max(0.0);
282
283 CVaRResult {
284 confidence,
285 cvar_usd,
286 var_result,
287 }
288 }
289
290 pub fn portfolio_var(individual_vars: &[f64], correlation_matrix: &[Vec<f64>]) -> f64 {
297 let n = individual_vars.len();
298 if n == 0 {
299 return 0.0;
300 }
301
302 let mut variance = 0.0_f64;
303 for i in 0..n {
304 for j in 0..n {
305 let corr = if i < correlation_matrix.len() && j < correlation_matrix[i].len() {
306 correlation_matrix[i][j]
307 } else {
308 if i == j { 1.0 } else { 0.0 }
309 };
310 variance += individual_vars[i] * individual_vars[j] * corr;
311 }
312 }
313 variance.max(0.0).sqrt()
314 }
315
316 pub fn cornish_fisher_var(
322 returns: &[f64],
323 position_value: f64,
324 confidence: f64,
325 horizon_days: u32,
326 ) -> VaRResult {
327 if returns.is_empty() {
328 return VaRResult {
329 confidence,
330 horizon_days,
331 var_usd: 0.0,
332 var_pct: 0.0,
333 method: VarMethod::CornishFisher,
334 };
335 }
336
337 let mu = Self::mean(returns);
338 let sigma = Self::std_dev(returns);
339 let skew = Self::skewness(returns);
340 let kurt = Self::excess_kurtosis(returns);
341
342 let z = -Self::probit(1.0 - confidence); let z_cf = z
349 - (z.powi(2) - 1.0) * skew / 6.0
350 + (z.powi(3) - 3.0 * z) * kurt / 24.0
351 - (2.0 * z.powi(3) - 5.0 * z) * skew.powi(2) / 36.0;
352
353 let daily_var_pct = -(mu - z_cf * sigma);
354 let var_pct = (daily_var_pct * (horizon_days as f64).sqrt()).max(0.0);
355 let var_usd = var_pct * position_value;
356
357 VaRResult {
358 confidence,
359 horizon_days,
360 var_usd,
361 var_pct,
362 method: VarMethod::CornishFisher,
363 }
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 fn sample_returns() -> Vec<f64> {
372 vec![
373 0.01, -0.02, 0.015, -0.03, 0.005, -0.01, 0.02, -0.025, 0.008, -0.015,
374 0.012, -0.018, 0.003, -0.04, 0.022, -0.011, 0.007, -0.009, 0.014, -0.035,
375 ]
376 }
377
378 #[test]
379 fn test_historical_var_basic() {
380 let returns = sample_returns();
381 let result = VaRCalculator::historical_var(&returns, 1_000_000.0, 0.95, 1);
382 assert!(result.var_usd > 0.0, "VaR should be positive");
383 assert!(result.var_pct > 0.0, "VaR pct should be positive");
384 assert!((result.confidence - 0.95).abs() < 1e-9);
385 assert_eq!(result.horizon_days, 1);
386 }
387
388 #[test]
389 fn test_historical_var_empty() {
390 let result = VaRCalculator::historical_var(&[], 1_000_000.0, 0.95, 1);
391 assert_eq!(result.var_usd, 0.0);
392 }
393
394 #[test]
395 fn test_historical_var_horizon_scaling() {
396 let returns = sample_returns();
397 let var_1 = VaRCalculator::historical_var(&returns, 1_000_000.0, 0.95, 1);
398 let var_10 = VaRCalculator::historical_var(&returns, 1_000_000.0, 0.95, 10);
399 let ratio = var_10.var_usd / var_1.var_usd;
400 assert!((ratio - 10.0_f64.sqrt()).abs() < 1e-6, "sqrt-of-time scaling, ratio={ratio}");
401 }
402
403 #[test]
404 fn test_parametric_var_95() {
405 let result = VaRCalculator::parametric_var(0.0, 0.01, 1_000_000.0, 0.95, 1);
407 assert!(result.var_usd > 10_000.0 && result.var_usd < 25_000.0,
409 "var_usd={}", result.var_usd);
410 }
411
412 #[test]
413 fn test_parametric_var_99() {
414 let result99 = VaRCalculator::parametric_var(0.0, 0.01, 1_000_000.0, 0.99, 1);
415 let result95 = VaRCalculator::parametric_var(0.0, 0.01, 1_000_000.0, 0.95, 1);
416 assert!(result99.var_usd > result95.var_usd, "99% VaR should exceed 95% VaR");
417 }
418
419 #[test]
420 fn test_monte_carlo_var_reasonable() {
421 let result = VaRCalculator::monte_carlo_var(
422 0.0005, 0.015, 1_000_000.0, 0.95, 10, 10_000, 42,
423 );
424 assert!(result.var_usd > 0.0);
425 assert!(result.var_pct < 0.5, "VaR fraction should be < 50%");
426 assert!(matches!(result.method, VarMethod::MonteCarlo { n_simulations: 10_000, seed: 42 }));
427 }
428
429 #[test]
430 fn test_conditional_var_exceeds_var() {
431 let returns = sample_returns();
432 let cvar = VaRCalculator::conditional_var(&returns, 1_000_000.0, 0.95, 1);
433 assert!(cvar.cvar_usd >= cvar.var_result.var_usd,
434 "CVaR should be >= VaR: cvar={}, var={}", cvar.cvar_usd, cvar.var_result.var_usd);
435 }
436
437 #[test]
438 fn test_conditional_var_empty() {
439 let result = VaRCalculator::conditional_var(&[], 1_000_000.0, 0.95, 1);
440 assert_eq!(result.cvar_usd, 0.0);
441 }
442
443 #[test]
444 fn test_portfolio_var_uncorrelated() {
445 let vars = vec![1000.0, 1000.0];
447 let corr = vec![
448 vec![1.0, 0.0],
449 vec![0.0, 1.0],
450 ];
451 let pvar = VaRCalculator::portfolio_var(&vars, &corr);
452 assert!((pvar - 2.0_f64.sqrt() * 1000.0).abs() < 1e-6, "pvar={pvar}");
453 }
454
455 #[test]
456 fn test_portfolio_var_perfectly_correlated() {
457 let vars = vec![1000.0, 1000.0];
459 let corr = vec![
460 vec![1.0, 1.0],
461 vec![1.0, 1.0],
462 ];
463 let pvar = VaRCalculator::portfolio_var(&vars, &corr);
464 assert!((pvar - 2000.0).abs() < 1e-6, "pvar={pvar}");
465 }
466
467 #[test]
468 fn test_portfolio_var_empty() {
469 let pvar = VaRCalculator::portfolio_var(&[], &[]);
470 assert_eq!(pvar, 0.0);
471 }
472
473 #[test]
474 fn test_cornish_fisher_var() {
475 let returns = sample_returns();
476 let result = VaRCalculator::cornish_fisher_var(&returns, 1_000_000.0, 0.95, 1);
477 assert!(result.var_usd > 0.0);
478 assert!(matches!(result.method, VarMethod::CornishFisher));
479 }
480
481 #[test]
482 fn test_cornish_fisher_vs_parametric_with_tail_risk() {
483 let mut returns: Vec<f64> = (0..100).map(|i| 0.001 * (i as f64 - 50.0) / 50.0).collect();
485 returns.extend_from_slice(&[-0.08, -0.09, -0.10, -0.07, -0.085]);
487 let cf_result = VaRCalculator::cornish_fisher_var(&returns, 1_000_000.0, 0.95, 1);
488 assert!(cf_result.var_usd > 0.0);
489 let mu = VaRCalculator::mean(&returns);
490 let sigma = VaRCalculator::std_dev(&returns);
491 let p_result = VaRCalculator::parametric_var(mu, sigma, 1_000_000.0, 0.95, 1);
492 assert!(
493 cf_result.var_usd > p_result.var_usd,
494 "negative skew + fat tails must raise CF VaR above normal: cf={} normal={}",
495 cf_result.var_usd,
496 p_result.var_usd
497 );
498 }
499
500 #[test]
501 fn test_probit_symmetry() {
502 let z = -VaRCalculator::probit(0.05);
504 assert!((z - 1.645).abs() < 0.01, "z={z}");
505 }
506}