Skip to main content

wickra_core/indicators/
mama.rs

1//! Ehlers MESA Adaptive Moving Average (MAMA) and its follower (FAMA).
2#![allow(
3    clippy::doc_markdown,
4    clippy::doc_lazy_continuation,
5    clippy::struct_field_names,
6    clippy::manual_clamp
7)]
8
9use std::f64::consts::PI;
10
11use crate::error::{Error, Result};
12use crate::traits::Indicator;
13
14/// MAMA + FAMA output pair.
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct MamaOutput {
17    /// MESA Adaptive Moving Average.
18    pub mama: f64,
19    /// Following Adaptive Moving Average (slower companion).
20    pub fama: f64,
21}
22
23/// Ehlers' MESA Adaptive Moving Average (MAMA).
24///
25/// MAMA adapts its smoothing constant from the rate-of-change of price phase,
26/// derived via a truncated Hilbert transform — full math in "Cycle Analytics
27/// for Traders" (Ehlers 2013, ch. 8) and the original 2001 MESA paper.
28///
29/// The two-parameter `(fast_limit, slow_limit)` is the range over which the
30/// adaptive alpha can vary; defaults `(0.5, 0.05)` match the canonical
31/// EasyLanguage implementation. The companion FAMA is `mama * 0.5 * fast_limit
32/// + fama_prev * (1 - 0.5 * fast_limit)`, lagging MAMA so crossovers signal
33/// trend reversals.
34///
35/// The indicator emits both lines as a [`MamaOutput`]. Use the [`crate::Fama`] wrapper
36/// in this module to expose just the slow line if needed (e.g. for chaining).
37///
38/// # Example
39///
40/// ```
41/// use wickra_core::{Indicator, Mama};
42///
43/// let mut mama = Mama::new(0.5, 0.05).unwrap();
44/// let mut last = None;
45/// for i in 0..100 {
46///     last = mama.update(100.0 + (f64::from(i) * 0.2).sin() * 5.0);
47/// }
48/// assert!(last.is_some());
49/// ```
50#[derive(Debug, Clone)]
51pub struct Mama {
52    fast_limit: f64,
53    slow_limit: f64,
54    smooth_buf: Vec<f64>,
55    detrender_buf: Vec<f64>,
56    q1_buf: Vec<f64>,
57    i1_buf: Vec<f64>,
58    prev_i2: f64,
59    prev_q2: f64,
60    prev_re: f64,
61    prev_im: f64,
62    prev_period: f64,
63    prev_phase: f64,
64    prev_mama: f64,
65    prev_fama: f64,
66    count: usize,
67    last_value: Option<MamaOutput>,
68}
69
70impl Mama {
71    /// Construct with custom `(fast_limit, slow_limit)` adaptive alpha bounds.
72    ///
73    /// # Errors
74    ///
75    /// Returns [`Error::InvalidPeriod`] if either limit is outside `(0, 1]`
76    /// or if `slow_limit > fast_limit`.
77    pub fn new(fast_limit: f64, slow_limit: f64) -> Result<Self> {
78        if !fast_limit.is_finite()
79            || !slow_limit.is_finite()
80            || fast_limit <= 0.0
81            || fast_limit > 1.0
82            || slow_limit <= 0.0
83            || slow_limit > 1.0
84            || slow_limit > fast_limit
85        {
86            return Err(Error::InvalidPeriod {
87                message: "fast_limit, slow_limit must satisfy 0 < slow_limit <= fast_limit <= 1",
88            });
89        }
90        Ok(Self {
91            fast_limit,
92            slow_limit,
93            smooth_buf: Vec::with_capacity(7),
94            detrender_buf: Vec::with_capacity(7),
95            q1_buf: Vec::with_capacity(7),
96            i1_buf: Vec::with_capacity(7),
97            prev_i2: 0.0,
98            prev_q2: 0.0,
99            prev_re: 0.0,
100            prev_im: 0.0,
101            prev_period: 0.0,
102            prev_phase: 0.0,
103            prev_mama: 0.0,
104            prev_fama: 0.0,
105            count: 0,
106            last_value: None,
107        })
108    }
109
110    /// Default `(0.5, 0.05)` parameters from Ehlers' original publication.
111    pub fn classic() -> Self {
112        Self::new(0.5, 0.05).expect("classic MAMA limits are valid")
113    }
114
115    /// Configured `(fast_limit, slow_limit)`.
116    pub const fn limits(&self) -> (f64, f64) {
117        (self.fast_limit, self.slow_limit)
118    }
119
120    /// Current `(mama, fama)` pair if available.
121    pub const fn value(&self) -> Option<MamaOutput> {
122        self.last_value
123    }
124
125    fn push_front(buf: &mut Vec<f64>, v: f64, cap: usize) {
126        buf.insert(0, v);
127        if buf.len() > cap {
128            buf.truncate(cap);
129        }
130    }
131}
132
133impl Indicator for Mama {
134    type Input = f64;
135    type Output = MamaOutput;
136
137    fn update(&mut self, input: f64) -> Option<MamaOutput> {
138        if !input.is_finite() {
139            return self.last_value;
140        }
141        self.count += 1;
142
143        Self::push_front(&mut self.smooth_buf, input, 7);
144        if self.smooth_buf.len() < 4 {
145            return None;
146        }
147        let smooth = (4.0 * self.smooth_buf[0]
148            + 3.0 * self.smooth_buf[1]
149            + 2.0 * self.smooth_buf[2]
150            + self.smooth_buf[3])
151            / 10.0;
152
153        let period = self.prev_period.max(6.0).min(50.0);
154        let adj = 0.075 * period + 0.54;
155
156        if self.smooth_buf.len() < 7 {
157            // Seed the EMA outputs with the smoothed price so early bars are
158            // well-behaved without producing a public value.
159            self.prev_mama = smooth;
160            self.prev_fama = smooth;
161            return None;
162        }
163        let s0 = smooth;
164        let s2 = self.smooth_buf[2];
165        let s4 = self.smooth_buf[4];
166        let s6 = self.smooth_buf[6];
167        let detrender = (0.0962 * s0 + 0.5769 * s2 - 0.5769 * s4 - 0.0962 * s6) * adj;
168        Self::push_front(&mut self.detrender_buf, detrender, 7);
169        if self.detrender_buf.len() < 7 {
170            return None;
171        }
172
173        let q1 = (0.0962 * self.detrender_buf[0] + 0.5769 * self.detrender_buf[2]
174            - 0.5769 * self.detrender_buf[4]
175            - 0.0962 * self.detrender_buf[6])
176            * adj;
177        let i1 = self.detrender_buf[3];
178        Self::push_front(&mut self.q1_buf, q1, 7);
179        Self::push_front(&mut self.i1_buf, i1, 7);
180        if self.q1_buf.len() < 7 || self.i1_buf.len() < 7 {
181            return None;
182        }
183
184        let ji = (0.0962 * self.i1_buf[0] + 0.5769 * self.i1_buf[2]
185            - 0.5769 * self.i1_buf[4]
186            - 0.0962 * self.i1_buf[6])
187            * adj;
188        let jq = (0.0962 * self.q1_buf[0] + 0.5769 * self.q1_buf[2]
189            - 0.5769 * self.q1_buf[4]
190            - 0.0962 * self.q1_buf[6])
191            * adj;
192
193        let mut i2 = i1 - jq;
194        let mut q2 = q1 + ji;
195        i2 = 0.2 * i2 + 0.8 * self.prev_i2;
196        q2 = 0.2 * q2 + 0.8 * self.prev_q2;
197
198        let mut re = i2 * self.prev_i2 + q2 * self.prev_q2;
199        let mut im = i2 * self.prev_q2 - q2 * self.prev_i2;
200        re = 0.2 * re + 0.8 * self.prev_re;
201        im = 0.2 * im + 0.8 * self.prev_im;
202
203        self.prev_i2 = i2;
204        self.prev_q2 = q2;
205        self.prev_re = re;
206        self.prev_im = im;
207
208        let mut new_period = if im.abs() > f64::EPSILON && re.abs() > f64::EPSILON {
209            2.0 * PI / im.atan2(re)
210        } else {
211            self.prev_period
212        };
213        new_period = new_period.min(1.5 * self.prev_period);
214        new_period = new_period.max(0.67 * self.prev_period);
215        new_period = new_period.clamp(6.0, 50.0);
216        self.prev_period = 0.2 * new_period + 0.8 * self.prev_period;
217
218        // Adaptive alpha derived from phase rate-of-change.
219        let phase = if i1.abs() > f64::EPSILON {
220            (q1 / i1).atan().to_degrees()
221        } else {
222            self.prev_phase
223        };
224        let mut delta_phase = self.prev_phase - phase;
225        self.prev_phase = phase;
226        if delta_phase < 1.0 {
227            delta_phase = 1.0;
228        }
229        // `delta_phase` is clamped to >= 1.0 above, so `fast_limit / delta_phase`
230        // never exceeds `fast_limit`; only the lower bound can bind.
231        let mut alpha = self.fast_limit / delta_phase;
232        if alpha < self.slow_limit {
233            alpha = self.slow_limit;
234        }
235
236        self.prev_mama = alpha * input + (1.0 - alpha) * self.prev_mama;
237        let fama_alpha = 0.5 * alpha;
238        self.prev_fama = fama_alpha * self.prev_mama + (1.0 - fama_alpha) * self.prev_fama;
239
240        if self.count < 33 {
241            return None;
242        }
243        let out = MamaOutput {
244            mama: self.prev_mama,
245            fama: self.prev_fama,
246        };
247        self.last_value = Some(out);
248        Some(out)
249    }
250
251    fn reset(&mut self) {
252        self.smooth_buf.clear();
253        self.detrender_buf.clear();
254        self.q1_buf.clear();
255        self.i1_buf.clear();
256        self.prev_i2 = 0.0;
257        self.prev_q2 = 0.0;
258        self.prev_re = 0.0;
259        self.prev_im = 0.0;
260        self.prev_period = 0.0;
261        self.prev_phase = 0.0;
262        self.prev_mama = 0.0;
263        self.prev_fama = 0.0;
264        self.count = 0;
265        self.last_value = None;
266    }
267
268    fn warmup_period(&self) -> usize {
269        33
270    }
271
272    fn is_ready(&self) -> bool {
273        self.last_value.is_some()
274    }
275
276    fn name(&self) -> &'static str {
277        "MAMA"
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use crate::traits::BatchExt;
285
286    #[test]
287    fn rejects_invalid_limits() {
288        assert!(matches!(
289            Mama::new(0.0, 0.05),
290            Err(Error::InvalidPeriod { .. })
291        ));
292        assert!(matches!(
293            Mama::new(0.5, 0.0),
294            Err(Error::InvalidPeriod { .. })
295        ));
296        assert!(matches!(
297            Mama::new(0.05, 0.5),
298            Err(Error::InvalidPeriod { .. })
299        ));
300        assert!(matches!(
301            Mama::new(1.5, 0.05),
302            Err(Error::InvalidPeriod { .. })
303        ));
304        assert!(matches!(
305            Mama::new(f64::NAN, 0.05),
306            Err(Error::InvalidPeriod { .. })
307        ));
308    }
309
310    #[test]
311    fn accessors_and_metadata() {
312        let mut mama = Mama::classic();
313        assert_eq!(mama.limits(), (0.5, 0.05));
314        assert_eq!(mama.warmup_period(), 33);
315        assert_eq!(mama.name(), "MAMA");
316        assert!(!mama.is_ready());
317        for i in 0..60 {
318            mama.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
319        }
320        assert!(mama.is_ready());
321        assert!(mama.value().is_some());
322    }
323
324    #[test]
325    fn fama_lags_or_equals_mama_on_constant_series() {
326        let mut mama = Mama::classic();
327        let out = mama.batch(&[100.0_f64; 200]);
328        let last = out.iter().flatten().last().unwrap();
329        // On a flat series both lines converge to the price.
330        assert!((last.mama - 100.0).abs() < 1.0);
331        assert!((last.fama - 100.0).abs() < 1.0);
332    }
333
334    #[test]
335    fn batch_equals_streaming() {
336        let prices: Vec<f64> = (0..120)
337            .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 5.0)
338            .collect();
339        let mut a = Mama::classic();
340        let mut b = Mama::classic();
341        let batch = a.batch(&prices);
342        let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
343        assert_eq!(batch, streamed);
344    }
345
346    #[test]
347    fn ignores_non_finite_input() {
348        let mut mama = Mama::classic();
349        let prices: Vec<f64> = (0..100)
350            .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
351            .collect();
352        mama.batch(&prices);
353        let before = mama.value();
354        assert!(before.is_some());
355        assert_eq!(mama.update(f64::NAN), before);
356    }
357
358    #[test]
359    fn reset_clears_state() {
360        let mut mama = Mama::classic();
361        let prices: Vec<f64> = (0..100)
362            .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
363            .collect();
364        mama.batch(&prices);
365        assert!(mama.is_ready());
366        mama.reset();
367        assert!(!mama.is_ready());
368    }
369
370    #[test]
371    fn flat_input_uses_phase_fallback() {
372        // Zero inputs make every smooth/detrender term arithmetically exact
373        // zero, so `i1 == 0.0` and the phase calculation takes the
374        // `self.prev_phase` fallback rather than `atan(q1/i1)`. A non-zero
375        // constant like `50.0` leaves a sub-EPSILON cancellation residue
376        // that flips the branch back to the `atan` path on real hardware.
377        let mut mama = Mama::classic();
378        let out = mama.batch(&[0.0_f64; 200]);
379        assert!(out.iter().flatten().count() > 100);
380    }
381}