1#[derive(Debug, Clone, PartialEq)]
7pub enum VarMethod {
8 Historical,
10 Parametric,
12 MonteCarlo {
14 simulations: usize,
16 seed: u64,
18 },
19}
20
21#[derive(Debug, Clone)]
23pub struct VarResult {
24 pub confidence_level: f64,
26 pub var_abs: f64,
28 pub var_pct: f64,
30 pub cvar_abs: f64,
32 pub cvar_pct: f64,
34 pub method: VarMethod,
36}
37
38#[derive(Debug, Clone)]
40pub struct PortfolioReturns {
41 pub returns: Vec<f64>,
43 pub weights: Vec<f64>,
45 pub portfolio_value: f64,
47}
48
49impl PortfolioReturns {
50 pub fn portfolio_returns(&self) -> Vec<f64> {
57 self.returns
58 .iter()
59 .zip(self.weights.iter())
60 .map(|(r, w)| r * w)
61 .collect()
62 }
63
64 pub fn mean(&self) -> f64 {
66 let pr = self.portfolio_returns();
67 if pr.is_empty() {
68 return 0.0;
69 }
70 pr.iter().sum::<f64>() / pr.len() as f64
71 }
72
73 pub fn std_dev(&self) -> f64 {
75 let pr = self.portfolio_returns();
76 if pr.len() < 2 {
77 return 0.0;
78 }
79 let mean = pr.iter().sum::<f64>() / pr.len() as f64;
80 let variance = pr.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / pr.len() as f64;
81 variance.sqrt()
82 }
83}
84
85pub struct VarEngine;
87
88impl VarEngine {
89 pub fn historical_var(returns: &[f64], confidence: f64, portfolio_value: f64) -> VarResult {
91 if returns.is_empty() {
92 return VarResult {
93 confidence_level: confidence,
94 var_abs: 0.0,
95 var_pct: 0.0,
96 cvar_abs: 0.0,
97 cvar_pct: 0.0,
98 method: VarMethod::Historical,
99 };
100 }
101
102 let mut sorted = returns.to_vec();
103 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
104
105 let n = sorted.len();
106 let var_idx = ((1.0 - confidence) * n as f64).floor() as usize;
107 let var_idx = var_idx.min(n - 1);
108
109 let var_pct = -sorted[var_idx].min(0.0);
110 let var_abs = var_pct * portfolio_value;
111
112 let cvar_pct = Self::cvar_from_var(&sorted, var_idx);
113 let cvar_abs = cvar_pct * portfolio_value;
114
115 VarResult {
116 confidence_level: confidence,
117 var_abs,
118 var_pct,
119 cvar_abs,
120 cvar_pct,
121 method: VarMethod::Historical,
122 }
123 }
124
125 pub fn parametric_var(
129 mean: f64,
130 std_dev: f64,
131 confidence: f64,
132 portfolio_value: f64,
133 horizon_days: u32,
134 ) -> VarResult {
135 let z = Self::cornish_fisher_z(confidence);
136 let horizon_scale = (horizon_days as f64).sqrt();
137 let var_pct = (z * std_dev - mean) * horizon_scale;
138 let var_pct = var_pct.max(0.0);
139 let var_abs = var_pct * portfolio_value;
140
141 let phi_z = Self::standard_normal_pdf(z);
143 let cvar_pct = if (1.0 - confidence).abs() < 1e-12 {
144 var_pct
145 } else {
146 (phi_z / (1.0 - confidence) * std_dev - mean) * horizon_scale
147 };
148 let cvar_pct = cvar_pct.max(var_pct);
149 let cvar_abs = cvar_pct * portfolio_value;
150
151 VarResult {
152 confidence_level: confidence,
153 var_abs,
154 var_pct,
155 cvar_abs,
156 cvar_pct,
157 method: VarMethod::Parametric,
158 }
159 }
160
161 pub fn monte_carlo_var(
163 mean: f64,
164 std_dev: f64,
165 confidence: f64,
166 portfolio_value: f64,
167 simulations: usize,
168 seed: u64,
169 ) -> VarResult {
170 let mut sim_returns: Vec<f64> = Vec::with_capacity(simulations);
171 let mut state = seed;
172
173 let mut i = 0;
174 while i < simulations {
175 state = state
177 .wrapping_mul(1_664_525)
178 .wrapping_add(1_013_904_223);
179 let u1 = (state as f64 + 0.5) / u64::MAX as f64;
180 state = state
181 .wrapping_mul(1_664_525)
182 .wrapping_add(1_013_904_223);
183 let u2 = (state as f64 + 0.5) / u64::MAX as f64;
184
185 let u1 = u1.clamp(1e-15, 1.0 - 1e-15);
187 let u2 = u2.clamp(1e-15, 1.0 - 1e-15);
188 let z0 = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
189 let z1 = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).sin();
190
191 sim_returns.push(mean + std_dev * z0);
192 i += 1;
193 if i < simulations {
194 sim_returns.push(mean + std_dev * z1);
195 i += 1;
196 }
197 }
198
199 sim_returns.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
200
201 let n = sim_returns.len();
202 let var_idx = ((1.0 - confidence) * n as f64).floor() as usize;
203 let var_idx = var_idx.min(n - 1);
204
205 let var_pct = -sim_returns[var_idx].min(0.0);
206 let var_abs = var_pct * portfolio_value;
207
208 let cvar_pct = Self::cvar_from_var(&sim_returns, var_idx);
209 let cvar_abs = cvar_pct * portfolio_value;
210
211 VarResult {
212 confidence_level: confidence,
213 var_abs,
214 var_pct,
215 cvar_abs,
216 cvar_pct,
217 method: VarMethod::MonteCarlo { simulations, seed },
218 }
219 }
220
221 pub fn cvar_from_var(sorted_returns: &[f64], var_idx: usize) -> f64 {
226 if var_idx == 0 {
227 return -sorted_returns[0].min(0.0);
228 }
229 let tail = &sorted_returns[..var_idx];
230 if tail.is_empty() {
231 return 0.0;
232 }
233 let mean_tail: f64 = tail.iter().sum::<f64>() / tail.len() as f64;
234 -mean_tail.min(0.0)
235 }
236
237 pub fn rolling_var(
239 returns: &[f64],
240 window: usize,
241 confidence: f64,
242 portfolio_value: f64,
243 ) -> Vec<VarResult> {
244 if window == 0 || returns.len() < window {
245 return Vec::new();
246 }
247 returns
248 .windows(window)
249 .map(|w| Self::historical_var(w, confidence, portfolio_value))
250 .collect()
251 }
252
253 fn cornish_fisher_z(confidence: f64) -> f64 {
257 if confidence >= 0.99 {
258 2.326
259 } else if confidence >= 0.95 {
260 1.645
261 } else if confidence >= 0.90 {
262 1.282
263 } else {
264 let alpha = 1.0 - confidence;
266 (-2.0 * (alpha * (2.0 * std::f64::consts::PI).sqrt()).ln()).sqrt()
268 }
269 }
270
271 fn standard_normal_pdf(x: f64) -> f64 {
273 (-0.5 * x * x).exp() / (2.0 * std::f64::consts::PI).sqrt()
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn historical_var_basic() {
283 let returns: Vec<f64> = (-50i32..=50).map(|x| x as f64 / 1000.0).collect();
284 let result = VarEngine::historical_var(&returns, 0.95, 100_000.0);
285 assert!(result.var_abs > 0.0);
286 assert!(result.cvar_abs >= result.var_abs);
287 assert_eq!(result.confidence_level, 0.95);
288 }
289
290 #[test]
291 fn parametric_var_99() {
292 let result = VarEngine::parametric_var(0.0, 0.01, 0.99, 1_000_000.0, 1);
293 assert!((result.var_abs - 23_260.0).abs() < 1000.0, "var_abs={}", result.var_abs);
295 }
296
297 #[test]
298 fn monte_carlo_var_reasonable() {
299 let result = VarEngine::monte_carlo_var(0.0, 0.01, 0.95, 1_000_000.0, 10_000, 42);
300 assert!(result.var_abs > 10_000.0);
301 assert!(result.var_abs < 30_000.0);
302 }
303
304 #[test]
305 fn rolling_var_length() {
306 let returns: Vec<f64> = (0..100).map(|i| i as f64 * 0.001 - 0.05).collect();
307 let rolling = VarEngine::rolling_var(&returns, 20, 0.95, 100_000.0);
308 assert_eq!(rolling.len(), 81);
309 }
310
311 #[test]
312 fn portfolio_returns_weighted() {
313 let pr = PortfolioReturns {
314 returns: vec![0.01, 0.02, -0.01],
315 weights: vec![0.5, 0.3, 0.2],
316 portfolio_value: 100_000.0,
317 };
318 let r = pr.portfolio_returns();
319 assert!((r[0] - 0.005).abs() < 1e-10);
320 assert!((r[1] - 0.006).abs() < 1e-10);
321 assert!((r[2] - (-0.002)).abs() < 1e-10);
322 }
323}