Skip to main content

quantwave_core/indicators/
hann.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4use std::f64::consts::PI;
5
6/// Hann Windowed Lowpass FIR Filter
7///
8/// Based on John Ehlers' "Just Ignore Them".
9/// A finite impulse response (FIR) filter using a Hann window for smoothing.
10#[derive(Debug, Clone)]
11pub struct HannFilter {
12    length: usize,
13    window: VecDeque<f64>,
14    coefficients: Vec<f64>,
15    coef_sum: f64,
16}
17
18impl HannFilter {
19    pub fn new(length: usize) -> Self {
20        let mut coefficients = Vec::with_capacity(length);
21        let mut coef_sum = 0.0;
22        for count in 1..=length {
23            let coef = 1.0 - (2.0 * PI * count as f64 / (length as f64 + 1.0)).cos();
24            coefficients.push(coef);
25            coef_sum += coef;
26        }
27        
28        Self {
29            length,
30            window: VecDeque::with_capacity(length),
31            coefficients,
32            coef_sum,
33        }
34    }
35}
36
37impl Default for HannFilter {
38    fn default() -> Self {
39        Self::new(20)
40    }
41}
42
43impl Next<f64> for HannFilter {
44    type Output = f64;
45
46    fn next(&mut self, input: f64) -> Self::Output {
47        self.window.push_front(input);
48        if self.window.len() > self.length {
49            self.window.pop_back();
50        }
51
52        if self.window.len() < self.length {
53            return input;
54        }
55
56        let mut filt = 0.0;
57        for (i, &val) in self.window.iter().enumerate() {
58            filt += self.coefficients[i] * val;
59        }
60
61        if self.coef_sum != 0.0 {
62            filt / self.coef_sum
63        } else {
64            input
65        }
66    }
67}
68
69pub const HANN_FILTER_METADATA: IndicatorMetadata = IndicatorMetadata {
70    name: "HannFilter",
71    description: "Hann windowed lowpass FIR filter.",
72    usage: "Use as a windowing function before FFT-based dominant cycle measurement to achieve clean spectral separation between market cycles.",
73    keywords: &["filter", "ehlers", "dsp", "windowing", "spectral"],
74    ehlers_summary: "The Hann window provides a smooth bell-shaped taper achieving -31.5 dB first sidelobe suppression. Ehlers uses it in Cycle Analytics for Traders as the preferred DFT window because it offers the best trade-off between frequency resolution and leakage rejection.",
75    params: &[
76        ParamDef {
77            name: "length",
78            default: "20",
79            description: "Filter length",
80        },
81    ],
82    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/JustIgnoreThem.pdf",
83    formula_latex: r#"
84\[
85H(n) = 1 - \cos\left(\frac{2\pi n}{L+1}\right)
86\]
87\[
88Filt = \frac{\sum_{n=1}^L H(n) \cdot Price_{t-n+1}}{\sum H(n)}
89\]
90"#,
91    gold_standard_file: "hann_filter.json",
92    category: "Ehlers DSP",
93};
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::traits::Next;
99    use proptest::prelude::*;
100
101    #[test]
102    fn test_hann_basic() {
103        let mut hann = HannFilter::new(20);
104        for _ in 0..50 {
105            let val = hann.next(100.0);
106            approx::assert_relative_eq!(val, 100.0, epsilon = 1e-10);
107        }
108    }
109
110    proptest! {
111        #[test]
112        fn test_hann_parity(
113            inputs in prop::collection::vec(1.0..100.0, 50..100),
114        ) {
115            let length = 20;
116            let mut hann = HannFilter::new(length);
117            let streaming_results: Vec<f64> = inputs.iter().map(|&x| hann.next(x)).collect();
118
119            // Batch implementation
120            let mut batch_results = Vec::with_capacity(inputs.len());
121            let mut coeffs = Vec::new();
122            let mut c_sum = 0.0;
123            for count in 1..=length {
124                let c = 1.0 - (2.0 * PI * count as f64 / (length as f64 + 1.0)).cos();
125                coeffs.push(c);
126                c_sum += c;
127            }
128
129            for i in 0..inputs.len() {
130                if i < length - 1 {
131                    batch_results.push(inputs[i]);
132                    continue;
133                }
134                let mut f = 0.0;
135                for j in 0..length {
136                    f += coeffs[j] * inputs[i - j];
137                }
138                batch_results.push(f / c_sum);
139            }
140
141            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
142                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
143            }
144        }
145    }
146}