Skip to main content

quantwave_core/indicators/
mad.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::SMA;
3use crate::traits::Next;
4
5/// Moving Average Difference (MAD) Indicator
6///
7/// Based on John Ehlers' "Cycle/Trend Analytics And The MAD Indicator" (2021).
8/// It computes the percentage difference between a short-term SMA and a long-term SMA.
9#[derive(Debug, Clone)]
10pub struct MAD {
11    short_sma: SMA,
12    long_sma: SMA,
13}
14
15impl MAD {
16    pub fn new(short_period: usize, long_period: usize) -> Self {
17        Self {
18            short_sma: SMA::new(short_period),
19            long_sma: SMA::new(long_period),
20        }
21    }
22}
23
24impl Next<f64> for MAD {
25    type Output = f64;
26
27    fn next(&mut self, input: f64) -> Self::Output {
28        let s = self.short_sma.next(input);
29        let l = self.long_sma.next(input);
30        if l != 0.0 {
31            100.0 * (s - l) / l
32        } else {
33            0.0
34        }
35    }
36}
37
38pub const MAD_METADATA: IndicatorMetadata = IndicatorMetadata {
39    name: "MAD",
40    description: "Moving Average Difference: 100 * (SMA(short) - SMA(long)) / SMA(long)",
41    params: &[
42        ParamDef {
43            name: "short_period",
44            default: "8",
45            description: "Short-term SMA period",
46        },
47        ParamDef {
48            name: "long_period",
49            default: "23",
50            description: "Long-term SMA period",
51        },
52    ],
53    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - OCTOBER 2021.html",
54    formula_latex: r#"
55\[
56MAD = 100 \times \frac{SMA(short) - SMA(long)}{SMA(long)}
57\]
58"#,
59    gold_standard_file: "mad.json",
60    category: "Ehlers DSP",
61};
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::traits::Next;
67    use crate::test_utils::{load_gold_standard, assert_indicator_parity};
68    use proptest::prelude::*;
69
70    #[test]
71    fn test_mad_gold_standard() {
72        let case = load_gold_standard("mad");
73        let mad = MAD::new(8, 23);
74        assert_indicator_parity(mad, &case.input, &case.expected);
75    }
76
77    #[test]
78    fn test_mad_basic() {
79        let mut mad = MAD::new(5, 10);
80        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
81        for input in inputs {
82            let res = mad.next(input);
83            assert!(!res.is_nan());
84        }
85    }
86
87    proptest! {
88        #[test]
89        fn test_mad_parity(
90            inputs in prop::collection::vec(1.0..100.0, 20..100),
91        ) {
92            let short = 8;
93            let long = 23;
94            let mut mad = MAD::new(short, long);
95            let streaming_results: Vec<f64> = inputs.iter().map(|&x| mad.next(x)).collect();
96
97            // Batch implementation
98            let mut batch_results = Vec::with_capacity(inputs.len());
99            for i in 0..inputs.len() {
100                let s_sum: f64 = inputs[(i.saturating_sub(short - 1))..=i].iter().sum();
101                let l_sum: f64 = inputs[(i.saturating_sub(long - 1))..=i].iter().sum();
102                
103                let s_count = (i + 1).min(short);
104                let l_count = (i + 1).min(long);
105                
106                let s = s_sum / s_count as f64;
107                let l = l_sum / l_count as f64;
108                
109                let res = if l != 0.0 {
110                    100.0 * (s - l) / l
111                } else {
112                    0.0
113                };
114                batch_results.push(res);
115            }
116
117            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
118                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
119            }
120        }
121    }
122}