quantwave_core/indicators/
hamming.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 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 for count in 0..length {
27 let deg = pedestal_deg
28 + (180.0 - 2.0 * pedestal_deg) * count as f64 / (length as f64 - 1.0).max(1.0);
29 let coef = (deg * PI / 180.0).sin();
30 coefficients.push(coef);
31 coef_sum += coef;
32 }
33
34 Self {
35 length,
36 _pedestal: pedestal_deg,
37 window: VecDeque::with_capacity(length),
38 coefficients,
39 coef_sum,
40 }
41 }
42}
43
44impl Default for HammingFilter {
45 fn default() -> Self {
46 Self::new(20, 10.0)
47 }
48}
49
50impl Next<f64> for HammingFilter {
51 type Output = f64;
52
53 fn next(&mut self, input: f64) -> Self::Output {
54 self.window.push_front(input);
55 if self.window.len() > self.length {
56 self.window.pop_back();
57 }
58
59 if self.window.len() < self.length {
60 return input;
61 }
62
63 let mut filt = 0.0;
64 for (i, &val) in self.window.iter().enumerate() {
65 filt += self.coefficients[i] * val;
66 }
67
68 if self.coef_sum.abs() > 1e-10 {
69 filt / self.coef_sum
70 } else {
71 input
72 }
73 }
74}
75
76pub const HAMMING_FILTER_METADATA: IndicatorMetadata = IndicatorMetadata {
77 name: "HammingFilter",
78 description: "Hamming windowed FIR filter with pedestal.",
79 usage: "Apply as a windowing function before DFT-based cycle detection to reduce sidelobe leakage and obtain cleaner dominant cycle estimates.",
80 keywords: &["filter", "ehlers", "dsp", "windowing", "spectral"],
81 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.",
82 params: &[
83 ParamDef {
84 name: "length",
85 default: "20",
86 description: "Filter length",
87 },
88 ParamDef {
89 name: "pedestal",
90 default: "10.0",
91 description: "Pedestal in degrees",
92 },
93 ],
94 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - SEPTEMBER 2021.html",
95 formula_latex: r#"
96\[
97Deg(n) = Pedestal + (180 - 2 \times Pedestal) \times \frac{n}{L-1}
98\]
99\[
100Coef(n) = \sin\left(\frac{Deg(n) \times \pi}{180}\right)
101\]
102\[
103Filt = \frac{\sum_{n=0}^{L-1} Coef(n) \cdot Price_{t-n}}{\sum Coef(n)}
104\]
105"#,
106 gold_standard_file: "hamming_filter.json",
107 category: "Ehlers DSP",
108};
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::traits::Next;
114 use proptest::prelude::*;
115
116 #[test]
117 fn test_hamming_basic() {
118 let mut ham = HammingFilter::new(20, 10.0);
119 for _ in 0..50 {
120 let val = ham.next(100.0);
121 approx::assert_relative_eq!(val, 100.0, epsilon = 1e-10);
122 }
123 }
124
125 proptest! {
126 #[test]
127 fn test_hamming_parity(
128 inputs in prop::collection::vec(1.0..100.0, 50..100),
129 ) {
130 let length = 20;
131 let pedestal = 10.0;
132 let mut ham = HammingFilter::new(length, pedestal);
133 let streaming_results: Vec<f64> = inputs.iter().map(|&x| ham.next(x)).collect();
134
135 let mut batch_results = Vec::with_capacity(inputs.len());
137 let mut coeffs = Vec::new();
138 let mut c_sum = 0.0;
139 for count in 0..length {
140 let deg = pedestal + (180.0 - 2.0 * pedestal) * count as f64 / (length as f64 - 1.0).max(1.0);
141 let coef = (deg * PI / 180.0).sin();
142 coeffs.push(coef);
143 c_sum += coef;
144 }
145
146 for i in 0..inputs.len() {
147 if i < length - 1 {
148 batch_results.push(inputs[i]);
149 continue;
150 }
151 let mut f = 0.0;
152 for j in 0..length {
153 f += coeffs[j] * inputs[i - j];
154 }
155 batch_results.push(f / c_sum);
156 }
157
158 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
159 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
160 }
161 }
162 }
163}