wickra_core/indicators/
std_dev.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
36pub struct StdDev {
37 period: usize,
38 window: VecDeque<f64>,
39 sum: f64,
40 sum_sq: f64,
41 last: Option<f64>,
42}
43
44impl StdDev {
45 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 last: None,
60 })
61 }
62
63 pub const fn period(&self) -> usize {
65 self.period
66 }
67
68 pub const fn value(&self) -> Option<f64> {
70 self.last
71 }
72}
73
74impl Indicator for StdDev {
75 type Input = f64;
76 type Output = f64;
77
78 fn update(&mut self, input: f64) -> Option<f64> {
79 if !input.is_finite() {
80 return self.last;
82 }
83 if self.window.len() == self.period {
84 let old = self.window.pop_front().expect("window is non-empty");
85 self.sum -= old;
86 self.sum_sq -= old * old;
87 }
88 self.window.push_back(input);
89 self.sum += input;
90 self.sum_sq += input * input;
91 if self.window.len() < self.period {
92 return None;
93 }
94 let n = self.period as f64;
95 let mean = self.sum / n;
96 let variance = (self.sum_sq / n - mean * mean).max(0.0);
98 let sd = variance.sqrt();
99 self.last = Some(sd);
100 Some(sd)
101 }
102
103 fn reset(&mut self) {
104 self.window.clear();
105 self.sum = 0.0;
106 self.sum_sq = 0.0;
107 self.last = None;
108 }
109
110 fn warmup_period(&self) -> usize {
111 self.period
112 }
113
114 fn is_ready(&self) -> bool {
115 self.last.is_some()
116 }
117
118 fn name(&self) -> &'static str {
119 "StdDev"
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::traits::BatchExt;
127 use approx::assert_relative_eq;
128
129 #[test]
130 fn new_rejects_zero_period() {
131 assert!(matches!(StdDev::new(0), Err(Error::PeriodZero)));
132 }
133
134 #[test]
138 fn accessors_and_metadata() {
139 let mut sd = StdDev::new(14).unwrap();
140 assert_eq!(sd.period(), 14);
141 assert_eq!(sd.warmup_period(), 14);
142 assert_eq!(sd.name(), "StdDev");
143 assert_eq!(sd.value(), None);
144 for i in 1..=14 {
145 sd.update(f64::from(i));
146 }
147 assert!(sd.value().is_some());
148 }
149
150 #[test]
151 fn reference_value() {
152 let mut sd = StdDev::new(3).unwrap();
154 let out = sd.batch(&[2.0, 4.0, 6.0]);
155 assert_eq!(out[0], None);
156 assert_eq!(out[1], None);
157 assert_relative_eq!(out[2].unwrap(), (8.0_f64 / 3.0).sqrt(), epsilon = 1e-12);
158 }
159
160 #[test]
161 fn constant_series_yields_zero() {
162 let mut sd = StdDev::new(5).unwrap();
163 let out = sd.batch(&[42.0; 20]);
164 for v in out.iter().skip(4).flatten() {
165 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
166 }
167 }
168
169 #[test]
170 fn matches_naive_definition() {
171 let prices: Vec<f64> = (1..=60)
172 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 8.0)
173 .collect();
174 let period = 10;
175 let got = StdDev::new(period).unwrap().batch(&prices);
176 for (i, g) in got.iter().enumerate() {
177 if let Some(value) = g {
178 let window = &prices[i + 1 - period..=i];
179 let mean = window.iter().sum::<f64>() / period as f64;
180 let var = window.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / period as f64;
181 assert_relative_eq!(*value, var.sqrt(), epsilon = 1e-9);
182 }
183 }
184 }
185
186 #[test]
187 fn ignores_non_finite_input() {
188 let mut sd = StdDev::new(3).unwrap();
189 let out = sd.batch(&[2.0, 4.0, 6.0]);
190 let last = out[2];
191 assert!(last.is_some());
192 assert_eq!(sd.update(f64::NAN), last);
193 assert_eq!(sd.update(f64::INFINITY), last);
194 }
195
196 #[test]
197 fn reset_clears_state() {
198 let mut sd = StdDev::new(3).unwrap();
199 sd.batch(&[1.0, 2.0, 3.0, 4.0]);
200 assert!(sd.is_ready());
201 sd.reset();
202 assert!(!sd.is_ready());
203 assert_eq!(sd.update(1.0), None);
204 }
205
206 #[test]
207 fn batch_equals_streaming() {
208 let prices: Vec<f64> = (1..=60)
209 .map(|i| 100.0 + (f64::from(i) * 0.3).cos() * 7.0)
210 .collect();
211 let batch = StdDev::new(14).unwrap().batch(&prices);
212 let mut b = StdDev::new(14).unwrap();
213 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
214 assert_eq!(batch, streamed);
215 }
216}