wickra_core/indicators/
mama.rs1#![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#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct MamaOutput {
17 pub mama: f64,
19 pub fama: f64,
21}
22
23#[derive(Debug, Clone)]
51pub struct Mama {
52 fast_limit: f64,
53 slow_limit: f64,
54 smooth_buf: Vec<f64>,
55 detrender_buf: Vec<f64>,
56 q1_buf: Vec<f64>,
57 i1_buf: Vec<f64>,
58 prev_i2: f64,
59 prev_q2: f64,
60 prev_re: f64,
61 prev_im: f64,
62 prev_period: f64,
63 prev_phase: f64,
64 prev_mama: f64,
65 prev_fama: f64,
66 count: usize,
67 last_value: Option<MamaOutput>,
68}
69
70impl Mama {
71 pub fn new(fast_limit: f64, slow_limit: f64) -> Result<Self> {
78 if !fast_limit.is_finite()
79 || !slow_limit.is_finite()
80 || fast_limit <= 0.0
81 || fast_limit > 1.0
82 || slow_limit <= 0.0
83 || slow_limit > 1.0
84 || slow_limit > fast_limit
85 {
86 return Err(Error::InvalidPeriod {
87 message: "fast_limit, slow_limit must satisfy 0 < slow_limit <= fast_limit <= 1",
88 });
89 }
90 Ok(Self {
91 fast_limit,
92 slow_limit,
93 smooth_buf: Vec::with_capacity(7),
94 detrender_buf: Vec::with_capacity(7),
95 q1_buf: Vec::with_capacity(7),
96 i1_buf: Vec::with_capacity(7),
97 prev_i2: 0.0,
98 prev_q2: 0.0,
99 prev_re: 0.0,
100 prev_im: 0.0,
101 prev_period: 0.0,
102 prev_phase: 0.0,
103 prev_mama: 0.0,
104 prev_fama: 0.0,
105 count: 0,
106 last_value: None,
107 })
108 }
109
110 pub fn classic() -> Self {
112 Self::new(0.5, 0.05).expect("classic MAMA limits are valid")
113 }
114
115 pub const fn limits(&self) -> (f64, f64) {
117 (self.fast_limit, self.slow_limit)
118 }
119
120 pub const fn value(&self) -> Option<MamaOutput> {
122 self.last_value
123 }
124
125 fn push_front(buf: &mut Vec<f64>, v: f64, cap: usize) {
126 buf.insert(0, v);
127 if buf.len() > cap {
128 buf.truncate(cap);
129 }
130 }
131}
132
133impl Indicator for Mama {
134 type Input = f64;
135 type Output = MamaOutput;
136
137 fn update(&mut self, input: f64) -> Option<MamaOutput> {
138 if !input.is_finite() {
139 return self.last_value;
140 }
141 self.count += 1;
142
143 Self::push_front(&mut self.smooth_buf, input, 7);
144 if self.smooth_buf.len() < 4 {
145 return None;
146 }
147 let smooth = (4.0 * self.smooth_buf[0]
148 + 3.0 * self.smooth_buf[1]
149 + 2.0 * self.smooth_buf[2]
150 + self.smooth_buf[3])
151 / 10.0;
152
153 let period = self.prev_period.max(6.0).min(50.0);
154 let adj = 0.075 * period + 0.54;
155
156 if self.smooth_buf.len() < 7 {
157 self.prev_mama = smooth;
160 self.prev_fama = smooth;
161 return None;
162 }
163 let s0 = smooth;
164 let s2 = self.smooth_buf[2];
165 let s4 = self.smooth_buf[4];
166 let s6 = self.smooth_buf[6];
167 let detrender = (0.0962 * s0 + 0.5769 * s2 - 0.5769 * s4 - 0.0962 * s6) * adj;
168 Self::push_front(&mut self.detrender_buf, detrender, 7);
169 if self.detrender_buf.len() < 7 {
170 return None;
171 }
172
173 let q1 = (0.0962 * self.detrender_buf[0] + 0.5769 * self.detrender_buf[2]
174 - 0.5769 * self.detrender_buf[4]
175 - 0.0962 * self.detrender_buf[6])
176 * adj;
177 let i1 = self.detrender_buf[3];
178 Self::push_front(&mut self.q1_buf, q1, 7);
179 Self::push_front(&mut self.i1_buf, i1, 7);
180 if self.q1_buf.len() < 7 || self.i1_buf.len() < 7 {
181 return None;
182 }
183
184 let ji = (0.0962 * self.i1_buf[0] + 0.5769 * self.i1_buf[2]
185 - 0.5769 * self.i1_buf[4]
186 - 0.0962 * self.i1_buf[6])
187 * adj;
188 let jq = (0.0962 * self.q1_buf[0] + 0.5769 * self.q1_buf[2]
189 - 0.5769 * self.q1_buf[4]
190 - 0.0962 * self.q1_buf[6])
191 * adj;
192
193 let mut i2 = i1 - jq;
194 let mut q2 = q1 + ji;
195 i2 = 0.2 * i2 + 0.8 * self.prev_i2;
196 q2 = 0.2 * q2 + 0.8 * self.prev_q2;
197
198 let mut re = i2 * self.prev_i2 + q2 * self.prev_q2;
199 let mut im = i2 * self.prev_q2 - q2 * self.prev_i2;
200 re = 0.2 * re + 0.8 * self.prev_re;
201 im = 0.2 * im + 0.8 * self.prev_im;
202
203 self.prev_i2 = i2;
204 self.prev_q2 = q2;
205 self.prev_re = re;
206 self.prev_im = im;
207
208 let mut new_period = if im.abs() > f64::EPSILON && re.abs() > f64::EPSILON {
209 2.0 * PI / im.atan2(re)
210 } else {
211 self.prev_period
212 };
213 new_period = new_period.min(1.5 * self.prev_period);
214 new_period = new_period.max(0.67 * self.prev_period);
215 new_period = new_period.clamp(6.0, 50.0);
216 self.prev_period = 0.2 * new_period + 0.8 * self.prev_period;
217
218 let phase = if i1.abs() > f64::EPSILON {
220 (q1 / i1).atan().to_degrees()
221 } else {
222 self.prev_phase
223 };
224 let mut delta_phase = self.prev_phase - phase;
225 self.prev_phase = phase;
226 if delta_phase < 1.0 {
227 delta_phase = 1.0;
228 }
229 let mut alpha = self.fast_limit / delta_phase;
232 if alpha < self.slow_limit {
233 alpha = self.slow_limit;
234 }
235
236 self.prev_mama = alpha * input + (1.0 - alpha) * self.prev_mama;
237 let fama_alpha = 0.5 * alpha;
238 self.prev_fama = fama_alpha * self.prev_mama + (1.0 - fama_alpha) * self.prev_fama;
239
240 if self.count < 33 {
241 return None;
242 }
243 let out = MamaOutput {
244 mama: self.prev_mama,
245 fama: self.prev_fama,
246 };
247 self.last_value = Some(out);
248 Some(out)
249 }
250
251 fn reset(&mut self) {
252 self.smooth_buf.clear();
253 self.detrender_buf.clear();
254 self.q1_buf.clear();
255 self.i1_buf.clear();
256 self.prev_i2 = 0.0;
257 self.prev_q2 = 0.0;
258 self.prev_re = 0.0;
259 self.prev_im = 0.0;
260 self.prev_period = 0.0;
261 self.prev_phase = 0.0;
262 self.prev_mama = 0.0;
263 self.prev_fama = 0.0;
264 self.count = 0;
265 self.last_value = None;
266 }
267
268 fn warmup_period(&self) -> usize {
269 33
270 }
271
272 fn is_ready(&self) -> bool {
273 self.last_value.is_some()
274 }
275
276 fn name(&self) -> &'static str {
277 "MAMA"
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::traits::BatchExt;
285
286 #[test]
287 fn rejects_invalid_limits() {
288 assert!(matches!(
289 Mama::new(0.0, 0.05),
290 Err(Error::InvalidPeriod { .. })
291 ));
292 assert!(matches!(
293 Mama::new(0.5, 0.0),
294 Err(Error::InvalidPeriod { .. })
295 ));
296 assert!(matches!(
297 Mama::new(0.05, 0.5),
298 Err(Error::InvalidPeriod { .. })
299 ));
300 assert!(matches!(
301 Mama::new(1.5, 0.05),
302 Err(Error::InvalidPeriod { .. })
303 ));
304 assert!(matches!(
305 Mama::new(f64::NAN, 0.05),
306 Err(Error::InvalidPeriod { .. })
307 ));
308 }
309
310 #[test]
311 fn accessors_and_metadata() {
312 let mut mama = Mama::classic();
313 assert_eq!(mama.limits(), (0.5, 0.05));
314 assert_eq!(mama.warmup_period(), 33);
315 assert_eq!(mama.name(), "MAMA");
316 assert!(!mama.is_ready());
317 for i in 0..60 {
318 mama.update(100.0 + (f64::from(i) * 0.3).sin() * 5.0);
319 }
320 assert!(mama.is_ready());
321 assert!(mama.value().is_some());
322 }
323
324 #[test]
325 fn fama_lags_or_equals_mama_on_constant_series() {
326 let mut mama = Mama::classic();
327 let out = mama.batch(&[100.0_f64; 200]);
328 let last = out.iter().flatten().last().unwrap();
329 assert!((last.mama - 100.0).abs() < 1.0);
331 assert!((last.fama - 100.0).abs() < 1.0);
332 }
333
334 #[test]
335 fn batch_equals_streaming() {
336 let prices: Vec<f64> = (0..120)
337 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 5.0)
338 .collect();
339 let mut a = Mama::classic();
340 let mut b = Mama::classic();
341 let batch = a.batch(&prices);
342 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
343 assert_eq!(batch, streamed);
344 }
345
346 #[test]
347 fn ignores_non_finite_input() {
348 let mut mama = Mama::classic();
349 let prices: Vec<f64> = (0..100)
350 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
351 .collect();
352 mama.batch(&prices);
353 let before = mama.value();
354 assert!(before.is_some());
355 assert_eq!(mama.update(f64::NAN), before);
356 }
357
358 #[test]
359 fn reset_clears_state() {
360 let mut mama = Mama::classic();
361 let prices: Vec<f64> = (0..100)
362 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
363 .collect();
364 mama.batch(&prices);
365 assert!(mama.is_ready());
366 mama.reset();
367 assert!(!mama.is_ready());
368 }
369
370 #[test]
371 fn flat_input_uses_phase_fallback() {
372 let mut mama = Mama::classic();
378 let out = mama.batch(&[0.0_f64; 200]);
379 assert!(out.iter().flatten().count() > 100);
380 }
381}