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        if period > 1.5 * self.period_prev {
152            period = 1.5 * self.period_prev;
153        }
154        if period < 0.67 * self.period_prev {
155            period = 0.67 * self.period_prev;
156        }
157        if period < 6.0 {
158            period = 6.0;
159        }
160        if period > 50.0 {
161            period = 50.0;
162        }
163        period = 0.2 * period + 0.8 * self.period_prev;
164        self.period_prev = period;
165
166        let _smooth_period = 0.33 * period + 0.67 * self.smooth_period_prev;
167        self.smooth_period_prev = _smooth_period;
168
169        let mut phase = 0.0;
170        if i1 != 0.0 {
171            phase = (q1 / i1).atan().to_degrees();
172        }
173
174        let mut delta_phase = self.phase_prev - phase;
175        self.phase_prev = phase;
176
177        if delta_phase < 1.0 {
178            delta_phase = 1.0;
179        }
180
181        let mut alpha = self.fast_limit / delta_phase;
182        if alpha < self.slow_limit {
183            alpha = self.slow_limit;
184        }
185        if alpha > self.fast_limit {
186            alpha = self.fast_limit;
187        }
188
189        let mama = alpha * price + (1.0 - alpha) * self.mama_prev;
190        let fama = 0.5 * alpha * mama + (1.0 - 0.5 * alpha) * self.fama_prev;
191
192        self.mama_prev = mama;
193        self.fama_prev = fama;
194
195        (mama, fama)
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use proptest::prelude::*;
203
204    #[test]
205    fn test_mama_basic() {
206        let mut mama = MAMA::new(0.5, 0.05);
207        let prices = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0];
208        for p in prices {
209            let (m, f) = mama.next(p);
210            assert!(!m.is_nan());
211            assert!(!f.is_nan());
212        }
213    }
214
215    fn mama_batch(data: Vec<f64>, fast: f64, slow: f64) -> Vec<(f64, f64)> {
216        let mut mama = MAMA::new(fast, slow);
217        data.into_iter().map(|x| mama.next(x)).collect()
218    }
219
220    proptest! {
221        #[test]
222        fn test_mama_parity(input in prop::collection::vec(1.0..100.0, 10..100)) {
223            let fast = 0.5;
224            let slow = 0.05;
225            let mut mama = MAMA::new(fast, slow);
226            let mut streaming_results = Vec::with_capacity(input.len());
227            for &val in &input {
228                streaming_results.push(mama.next(val));
229            }
230
231            let batch_results = mama_batch(input, fast, slow);
232
233            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
234                approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
235                approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-6);
236            }
237        }
238    }
239}
240
241pub const MAMA_METADATA: IndicatorMetadata = IndicatorMetadata {
242    name: "MESA Adaptive Moving Average",
243    description: "MAMA adapts to price movement in an entirely new and unique way based on the rate change of phase.",
244    params: &[
245        ParamDef {
246            name: "fast_limit",
247            default: "0.5",
248            description: "Fast limit for alpha",
249        },
250        ParamDef {
251            name: "slow_limit",
252            default: "0.05",
253            description: "Slow limit for alpha",
254        },
255    ],
256    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/implemented/MAMA.pdf",
257    formula_latex: r#"
258\[
259\text{MAMA} = \alpha \cdot \text{Price} + (1 - \alpha) \cdot \text{MAMA}_{1}
260\]
261\[
262\text{FAMA} = 0.5\alpha \cdot \text{MAMA} + (1 - 0.5\alpha) \cdot \text{FAMA}_{1}
263\]
264"#,
265    gold_standard_file: "mama.json",
266    category: "Ehlers DSP",
267};