quantwave_core/indicators/
hamming.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::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 + (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 params: &[
79 ParamDef {
80 name: "length",
81 default: "20",
82 description: "Filter length",
83 },
84 ParamDef {
85 name: "pedestal",
86 default: "10.0",
87 description: "Pedestal in degrees",
88 },
89 ],
90 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - SEPTEMBER 2021.html",
91 formula_latex: r#"
92\[
93Deg(n) = Pedestal + (180 - 2 \times Pedestal) \times \frac{n}{L-1}
94\]
95\[
96Coef(n) = \sin\left(\frac{Deg(n) \times \pi}{180}\right)
97\]
98\[
99Filt = \frac{\sum_{n=0}^{L-1} Coef(n) \cdot Price_{t-n}}{\sum Coef(n)}
100\]
101"#,
102 gold_standard_file: "hamming_filter.json",
103 category: "Ehlers DSP",
104};
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::traits::Next;
110 use proptest::prelude::*;
111
112 #[test]
113 fn test_hamming_basic() {
114 let mut ham = HammingFilter::new(20, 10.0);
115 for _ in 0..50 {
116 let val = ham.next(100.0);
117 approx::assert_relative_eq!(val, 100.0, epsilon = 1e-10);
118 }
119 }
120
121 proptest! {
122 #[test]
123 fn test_hamming_parity(
124 inputs in prop::collection::vec(1.0..100.0, 50..100),
125 ) {
126 let length = 20;
127 let pedestal = 10.0;
128 let mut ham = HammingFilter::new(length, pedestal);
129 let streaming_results: Vec<f64> = inputs.iter().map(|&x| ham.next(x)).collect();
130
131 let mut batch_results = Vec::with_capacity(inputs.len());
133 let mut coeffs = Vec::new();
134 let mut c_sum = 0.0;
135 for count in 0..length {
136 let deg = pedestal + (180.0 - 2.0 * pedestal) * count as f64 / (length as f64 - 1.0).max(1.0);
137 let coef = (deg * PI / 180.0).sin();
138 coeffs.push(coef);
139 c_sum += coef;
140 }
141
142 for i in 0..inputs.len() {
143 if i < length - 1 {
144 batch_results.push(inputs[i]);
145 continue;
146 }
147 let mut f = 0.0;
148 for j in 0..length {
149 f += coeffs[j] * inputs[i - j];
150 }
151 batch_results.push(f / c_sum);
152 }
153
154 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
155 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
156 }
157 }
158 }
159}