Skip to main content

wickra_embed_core/indicators/
ema.rs

1//! Exponential Moving Average — no-alloc, byte-parity with `wickra_core::Ema`.
2
3use crate::math;
4use crate::traits::Indicator;
5
6/// Exponential Moving Average with smoothing factor `alpha = 2 / (period + 1)`.
7///
8/// The first value is the simple mean of the first `period` inputs (the TA-Lib
9/// convention); each subsequent input contributes
10/// `alpha * input + (1 - alpha) * previous`, computed with the same fused
11/// multiply-add and precomputed `1 - alpha` as `wickra_core::Ema`, so the
12/// steady-state output is bit-for-bit identical.
13///
14/// The `period` is a runtime value: EMA needs no window buffer (only a running
15/// warmup sum), so it stays allocation-free without a const generic.
16///
17/// # Example
18///
19/// ```
20/// use wickra_embed_core::{Ema, Indicator};
21///
22/// let mut ema = Ema::new(3);
23/// let mut last = None;
24/// for i in 0..10 {
25///     last = ema.update(100.0 + f64::from(i));
26/// }
27/// assert!(last.is_some());
28/// ```
29#[derive(Debug, Clone)]
30pub struct Ema {
31    period: usize,
32    alpha: f64,
33    /// `1 - alpha`, precomputed so the recurrence avoids a subtraction per tick
34    /// — and, crucially, matches wickra-core's operand order to the bit.
35    one_minus_alpha: f64,
36    current: f64,
37    seeded: bool,
38    /// Running sum and count of the warmup inputs, replacing wickra-core's
39    /// `Vec<f64>` warmup buffer. Summing incrementally in arrival order is the
40    /// identical fold to `warmup_buf.iter().sum()`, so the seed is byte-equal.
41    warm_sum: f64,
42    warm_count: usize,
43}
44
45impl Ema {
46    /// Construct an EMA with the given period. A `period` of `0` yields an alpha
47    /// of `2.0` and never produces a meaningful value; callers pass a positive
48    /// period (the bindings validate up front).
49    #[must_use]
50    pub fn new(period: usize) -> Self {
51        let alpha = 2.0 / (period as f64 + 1.0);
52        Self {
53            period,
54            alpha,
55            one_minus_alpha: 1.0 - alpha,
56            current: 0.0,
57            seeded: false,
58            warm_sum: 0.0,
59            warm_count: 0,
60        }
61    }
62
63    /// Configured period.
64    #[must_use]
65    pub const fn period(&self) -> usize {
66        self.period
67    }
68
69    /// Smoothing factor `alpha`.
70    #[must_use]
71    pub const fn alpha(&self) -> f64 {
72        self.alpha
73    }
74
75    /// Current value if seeded.
76    #[must_use]
77    pub const fn value(&self) -> Option<f64> {
78        if self.seeded {
79            Some(self.current)
80        } else {
81            None
82        }
83    }
84}
85
86impl Indicator for Ema {
87    type Input = f64;
88
89    fn update(&mut self, input: f64) -> Option<f64> {
90        if !input.is_finite() {
91            return self.value();
92        }
93        if self.seeded {
94            let new = math::mul_add(self.alpha, input, self.one_minus_alpha * self.current);
95            self.current = new;
96            return Some(new);
97        }
98        self.warm_sum += input;
99        self.warm_count += 1;
100        if self.warm_count == self.period {
101            let seed = self.warm_sum / self.period as f64;
102            self.current = seed;
103            self.seeded = true;
104            return Some(seed);
105        }
106        None
107    }
108
109    fn reset(&mut self) {
110        self.current = 0.0;
111        self.seeded = false;
112        self.warm_sum = 0.0;
113        self.warm_count = 0;
114    }
115
116    fn warmup_period(&self) -> usize {
117        self.period
118    }
119
120    fn is_ready(&self) -> bool {
121        self.seeded
122    }
123
124    fn name(&self) -> &'static str {
125        "ema"
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::Ema;
132    use crate::traits::Indicator;
133
134    #[test]
135    fn alpha_and_seed() {
136        let mut ema = Ema::new(3);
137        assert_eq!(ema.alpha().to_bits(), (2.0_f64 / 4.0).to_bits());
138        // Seed is the mean of the first three inputs.
139        assert_eq!(ema.update(1.0), None);
140        assert_eq!(ema.update(2.0), None);
141        assert_eq!(ema.update(3.0), Some(2.0));
142        assert!(ema.is_ready());
143    }
144
145    #[test]
146    fn reset_clears_state() {
147        let mut ema = Ema::new(4);
148        for i in 0..10 {
149            ema.update(f64::from(i));
150        }
151        ema.reset();
152        assert!(!ema.is_ready());
153        assert_eq!(ema.value(), None);
154    }
155
156    #[test]
157    fn byte_parity_with_wickra_core() {
158        let mut ours = Ema::new(12);
159        let mut theirs = wickra_core::Ema::new(12).unwrap();
160        for i in 0..1000 {
161            let x = 100.0 + 15.0 * (f64::from(i) * 0.07).sin() + 0.02 * f64::from(i);
162            let a = ours.update(x);
163            let b = <wickra_core::Ema as wickra_core::Indicator>::update(&mut theirs, x);
164            assert_eq!(a.map(f64::to_bits), b.map(f64::to_bits), "index {i}");
165        }
166    }
167}