wickra_core/indicators/
variance.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
38pub struct Variance {
39 period: usize,
40 window: VecDeque<f64>,
41 sum: f64,
42 sum_sq: f64,
43}
44
45impl Variance {
46 pub fn new(period: usize) -> Result<Self> {
51 if period == 0 {
52 return Err(Error::PeriodZero);
53 }
54 Ok(Self {
55 period,
56 window: VecDeque::with_capacity(period),
57 sum: 0.0,
58 sum_sq: 0.0,
59 })
60 }
61
62 pub const fn period(&self) -> usize {
64 self.period
65 }
66}
67
68impl Indicator for Variance {
69 type Input = f64;
70 type Output = f64;
71
72 fn update(&mut self, value: f64) -> Option<f64> {
73 if !value.is_finite() {
74 return None;
75 }
76 if self.window.len() == self.period {
77 let old = self.window.pop_front().expect("non-empty");
78 self.sum -= old;
79 self.sum_sq -= old * old;
80 }
81 self.window.push_back(value);
82 self.sum += value;
83 self.sum_sq += value * value;
84 if self.window.len() < self.period {
85 return None;
86 }
87 let n = self.period as f64;
88 let mean = self.sum / n;
89 Some((self.sum_sq / n - mean * mean).max(0.0))
90 }
91
92 fn reset(&mut self) {
93 self.window.clear();
94 self.sum = 0.0;
95 self.sum_sq = 0.0;
96 }
97
98 fn warmup_period(&self) -> usize {
99 self.period
100 }
101
102 fn is_ready(&self) -> bool {
103 self.window.len() == self.period
104 }
105
106 fn name(&self) -> &'static str {
107 "Variance"
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::traits::BatchExt;
115 use approx::assert_relative_eq;
116
117 #[test]
118 fn rejects_zero_period() {
119 assert!(matches!(Variance::new(0), Err(Error::PeriodZero)));
120 }
121
122 #[test]
123 fn accessors_and_metadata() {
124 let v = Variance::new(14).unwrap();
125 assert_eq!(v.period(), 14);
126 assert_eq!(v.warmup_period(), 14);
127 assert_eq!(v.name(), "Variance");
128 }
129
130 #[test]
131 fn reference_value() {
132 let mut v = Variance::new(3).unwrap();
134 let out = v.batch(&[2.0, 4.0, 6.0]);
135 assert_eq!(out[0], None);
136 assert_eq!(out[1], None);
137 assert_relative_eq!(out[2].unwrap(), 8.0 / 3.0, epsilon = 1e-12);
138 }
139
140 #[test]
141 fn constant_series_yields_zero() {
142 let mut v = Variance::new(5).unwrap();
143 for o in v.batch(&[42.0; 20]).into_iter().flatten() {
144 assert_relative_eq!(o, 0.0, epsilon = 1e-12);
145 }
146 }
147
148 #[test]
149 fn first_value_on_period_th_input() {
150 let mut v = Variance::new(5).unwrap();
151 let out = v.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
152 for (i, x) in out.iter().enumerate().take(4) {
153 assert!(x.is_none(), "index {i} must be None during warmup");
154 }
155 assert!(out[4].is_some());
156 }
157
158 #[test]
159 fn reset_clears_state() {
160 let mut v = Variance::new(5).unwrap();
161 v.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
162 assert!(v.is_ready());
163 v.reset();
164 assert!(!v.is_ready());
165 assert_eq!(v.update(1.0), None);
166 }
167
168 #[test]
169 fn equals_stddev_squared() {
170 let prices: Vec<f64> = (0..60)
172 .map(|i| 50.0 + (f64::from(i) * 0.3).sin() * 7.0)
173 .collect();
174 let mut var = Variance::new(14).unwrap();
175 let mut sd = crate::StdDev::new(14).unwrap();
176 for &p in &prices {
177 let (v, s) = (var.update(p), sd.update(p));
178 assert_eq!(v.is_some(), s.is_some());
179 if let (Some(v), Some(s)) = (v, s) {
180 assert_relative_eq!(v, s * s, epsilon = 1e-9);
181 }
182 }
183 }
184
185 #[test]
186 fn batch_equals_streaming() {
187 let prices: Vec<f64> = (0..60)
188 .map(|i| 50.0 + (f64::from(i) * 0.3).cos() * 10.0)
189 .collect();
190 let batch = Variance::new(14).unwrap().batch(&prices);
191 let mut b = Variance::new(14).unwrap();
192 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
193 assert_eq!(batch, streamed);
194 }
195}