Skip to main content

quantwave_core/indicators/
truncated_bandpass.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5/// Truncated Bandpass Filter
6///
7/// Based on John Ehlers' "Truncated Indicators" (TASC July 2020).
8/// It applies a Bandpass filter but truncates the feedback loop at a specific length
9/// to prevent "ringing" and better handle sharp price movements.
10#[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        // Truncated calculation
59        // Trunc[Length + 2] = 0;
60        // Trunc[Length + 1] = 0;
61        let mut t2 = 0.0;
62        let mut t1 = 0.0;
63        let mut bpt = 0.0;
64
65        // In the EasyLanguage code:
66        // for count = Length downto 1
67        // Trunc[count] = .5*(1-S1)*(Close[count-1] - Close[count+1]) + L1*(1+S1)*Trunc[count+1] - S1*Trunc[count+2]
68        
69        // Let's iterate from the oldest relevant bar (Length) to the newest (1)
70        for i in (0..self.length).rev() {
71            // Close[i] in EL is price at index i (0 is current)
72            // Close[count-1] -> prices[i]
73            // Close[count+1] -> prices[i+2]
74            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        // On constant input, Bandpass should output 0
137        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            // Batch implementation
152            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}