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