quantwave_core/indicators/
truncated_bandpass.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5#[derive(Debug, Clone)]
11pub struct TruncatedBandpass {
12 _period: f64,
13 _bandwidth: f64,
14 length: usize,
15 prices: VecDeque<f64>,
16 l1: f64,
17 s1: f64,
18}
19
20impl TruncatedBandpass {
21 pub fn new(period: usize, bandwidth: f64, length: usize) -> Self {
22 let p = period as f64;
23 let deg_to_rad = std::f64::consts::PI / 180.0;
24 let l1 = (360.0 / p * deg_to_rad).cos();
25 let g1 = (bandwidth * 360.0 / p * deg_to_rad).cos();
26 let s1 = 1.0 / g1 - (1.0 / (g1 * g1) - 1.0).sqrt();
27
28 Self {
29 _period: p,
30 _bandwidth: bandwidth,
31 length,
32 prices: VecDeque::with_capacity(length + 2),
33 l1,
34 s1,
35 }
36 }
37}
38
39impl Default for TruncatedBandpass {
40 fn default() -> Self {
41 Self::new(20, 0.1, 10)
42 }
43}
44
45impl Next<f64> for TruncatedBandpass {
46 type Output = f64;
47
48 fn next(&mut self, input: f64) -> Self::Output {
49 self.prices.push_front(input);
50 if self.prices.len() > self.length + 2 {
51 self.prices.pop_back();
52 }
53
54 if self.prices.len() < self.length + 2 {
55 return 0.0;
56 }
57
58 let mut t2 = 0.0;
62 let mut t1 = 0.0;
63 let mut bpt = 0.0;
64
65 for i in (0..self.length).rev() {
71 let val = 0.5 * (1.0 - self.s1) * (self.prices[i] - self.prices[i + 2])
75 + self.l1 * (1.0 + self.s1) * t1
76 - self.s1 * t2;
77
78 t2 = t1;
79 t1 = val;
80 bpt = val;
81 }
82
83 bpt
84 }
85}
86
87pub const TRUNCATED_BANDPASS_METADATA: IndicatorMetadata = IndicatorMetadata {
88 name: "TruncatedBandpass",
89 description: "Truncated Bandpass filter for handling sharp price movements.",
90 usage: "Use to isolate cyclic components while minimizing 'ringing' effects caused by sudden price shocks. Ideal for cycle-based trading systems in volatile markets.",
91 keywords: &["filter", "ehlers", "dsp", "bandpass", "cycle", "robust"],
92 ehlers_summary: "Finite Impulse Response (FIR) filters have a fixed history, while Infinite Impulse Response (IIR) filters technically have an infinite history. Truncation limits the IIR feedback loop to a specific length, combining the sharp selectivity of IIR with the outlier-rejection of FIR.",
93 params: &[
94 ParamDef {
95 name: "period",
96 default: "20",
97 description: "Cycle period to isolate",
98 },
99 ParamDef {
100 name: "bandwidth",
101 default: "0.1",
102 description: "Bandwidth of the filter",
103 },
104 ParamDef {
105 name: "length",
106 default: "10",
107 description: "Truncation length",
108 },
109 ],
110 formula_source: "https://www.traders.com/Documentation/FEEDbk_docs/2020/07/TradersTips.html",
111 formula_latex: r#"
112\[
113L1 = \cos(360/P), \quad G1 = \cos(BW \cdot 360/P), \quad S1 = 1/G1 - \sqrt{1/G1^2 - 1}
114\]
115\[
116BPT_t = \text{IIR window of length } L \text{ with zero initial conditions}
117\]
118"#,
119 gold_standard_file: "truncated_bandpass.json",
120 category: "Ehlers DSP",
121};
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::traits::Next;
127 use proptest::prelude::*;
128
129 #[test]
130 fn test_truncated_bandpass_basic() {
131 let mut tbp = TruncatedBandpass::new(20, 0.1, 10);
132 for _ in 0..50 {
133 let _ = tbp.next(100.0);
134 }
135 let val = tbp.next(100.0);
136 approx::assert_relative_eq!(val, 0.0, epsilon = 1e-10);
138 }
139
140 proptest! {
141 #[test]
142 fn test_truncated_bandpass_parity(
143 inputs in prop::collection::vec(1.0..100.0, 50..100),
144 ) {
145 let period = 20;
146 let bandwidth = 0.1;
147 let length = 10;
148 let mut tbp = TruncatedBandpass::new(period, bandwidth, length);
149 let streaming_results: Vec<f64> = inputs.iter().map(|&x| tbp.next(x)).collect();
150
151 let mut batch_results = Vec::with_capacity(inputs.len());
153 let p = period as f64;
154 let deg_to_rad = std::f64::consts::PI / 180.0;
155 let l1 = (360.0 / p * deg_to_rad).cos();
156 let g1 = (bandwidth * 360.0 / p * deg_to_rad).cos();
157 let s1 = 1.0 / g1 - (1.0 / (g1 * g1) - 1.0).sqrt();
158
159 for i in 0..inputs.len() {
160 if i < length + 1 {
161 batch_results.push(0.0);
162 continue;
163 }
164
165 let mut t2 = 0.0;
166 let mut t1 = 0.0;
167 let mut bpt = 0.0;
168 for k in (0..length).rev() {
169 let val = 0.5 * (1.0 - s1) * (inputs[i - k] - inputs[i - k - 2])
170 + l1 * (1.0 + s1) * t1
171 - s1 * t2;
172 t2 = t1;
173 t1 = val;
174 bpt = val;
175 }
176 batch_results.push(bpt);
177 }
178
179 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
180 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
181 }
182 }
183 }
184}