wickra_core/indicators/
ewma_volatility.rs1use crate::error::{Error, Result};
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone)]
47pub struct EwmaVolatility {
48 lambda: f64,
49 prev_price: Option<f64>,
50 variance: Option<f64>,
52 last: Option<f64>,
53}
54
55impl EwmaVolatility {
56 pub fn new(lambda: f64) -> Result<Self> {
66 if !lambda.is_finite() || lambda <= 0.0 || lambda >= 1.0 {
67 return Err(Error::InvalidParameter {
68 message: "EWMA volatility lambda must be in the open interval (0, 1)",
69 });
70 }
71 Ok(Self {
72 lambda,
73 prev_price: None,
74 variance: None,
75 last: None,
76 })
77 }
78
79 pub const fn lambda(&self) -> f64 {
81 self.lambda
82 }
83
84 pub const fn value(&self) -> Option<f64> {
86 self.last
87 }
88}
89
90impl Indicator for EwmaVolatility {
91 type Input = f64;
92 type Output = f64;
93
94 fn update(&mut self, input: f64) -> Option<f64> {
95 if !input.is_finite() || input <= 0.0 {
98 return self.last;
99 }
100 let Some(prev) = self.prev_price else {
101 self.prev_price = Some(input);
102 return None;
103 };
104 self.prev_price = Some(input);
105 let r = (input / prev).ln();
108 let var = match self.variance {
109 None => r * r,
111 Some(prev_var) => self.lambda * prev_var + (1.0 - self.lambda) * r * r,
112 };
113 self.variance = Some(var);
114 let vol = var.max(0.0).sqrt();
117 self.last = Some(vol);
118 Some(vol)
119 }
120
121 fn reset(&mut self) {
122 self.prev_price = None;
123 self.variance = None;
124 self.last = None;
125 }
126
127 fn warmup_period(&self) -> usize {
128 2
131 }
132
133 fn is_ready(&self) -> bool {
134 self.last.is_some()
135 }
136
137 fn name(&self) -> &'static str {
138 "EwmaVolatility"
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::traits::BatchExt;
146 use approx::assert_relative_eq;
147
148 #[test]
149 fn rejects_invalid_lambda() {
150 for bad in [0.0, 1.0, -0.5, 1.5, f64::NAN, f64::INFINITY] {
151 assert!(matches!(
152 EwmaVolatility::new(bad),
153 Err(Error::InvalidParameter { .. })
154 ));
155 }
156 }
157
158 #[test]
159 fn accessors_and_metadata() {
160 let ewma = EwmaVolatility::new(0.94).unwrap();
161 assert_relative_eq!(ewma.lambda(), 0.94);
162 assert_eq!(ewma.warmup_period(), 2);
163 assert_eq!(ewma.name(), "EwmaVolatility");
164 assert!(!ewma.is_ready());
165 assert_eq!(ewma.value(), None);
166 }
167
168 #[test]
169 fn first_emission_at_warmup_period() {
170 let mut ewma = EwmaVolatility::new(0.94).unwrap();
171 assert_eq!(ewma.update(100.0), None);
172 let out = ewma.update(110.0);
173 assert!(out.is_some());
174 assert!(ewma.is_ready());
175 }
176
177 #[test]
178 fn known_value() {
179 let lambda = 0.94;
182 let mut ewma = EwmaVolatility::new(lambda).unwrap();
183 let out = ewma.batch(&[100.0, 110.0, 99.0]);
184 let r1 = (110.0_f64 / 100.0).ln();
185 let r2 = (99.0_f64 / 110.0).ln();
186 assert_relative_eq!(out[1].unwrap(), r1.abs(), epsilon = 1e-12);
187 let var2 = lambda * r1 * r1 + (1.0 - lambda) * r2 * r2;
188 assert_relative_eq!(out[2].unwrap(), var2.sqrt(), epsilon = 1e-12);
189 }
190
191 #[test]
192 fn constant_series_yields_zero() {
193 let mut ewma = EwmaVolatility::new(0.9).unwrap();
194 for v in ewma.batch(&[100.0; 40]).into_iter().flatten() {
195 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
196 }
197 }
198
199 #[test]
200 fn output_is_non_negative() {
201 let mut ewma = EwmaVolatility::new(0.94).unwrap();
202 let prices: Vec<f64> = (1..=200)
203 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 12.0)
204 .collect();
205 for v in ewma.batch(&prices).into_iter().flatten() {
206 assert!(v >= 0.0, "EWMA volatility must be non-negative, got {v}");
207 }
208 }
209
210 #[test]
211 fn ignores_non_finite_input() {
212 let mut ewma = EwmaVolatility::new(0.94).unwrap();
213 let out = ewma.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
214 let last = *out.last().unwrap();
215 assert!(last.is_some());
216 assert_eq!(ewma.update(f64::NAN), last);
217 assert_eq!(ewma.update(f64::INFINITY), last);
218 }
219
220 #[test]
221 fn skips_non_positive_prices() {
222 let mut ewma = EwmaVolatility::new(0.94).unwrap();
223 let warmup = ewma.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
224 let baseline = warmup.last().copied().flatten().expect("warmed up");
225 assert_eq!(ewma.update(-5.0), Some(baseline));
226 assert_eq!(ewma.update(0.0), Some(baseline));
227 let mut control = ewma.clone();
229 let after = ewma.update(21.0).expect("ready");
230 assert_eq!(control.update(21.0).expect("ready"), after);
231 }
232
233 #[test]
234 fn skips_non_positive_before_first_price() {
235 let mut ewma = EwmaVolatility::new(0.94).unwrap();
237 assert_eq!(ewma.update(0.0), None);
238 assert_eq!(ewma.update(f64::NAN), None);
239 assert_eq!(ewma.update(100.0), None);
240 assert!(ewma.update(110.0).is_some());
241 }
242
243 #[test]
244 fn reset_clears_state() {
245 let mut ewma = EwmaVolatility::new(0.94).unwrap();
246 ewma.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
247 assert!(ewma.is_ready());
248 ewma.reset();
249 assert!(!ewma.is_ready());
250 assert_eq!(ewma.value(), None);
251 assert_eq!(ewma.update(1.0), None);
252 }
253
254 #[test]
255 fn batch_equals_streaming() {
256 let prices: Vec<f64> = (1..=120)
257 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
258 .collect();
259 let batch = EwmaVolatility::new(0.94).unwrap().batch(&prices);
260 let mut b = EwmaVolatility::new(0.94).unwrap();
261 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
262 assert_eq!(batch, streamed);
263 }
264}