Skip to main content

quantwave_core/indicators/
madh.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::hann::HannFilter;
3use crate::traits::Next;
4
5/// Moving Average Difference with Hann Windowing (MADH)
6///
7/// Based on John Ehlers' "The MADH: The MAD Indicator, Enhanced" (S&C 2021).
8/// It computes the percentage difference between a short-term Hann-windowed FIR filter
9/// and a long-term Hann-windowed FIR filter.
10#[derive(Debug, Clone)]
11pub struct MADH {
12    filter1: HannFilter,
13    filter2: HannFilter,
14    _short_length: usize,
15    _dominant_cycle: usize,
16}
17
18impl MADH {
19    pub fn new(short_length: usize, dominant_cycle: usize) -> Self {
20        let long_length = short_length + dominant_cycle / 2;
21        Self {
22            filter1: HannFilter::new(short_length),
23            filter2: HannFilter::new(long_length),
24            _short_length: short_length,
25            _dominant_cycle: dominant_cycle,
26        }
27    }
28}
29
30impl Default for MADH {
31    fn default() -> Self {
32        Self::new(8, 27)
33    }
34}
35
36impl Next<f64> for MADH {
37    type Output = f64;
38
39    fn next(&mut self, input: f64) -> Self::Output {
40        let f1 = self.filter1.next(input);
41        let f2 = self.filter2.next(input);
42        if f2.abs() > 1e-10 {
43            100.0 * (f1 - f2) / f2
44        } else {
45            0.0
46        }
47    }
48}
49
50pub const MADH_METADATA: IndicatorMetadata = IndicatorMetadata {
51    name: "MADH",
52    description: "Moving Average Difference with Hann Windowing: 100 * (Hann(short) - Hann(long)) / Hann(long)",
53    params: &[
54        ParamDef {
55            name: "short_length",
56            default: "8",
57            description: "Short-term filter length",
58        },
59        ParamDef {
60            name: "dominant_cycle",
61            default: "27",
62            description: "Dominant cycle for calculating long length",
63        },
64    ],
65    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - NOVEMBER 2021.html",
66    formula_latex: r#"
67\[
68LongLength = \lfloor ShortLength + DominantCycle / 2 \rfloor
69\]
70\[
71Filt1 = HannWindow(Price, ShortLength)
72\]
73\[
74Filt2 = HannWindow(Price, LongLength)
75\]
76\[
77MADH = 100 \times \frac{Filt1 - Filt2}{Filt2}
78\]
79"#,
80    gold_standard_file: "madh.json",
81    category: "Ehlers DSP",
82};
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::traits::Next;
88    use proptest::prelude::*;
89    use std::f64::consts::PI;
90
91    #[test]
92    fn test_madh_basic() {
93        let mut madh = MADH::new(8, 27);
94        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
95        for input in inputs {
96            let res = madh.next(input);
97            assert!(!res.is_nan());
98        }
99    }
100
101    proptest! {
102        #[test]
103        fn test_madh_parity(
104            inputs in prop::collection::vec(1.0..100.0, 50..100),
105        ) {
106            let short = 8;
107            let dc = 27;
108            let long = short + dc / 2;
109            let mut madh = MADH::new(short, dc);
110            let streaming_results: Vec<f64> = inputs.iter().map(|&x| madh.next(x)).collect();
111
112            // Reference implementation
113            let mut batch_results = Vec::with_capacity(inputs.len());
114            
115            let mut coeffs1 = Vec::new();
116            let mut sum1 = 0.0;
117            for count in 1..=short {
118                let c = 1.0 - (2.0 * PI * count as f64 / (short as f64 + 1.0)).cos();
119                coeffs1.push(c);
120                sum1 += c;
121            }
122            
123            let mut coeffs2 = Vec::new();
124            let mut sum2 = 0.0;
125            for count in 1..=long {
126                let c = 1.0 - (2.0 * PI * count as f64 / (long as f64 + 1.0)).cos();
127                coeffs2.push(c);
128                sum2 += c;
129            }
130
131            for i in 0..inputs.len() {
132                let f1 = if i < short - 1 {
133                    inputs[i]
134                } else {
135                    let mut sum = 0.0;
136                    for j in 0..short {
137                        sum += coeffs1[j] * inputs[i - j];
138                    }
139                    sum / sum1
140                };
141
142                let f2 = if i < long - 1 {
143                    inputs[i]
144                } else {
145                    let mut sum = 0.0;
146                    for j in 0..long {
147                        sum += coeffs2[j] * inputs[i - j];
148                    }
149                    sum / sum2
150                };
151
152                let res = if f2.abs() > 1e-10 {
153                    100.0 * (f1 - f2) / f2
154                } else {
155                    0.0
156                };
157                batch_results.push(res);
158            }
159
160            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
161                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
162            }
163        }
164    }
165}