1use crate::error::{Error, Result};
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone)]
25pub struct Ema {
26 period: usize,
27 alpha: f64,
28 one_minus_alpha: f64,
31 current: f64,
35 seeded: bool,
37 warmup_buf: Vec<f64>,
38}
39
40impl Ema {
41 pub fn new(period: usize) -> Result<Self> {
47 if period == 0 {
48 return Err(Error::PeriodZero);
49 }
50 if period > crate::error::MAX_PERIOD {
51 return Err(Error::InvalidPeriod {
52 message: crate::error::PERIOD_ABOVE_MAX,
53 });
54 }
55 let alpha = 2.0 / (period as f64 + 1.0);
56 Ok(Self {
57 period,
58 alpha,
59 one_minus_alpha: 1.0 - alpha,
60 current: 0.0,
61 seeded: false,
62 warmup_buf: Vec::with_capacity(period),
63 })
64 }
65
66 pub fn with_alpha(alpha: f64) -> Result<Self> {
76 if !alpha.is_finite() || alpha <= 0.0 || alpha > 1.0 {
77 return Err(Error::InvalidPeriod {
78 message: "alpha must be in (0.0, 1.0]",
79 });
80 }
81 Ok(Self {
82 period: 1,
83 alpha,
84 one_minus_alpha: 1.0 - alpha,
85 current: 0.0,
86 seeded: false,
87 warmup_buf: Vec::with_capacity(1),
88 })
89 }
90
91 pub const fn period(&self) -> usize {
93 self.period
94 }
95
96 pub const fn alpha(&self) -> f64 {
98 self.alpha
99 }
100
101 pub const fn value(&self) -> Option<f64> {
103 if self.seeded {
104 Some(self.current)
105 } else {
106 None
107 }
108 }
109
110 pub(crate) fn is_fresh(&self) -> bool {
113 !self.seeded && self.warmup_buf.is_empty()
114 }
115
116 pub(crate) fn seed_to(&mut self, current: f64) {
122 self.current = current;
123 self.seeded = true;
124 }
125
126 pub fn batch_nan(&mut self, inputs: &[f64]) -> Vec<f64> {
137 let p = self.period;
138 if self.seeded || !self.warmup_buf.is_empty() || !inputs.iter().all(|x| x.is_finite()) {
139 return inputs
140 .iter()
141 .map(|&x| self.update(x).unwrap_or(f64::NAN))
142 .collect();
143 }
144
145 let n = inputs.len();
146 if n < p {
147 self.warmup_buf.extend_from_slice(inputs);
149 return vec![f64::NAN; n];
150 }
151
152 let mut out = vec![f64::NAN; p - 1];
154 out.reserve(n - (p - 1));
155 let seed = inputs[..p].iter().copied().sum::<f64>() / p as f64;
156 let mut cur = seed;
157 out.push(seed);
158 let (alpha, oma) = (self.alpha, self.one_minus_alpha);
159 for &x in &inputs[p..] {
160 cur = alpha.mul_add(x, oma * cur);
161 out.push(cur);
162 }
163
164 self.current = cur;
167 self.seeded = true;
168 self.warmup_buf.extend_from_slice(&inputs[..p]);
169 out
170 }
171
172 pub(crate) fn step_unchecked(&mut self, input: f64) -> Option<f64> {
175 if self.seeded {
176 let new = self
177 .alpha
178 .mul_add(input, self.one_minus_alpha * self.current);
179 self.current = new;
180 return Some(new);
181 }
182 self.warmup_buf.push(input);
183 if self.warmup_buf.len() == self.period {
184 let seed = self.warmup_buf.iter().copied().sum::<f64>() / self.period as f64;
185 self.current = seed;
186 self.seeded = true;
187 return Some(seed);
188 }
189 None
190 }
191}
192
193impl Indicator for Ema {
194 type Input = f64;
195 type Output = f64;
196
197 #[inline]
198 fn update(&mut self, input: f64) -> Option<f64> {
199 if !input.is_finite() {
200 return None;
201 }
202 self.step_unchecked(input)
203 }
204
205 fn reset(&mut self) {
206 self.current = 0.0;
207 self.seeded = false;
208 self.warmup_buf.clear();
209 }
210
211 #[inline]
212 fn warmup_period(&self) -> usize {
213 self.period
214 }
215
216 #[inline]
217 fn is_ready(&self) -> bool {
218 self.seeded
219 }
220
221 #[inline]
222 fn name(&self) -> &'static str {
223 "EMA"
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
236 fn rejects_a_period_above_the_maximum() {
237 assert!(matches!(
238 Ema::new(usize::MAX),
239 Err(Error::InvalidPeriod { .. })
240 ));
241 assert!(matches!(
242 Ema::new(crate::error::MAX_PERIOD + 1),
243 Err(Error::InvalidPeriod { .. })
244 ));
245 assert!(matches!(
246 Ema::new(1_000_000_000),
247 Err(Error::InvalidPeriod { .. })
248 ));
249 assert!(Ema::new(14).is_ok());
251 }
252 use crate::traits::BatchExt;
253 use approx::assert_relative_eq;
254
255 fn ema_naive(prices: &[f64], period: usize) -> Vec<Option<f64>> {
257 let alpha = 2.0 / (period as f64 + 1.0);
258 let mut out = Vec::with_capacity(prices.len());
259 let mut state: Option<f64> = None;
260 for (i, &p) in prices.iter().enumerate() {
261 if let Some(prev) = state {
262 let v = alpha * p + (1.0 - alpha) * prev;
263 state = Some(v);
264 out.push(Some(v));
265 } else if i + 1 == period {
266 let seed = prices[..period].iter().sum::<f64>() / period as f64;
267 state = Some(seed);
268 out.push(Some(seed));
269 } else {
270 out.push(None);
271 }
272 }
273 out
274 }
275
276 #[test]
277 fn new_rejects_zero_period() {
278 assert!(matches!(Ema::new(0), Err(Error::PeriodZero)));
279 }
280
281 #[test]
286 fn accessors_and_metadata() {
287 let ema = Ema::new(14).unwrap();
288 assert_eq!(ema.period(), 14);
289 assert_eq!(ema.warmup_period(), 14);
290 assert_eq!(ema.name(), "EMA");
291 }
292
293 #[test]
294 fn warmup_returns_none_until_seed() {
295 let mut ema = Ema::new(3).unwrap();
296 assert_eq!(ema.update(1.0), None);
297 assert_eq!(ema.update(2.0), None);
298 assert_eq!(ema.update(3.0), Some(2.0)); }
300
301 #[test]
302 fn first_value_equals_sma_seed() {
303 let mut ema = Ema::new(5).unwrap();
304 let inputs = [10.0, 20.0, 30.0, 40.0, 50.0];
305 let mut last = None;
306 for v in inputs {
307 last = ema.update(v);
308 }
309 assert_relative_eq!(last.unwrap(), 30.0, epsilon = 1e-12);
310 }
311
312 #[test]
313 fn alpha_matches_period_formula() {
314 let ema = Ema::new(10).unwrap();
315 assert_relative_eq!(ema.alpha(), 2.0 / 11.0, epsilon = 1e-15);
316 }
317
318 #[test]
319 fn step_after_seed_uses_alpha_formula() {
320 let mut ema = Ema::new(3).unwrap();
323 ema.batch(&[1.0, 2.0, 3.0]);
324 assert_relative_eq!(ema.update(10.0).unwrap(), 6.0, epsilon = 1e-12);
325 }
326
327 #[test]
328 fn constant_series_converges_to_constant() {
329 let mut ema = Ema::new(10).unwrap();
330 let out = ema.batch(&[42.0_f64; 100]);
331 for x in out.iter().skip(9) {
332 assert_relative_eq!(x.unwrap(), 42.0, epsilon = 1e-9);
333 }
334 }
335
336 #[test]
337 fn with_alpha_validates_range() {
338 assert!(Ema::with_alpha(0.5).is_ok());
339 assert!(Ema::with_alpha(1.0).is_ok());
340 assert!(matches!(
341 Ema::with_alpha(0.0),
342 Err(Error::InvalidPeriod { .. })
343 ));
344 assert!(matches!(
345 Ema::with_alpha(1.5),
346 Err(Error::InvalidPeriod { .. })
347 ));
348 assert!(matches!(
349 Ema::with_alpha(f64::NAN),
350 Err(Error::InvalidPeriod { .. })
351 ));
352 }
353
354 #[test]
355 fn reset_clears_state() {
356 let mut ema = Ema::new(3).unwrap();
357 ema.batch(&[1.0, 2.0, 3.0]);
358 assert!(ema.is_ready());
359 ema.reset();
360 assert!(!ema.is_ready());
361 assert_eq!(ema.update(1.0), None);
362 }
363
364 #[test]
365 fn batch_equals_streaming() {
366 let prices: Vec<f64> = (1..=30).map(f64::from).collect();
367 let mut a = Ema::new(5).unwrap();
368 let mut b = Ema::new(5).unwrap();
369 assert_eq!(
370 a.batch(&prices),
371 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
372 );
373 }
374
375 #[test]
376 fn ignores_non_finite_input() {
377 let mut ema = Ema::new(3).unwrap();
378 ema.batch(&[1.0, 2.0, 3.0]);
379 let before = ema.value();
380 assert_eq!(ema.update(f64::NAN), None);
381 assert_eq!(ema.update(f64::INFINITY), None);
382 assert_eq!(ema.value(), before);
384 }
385
386 fn bits_eq(a: &[f64], b: &[f64]) -> bool {
387 a.len() == b.len()
388 && a.iter()
389 .zip(b)
390 .all(|(x, y)| x == y || (x.is_nan() && y.is_nan()))
391 }
392
393 fn ema_replay(period: usize, series: &[f64]) -> Vec<f64> {
394 let mut e = Ema::new(period).unwrap();
395 series
396 .iter()
397 .map(|&x| e.update(x).unwrap_or(f64::NAN))
398 .collect()
399 }
400
401 #[test]
402 fn batch_nan_fast_path_is_bit_identical() {
403 let series: Vec<f64> = (0..300)
404 .map(|i| (f64::from(i) * 0.25).cos() * 8.0 + 40.0)
405 .collect();
406 let mut ema = Ema::new(14).unwrap();
407 let got = ema.batch_nan(&series);
408 assert!(bits_eq(&got, &ema_replay(14, &series)));
409 let mut ref_ema = Ema::new(14).unwrap();
410 for &x in &series {
411 ref_ema.update(x);
412 }
413 assert_eq!(ema.update(7.5), ref_ema.update(7.5));
414 }
415
416 #[test]
417 fn batch_nan_falls_back_on_non_finite() {
418 let series = [1.0, 2.0, 3.0, f64::INFINITY, 5.0, 6.0, 7.0];
419 let mut ema = Ema::new(3).unwrap();
420 assert!(bits_eq(&ema.batch_nan(&series), &ema_replay(3, &series)));
421 }
422
423 #[test]
424 fn batch_nan_falls_back_when_warming() {
425 let mut ema = Ema::new(3).unwrap();
426 ema.update(10.0); let series = [1.0, 2.0, 3.0, 4.0];
428 let mut ref_ema = Ema::new(3).unwrap();
429 ref_ema.update(10.0);
430 let want: Vec<f64> = series
431 .iter()
432 .map(|&x| ref_ema.update(x).unwrap_or(f64::NAN))
433 .collect();
434 assert!(bits_eq(&ema.batch_nan(&series), &want));
435 }
436
437 #[test]
438 fn batch_nan_sub_period_slice_stays_unseeded() {
439 let series = [1.0, 2.0];
440 let mut ema = Ema::new(5).unwrap();
441 let got = ema.batch_nan(&series);
442 assert!(got.iter().all(|x| x.is_nan()) && got.len() == 2);
443 assert!(!ema.is_ready());
444 assert!(bits_eq(
446 &[ema.update(3.0).unwrap_or(f64::NAN)],
447 &[ema_replay(5, &[1.0, 2.0, 3.0])[2]]
448 ));
449 }
450
451 proptest::proptest! {
452 #![proptest_config(proptest::test_runner::Config::with_cases(48))]
453 #[test]
454 fn ema_matches_naive(
455 period in 1usize..20,
456 prices in proptest::collection::vec(-1000.0_f64..1000.0, 0..150),
457 ) {
458 let mut ema = Ema::new(period).unwrap();
459 let got = ema.batch(&prices);
460 let want = ema_naive(&prices, period);
461 proptest::prop_assert_eq!(got.len(), want.len());
462 for (g, w) in got.iter().zip(want.iter()) {
463 match (g, w) {
464 (None, None) => {}
465 (Some(a), Some(b)) => proptest::prop_assert!(
466 (a - b).abs() <= 1e-9 * a.abs().max(1.0),
467 "got={a} want={b}"
468 ),
469 _ => proptest::prop_assert!(false, "warmup mismatch"),
470 }
471 }
472 }
473 }
474}