quantwave_core/indicators/
butterworth.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::f64::consts::PI;
4
5#[derive(Debug, Clone)]
10pub struct Butterworth2 {
11 c1: f64,
12 b: f64,
13 aa: f64,
14 price_history: [f64; 2],
15 filt_history: [f64; 2],
16 count: usize,
17}
18
19impl Butterworth2 {
20 pub fn new(period: usize) -> Self {
21 let p = period as f64;
22 let a = (-1.414 * PI / p).exp();
23 let b = 2.0 * a * (1.414 * PI / p).cos();
24 let aa = a * a;
25 let c1 = (1.0 - b + aa) / 4.0;
26 Self {
27 c1,
28 b,
29 aa,
30 price_history: [0.0; 2],
31 filt_history: [0.0; 2],
32 count: 0,
33 }
34 }
35}
36
37impl Next<f64> for Butterworth2 {
38 type Output = f64;
39
40 fn next(&mut self, input: f64) -> Self::Output {
41 self.count += 1;
42 let res = if self.count < 3 {
43 input
44 } else {
45 self.b * self.filt_history[0] - self.aa * self.filt_history[1]
46 + self.c1 * (input + 2.0 * self.price_history[0] + self.price_history[1])
47 };
48
49 self.filt_history[1] = self.filt_history[0];
50 self.filt_history[0] = res;
51 self.price_history[1] = self.price_history[0];
52 self.price_history[0] = input;
53 res
54 }
55}
56
57#[derive(Debug, Clone)]
62pub struct Butterworth3 {
63 c1: f64,
64 b: f64,
65 c: f64,
66 bc: f64,
67 cc: f64,
68 price_history: [f64; 3],
69 filt_history: [f64; 3],
70 count: usize,
71}
72
73impl Butterworth3 {
74 pub fn new(period: usize) -> Self {
75 let p = period as f64;
76 let a = (-PI / p).exp();
77 let b = 2.0 * a * (1.738 * PI / p).cos();
78 let c = a * a;
79 let bc = b * c;
80 let cc = c * c;
81 let c1 = (1.0 - b + c) * (1.0 - c) / 8.0;
82 Self {
83 c1,
84 b,
85 c,
86 bc,
87 cc,
88 price_history: [0.0; 3],
89 filt_history: [0.0; 3],
90 count: 0,
91 }
92 }
93}
94
95impl Next<f64> for Butterworth3 {
96 type Output = f64;
97
98 fn next(&mut self, input: f64) -> Self::Output {
99 self.count += 1;
100 let res = if self.count < 4 {
101 input
102 } else {
103 (self.b + self.c) * self.filt_history[0]
104 - (self.c + self.bc) * self.filt_history[1]
105 + self.cc * self.filt_history[2]
106 + self.c1 * (input + 3.0 * self.price_history[0] + 3.0 * self.price_history[1] + self.price_history[2])
107 };
108
109 self.filt_history[2] = self.filt_history[1];
110 self.filt_history[1] = self.filt_history[0];
111 self.filt_history[0] = res;
112 self.price_history[2] = self.price_history[1];
113 self.price_history[1] = self.price_history[0];
114 self.price_history[0] = input;
115 res
116 }
117}
118
119pub const BUTTERWORTH2_METADATA: IndicatorMetadata = IndicatorMetadata {
120 name: "Butterworth2",
121 description: "2-pole Butterworth low-pass filter.",
122 params: &[ParamDef {
123 name: "period",
124 default: "14",
125 description: "Critical period",
126 }],
127 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/Poles.pdf",
128 formula_latex: r#"
129\[
130a = \exp(-1.414\pi/P)
131\]
132\[
133b = 2a \cos(1.414\pi/P)
134\]
135\[
136f = bf_{t-1} - a^2f_{t-2} + \frac{1-b+a^2}{4}(g + 2g_{t-1} + g_{t-2})
137\]
138"#,
139 gold_standard_file: "butterworth2.json",
140 category: "Ehlers DSP",
141};
142
143pub const BUTTERWORTH3_METADATA: IndicatorMetadata = IndicatorMetadata {
144 name: "Butterworth3",
145 description: "3-pole Butterworth low-pass filter.",
146 params: &[ParamDef {
147 name: "period",
148 default: "14",
149 description: "Critical period",
150 }],
151 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/Poles.pdf",
152 formula_latex: r#"
153\[
154a = \exp(-\pi/P)
155\]
156\[
157b = 2a \cos(1.738\pi/P)
158\]
159\[
160c = a^2
161\]
162\[
163f = (b+c)f_{t-1} - (c+bc)f_{t-2} + c^2f_{t-3} + \frac{(1-b+c)(1-c)}{8}(g + 3g_{t-1} + 3g_{t-2} + g_{t-3})
164\]
165"#,
166 gold_standard_file: "butterworth3.json",
167 category: "Ehlers DSP",
168};
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::traits::Next;
174 use proptest::prelude::*;
175
176 #[test]
177 fn test_butterworth_basic() {
178 let mut b2 = Butterworth2::new(14);
179 let mut b3 = Butterworth3::new(14);
180 for i in 0..20 {
181 let val = i as f64;
182 assert!(!b2.next(val).is_nan());
183 assert!(!b3.next(val).is_nan());
184 }
185 }
186
187 proptest! {
188 #[test]
189 fn test_butterworth2_parity(
190 inputs in prop::collection::vec(1.0..100.0, 10..100),
191 ) {
192 let p = 14;
193 let mut b2 = Butterworth2::new(p);
194 let streaming_results: Vec<f64> = inputs.iter().map(|&x| b2.next(x)).collect();
195
196 let mut batch_results = Vec::with_capacity(inputs.len());
198 let p_f = p as f64;
199 let a = (-1.414 * PI / p_f).exp();
200 let b = 2.0 * a * (1.414 * PI / p_f).cos();
201 let aa = a * a;
202 let c1 = (1.0 - b + aa) / 4.0;
203
204 let mut f_hist = [0.0; 2];
205 let mut g_hist = [0.0; 2];
206
207 for (i, &input) in inputs.iter().enumerate() {
208 let bar = i + 1;
209 let res = if bar < 3 {
210 input
211 } else {
212 b * f_hist[0] - aa * f_hist[1] + c1 * (input + 2.0 * g_hist[0] + g_hist[1])
213 };
214 f_hist[1] = f_hist[0];
215 f_hist[0] = res;
216 g_hist[1] = g_hist[0];
217 g_hist[0] = input;
218 batch_results.push(res);
219 }
220
221 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
222 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
223 }
224 }
225
226 #[test]
227 fn test_butterworth3_parity(
228 inputs in prop::collection::vec(1.0..100.0, 10..100),
229 ) {
230 let p = 14;
231 let mut b3 = Butterworth3::new(p);
232 let streaming_results: Vec<f64> = inputs.iter().map(|&x| b3.next(x)).collect();
233
234 let mut batch_results = Vec::with_capacity(inputs.len());
236 let p_f = p as f64;
237 let a = (-PI / p_f).exp();
238 let b = 2.0 * a * (1.738 * PI / p_f).cos();
239 let c = a * a;
240 let bc = b * c;
241 let cc = c * c;
242 let c1 = (1.0 - b + c) * (1.0 - c) / 8.0;
243
244 let mut f_hist = [0.0; 3];
245 let mut g_hist = [0.0; 3];
246
247 for (i, &input) in inputs.iter().enumerate() {
248 let bar = i + 1;
249 let res = if bar < 4 {
250 input
251 } else {
252 (b + c) * f_hist[0] - (c + bc) * f_hist[1] + cc * f_hist[2]
253 + c1 * (input + 3.0 * g_hist[0] + 3.0 * g_hist[1] + g_hist[2])
254 };
255 f_hist[2] = f_hist[1];
256 f_hist[1] = f_hist[0];
257 f_hist[0] = res;
258 g_hist[2] = g_hist[1];
259 g_hist[1] = g_hist[0];
260 g_hist[0] = input;
261 batch_results.push(res);
262 }
263
264 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
265 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
266 }
267 }
268 }
269}