Skip to main content

rustyqlib/risk/
volatility.rs

1//! Volatility estimation from return series: realized (close-to-close)
2//! and EWMA (RiskMetrics).
3
4/// Annualized realized volatility of a per-period return series
5/// (sample standard deviation, mean removed).
6pub 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
14/// EWMA (RiskMetrics) volatility: `sigma_t^2 = lambda sigma_{t-1}^2 +
15/// (1 - lambda) r_t^2`, seeded with the first squared return. Returns
16/// the **annualized** latest estimate; `lambda = 0.94` is the classic
17/// daily-decay choice.
18pub 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        // constant series: realized vol is zero (mean removed), EWMA is not
57        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}