Skip to main content

quantwave_core/indicators/
butterworth.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::f64::consts::PI;
4
5/// 2-Pole Butterworth Filter
6///
7/// Based on John Ehlers' "Poles, Zeros, and Higher Order Filters".
8/// A second-order IIR filter with two poles and two zeros at Z=-1.
9#[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/// 3-Pole Butterworth Filter
58///
59/// Based on John Ehlers' "Poles, Zeros, and Higher Order Filters".
60/// A third-order IIR filter with three poles and three zeros at Z=-1.
61#[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            // Batch implementation
197            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            // Batch implementation
235            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}