quantwave_core/indicators/
madh.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::hann::HannFilter;
3use crate::traits::Next;
4
5#[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 usage: "Use to measure the volatility of the cyclical price component only, filtering out trend-driven amplitude changes that inflate standard volatility measures in trending markets.",
54 keywords: &["volatility", "statistics", "ehlers", "high-pass"],
55 ehlers_summary: "MADH applies Mean Absolute Deviation to the high-pass filtered price series rather than raw price. By isolating the cyclical component before measuring dispersion, it quantifies the noise level within the current market cycle rather than conflating it with trend amplitude.",
56 params: &[
57 ParamDef {
58 name: "short_length",
59 default: "8",
60 description: "Short-term filter length",
61 },
62 ParamDef {
63 name: "dominant_cycle",
64 default: "27",
65 description: "Dominant cycle for calculating long length",
66 },
67 ],
68 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - NOVEMBER 2021.html",
69 formula_latex: r#"
70\[
71LongLength = \lfloor ShortLength + DominantCycle / 2 \rfloor
72\]
73\[
74Filt1 = HannWindow(Price, ShortLength)
75\]
76\[
77Filt2 = HannWindow(Price, LongLength)
78\]
79\[
80MADH = 100 \times \frac{Filt1 - Filt2}{Filt2}
81\]
82"#,
83 gold_standard_file: "madh.json",
84 category: "Ehlers DSP",
85};
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::traits::Next;
91 use proptest::prelude::*;
92 use std::f64::consts::PI;
93
94 #[test]
95 fn test_madh_basic() {
96 let mut madh = MADH::new(8, 27);
97 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
98 for input in inputs {
99 let res = madh.next(input);
100 assert!(!res.is_nan());
101 }
102 }
103
104 proptest! {
105 #[test]
106 fn test_madh_parity(
107 inputs in prop::collection::vec(1.0..100.0, 50..100),
108 ) {
109 let short = 8;
110 let dc = 27;
111 let long = short + dc / 2;
112 let mut madh = MADH::new(short, dc);
113 let streaming_results: Vec<f64> = inputs.iter().map(|&x| madh.next(x)).collect();
114
115 let mut batch_results = Vec::with_capacity(inputs.len());
117
118 let mut coeffs1 = Vec::new();
119 let mut sum1 = 0.0;
120 for count in 1..=short {
121 let c = 1.0 - (2.0 * PI * count as f64 / (short as f64 + 1.0)).cos();
122 coeffs1.push(c);
123 sum1 += c;
124 }
125
126 let mut coeffs2 = Vec::new();
127 let mut sum2 = 0.0;
128 for count in 1..=long {
129 let c = 1.0 - (2.0 * PI * count as f64 / (long as f64 + 1.0)).cos();
130 coeffs2.push(c);
131 sum2 += c;
132 }
133
134 for i in 0..inputs.len() {
135 let f1 = if i < short - 1 {
136 inputs[i]
137 } else {
138 let mut sum = 0.0;
139 for j in 0..short {
140 sum += coeffs1[j] * inputs[i - j];
141 }
142 sum / sum1
143 };
144
145 let f2 = if i < long - 1 {
146 inputs[i]
147 } else {
148 let mut sum = 0.0;
149 for j in 0..long {
150 sum += coeffs2[j] * inputs[i - j];
151 }
152 sum / sum2
153 };
154
155 let res = if f2.abs() > 1e-10 {
156 100.0 * (f1 - f2) / f2
157 } else {
158 0.0
159 };
160 batch_results.push(res);
161 }
162
163 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
164 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
165 }
166 }
167 }
168}