rustyqlib/risk/
volatility.rs1pub fn realized_volatility(returns: &[f64], periods_per_year: f64) -> f64 {
7 let n = returns.len();
8 assert!(n >= 2, "need at least two returns");
9 let mean = returns.iter().sum::<f64>() / n as f64;
10 let var = returns.iter().map(|r| (r - mean) * (r - mean)).sum::<f64>() / (n as f64 - 1.0);
11 (var * periods_per_year).sqrt()
12}
13
14pub fn ewma_volatility(returns: &[f64], lambda: f64, periods_per_year: f64) -> f64 {
19 assert!(!returns.is_empty());
20 assert!((0.0..1.0).contains(&lambda));
21 let mut variance = returns[0] * returns[0];
22 for &r in &returns[1..] {
23 variance = lambda * variance + (1.0 - lambda) * r * r;
24 }
25 (variance * periods_per_year).sqrt()
26}
27
28#[cfg(test)]
29mod tests {
30 use super::*;
31
32 #[test]
33 fn realized_vol_recovers_the_generating_sigma() {
34 use crate::core::montecarlo::path_rng;
35 use rand::Rng;
36 let daily = 0.2 / 252.0_f64.sqrt();
37 let mut rng = path_rng(3, 0);
38 let returns: Vec<f64> = (0..20_000)
39 .map(|_| daily * rng.sample::<f64, _>(rand_distr::StandardNormal))
40 .collect();
41 let vol = realized_volatility(&returns, 252.0);
42 assert!((vol - 0.2).abs() < 0.005, "{vol}");
43 let ewma = ewma_volatility(&returns, 0.94, 252.0);
44 assert!((ewma - 0.2).abs() < 0.05, "{ewma}");
45 }
46
47 #[test]
48 fn ewma_recursion_matches_a_hand_computation() {
49 let returns = [0.01, -0.02, 0.015];
50 let lambda = 0.9;
51 let v1 = 0.01f64 * 0.01;
52 let v2 = lambda * v1 + 0.1 * 0.02 * 0.02;
53 let v3 = lambda * v2 + 0.1 * 0.015 * 0.015;
54 let expect = (v3 * 252.0).sqrt();
55 assert!((ewma_volatility(&returns, lambda, 252.0) - expect).abs() < 1e-12);
56 let flat = [0.01; 10];
58 assert!(realized_volatility(&flat, 252.0).abs() < 1e-15);
59 assert!(ewma_volatility(&flat, 0.94, 252.0) > 0.0);
60 }
61}