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 #[inline]
95 fn update(&mut self, input: f64) -> Option<f64> {
96 if !input.is_finite() || input <= 0.0 {
99 return None;
100 }
101 let Some(prev) = self.prev_price else {
102 self.prev_price = Some(input);
103 return None;
104 };
105 self.prev_price = Some(input);
106 let r = (input / prev).ln();
109 let var = match self.variance {
110 None => r * r,
112 Some(prev_var) => self.lambda * prev_var + (1.0 - self.lambda) * r * r,
113 };
114 self.variance = Some(var);
115 let vol = var.max(0.0).sqrt();
118 self.last = Some(vol);
119 Some(vol)
120 }
121
122 fn reset(&mut self) {
123 self.prev_price = None;
124 self.variance = None;
125 self.last = None;
126 }
127
128 #[inline]
129 fn warmup_period(&self) -> usize {
130 2
133 }
134
135 #[inline]
136 fn is_ready(&self) -> bool {
137 self.last.is_some()
138 }
139
140 #[inline]
141 fn name(&self) -> &'static str {
142 "EwmaVolatility"
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::traits::BatchExt;
150 use approx::assert_relative_eq;
151
152 #[test]
153 fn rejects_invalid_lambda() {
154 for bad in [0.0, 1.0, -0.5, 1.5, f64::NAN, f64::INFINITY] {
155 assert!(matches!(
156 EwmaVolatility::new(bad),
157 Err(Error::InvalidParameter { .. })
158 ));
159 }
160 }
161
162 #[test]
163 fn accessors_and_metadata() {
164 let ewma = EwmaVolatility::new(0.94).unwrap();
165 assert_relative_eq!(ewma.lambda(), 0.94);
166 assert_eq!(ewma.warmup_period(), 2);
167 assert_eq!(ewma.name(), "EwmaVolatility");
168 assert!(!ewma.is_ready());
169 assert_eq!(ewma.value(), None);
170 }
171
172 #[test]
173 fn first_emission_at_warmup_period() {
174 let mut ewma = EwmaVolatility::new(0.94).unwrap();
175 assert_eq!(ewma.update(100.0), None);
176 let out = ewma.update(110.0);
177 assert!(out.is_some());
178 assert!(ewma.is_ready());
179 }
180
181 #[test]
182 fn known_value() {
183 let lambda = 0.94;
186 let mut ewma = EwmaVolatility::new(lambda).unwrap();
187 let out = ewma.batch(&[100.0, 110.0, 99.0]);
188 let r1 = (110.0_f64 / 100.0).ln();
189 let r2 = (99.0_f64 / 110.0).ln();
190 assert_relative_eq!(out[1].unwrap(), r1.abs(), epsilon = 1e-12);
191 let var2 = lambda * r1 * r1 + (1.0 - lambda) * r2 * r2;
192 assert_relative_eq!(out[2].unwrap(), var2.sqrt(), epsilon = 1e-12);
193 }
194
195 #[test]
196 fn constant_series_yields_zero() {
197 let mut ewma = EwmaVolatility::new(0.9).unwrap();
198 for v in ewma.batch(&[100.0; 40]).into_iter().flatten() {
199 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
200 }
201 }
202
203 #[test]
204 fn output_is_non_negative() {
205 let mut ewma = EwmaVolatility::new(0.94).unwrap();
206 let prices: Vec<f64> = (1..=200)
207 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 12.0)
208 .collect();
209 for v in ewma.batch(&prices).into_iter().flatten() {
210 assert!(v >= 0.0, "EWMA volatility must be non-negative, got {v}");
211 }
212 }
213
214 #[test]
215 fn ignores_non_finite_input() {
216 let mut ewma = EwmaVolatility::new(0.94).unwrap();
217 let out = ewma.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
218 let last = *out.last().unwrap();
219 assert!(last.is_some());
220 assert_eq!(ewma.update(f64::NAN), None);
221 assert_eq!(ewma.update(f64::INFINITY), None);
222 }
223
224 #[test]
225 fn skips_non_positive_prices() {
226 let mut ewma = EwmaVolatility::new(0.94).unwrap();
227 let warmup = ewma.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
228 warmup.last().copied().flatten().expect("warmed up");
229 assert_eq!(ewma.update(-5.0), None);
230 assert_eq!(ewma.update(0.0), None);
231 let mut control = ewma.clone();
233 let after = ewma.update(21.0).expect("ready");
234 assert_eq!(control.update(21.0).expect("ready"), after);
235 }
236
237 #[test]
238 fn skips_non_positive_before_first_price() {
239 let mut ewma = EwmaVolatility::new(0.94).unwrap();
241 assert_eq!(ewma.update(0.0), None);
242 assert_eq!(ewma.update(f64::NAN), None);
243 assert_eq!(ewma.update(100.0), None);
244 assert!(ewma.update(110.0).is_some());
245 }
246
247 #[test]
248 fn reset_clears_state() {
249 let mut ewma = EwmaVolatility::new(0.94).unwrap();
250 ewma.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
251 assert!(ewma.is_ready());
252 ewma.reset();
253 assert!(!ewma.is_ready());
254 assert_eq!(ewma.value(), None);
255 assert_eq!(ewma.update(1.0), None);
256 }
257
258 #[test]
259 fn batch_equals_streaming() {
260 let prices: Vec<f64> = (1..=120)
261 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
262 .collect();
263 let batch = EwmaVolatility::new(0.94).unwrap().batch(&prices);
264 let mut b = EwmaVolatility::new(0.94).unwrap();
265 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
266 assert_eq!(batch, streamed);
267 }
268}