quantwave_core/indicators/
mad.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::SMA;
3use crate::traits::Next;
4
5#[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 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}