Skip to main content

wickra_core/indicators/
variance.rs

1//! Rolling population variance.
2
3use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rolling_moments::ShiftedMoments;
7use crate::traits::Indicator;
8
9/// Rolling population variance over the last `period` values.
10///
11/// ```text
12/// mean     = (1/n) · Σ price
13/// Variance = (1/n) · Σ price² − mean²
14/// ```
15///
16/// Variance is the squared standard deviation. It is the second central
17/// moment of the rolling distribution and the natural input to risk
18/// calculations that expect squared returns (e.g. portfolio variance,
19/// covariance matrices). Use [`crate::StdDev`] when you need the
20/// scale-preserving square root instead.
21///
22/// Floating-point cancellation can drive the running expression slightly
23/// negative on perfectly constant inputs; the result is clamped to zero
24/// before being returned so it stays a valid variance.
25///
26/// # Example
27///
28/// ```
29/// use wickra_core::{Indicator, Variance};
30///
31/// let mut indicator = Variance::new(20).unwrap();
32/// let mut last = None;
33/// for i in 0..40 {
34///     last = indicator.update(100.0 + f64::from(i));
35/// }
36/// assert!(last.is_some());
37/// ```
38#[derive(Debug, Clone)]
39pub struct Variance {
40    period: usize,
41    window: VecDeque<f64>,
42    moments: ShiftedMoments,
43}
44
45impl Variance {
46    /// Construct a new rolling variance with the given period.
47    ///
48    /// # Errors
49    /// Returns [`Error::PeriodZero`] if `period == 0`.
50    pub fn new(period: usize) -> Result<Self> {
51        if period == 0 {
52            return Err(Error::PeriodZero);
53        }
54        if period > crate::error::MAX_PERIOD {
55            return Err(Error::InvalidPeriod {
56                message: crate::error::PERIOD_ABOVE_MAX,
57            });
58        }
59        Ok(Self {
60            period,
61            window: VecDeque::with_capacity(period),
62            moments: ShiftedMoments::new(),
63        })
64    }
65
66    /// Configured period.
67    pub const fn period(&self) -> usize {
68        self.period
69    }
70}
71
72impl Indicator for Variance {
73    type Input = f64;
74    type Output = f64;
75
76    #[inline]
77    fn update(&mut self, value: f64) -> Option<f64> {
78        if !value.is_finite() {
79            return None;
80        }
81        if self.window.len() == self.period {
82            let old = self.window.pop_front().expect("non-empty");
83            self.moments.evict(old);
84        }
85        self.window.push_back(value);
86        self.moments.push(value);
87        if self.moments.needs_reseed(self.period) {
88            self.moments.reseed(self.window.iter().copied());
89        }
90        if self.window.len() < self.period {
91            return None;
92        }
93        Some(self.moments.variance(self.period))
94    }
95
96    fn reset(&mut self) {
97        self.window.clear();
98        self.moments.reset();
99    }
100
101    #[inline]
102    fn warmup_period(&self) -> usize {
103        self.period
104    }
105
106    #[inline]
107    fn is_ready(&self) -> bool {
108        self.window.len() == self.period
109    }
110
111    #[inline]
112    fn name(&self) -> &'static str {
113        "Variance"
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::traits::BatchExt;
121    use approx::assert_relative_eq;
122
123    #[test]
124    fn rejects_zero_period() {
125        assert!(matches!(Variance::new(0), Err(Error::PeriodZero)));
126    }
127
128    #[test]
129    fn accessors_and_metadata() {
130        let v = Variance::new(14).unwrap();
131        assert_eq!(v.period(), 14);
132        assert_eq!(v.warmup_period(), 14);
133        assert_eq!(v.name(), "Variance");
134    }
135
136    #[test]
137    fn reference_value() {
138        // Variance(3) of [2, 4, 6]: mean = 4, variance = (4 + 0 + 4) / 3 = 8/3.
139        let mut v = Variance::new(3).unwrap();
140        let out = v.batch(&[2.0, 4.0, 6.0]);
141        assert_eq!(out[0], None);
142        assert_eq!(out[1], None);
143        assert_relative_eq!(out[2].unwrap(), 8.0 / 3.0, epsilon = 1e-12);
144    }
145
146    #[test]
147    fn constant_series_yields_zero() {
148        let mut v = Variance::new(5).unwrap();
149        for o in v.batch(&[42.0; 20]).into_iter().flatten() {
150            assert_relative_eq!(o, 0.0, epsilon = 1e-12);
151        }
152    }
153
154    #[test]
155    fn first_value_on_period_th_input() {
156        let mut v = Variance::new(5).unwrap();
157        let out = v.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
158        for (i, x) in out.iter().enumerate().take(4) {
159            assert!(x.is_none(), "index {i} must be None during warmup");
160        }
161        assert!(out[4].is_some());
162    }
163
164    #[test]
165    fn reset_clears_state() {
166        let mut v = Variance::new(5).unwrap();
167        v.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
168        assert!(v.is_ready());
169        v.reset();
170        assert!(!v.is_ready());
171        assert_eq!(v.update(1.0), None);
172    }
173
174    #[test]
175    fn equals_stddev_squared() {
176        // The rolling Variance must equal the rolling population StdDev squared.
177        let prices: Vec<f64> = (0..60)
178            .map(|i| 50.0 + (f64::from(i) * 0.3).sin() * 7.0)
179            .collect();
180        let mut var = Variance::new(14).unwrap();
181        let mut sd = crate::StdDev::new(14).unwrap();
182        for &p in &prices {
183            let (v, s) = (var.update(p), sd.update(p));
184            assert_eq!(v.is_some(), s.is_some());
185            if let (Some(v), Some(s)) = (v, s) {
186                assert_relative_eq!(v, s * s, epsilon = 1e-9);
187            }
188        }
189    }
190
191    #[test]
192    fn batch_equals_streaming() {
193        let prices: Vec<f64> = (0..60)
194            .map(|i| 50.0 + (f64::from(i) * 0.3).cos() * 10.0)
195            .collect();
196        let batch = Variance::new(14).unwrap().batch(&prices);
197        let mut b = Variance::new(14).unwrap();
198        let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
199        assert_eq!(batch, streamed);
200    }
201}