quantwave_core/indicators/
bandpass.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::f64::consts::PI;
4
5#[derive(Debug, Clone)]
10pub struct BandPass {
11 alpha: f64,
12 beta: f64,
13 price_prev1: f64,
14 price_prev2: f64,
15 bp_history: [f64; 2],
16 count: usize,
17}
18
19impl BandPass {
20 pub fn new(period: usize, bandwidth: f64) -> Self {
21 let beta = (2.0 * PI / period as f64).cos();
22 let gamma = 1.0 / (2.0 * PI * bandwidth / period as f64).cos();
23 let alpha = gamma - (gamma * gamma - 1.0).sqrt();
24
25 Self {
26 alpha,
27 beta,
28 price_prev1: 0.0,
29 price_prev2: 0.0,
30 bp_history: [0.0; 2],
31 count: 0,
32 }
33 }
34}
35
36impl Next<f64> for BandPass {
37 type Output = f64;
38
39 fn next(&mut self, input: f64) -> Self::Output {
40 self.count += 1;
41
42 let bp = 0.5 * (1.0 - self.alpha) * (input - self.price_prev2)
44 + self.beta * (1.0 + self.alpha) * self.bp_history[0]
45 - self.alpha * self.bp_history[1];
46
47 self.bp_history[1] = self.bp_history[0];
48 self.bp_history[0] = bp;
49 self.price_prev2 = self.price_prev1;
50 self.price_prev1 = input;
51
52 bp
53 }
54}
55
56pub const BANDPASS_METADATA: IndicatorMetadata = IndicatorMetadata {
57 name: "BandPass",
58 description: "A bandpass filter that isolates cycle components around a center period.",
59 params: &[
60 ParamDef {
61 name: "period",
62 default: "20",
63 description: "Center period of the passband",
64 },
65 ParamDef {
66 name: "bandwidth",
67 default: "0.1",
68 description: "Relative bandwidth (delta)",
69 },
70 ],
71 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/EmpiricalModeDecomposition.pdf",
72 formula_latex: r#"
73\[
74\beta = \cos(360/P), \gamma = 1/\cos(720\delta/P), \alpha = \gamma - \sqrt{\gamma^2 - 1}
75\]
76\[
77BP = 0.5(1 - \alpha)(Price - Price_{t-2}) + \beta(1 + \alpha)BP_{t-1} - \alpha BP_{t-2}
78\]
79"#,
80 gold_standard_file: "bandpass.json",
81 category: "Ehlers DSP",
82};
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::traits::Next;
88 use proptest::prelude::*;
89
90 #[test]
91 fn test_bandpass_basic() {
92 let mut bp = BandPass::new(20, 0.1);
93 for i in 0..50 {
94 let val = bp.next(100.0 + (i as f64 * 0.1).sin());
95 assert!(!val.is_nan());
96 }
97 }
98
99 proptest! {
100 #[test]
101 fn test_bandpass_parity(
102 inputs in prop::collection::vec(1.0..100.0, 50..100),
103 ) {
104 let period = 20;
105 let bandwidth = 0.1;
106 let mut bp_obj = BandPass::new(period, bandwidth);
107 let streaming_results: Vec<f64> = inputs.iter().map(|&x| bp_obj.next(x)).collect();
108
109 let mut batch_results = Vec::with_capacity(inputs.len());
111 let beta = (2.0 * PI / period as f64).cos();
112 let _gamma = 1.0 / (2.0 * PI * 2.0 * bandwidth / period as f64).cos(); let alpha = {
120 let g = 1.0 / (2.0 * PI * bandwidth / period as f64).cos();
121 g - (g * g - 1.0).sqrt()
122 };
123
124 let mut p_hist = vec![0.0; inputs.len() + 2];
125 let mut b_hist = vec![0.0; inputs.len() + 2];
126
127 for (i, &input) in inputs.iter().enumerate() {
128 let idx = i + 2;
129 p_hist[idx] = input;
130 let bp = 0.5 * (1.0 - alpha) * (p_hist[idx] - p_hist[idx-2])
131 + beta * (1.0 + alpha) * b_hist[idx-1]
132 - alpha * b_hist[idx-2];
133 b_hist[idx] = bp;
134 batch_results.push(bp);
135 }
136
137 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
138 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
139 }
140 }
141 }
142}