quantwave_core/indicators/
hann.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as VecDeque;
4use std::f64::consts::PI;
5
6#[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: &[ParamDef {
76 name: "length",
77 default: "20",
78 description: "Filter length",
79 }],
80 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/JustIgnoreThem.pdf",
81 formula_latex: r#"
82\[
83H(n) = 1 - \cos\left(\frac{2\pi n}{L+1}\right)
84\]
85\[
86Filt = \frac{\sum_{n=1}^L H(n) \cdot Price_{t-n+1}}{\sum H(n)}
87\]
88"#,
89 gold_standard_file: "hann_filter.json",
90 category: "Ehlers DSP",
91};
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::traits::Next;
97 use proptest::prelude::*;
98
99 #[test]
100 fn test_hann_basic() {
101 let mut hann = HannFilter::new(20);
102 for _ in 0..50 {
103 let val = hann.next(100.0);
104 approx::assert_relative_eq!(val, 100.0, epsilon = 1e-10);
105 }
106 }
107
108 proptest! {
109 #[test]
110 fn test_hann_parity(
111 inputs in prop::collection::vec(1.0..100.0, 50..100),
112 ) {
113 let length = 20;
114 let mut hann = HannFilter::new(length);
115 let streaming_results: Vec<f64> = inputs.iter().map(|&x| hann.next(x)).collect();
116
117 let mut batch_results = Vec::with_capacity(inputs.len());
119 let mut coeffs = Vec::new();
120 let mut c_sum = 0.0;
121 for count in 1..=length {
122 let c = 1.0 - (2.0 * PI * count as f64 / (length as f64 + 1.0)).cos();
123 coeffs.push(c);
124 c_sum += c;
125 }
126
127 for i in 0..inputs.len() {
128 if i < length - 1 {
129 batch_results.push(inputs[i]);
130 continue;
131 }
132 let mut f = 0.0;
133 for j in 0..length {
134 f += coeffs[j] * inputs[i - j];
135 }
136 batch_results.push(f / c_sum);
137 }
138
139 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
140 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
141 }
142 }
143 }
144}