wickra_core/indicators/
std_dev.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)]
40pub struct StdDev {
41 period: usize,
42 window: VecDeque<f64>,
43 moments: ShiftedMoments,
44 last: Option<f64>,
45}
46
47impl StdDev {
48 pub fn new(period: usize) -> Result<Self> {
54 if period == 0 {
55 return Err(Error::PeriodZero);
56 }
57 if period > crate::error::MAX_PERIOD {
58 return Err(Error::InvalidPeriod {
59 message: crate::error::PERIOD_ABOVE_MAX,
60 });
61 }
62 Ok(Self {
63 period,
64 window: VecDeque::with_capacity(period),
65 moments: ShiftedMoments::new(),
66 last: None,
67 })
68 }
69
70 pub const fn period(&self) -> usize {
72 self.period
73 }
74
75 pub const fn value(&self) -> Option<f64> {
77 self.last
78 }
79}
80
81impl Indicator for StdDev {
82 type Input = f64;
83 type Output = f64;
84
85 #[inline]
86 fn update(&mut self, input: f64) -> Option<f64> {
87 if !input.is_finite() {
88 return None;
90 }
91 if self.window.len() == self.period {
92 let old = self.window.pop_front().expect("window is non-empty");
93 self.moments.evict(old);
94 }
95 self.window.push_back(input);
96 self.moments.push(input);
97 if self.moments.needs_reseed(self.period) {
98 self.moments.reseed(self.window.iter().copied());
99 }
100 if self.window.len() < self.period {
101 return None;
102 }
103 let sd = self.moments.std_dev(self.period);
104 self.last = Some(sd);
105 Some(sd)
106 }
107
108 fn reset(&mut self) {
109 self.window.clear();
110 self.moments.reset();
111 self.last = None;
112 }
113
114 #[inline]
115 fn warmup_period(&self) -> usize {
116 self.period
117 }
118
119 #[inline]
120 fn is_ready(&self) -> bool {
121 self.last.is_some()
122 }
123
124 #[inline]
125 fn name(&self) -> &'static str {
126 "StdDev"
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::traits::BatchExt;
134 use approx::assert_relative_eq;
135
136 fn reference_std_dev(window: &[f64]) -> f64 {
139 let n = window.len() as f64;
140 let mean = window.iter().sum::<f64>() / n;
141 (window.iter().map(|x| (x - mean) * (x - mean)).sum::<f64>() / n).sqrt()
142 }
143
144 #[test]
149 fn stays_accurate_when_the_level_dwarfs_the_spread() {
150 for level in [1.0e2_f64, 1.0e5, 1.0e8] {
151 let prices: Vec<f64> = (0..60)
152 .map(|i| level + (f64::from(i) * 0.7).sin())
153 .collect();
154 let mut sd = StdDev::new(20).unwrap();
155 let mut got = 0.0;
156 for price in &prices {
157 if let Some(v) = sd.update(*price) {
158 got = v;
159 }
160 }
161 assert_relative_eq!(got, reference_std_dev(&prices[40..]), max_relative = 1e-9);
162 }
163 }
164
165 #[test]
166 fn new_rejects_zero_period() {
167 assert!(matches!(StdDev::new(0), Err(Error::PeriodZero)));
168 }
169
170 #[test]
174 fn accessors_and_metadata() {
175 let mut sd = StdDev::new(14).unwrap();
176 assert_eq!(sd.period(), 14);
177 assert_eq!(sd.warmup_period(), 14);
178 assert_eq!(sd.name(), "StdDev");
179 assert_eq!(sd.value(), None);
180 for i in 1..=14 {
181 sd.update(f64::from(i));
182 }
183 assert!(sd.value().is_some());
184 }
185
186 #[test]
187 fn reference_value() {
188 let mut sd = StdDev::new(3).unwrap();
190 let out = sd.batch(&[2.0, 4.0, 6.0]);
191 assert_eq!(out[0], None);
192 assert_eq!(out[1], None);
193 assert_relative_eq!(out[2].unwrap(), (8.0_f64 / 3.0).sqrt(), epsilon = 1e-12);
194 }
195
196 #[test]
197 fn constant_series_yields_zero() {
198 let mut sd = StdDev::new(5).unwrap();
199 let out = sd.batch(&[42.0; 20]);
200 for v in out.iter().skip(4).flatten() {
201 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
202 }
203 }
204
205 #[test]
206 fn matches_naive_definition() {
207 let prices: Vec<f64> = (1..=60)
208 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 8.0)
209 .collect();
210 let period = 10;
211 let got = StdDev::new(period).unwrap().batch(&prices);
212 for (i, g) in got.iter().enumerate() {
213 if let Some(value) = g {
214 let window = &prices[i + 1 - period..=i];
215 let mean = window.iter().sum::<f64>() / period as f64;
216 let var = window.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / period as f64;
217 assert_relative_eq!(*value, var.sqrt(), epsilon = 1e-9);
218 }
219 }
220 }
221
222 #[test]
223 fn ignores_non_finite_input() {
224 let mut sd = StdDev::new(3).unwrap();
225 let out = sd.batch(&[2.0, 4.0, 6.0]);
226 let last = out[2];
227 assert!(last.is_some());
228 assert_eq!(sd.update(f64::NAN), None);
229 assert_eq!(sd.update(f64::INFINITY), None);
230 }
231
232 #[test]
233 fn reset_clears_state() {
234 let mut sd = StdDev::new(3).unwrap();
235 sd.batch(&[1.0, 2.0, 3.0, 4.0]);
236 assert!(sd.is_ready());
237 sd.reset();
238 assert!(!sd.is_ready());
239 assert_eq!(sd.update(1.0), None);
240 }
241
242 #[test]
243 fn batch_equals_streaming() {
244 let prices: Vec<f64> = (1..=60)
245 .map(|i| 100.0 + (f64::from(i) * 0.3).cos() * 7.0)
246 .collect();
247 let batch = StdDev::new(14).unwrap().batch(&prices);
248 let mut b = StdDev::new(14).unwrap();
249 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
250 assert_eq!(batch, streamed);
251 }
252}