Skip to main content

quantwave_core/indicators/
hamming.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4use std::f64::consts::PI;
5
6/// Hamming Windowed FIR Filter
7///
8/// Based on John Ehlers' "Windowing" (S&C 2021).
9/// A finite impulse response (FIR) filter using a Hamming-like window with a pedestal for smoothing.
10#[derive(Debug, Clone)]
11pub struct HammingFilter {
12    length: usize,
13    _pedestal: f64,
14    window: VecDeque<f64>,
15    coefficients: Vec<f64>,
16    coef_sum: f64,
17}
18
19impl HammingFilter {
20    pub fn new(length: usize, pedestal_deg: f64) -> Self {
21        let mut coefficients = Vec::with_capacity(length);
22        let mut coef_sum = 0.0;
23        
24        // Follows Ehlers' formula: Sine(Pedestal + (180 - 2*Pedestal)*count / (Length - 1))
25        // count from 0 to Length - 1
26        for count in 0..length {
27            let deg = pedestal_deg + (180.0 - 2.0 * pedestal_deg) * count as f64 / (length as f64 - 1.0).max(1.0);
28            let coef = (deg * PI / 180.0).sin();
29            coefficients.push(coef);
30            coef_sum += coef;
31        }
32
33        Self {
34            length,
35            _pedestal: pedestal_deg,
36            window: VecDeque::with_capacity(length),
37            coefficients,
38            coef_sum,
39        }
40    }
41}
42
43impl Default for HammingFilter {
44    fn default() -> Self {
45        Self::new(20, 10.0)
46    }
47}
48
49impl Next<f64> for HammingFilter {
50    type Output = f64;
51
52    fn next(&mut self, input: f64) -> Self::Output {
53        self.window.push_front(input);
54        if self.window.len() > self.length {
55            self.window.pop_back();
56        }
57
58        if self.window.len() < self.length {
59            return input;
60        }
61
62        let mut filt = 0.0;
63        for (i, &val) in self.window.iter().enumerate() {
64            filt += self.coefficients[i] * val;
65        }
66
67        if self.coef_sum.abs() > 1e-10 {
68            filt / self.coef_sum
69        } else {
70            input
71        }
72    }
73}
74
75pub const HAMMING_FILTER_METADATA: IndicatorMetadata = IndicatorMetadata {
76    name: "HammingFilter",
77    description: "Hamming windowed FIR filter with pedestal.",
78    usage: "Apply as a windowing function before DFT-based cycle detection to reduce sidelobe leakage and obtain cleaner dominant cycle estimates.",
79    keywords: &["filter", "ehlers", "dsp", "windowing", "spectral"],
80    ehlers_summary: "The Hamming window is a raised-cosine weighting function that reduces spectral leakage by tapering the edges of a data block. Ehlers uses it in DFT-based cycle measurement tools to prevent energy in one frequency bin from contaminating adjacent bins, improving cycle period resolution.",
81    params: &[
82        ParamDef {
83            name: "length",
84            default: "20",
85            description: "Filter length",
86        },
87        ParamDef {
88            name: "pedestal",
89            default: "10.0",
90            description: "Pedestal in degrees",
91        },
92    ],
93    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - SEPTEMBER 2021.html",
94    formula_latex: r#"
95\[
96Deg(n) = Pedestal + (180 - 2 \times Pedestal) \times \frac{n}{L-1}
97\]
98\[
99Coef(n) = \sin\left(\frac{Deg(n) \times \pi}{180}\right)
100\]
101\[
102Filt = \frac{\sum_{n=0}^{L-1} Coef(n) \cdot Price_{t-n}}{\sum Coef(n)}
103\]
104"#,
105    gold_standard_file: "hamming_filter.json",
106    category: "Ehlers DSP",
107};
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::traits::Next;
113    use proptest::prelude::*;
114
115    #[test]
116    fn test_hamming_basic() {
117        let mut ham = HammingFilter::new(20, 10.0);
118        for _ in 0..50 {
119            let val = ham.next(100.0);
120            approx::assert_relative_eq!(val, 100.0, epsilon = 1e-10);
121        }
122    }
123
124    proptest! {
125        #[test]
126        fn test_hamming_parity(
127            inputs in prop::collection::vec(1.0..100.0, 50..100),
128        ) {
129            let length = 20;
130            let pedestal = 10.0;
131            let mut ham = HammingFilter::new(length, pedestal);
132            let streaming_results: Vec<f64> = inputs.iter().map(|&x| ham.next(x)).collect();
133
134            // Batch implementation
135            let mut batch_results = Vec::with_capacity(inputs.len());
136            let mut coeffs = Vec::new();
137            let mut c_sum = 0.0;
138            for count in 0..length {
139                let deg = pedestal + (180.0 - 2.0 * pedestal) * count as f64 / (length as f64 - 1.0).max(1.0);
140                let coef = (deg * PI / 180.0).sin();
141                coeffs.push(coef);
142                c_sum += coef;
143            }
144
145            for i in 0..inputs.len() {
146                if i < length - 1 {
147                    batch_results.push(inputs[i]);
148                    continue;
149                }
150                let mut f = 0.0;
151                for j in 0..length {
152                    f += coeffs[j] * inputs[i - j];
153                }
154                batch_results.push(f / c_sum);
155            }
156
157            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
158                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
159            }
160        }
161    }
162}