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    // Raw input window for the 4-bar WMA.
55    price_buf: Vec<f64>,
56    // WMA-smoothed price history feeding the Hilbert detrender taps.
57    smooth_buf: Vec<f64>,
58    detrender_buf: Vec<f64>,
59    q1_buf: Vec<f64>,
60    i1_buf: Vec<f64>,
61    prev_i2: f64,
62    prev_q2: f64,
63    prev_re: f64,
64    prev_im: f64,
65    prev_period: f64,
66    prev_phase: f64,
67    prev_mama: f64,
68    prev_fama: f64,
69    count: usize,
70    last_value: Option<MamaOutput>,
71}
72
73impl Mama {
74    /// Construct with custom `(fast_limit, slow_limit)` adaptive alpha bounds.
75    ///
76    /// # Errors
77    ///
78    /// Returns [`Error::InvalidPeriod`] if either limit is outside `(0, 1]`
79    /// or if `slow_limit > fast_limit`.
80    pub fn new(fast_limit: f64, slow_limit: f64) -> Result<Self> {
81        if !fast_limit.is_finite()
82            || !slow_limit.is_finite()
83            || fast_limit <= 0.0
84            || fast_limit > 1.0
85            || slow_limit <= 0.0
86            || slow_limit > 1.0
87            || slow_limit > fast_limit
88        {
89            return Err(Error::InvalidPeriod {
90                message: "fast_limit, slow_limit must satisfy 0 < slow_limit <= fast_limit <= 1",
91            });
92        }
93        Ok(Self {
94            fast_limit,
95            slow_limit,
96            price_buf: Vec::with_capacity(4),
97            smooth_buf: Vec::with_capacity(7),
98            detrender_buf: Vec::with_capacity(7),
99            q1_buf: Vec::with_capacity(7),
100            i1_buf: Vec::with_capacity(7),
101            prev_i2: 0.0,
102            prev_q2: 0.0,
103            prev_re: 0.0,
104            prev_im: 0.0,
105            prev_period: 0.0,
106            prev_phase: 0.0,
107            prev_mama: 0.0,
108            prev_fama: 0.0,
109            count: 0,
110            last_value: None,
111        })
112    }
113
114    /// Default `(0.5, 0.05)` parameters from Ehlers' original publication.
115    pub fn classic() -> Self {
116        Self::new(0.5, 0.05).expect("classic MAMA limits are valid")
117    }
118
119    /// Configured `(fast_limit, slow_limit)`.
120    pub const fn limits(&self) -> (f64, f64) {
121        (self.fast_limit, self.slow_limit)
122    }
123
124    /// Current `(mama, fama)` pair if available.
125    pub const fn value(&self) -> Option<MamaOutput> {
126        self.last_value
127    }
128
129    fn push_front(buf: &mut Vec<f64>, v: f64, cap: usize) {
130        buf.insert(0, v);
131        if buf.len() > cap {
132            buf.truncate(cap);
133        }
134    }
135}
136
137impl Indicator for Mama {
138    type Input = f64;
139    type Output = MamaOutput;
140
141    fn update(&mut self, input: f64) -> Option<MamaOutput> {
142        if !input.is_finite() {
143            return None;
144        }
145        self.count += 1;
146
147        Self::push_front(&mut self.price_buf, input, 4);
148        if self.price_buf.len() < 4 {
149            return None;
150        }
151        let smooth = (4.0 * self.price_buf[0]
152            + 3.0 * self.price_buf[1]
153            + 2.0 * self.price_buf[2]
154            + self.price_buf[3])
155            / 10.0;
156        Self::push_front(&mut self.smooth_buf, smooth, 7);
157
158        let period = self.prev_period.max(6.0).min(50.0);
159        let adj = 0.075 * period + 0.54;
160
161        if self.smooth_buf.len() < 7 {
162            // Seed the EMA outputs with the smoothed price so early bars are
163            // well-behaved without producing a public value.
164            self.prev_mama = smooth;
165            self.prev_fama = smooth;
166            return None;
167        }
168        let s0 = smooth;
169        let s2 = self.smooth_buf[2];
170        let s4 = self.smooth_buf[4];
171        let s6 = self.smooth_buf[6];
172        let detrender = (0.0962 * s0 + 0.5769 * s2 - 0.5769 * s4 - 0.0962 * s6) * adj;
173        Self::push_front(&mut self.detrender_buf, detrender, 7);
174        if self.detrender_buf.len() < 7 {
175            return None;
176        }
177
178        let q1 = (0.0962 * self.detrender_buf[0] + 0.5769 * self.detrender_buf[2]
179            - 0.5769 * self.detrender_buf[4]
180            - 0.0962 * self.detrender_buf[6])
181            * adj;
182        let i1 = self.detrender_buf[3];
183        Self::push_front(&mut self.q1_buf, q1, 7);
184        Self::push_front(&mut self.i1_buf, i1, 7);
185        if self.q1_buf.len() < 7 || self.i1_buf.len() < 7 {
186            return None;
187        }
188
189        let ji = (0.0962 * self.i1_buf[0] + 0.5769 * self.i1_buf[2]
190            - 0.5769 * self.i1_buf[4]
191            - 0.0962 * self.i1_buf[6])
192            * adj;
193        let jq = (0.0962 * self.q1_buf[0] + 0.5769 * self.q1_buf[2]
194            - 0.5769 * self.q1_buf[4]
195            - 0.0962 * self.q1_buf[6])
196            * adj;
197
198        let mut i2 = i1 - jq;
199        let mut q2 = q1 + ji;
200        i2 = 0.2 * i2 + 0.8 * self.prev_i2;
201        q2 = 0.2 * q2 + 0.8 * self.prev_q2;
202
203        let mut re = i2 * self.prev_i2 + q2 * self.prev_q2;
204        let mut im = i2 * self.prev_q2 - q2 * self.prev_i2;
205        re = 0.2 * re + 0.8 * self.prev_re;
206        im = 0.2 * im + 0.8 * self.prev_im;
207
208        self.prev_i2 = i2;
209        self.prev_q2 = q2;
210        self.prev_re = re;
211        self.prev_im = im;
212
213        let mut new_period = if im.abs() > f64::EPSILON && re.abs() > f64::EPSILON {
214            2.0 * PI / im.atan2(re)
215        } else {
216            self.prev_period
217        };
218        new_period = new_period.min(1.5 * self.prev_period);
219        new_period = new_period.max(0.67 * self.prev_period);
220        new_period = new_period.clamp(6.0, 50.0);
221        self.prev_period = 0.2 * new_period + 0.8 * self.prev_period;
222
223        // Adaptive alpha derived from phase rate-of-change.
224        let phase = if i1.abs() > f64::EPSILON {
225            (q1 / i1).atan().to_degrees()
226        } else {
227            self.prev_phase
228        };
229        let mut delta_phase = self.prev_phase - phase;
230        self.prev_phase = phase;
231        if delta_phase < 1.0 {
232            delta_phase = 1.0;
233        }
234        // `delta_phase` is clamped to >= 1.0 above, so `fast_limit / delta_phase`
235        // never exceeds `fast_limit`; only the lower bound can bind.
236        let mut alpha = self.fast_limit / delta_phase;
237        if alpha < self.slow_limit {
238            alpha = self.slow_limit;
239        }
240
241        self.prev_mama = alpha * input + (1.0 - alpha) * self.prev_mama;
242        let fama_alpha = 0.5 * alpha;
243        self.prev_fama = fama_alpha * self.prev_mama + (1.0 - fama_alpha) * self.prev_fama;
244
245        if self.count < 33 {
246            return None;
247        }
248        let out = MamaOutput {
249            mama: self.prev_mama,
250            fama: self.prev_fama,
251        };
252        self.last_value = Some(out);
253        Some(out)
254    }
255
256    fn reset(&mut self) {
257        self.price_buf.clear();
258        self.smooth_buf.clear();
259        self.detrender_buf.clear();
260        self.q1_buf.clear();
261        self.i1_buf.clear();
262        self.prev_i2 = 0.0;
263        self.prev_q2 = 0.0;
264        self.prev_re = 0.0;
265        self.prev_im = 0.0;
266        self.prev_period = 0.0;
267        self.prev_phase = 0.0;
268        self.prev_mama = 0.0;
269        self.prev_fama = 0.0;
270        self.count = 0;
271        self.last_value = None;
272    }
273
274    #[inline]
275    fn warmup_period(&self) -> usize {
276        33
277    }
278
279    #[inline]
280    fn is_ready(&self) -> bool {
281        self.last_value.is_some()
282    }
283
284    #[inline]
285    fn name(&self) -> &'static str {
286        "MAMA"
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use crate::traits::BatchExt;
294
295    #[test]
296    fn rejects_invalid_limits() {
297        assert!(matches!(
298            Mama::new(0.0, 0.05),
299            Err(Error::InvalidPeriod { .. })
300        ));
301        assert!(matches!(
302            Mama::new(0.5, 0.0),
303            Err(Error::InvalidPeriod { .. })
304        ));
305        assert!(matches!(
306            Mama::new(0.05, 0.5),
307            Err(Error::InvalidPeriod { .. })
308        ));
309        assert!(matches!(
310            Mama::new(1.5, 0.05),
311            Err(Error::InvalidPeriod { .. })
312        ));
313        assert!(matches!(
314            Mama::new(f64::NAN, 0.05),
315            Err(Error::InvalidPeriod { .. })
316        ));
317    }
318
319    #[test]
320    fn accessors_and_metadata() {
321        let mut mama = Mama::classic();
322        assert_eq!(mama.limits(), (0.5, 0.05));
323        assert_eq!(mama.warmup_period(), 33);
324        assert_eq!(mama.name(), "MAMA");
325        assert!(!mama.is_ready());
326        for i in 0..60 {
327            mama.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
328        }
329        assert!(mama.is_ready());
330        assert!(mama.value().is_some());
331    }
332
333    #[test]
334    fn fama_lags_or_equals_mama_on_constant_series() {
335        let mut mama = Mama::classic();
336        let out = mama.batch(&[100.0_f64; 200]);
337        let last = out.iter().flatten().last().unwrap();
338        // On a flat series both lines converge to the price.
339        assert!((last.mama - 100.0).abs() < 1.0);
340        assert!((last.fama - 100.0).abs() < 1.0);
341    }
342
343    #[test]
344    fn batch_equals_streaming() {
345        let prices: Vec<f64> = (0..120)
346            .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 5.0)
347            .collect();
348        let mut a = Mama::classic();
349        let mut b = Mama::classic();
350        let batch = a.batch(&prices);
351        let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
352        assert_eq!(batch, streamed);
353    }
354
355    #[test]
356    fn ignores_non_finite_input() {
357        let mut mama = Mama::classic();
358        let prices: Vec<f64> = (0..100)
359            .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
360            .collect();
361        mama.batch(&prices);
362        let before = mama.value();
363        assert!(before.is_some());
364        assert_eq!(mama.update(f64::NAN), None);
365    }
366
367    #[test]
368    fn reset_clears_state() {
369        let mut mama = Mama::classic();
370        let prices: Vec<f64> = (0..100)
371            .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
372            .collect();
373        mama.batch(&prices);
374        assert!(mama.is_ready());
375        mama.reset();
376        assert!(!mama.is_ready());
377    }
378
379    #[test]
380    fn flat_input_uses_phase_fallback() {
381        // Zero inputs make every smooth/detrender term arithmetically exact
382        // zero, so `i1 == 0.0` and the phase calculation takes the
383        // `self.prev_phase` fallback rather than `atan(q1/i1)`. A non-zero
384        // constant like `50.0` leaves a sub-EPSILON cancellation residue
385        // that flips the branch back to the `atan` path on real hardware.
386        let mut mama = Mama::classic();
387        let out = mama.batch(&[0.0_f64; 200]);
388        assert!(out.iter().flatten().count() > 100);
389    }
390
391    use approx::assert_relative_eq;
392
393    fn sine_prices(n: u32) -> Vec<f64> {
394        (0..n)
395            .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
396            .collect()
397    }
398
399    #[test]
400    fn rejects_non_finite_and_out_of_range_limits() {
401        assert!(matches!(
402            Mama::new(f64::INFINITY, 0.05),
403            Err(Error::InvalidPeriod { .. })
404        ));
405        assert!(matches!(
406            Mama::new(0.5, f64::NAN),
407            Err(Error::InvalidPeriod { .. })
408        ));
409        assert!(matches!(
410            Mama::new(1.0, 1.5),
411            Err(Error::InvalidPeriod { .. })
412        ));
413        assert!(matches!(
414            Mama::new(-0.5, -0.6),
415            Err(Error::InvalidPeriod { .. })
416        ));
417        // Boundary values are accepted: slow == fast == 1.
418        assert_eq!(Mama::new(1.0, 1.0).unwrap().limits(), (1.0, 1.0));
419    }
420
421    #[test]
422    fn first_value_lands_exactly_at_warmup() {
423        let mut mama = Mama::classic();
424        let out = mama.batch(&sine_prices(80));
425        let warmup = mama.warmup_period();
426        assert!(out[..warmup - 1].iter().all(Option::is_none));
427        assert!(out[warmup - 1].is_some());
428    }
429
430    #[test]
431    fn reset_replays_identically() {
432        let prices = sine_prices(150);
433        let fresh = Mama::classic().batch(&prices);
434        let mut mama = Mama::classic();
435        let first = mama.batch(&prices);
436        mama.reset();
437        let second = mama.batch(&prices);
438        assert_eq!(first, fresh);
439        assert_eq!(second, fresh);
440    }
441
442    #[test]
443    fn wma_of_raw_inputs_seeds_lines_and_feeds_detrender_taps() {
444        let mut mama = Mama::classic();
445        // After exactly 4 inputs the WMA is (4*40 + 3*30 + 2*20 + 10) / 10 = 30,
446        // and while the taps fill both lines are seeded with it.
447        for p in [10.0, 20.0, 30.0, 40.0] {
448            assert_eq!(mama.update(p), None);
449        }
450        assert_eq!(mama.smooth_buf, vec![30.0]);
451        assert_eq!(mama.prev_mama, 30.0);
452        assert_eq!(mama.prev_fama, 30.0);
453
454        // Spike of 10 at index 7 in a zero series: smoothed values 4, 3, 2 at
455        // indices 7, 8, 9, so the smooth history is [2, 3, 4, 0, 0, 0, 0].
456        // adj = 0.075*6 + 0.54 = 0.99 and the detrender reads the smoothed taps:
457        //   (0.0962*2 + 0.5769*4 - 0.5769*0 - 0.0962*0) * 0.99 = 2.475.
458        let mut mama = Mama::classic();
459        let mut series = [0.0; 10];
460        series[7] = 10.0;
461        let _ = mama.batch(&series);
462        assert_eq!(mama.smooth_buf, vec![2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0]);
463        assert_eq!(mama.detrender_buf.len(), 1);
464        assert_relative_eq!(mama.detrender_buf[0], 2.475, epsilon = 1e-12);
465    }
466
467    #[test]
468    fn alpha_bounds_hand_computed() {
469        // fast_limit == slow_limit == 1 pins alpha to 1 whatever the phase rate,
470        // so MAMA equals the input and FAMA = 0.5*MAMA + 0.5*FAMA[-1].
471        let mut mama = Mama::new(1.0, 1.0).unwrap();
472        let prices = sine_prices(60);
473        let out = mama.batch(&prices);
474        let first = out[32].unwrap();
475        let second = out[33].unwrap();
476        assert_eq!(first.mama, prices[32]);
477        assert_eq!(second.mama, prices[33]);
478        assert_relative_eq!(
479            second.fama,
480            0.5 * prices[33] + 0.5 * first.fama,
481            epsilon = 1e-12
482        );
483    }
484
485    #[test]
486    fn slow_limit_floor_binds_on_fast_phase_rotation() {
487        // With fast_limit = 0.1 and slow_limit = 0.09 any phase step above
488        // 0.1 / 0.09 = 1.11 degrees makes fast_limit / delta_phase drop below the
489        // floor, which a fast cycle does on most bars. Both lines are convex
490        // blends of the inputs, so they stay inside the input range.
491        let mut mama = Mama::new(0.1, 0.09).unwrap();
492        let prices: Vec<f64> = (0..200)
493            .map(|i| 100.0 + (f64::from(i) * 2.5).sin() * 5.0)
494            .collect();
495        let out = mama.batch(&prices);
496        assert!(out
497            .iter()
498            .flatten()
499            .all(|o| (90.0..=110.0).contains(&o.mama) && (90.0..=110.0).contains(&o.fama)));
500    }
501}