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 {
59 crate::normal::inv_cdf(p.clamp(1e-10, 1.0 - 1e-10))
60 }
61
62 fn mean(data: &[f64]) -> f64 {
64 if data.is_empty() {
65 return 0.0;
66 }
67 data.iter().sum::<f64>() / data.len() as f64
68 }
69
70 fn std_dev(data: &[f64]) -> f64 {
72 if data.len() < 2 {
73 return 0.0;
74 }
75 let m = Self::mean(data);
76 let var = data.iter().map(|x| (x - m).powi(2)).sum::<f64>() / (data.len() - 1) as f64;
77 var.sqrt()
78 }
79
80 fn skewness(data: &[f64]) -> f64 {
82 let n = data.len() as f64;
83 if n < 3.0 {
84 return 0.0;
85 }
86 let m = Self::mean(data);
87 let s = Self::std_dev(data);
88 if s == 0.0 {
89 return 0.0;
90 }
91 let sum3 = data.iter().map(|x| ((x - m) / s).powi(3)).sum::<f64>();
92 (n / ((n - 1.0) * (n - 2.0))) * sum3
93 }
94
95 fn excess_kurtosis(data: &[f64]) -> f64 {
97 let n = data.len() as f64;
98 if n < 4.0 {
99 return 0.0;
100 }
101 let m = Self::mean(data);
102 let s = Self::std_dev(data);
103 if s == 0.0 {
104 return 0.0;
105 }
106 let sum4 = data.iter().map(|x| ((x - m) / s).powi(4)).sum::<f64>();
107
108 (n * (n + 1.0) / ((n - 1.0) * (n - 2.0) * (n - 3.0))) * sum4
109 - 3.0 * (n - 1.0).powi(2) / ((n - 2.0) * (n - 3.0))
110 }
111
112 fn lcg_random(seed: &mut u64) -> f64 {
114 *seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
115 let bits = 0x3FF0000000000000_u64 | (*seed >> 12);
116 f64::from_bits(bits) - 1.0
117 }
118
119 fn standard_normal(seed: &mut u64) -> f64 {
121 let u1 = Self::lcg_random(seed).max(1e-10);
122 let u2 = Self::lcg_random(seed);
123 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
124 }
125
126 pub fn historical_var(
132 returns: &[f64],
133 position_value: f64,
134 confidence: f64,
135 horizon_days: u32,
136 ) -> VaRResult {
137 if returns.is_empty() {
138 return VaRResult {
139 confidence,
140 horizon_days,
141 var_usd: 0.0,
142 var_pct: 0.0,
143 method: VarMethod::Historical,
144 };
145 }
146 let mut sorted = returns.to_vec();
147 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
148
149 let alpha = 1.0 - confidence;
150 let idx = ((alpha * sorted.len() as f64).floor() as usize).min(sorted.len() - 1);
151 let daily_var_pct = -sorted[idx]; let var_pct = daily_var_pct * (horizon_days as f64).sqrt();
155 let var_usd = var_pct * position_value;
156
157 VaRResult {
158 confidence,
159 horizon_days,
160 var_usd: var_usd.max(0.0),
161 var_pct: var_pct.max(0.0),
162 method: VarMethod::Historical,
163 }
164 }
165
166 pub fn parametric_var(
170 mu: f64,
171 sigma: f64,
172 position_value: f64,
173 confidence: f64,
174 horizon_days: u32,
175 ) -> VaRResult {
176 let z = -Self::probit(1.0 - confidence); let daily_var_pct = -(mu - z * sigma);
179 let var_pct = (daily_var_pct * (horizon_days as f64).sqrt()).max(0.0);
180 let var_usd = var_pct * position_value;
181
182 VaRResult {
183 confidence,
184 horizon_days,
185 var_usd,
186 var_pct,
187 method: VarMethod::Parametric,
188 }
189 }
190
191 pub fn monte_carlo_var(
196 mu: f64,
197 sigma: f64,
198 position_value: f64,
199 confidence: f64,
200 horizon_days: u32,
201 n: usize,
202 seed: u64,
203 ) -> VaRResult {
204 let mut rng_seed = seed;
205 let dt = 1.0; let mut terminal_returns: Vec<f64> = Vec::with_capacity(n);
207
208 for _ in 0..n {
209 let mut log_return = 0.0_f64;
210 for _ in 0..horizon_days {
211 let z = Self::standard_normal(&mut rng_seed);
212 log_return += (mu - 0.5 * sigma * sigma) * dt + sigma * dt.sqrt() * z;
213 }
214 terminal_returns.push(log_return.exp() - 1.0);
216 }
217
218 terminal_returns.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
219 let alpha = 1.0 - confidence;
220 let idx = ((alpha * n as f64).floor() as usize).min(n.saturating_sub(1));
221 let var_pct = (-terminal_returns[idx]).max(0.0);
222 let var_usd = var_pct * position_value;
223
224 VaRResult {
225 confidence,
226 horizon_days,
227 var_usd,
228 var_pct,
229 method: VarMethod::MonteCarlo {
230 n_simulations: n,
231 seed,
232 },
233 }
234 }
235
236 pub fn conditional_var(
238 returns: &[f64],
239 position_value: f64,
240 confidence: f64,
241 horizon_days: u32,
242 ) -> CVaRResult {
243 let var_result =
244 Self::historical_var(returns, position_value, confidence, horizon_days);
245
246 if returns.is_empty() {
247 return CVaRResult {
248 confidence,
249 cvar_usd: 0.0,
250 var_result,
251 };
252 }
253
254 let mut sorted = returns.to_vec();
255 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
256
257 let alpha = 1.0 - confidence;
258 let cutoff_idx = ((alpha * sorted.len() as f64).floor() as usize).min(sorted.len() - 1);
259
260 let tail: Vec<f64> = sorted[..=cutoff_idx].to_vec();
262 let cvar_pct = if tail.is_empty() {
263 0.0
264 } else {
265 -Self::mean(&tail) * (horizon_days as f64).sqrt()
266 };
267 let cvar_usd = (cvar_pct * position_value).max(0.0);
268
269 CVaRResult {
270 confidence,
271 cvar_usd,
272 var_result,
273 }
274 }
275
276 pub fn portfolio_var(individual_vars: &[f64], correlation_matrix: &[Vec<f64>]) -> f64 {
283 let n = individual_vars.len();
284 if n == 0 {
285 return 0.0;
286 }
287
288 let mut variance = 0.0_f64;
289 for i in 0..n {
290 for j in 0..n {
291 let corr = if i < correlation_matrix.len() && j < correlation_matrix[i].len() {
292 correlation_matrix[i][j]
293 } else if i == j { 1.0 } else { 0.0 };
294 variance += individual_vars[i] * individual_vars[j] * corr;
295 }
296 }
297 variance.max(0.0).sqrt()
298 }
299
300 pub fn cornish_fisher_var(
306 returns: &[f64],
307 position_value: f64,
308 confidence: f64,
309 horizon_days: u32,
310 ) -> VaRResult {
311 if returns.is_empty() {
312 return VaRResult {
313 confidence,
314 horizon_days,
315 var_usd: 0.0,
316 var_pct: 0.0,
317 method: VarMethod::CornishFisher,
318 };
319 }
320
321 let mu = Self::mean(returns);
322 let sigma = Self::std_dev(returns);
323 let skew = Self::skewness(returns);
324 let kurt = Self::excess_kurtosis(returns);
325
326 let z = -Self::probit(1.0 - confidence); let z_cf = z
333 - (z.powi(2) - 1.0) * skew / 6.0
334 + (z.powi(3) - 3.0 * z) * kurt / 24.0
335 - (2.0 * z.powi(3) - 5.0 * z) * skew.powi(2) / 36.0;
336
337 let daily_var_pct = -(mu - z_cf * sigma);
338 let var_pct = (daily_var_pct * (horizon_days as f64).sqrt()).max(0.0);
339 let var_usd = var_pct * position_value;
340
341 VaRResult {
342 confidence,
343 horizon_days,
344 var_usd,
345 var_pct,
346 method: VarMethod::CornishFisher,
347 }
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 fn sample_returns() -> Vec<f64> {
356 vec![
357 0.01, -0.02, 0.015, -0.03, 0.005, -0.01, 0.02, -0.025, 0.008, -0.015,
358 0.012, -0.018, 0.003, -0.04, 0.022, -0.011, 0.007, -0.009, 0.014, -0.035,
359 ]
360 }
361
362 #[test]
363 fn test_historical_var_basic() {
364 let returns = sample_returns();
365 let result = VaRCalculator::historical_var(&returns, 1_000_000.0, 0.95, 1);
366 assert!(result.var_usd > 0.0, "VaR should be positive");
367 assert!(result.var_pct > 0.0, "VaR pct should be positive");
368 assert!((result.confidence - 0.95).abs() < 1e-9);
369 assert_eq!(result.horizon_days, 1);
370 }
371
372 #[test]
373 fn test_historical_var_empty() {
374 let result = VaRCalculator::historical_var(&[], 1_000_000.0, 0.95, 1);
375 assert_eq!(result.var_usd, 0.0);
376 }
377
378 #[test]
379 fn test_historical_var_horizon_scaling() {
380 let returns = sample_returns();
381 let var_1 = VaRCalculator::historical_var(&returns, 1_000_000.0, 0.95, 1);
382 let var_10 = VaRCalculator::historical_var(&returns, 1_000_000.0, 0.95, 10);
383 let ratio = var_10.var_usd / var_1.var_usd;
384 assert!((ratio - 10.0_f64.sqrt()).abs() < 1e-6, "sqrt-of-time scaling, ratio={ratio}");
385 }
386
387 #[test]
388 fn test_parametric_var_95() {
389 let result = VaRCalculator::parametric_var(0.0, 0.01, 1_000_000.0, 0.95, 1);
391 assert!(result.var_usd > 10_000.0 && result.var_usd < 25_000.0,
393 "var_usd={}", result.var_usd);
394 }
395
396 #[test]
397 fn test_parametric_var_99() {
398 let result99 = VaRCalculator::parametric_var(0.0, 0.01, 1_000_000.0, 0.99, 1);
399 let result95 = VaRCalculator::parametric_var(0.0, 0.01, 1_000_000.0, 0.95, 1);
400 assert!(result99.var_usd > result95.var_usd, "99% VaR should exceed 95% VaR");
401 }
402
403 #[test]
404 fn test_monte_carlo_var_reasonable() {
405 let result = VaRCalculator::monte_carlo_var(
406 0.0005, 0.015, 1_000_000.0, 0.95, 10, 10_000, 42,
407 );
408 assert!(result.var_usd > 0.0);
409 assert!(result.var_pct < 0.5, "VaR fraction should be < 50%");
410 assert!(matches!(result.method, VarMethod::MonteCarlo { n_simulations: 10_000, seed: 42 }));
411 }
412
413 #[test]
414 fn test_conditional_var_exceeds_var() {
415 let returns = sample_returns();
416 let cvar = VaRCalculator::conditional_var(&returns, 1_000_000.0, 0.95, 1);
417 assert!(cvar.cvar_usd >= cvar.var_result.var_usd,
418 "CVaR should be >= VaR: cvar={}, var={}", cvar.cvar_usd, cvar.var_result.var_usd);
419 }
420
421 #[test]
422 fn test_conditional_var_empty() {
423 let result = VaRCalculator::conditional_var(&[], 1_000_000.0, 0.95, 1);
424 assert_eq!(result.cvar_usd, 0.0);
425 }
426
427 #[test]
428 fn test_portfolio_var_uncorrelated() {
429 let vars = vec![1000.0, 1000.0];
431 let corr = vec![
432 vec![1.0, 0.0],
433 vec![0.0, 1.0],
434 ];
435 let pvar = VaRCalculator::portfolio_var(&vars, &corr);
436 assert!((pvar - 2.0_f64.sqrt() * 1000.0).abs() < 1e-6, "pvar={pvar}");
437 }
438
439 #[test]
440 fn test_portfolio_var_perfectly_correlated() {
441 let vars = vec![1000.0, 1000.0];
443 let corr = vec![
444 vec![1.0, 1.0],
445 vec![1.0, 1.0],
446 ];
447 let pvar = VaRCalculator::portfolio_var(&vars, &corr);
448 assert!((pvar - 2000.0).abs() < 1e-6, "pvar={pvar}");
449 }
450
451 #[test]
452 fn test_portfolio_var_empty() {
453 let pvar = VaRCalculator::portfolio_var(&[], &[]);
454 assert_eq!(pvar, 0.0);
455 }
456
457 #[test]
458 fn test_cornish_fisher_var() {
459 let returns = sample_returns();
460 let result = VaRCalculator::cornish_fisher_var(&returns, 1_000_000.0, 0.95, 1);
461 assert!(result.var_usd > 0.0);
462 assert!(matches!(result.method, VarMethod::CornishFisher));
463 }
464
465 #[test]
466 fn test_cornish_fisher_vs_parametric_with_tail_risk() {
467 let mut returns: Vec<f64> = (0..100).map(|i| 0.001 * (i as f64 - 50.0) / 50.0).collect();
469 returns.extend_from_slice(&[-0.08, -0.09, -0.10, -0.07, -0.085]);
471 let cf_result = VaRCalculator::cornish_fisher_var(&returns, 1_000_000.0, 0.95, 1);
472 assert!(cf_result.var_usd > 0.0);
473 let mu = VaRCalculator::mean(&returns);
474 let sigma = VaRCalculator::std_dev(&returns);
475 let p_result = VaRCalculator::parametric_var(mu, sigma, 1_000_000.0, 0.95, 1);
476 assert!(
477 cf_result.var_usd > p_result.var_usd,
478 "negative skew + fat tails must raise CF VaR above normal: cf={} normal={}",
479 cf_result.var_usd,
480 p_result.var_usd
481 );
482 }
483
484 #[test]
485 fn test_probit_symmetry() {
486 let z = -VaRCalculator::probit(0.05);
488 assert!((z - 1.645).abs() < 0.01, "z={z}");
489 }
490}