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 { 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 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}