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    usage: "Use as a robust volatility measure when outliers or fat-tailed distributions would distort standard deviation. Works well for position sizing and volatility-based stop placement.",
42    keywords: &["volatility", "statistics", "robust", "ehlers"],
43    ehlers_summary: "Mean Absolute Deviation measures dispersion as the average absolute difference from the median rather than the squared difference from the mean used by standard deviation. It is less sensitive to outliers, making it a more robust volatility estimate for financial time series with fat tails.",
44    params: &[
45        ParamDef {
46            name: "short_period",
47            default: "8",
48            description: "Short-term SMA period",
49        },
50        ParamDef {
51            name: "long_period",
52            default: "23",
53            description: "Long-term SMA period",
54        },
55    ],
56    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - OCTOBER 2021.html",
57    formula_latex: r#"
58\[
59MAD = 100 \times \frac{SMA(short) - SMA(long)}{SMA(long)}
60\]
61"#,
62    gold_standard_file: "mad.json",
63    category: "Ehlers DSP",
64};
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use crate::traits::Next;
70    use crate::test_utils::{load_gold_standard, assert_indicator_parity};
71    use proptest::prelude::*;
72
73    #[test]
74    fn test_mad_gold_standard() {
75        let case = load_gold_standard("mad");
76        let mad = MAD::new(8, 23);
77        assert_indicator_parity(mad, &case.input, &case.expected);
78    }
79
80    #[test]
81    fn test_mad_basic() {
82        let mut mad = MAD::new(5, 10);
83        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
84        for input in inputs {
85            let res = mad.next(input);
86            assert!(!res.is_nan());
87        }
88    }
89
90    proptest! {
91        #[test]
92        fn test_mad_parity(
93            inputs in prop::collection::vec(1.0..100.0, 20..100),
94        ) {
95            let short = 8;
96            let long = 23;
97            let mut mad = MAD::new(short, long);
98            let streaming_results: Vec<f64> = inputs.iter().map(|&x| mad.next(x)).collect();
99
100            // Batch implementation
101            let mut batch_results = Vec::with_capacity(inputs.len());
102            for i in 0..inputs.len() {
103                let s_sum: f64 = inputs[(i.saturating_sub(short - 1))..=i].iter().sum();
104                let l_sum: f64 = inputs[(i.saturating_sub(long - 1))..=i].iter().sum();
105                
106                let s_count = (i + 1).min(short);
107                let l_count = (i + 1).min(long);
108                
109                let s = s_sum / s_count as f64;
110                let l = l_sum / l_count as f64;
111                
112                let res = if l != 0.0 {
113                    100.0 * (s - l) / l
114                } else {
115                    0.0
116                };
117                batch_results.push(res);
118            }
119
120            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
121                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
122            }
123        }
124    }
125}