Skip to main content

quantwave_core/indicators/
mama.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::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        self.count += 1;
64
65        self.price_history.pop_back();
66        self.price_history.push_front(price);
67
68        if self.count < 6 {
69            self.mama_prev = price;
70            self.fama_prev = price;
71            return (price, price);
72        }
73
74        // Smooth = (4*Price + 3*Price[1] + 2*Price[2] + Price[3]) / 10;
75        let smooth = (4.0 * self.price_history[0]
76            + 3.0 * self.price_history[1]
77            + 2.0 * self.price_history[2]
78            + self.price_history[3])
79            / 10.0;
80
81        self.smooth_history.pop_back();
82        self.smooth_history.push_front(smooth);
83
84        // Detrender = (.0962*Smooth + .5769*Smooth[2] - .5769*Smooth[4] - .0962*Smooth[6])*(.075*Period[1] + .54);
85        let detrender = (0.0962 * self.smooth_history[0] + 0.5769 * self.smooth_history[2]
86            - 0.5769 * self.smooth_history[4]
87            - 0.0962 * self.smooth_history[6])
88            * (0.075 * self.period_prev + 0.54);
89
90        self.detrender_history.pop_back();
91        self.detrender_history.push_front(detrender);
92
93        // Q1 = (.0962*Detrender + .5769*Detrender[2] - .5769*Detrender[4] - .0962*Detrender[6])*(.075*Period[1] + .54);
94        let q1 = (0.0962 * self.detrender_history[0] + 0.5769 * self.detrender_history[2]
95            - 0.5769 * self.detrender_history[4]
96            - 0.0962 * self.detrender_history[6])
97            * (0.075 * self.period_prev + 0.54);
98
99        // I1 = Detrender[3];
100        let i1 = self.detrender_history[3];
101
102        self.i1_history.pop_back();
103        self.i1_history.push_front(i1);
104        self.q1_history.pop_back();
105        self.q1_history.push_front(q1);
106
107        // jI = (.0962*I1 + .5769*I1[2] - .5769*I1[4] - .0962*I1[6])*(.075*Period[1] + .54);
108        let ji = (0.0962 * self.i1_history[0] + 0.5769 * self.i1_history[2]
109            - 0.5769 * self.i1_history[4]
110            - 0.0962 * self.i1_history[6])
111            * (0.075 * self.period_prev + 0.54);
112
113        // jQ = (.0962*Q1 + .5769*Q1[2] - .5769*Q1[4] - .0962*Q1[6])*(.075*Period[1] + .54);
114        let jq = (0.0962 * self.q1_history[0] + 0.5769 * self.q1_history[2]
115            - 0.5769 * self.q1_history[4]
116            - 0.0962 * self.q1_history[6])
117            * (0.075 * self.period_prev + 0.54);
118
119        // I2 = I1 - jQ;
120        // Q2 = Q1 + jI;
121        let mut i2 = i1 - jq;
122        let mut q2 = q1 + ji;
123
124        // I2 = .2*I2 + .8*I2[1];
125        // Q2 = .2*Q2 + .8*Q2[1];
126        i2 = 0.2 * i2 + 0.8 * self.i2_prev;
127        q2 = 0.2 * q2 + 0.8 * self.q2_prev;
128        self.i2_prev = i2;
129        self.q2_prev = q2;
130
131        // Homodyne Discriminator
132        // Re = I2*I2[1] + Q2*Q2[1];
133        // Im = I2*Q2[1] - Q2*I2[1];
134        let mut re = i2 * self.i2_prev + q2 * self.q2_prev;
135        let mut im = i2 * self.q2_prev - q2 * self.i2_prev;
136
137        // Note: The EL code uses I2[1] and Q2[1] which are the values BEFORE the current i2/q2 update.
138        // Wait, in EL, Vars are updated at the end of the bar or immediately.
139        // I2 = .2*I2 + .8*I2[1] update i2. Then Re = I2*I2[1].
140        // This means I2[1] is indeed the PREVIOUS value.
141
142        re = 0.2 * re + 0.8 * self.re_prev;
143        im = 0.2 * im + 0.8 * self.im_prev;
144        self.re_prev = re;
145        self.im_prev = im;
146
147        let mut period = self.period_prev;
148        if im != 0.0 && re != 0.0 {
149            period = 360.0 / (im / re).atan().to_degrees();
150        }
151        period = period.clamp(0.67 * self.period_prev, 1.5 * self.period_prev);
152        period = period.clamp(6.0, 50.0);
153        period = 0.2 * period + 0.8 * self.period_prev;
154        self.period_prev = period;
155
156        let _smooth_period = 0.33 * period + 0.67 * self.smooth_period_prev;
157        self.smooth_period_prev = _smooth_period;
158
159        let mut phase = 0.0;
160        if i1 != 0.0 {
161            phase = (q1 / i1).atan().to_degrees();
162        }
163
164        let mut delta_phase = self.phase_prev - phase;
165        self.phase_prev = phase;
166
167        if delta_phase < 1.0 {
168            delta_phase = 1.0;
169        }
170
171        let alpha = (self.fast_limit / delta_phase).clamp(self.slow_limit, self.fast_limit);
172
173        let mama = alpha * price + (1.0 - alpha) * self.mama_prev;
174        let fama = 0.5 * alpha * mama + (1.0 - 0.5 * alpha) * self.fama_prev;
175
176        self.mama_prev = mama;
177        self.fama_prev = fama;
178
179        (mama, fama)
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use proptest::prelude::*;
187
188    #[test]
189    fn test_mama_basic() {
190        let mut mama = MAMA::new(0.5, 0.05);
191        let prices = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0];
192        for p in prices {
193            let (m, f) = mama.next(p);
194            assert!(!m.is_nan());
195            assert!(!f.is_nan());
196        }
197    }
198
199    fn mama_batch(data: Vec<f64>, fast: f64, slow: f64) -> Vec<(f64, f64)> {
200        let mut mama = MAMA::new(fast, slow);
201        data.into_iter().map(|x| mama.next(x)).collect()
202    }
203
204    proptest! {
205        #[test]
206        fn test_mama_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
207            let fast = 0.5;
208            let slow = 0.05;
209            let mut mama = MAMA::new(fast, slow);
210            let mut streaming_results = Vec::with_capacity(input.len());
211            for &val in &input {
212                streaming_results.push(mama.next(val));
213            }
214
215            let batch_results = mama_batch(input, fast, slow);
216
217            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
218                approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
219                approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-6);
220            }
221        }
222    }
223}
224
225pub const MAMA_METADATA: IndicatorMetadata = IndicatorMetadata {
226    name: "MESA Adaptive Moving Average",
227    description: "MAMA adapts to price movement in an entirely new and unique way based on the rate change of phase.",
228    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.",
229    keywords: &["moving-average", "adaptive", "ehlers", "dsp", "trend"],
230    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.",
231    params: &[
232        ParamDef {
233            name: "fast_limit",
234            default: "0.5",
235            description: "Fast limit for alpha",
236        },
237        ParamDef {
238            name: "slow_limit",
239            default: "0.05",
240            description: "Slow limit for alpha",
241        },
242    ],
243    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/implemented/MAMA.pdf",
244    formula_latex: r#"
245\[
246\text{MAMA} = \alpha \cdot \text{Price} + (1 - \alpha) \cdot \text{MAMA}_{1}
247\]
248\[
249\text{FAMA} = 0.5\alpha \cdot \text{MAMA} + (1 - 0.5\alpha) \cdot \text{FAMA}_{1}
250\]
251"#,
252    gold_standard_file: "mama.json",
253    category: "Ehlers DSP",
254};