quantwave_core/indicators/
mama.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as VecDeque;
4
5#[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 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 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 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 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 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 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 let mut i2 = i1 - jq;
126 let mut q2 = q1 + ji;
127
128 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 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 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 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 assert!(outputs[100].0.is_nan());
243 assert!(outputs[100].1.is_nan());
244
245 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 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};