wickra_core/indicators/
variance.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rolling_moments::ShiftedMoments;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
39pub struct Variance {
40 period: usize,
41 window: VecDeque<f64>,
42 moments: ShiftedMoments,
43}
44
45impl Variance {
46 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 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 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 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}