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 { 100.0 * (s - l) / l } else { 0.0 }
31    }
32}
33
34pub const MAD_METADATA: IndicatorMetadata = IndicatorMetadata {
35    name: "MAD",
36    description: "Moving Average Difference: 100 * (SMA(short) - SMA(long)) / SMA(long)",
37    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.",
38    keywords: &["volatility", "statistics", "robust", "ehlers"],
39    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.",
40    params: &[
41        ParamDef {
42            name: "short_period",
43            default: "8",
44            description: "Short-term SMA period",
45        },
46        ParamDef {
47            name: "long_period",
48            default: "23",
49            description: "Long-term SMA period",
50        },
51    ],
52    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - OCTOBER 2021.html",
53    formula_latex: r#"
54\[
55MAD = 100 \times \frac{SMA(short) - SMA(long)}{SMA(long)}
56\]
57"#,
58    gold_standard_file: "mad.json",
59    category: "Ehlers DSP",
60};
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use crate::test_utils::{assert_indicator_parity, load_gold_standard};
66    use crate::traits::Next;
67    use proptest::prelude::*;
68
69    #[test]
70    fn test_mad_gold_standard() {
71        let case = load_gold_standard("mad");
72        let mad = MAD::new(8, 23);
73        assert_indicator_parity(mad, &case.input, &case.expected);
74    }
75
76    #[test]
77    fn test_mad_basic() {
78        let mut mad = MAD::new(5, 10);
79        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
80        for input in inputs {
81            let res = mad.next(input);
82            assert!(!res.is_nan());
83        }
84    }
85
86    proptest! {
87        #[test]
88        fn test_mad_parity(
89            inputs in prop::collection::vec(1.0..100.0, 20..100),
90        ) {
91            let short = 8;
92            let long = 23;
93            let mut mad = MAD::new(short, long);
94            let streaming_results: Vec<f64> = inputs.iter().map(|&x| mad.next(x)).collect();
95
96            // Batch implementation
97            let mut batch_results = Vec::with_capacity(inputs.len());
98            for i in 0..inputs.len() {
99                let s_sum: f64 = inputs[(i.saturating_sub(short - 1))..=i].iter().sum();
100                let l_sum: f64 = inputs[(i.saturating_sub(long - 1))..=i].iter().sum();
101
102                let s_count = (i + 1).min(short);
103                let l_count = (i + 1).min(long);
104
105                let s = s_sum / s_count as f64;
106                let l = l_sum / l_count as f64;
107
108                let res = if l != 0.0 {
109                    100.0 * (s - l) / l
110                } else {
111                    0.0
112                };
113                batch_results.push(res);
114            }
115
116            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
117                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
118            }
119        }
120    }
121}