Skip to main content

quantwave_core/indicators/
mama.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as VecDeque;
4
5/// MESA Adaptive Moving Average (MAMA)
6/// Adapts to price movement based on the rate change of phase as measured by the Hilbert Transform Discriminator.
7/// Returns (MAMA, FAMA).
8#[derive(Debug, Clone)]
9pub struct MAMA {
10    fast_limit: f64,
11    slow_limit: f64,
12    price_history: VecDeque<f64>,
13    smooth_history: VecDeque<f64>,
14    detrender_history: VecDeque<f64>,
15    i1_history: VecDeque<f64>,
16    q1_history: VecDeque<f64>,
17    i2_prev: f64,
18    q2_prev: f64,
19    re_prev: f64,
20    im_prev: f64,
21    period_prev: f64,
22    smooth_period_prev: f64,
23    phase_prev: f64,
24    mama_prev: f64,
25    fama_prev: f64,
26    count: usize,
27}
28
29impl MAMA {
30    pub fn new(fast_limit: f64, slow_limit: f64) -> Self {
31        Self {
32            fast_limit,
33            slow_limit,
34            price_history: VecDeque::from(vec![0.0; 4]),
35            smooth_history: VecDeque::from(vec![0.0; 7]),
36            detrender_history: VecDeque::from(vec![0.0; 7]),
37            i1_history: VecDeque::from(vec![0.0; 7]),
38            q1_history: VecDeque::from(vec![0.0; 7]),
39            i2_prev: 0.0,
40            q2_prev: 0.0,
41            re_prev: 0.0,
42            im_prev: 0.0,
43            period_prev: 0.0,
44            smooth_period_prev: 0.0,
45            phase_prev: 0.0,
46            mama_prev: 0.0,
47            fama_prev: 0.0,
48            count: 0,
49        }
50    }
51}
52
53impl Default for MAMA {
54    fn default() -> Self {
55        Self::new(0.5, 0.05)
56    }
57}
58
59impl Next<f64> for MAMA {
60    type Output = (f64, f64);
61
62    fn next(&mut self, price: f64) -> Self::Output {
63        if price.is_nan() {
64            return (f64::NAN, f64::NAN);
65        }
66
67        self.count += 1;
68
69        self.price_history.pop_back();
70        self.price_history.push_front(price);
71
72        if self.count < 6 {
73            self.mama_prev = price;
74            self.fama_prev = price;
75            return (price, price);
76        }
77
78        // Smooth = (4*Price + 3*Price[1] + 2*Price[2] + Price[3]) / 10;
79        let smooth = (4.0 * self.price_history[0]
80            + 3.0 * self.price_history[1]
81            + 2.0 * self.price_history[2]
82            + self.price_history[3])
83            / 10.0;
84
85        self.smooth_history.pop_back();
86        self.smooth_history.push_front(smooth);
87
88        // Detrender = (.0962*Smooth + .5769*Smooth[2] - .5769*Smooth[4] - .0962*Smooth[6])*(.075*Period[1] + .54);
89        let detrender = (0.0962 * self.smooth_history[0] + 0.5769 * self.smooth_history[2]
90            - 0.5769 * self.smooth_history[4]
91            - 0.0962 * self.smooth_history[6])
92            * (0.075 * self.period_prev + 0.54);
93
94        self.detrender_history.pop_back();
95        self.detrender_history.push_front(detrender);
96
97        // Q1 = (.0962*Detrender + .5769*Detrender[2] - .5769*Detrender[4] - .0962*Detrender[6])*(.075*Period[1] + .54);
98        let q1 = (0.0962 * self.detrender_history[0] + 0.5769 * self.detrender_history[2]
99            - 0.5769 * self.detrender_history[4]
100            - 0.0962 * self.detrender_history[6])
101            * (0.075 * self.period_prev + 0.54);
102
103        // I1 = Detrender[3];
104        let i1 = self.detrender_history[3];
105
106        self.i1_history.pop_back();
107        self.i1_history.push_front(i1);
108        self.q1_history.pop_back();
109        self.q1_history.push_front(q1);
110
111        // jI = (.0962*I1 + .5769*I1[2] - .5769*I1[4] - .0962*I1[6])*(.075*Period[1] + .54);
112        let ji = (0.0962 * self.i1_history[0] + 0.5769 * self.i1_history[2]
113            - 0.5769 * self.i1_history[4]
114            - 0.0962 * self.i1_history[6])
115            * (0.075 * self.period_prev + 0.54);
116
117        // jQ = (.0962*Q1 + .5769*Q1[2] - .5769*Q1[4] - .0962*Q1[6])*(.075*Period[1] + .54);
118        let jq = (0.0962 * self.q1_history[0] + 0.5769 * self.q1_history[2]
119            - 0.5769 * self.q1_history[4]
120            - 0.0962 * self.q1_history[6])
121            * (0.075 * self.period_prev + 0.54);
122
123        // I2 = I1 - jQ;
124        // Q2 = Q1 + jI;
125        let mut i2 = i1 - jq;
126        let mut q2 = q1 + ji;
127
128        // I2 = .2*I2 + .8*I2[1];
129        // Q2 = .2*Q2 + .8*Q2[1];
130        i2 = 0.2 * i2 + 0.8 * self.i2_prev;
131        q2 = 0.2 * q2 + 0.8 * self.q2_prev;
132        self.i2_prev = i2;
133        self.q2_prev = q2;
134
135        // Homodyne Discriminator
136        // Re = I2*I2[1] + Q2*Q2[1];
137        // Im = I2*Q2[1] - Q2*I2[1];
138        let mut re = i2 * self.i2_prev + q2 * self.q2_prev;
139        let mut im = i2 * self.q2_prev - q2 * self.i2_prev;
140
141        // Note: The EL code uses I2[1] and Q2[1] which are the values BEFORE the current i2/q2 update.
142        // Wait, in EL, Vars are updated at the end of the bar or immediately.
143        // I2 = .2*I2 + .8*I2[1] update i2. Then Re = I2*I2[1].
144        // This means I2[1] is indeed the PREVIOUS value.
145
146        re = 0.2 * re + 0.8 * self.re_prev;
147        im = 0.2 * im + 0.8 * self.im_prev;
148        self.re_prev = re;
149        self.im_prev = im;
150
151        let mut period = self.period_prev;
152        if im != 0.0 && re != 0.0 {
153            period = 360.0 / (im / re).atan().to_degrees();
154        }
155        period = period.clamp(0.67 * self.period_prev, 1.5 * self.period_prev);
156        period = period.clamp(6.0, 50.0);
157        period = 0.2 * period + 0.8 * self.period_prev;
158        self.period_prev = period;
159
160        let _smooth_period = 0.33 * period + 0.67 * self.smooth_period_prev;
161        self.smooth_period_prev = _smooth_period;
162
163        let mut phase = 0.0;
164        if i1 != 0.0 {
165            phase = (q1 / i1).atan().to_degrees();
166        }
167
168        let mut delta_phase = self.phase_prev - phase;
169        self.phase_prev = phase;
170
171        if delta_phase < 1.0 {
172            delta_phase = 1.0;
173        }
174
175        let alpha = (self.fast_limit / delta_phase).clamp(self.slow_limit, self.fast_limit);
176
177        let mama = alpha * price + (1.0 - alpha) * self.mama_prev;
178        let fama = 0.5 * alpha * mama + (1.0 - 0.5 * alpha) * self.fama_prev;
179
180        self.mama_prev = mama;
181        self.fama_prev = fama;
182
183        (mama, fama)
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use proptest::prelude::*;
191
192    #[test]
193    fn test_mama_basic() {
194        let mut mama = MAMA::new(0.5, 0.05);
195        let prices = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0];
196        for p in prices {
197            let (m, f) = mama.next(p);
198            assert!(!m.is_nan());
199            assert!(!f.is_nan());
200        }
201    }
202
203    fn mama_batch(data: Vec<f64>, fast: f64, slow: f64) -> Vec<(f64, f64)> {
204        let mut mama = MAMA::new(fast, slow);
205        data.into_iter().map(|x| mama.next(x)).collect()
206    }
207
208    proptest! {
209        #[test]
210        fn test_mama_streaming_consistency(input in prop::collection::vec(1.0..100.0, 10..100)) {
211            let fast = 0.5;
212            let slow = 0.05;
213            let mut mama = MAMA::new(fast, slow);
214            let streaming: Vec<_> = input.iter().map(|&x| mama.next(x)).collect();
215            let batch = mama_batch(input, fast, slow);
216            for (s, b) in streaming.iter().zip(batch.iter()) {
217                approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
218                approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-6);
219            }
220        }
221    }
222
223    #[test]
224    fn test_mama_single_injected_nan_does_not_panic() {
225        // Reproduces quantwave-j3e1: a single NaN mid-series used to poison
226        // self.period_prev, which later caused `.clamp(min, max)` to panic
227        // once min/max both became NaN.
228        let mut mama = MAMA::new(0.5, 0.05);
229        let n = 150;
230        let mut outputs = Vec::with_capacity(n);
231
232        for i in 0..n {
233            let price = if i == 100 {
234                f64::NAN
235            } else {
236                100.0 + 5.0 * (i as f64 * 0.2).sin()
237            };
238            outputs.push(mama.next(price));
239        }
240
241        // The injected bar itself must report NaN, not panic.
242        assert!(outputs[100].0.is_nan());
243        assert!(outputs[100].1.is_nan());
244
245        // Output should resume to finite values shortly after the bad bar.
246        // Internal state (price_history, period_prev, etc.) is completely
247        // untouched by the NaN bar, so recovery is essentially immediate;
248        // allow a small margin for any residual recursive smoothing.
249        let recovery_bar =
250            (101..=105).find(|&i| outputs[i].0.is_finite() && outputs[i].1.is_finite());
251        assert!(
252            recovery_bar.is_some(),
253            "MAMA/FAMA did not return finite output within 5 bars of the injected NaN"
254        );
255
256        // All bars after recovery must remain finite (no lingering NaN propagation).
257        for i in recovery_bar.unwrap()..n {
258            assert!(
259                outputs[i].0.is_finite(),
260                "mama went non-finite again at bar {i}"
261            );
262            assert!(
263                outputs[i].1.is_finite(),
264                "fama went non-finite again at bar {i}"
265            );
266        }
267    }
268}
269
270pub const MAMA_METADATA: IndicatorMetadata = IndicatorMetadata {
271    name: "MESA Adaptive Moving Average",
272    description: "MAMA adapts to price movement in an entirely new and unique way based on the rate change of phase.",
273    usage: "Use as an adaptive trend filter that automatically speeds up in fast markets and slows in choppy ones. The FAMA line crossing MAMA provides high-probability trend change signals.",
274    keywords: &["moving-average", "adaptive", "ehlers", "dsp", "trend"],
275    ehlers_summary: "Presented in Rocket Science for Traders (2001), MAMA adapts its alpha based on the rate of phase change measured by the Hilbert Transform Discriminator. Fast cycles produce large alpha for responsiveness; slow cycles produce small alpha to reduce noise.",
276    params: &[
277        ParamDef {
278            name: "fastlimit",
279            default: "0.5",
280            description: "Fast limit for alpha",
281        },
282        ParamDef {
283            name: "slowlimit",
284            default: "0.05",
285            description: "Slow limit for alpha",
286        },
287    ],
288    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/implemented/MAMA.pdf",
289    formula_latex: r#"
290\[
291\text{MAMA} = \alpha \cdot \text{Price} + (1 - \alpha) \cdot \text{MAMA}_{1}
292\]
293\[
294\text{FAMA} = 0.5\alpha \cdot \text{MAMA} + (1 - 0.5\alpha) \cdot \text{FAMA}_{1}
295\]
296"#,
297    gold_standard_file: "mama.json",
298    category: "Ehlers DSP",
299};