Skip to main content

quantwave_core/indicators/
high_pass.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::f64::consts::PI;
4
5/// HighPass Filter
6///
7/// Based on John Ehlers' "The Ultimate Smoother"
8/// A second-order High Pass filter that rejects low-frequency components
9/// and passes high-frequency components unattenuated.
10#[derive(Debug, Clone)]
11pub struct HighPass {
12    c1: f64,
13    c2: f64,
14    c3: f64,
15    price_history: [f64; 2],
16    hp_history: [f64; 2],
17    count: usize,
18}
19
20impl HighPass {
21    pub fn new(period: usize) -> Self {
22        let period_f = period as f64;
23        let a1 = (-1.414 * PI / period_f).exp();
24        let c2 = 2.0 * a1 * (1.414 * PI / period_f).cos();
25        let c3 = -a1 * a1;
26        let c1 = (1.0 + c2 - c3) / 4.0;
27        Self {
28            c1,
29            c2,
30            c3,
31            price_history: [0.0; 2],
32            hp_history: [0.0; 2],
33            count: 0,
34        }
35    }
36}
37
38impl Next<f64> for HighPass {
39    type Output = f64;
40
41    fn next(&mut self, input: f64) -> Self::Output {
42        self.count += 1;
43        let res = if self.count < 4 {
44            0.0
45        } else {
46            self.c1 * (input - 2.0 * self.price_history[0] + self.price_history[1])
47                + self.c2 * self.hp_history[0]
48                + self.c3 * self.hp_history[1]
49        };
50
51        self.hp_history[1] = self.hp_history[0];
52        self.hp_history[0] = res;
53        self.price_history[1] = self.price_history[0];
54        self.price_history[0] = input;
55        res
56    }
57}
58
59pub const HIGH_PASS_METADATA: IndicatorMetadata = IndicatorMetadata {
60    name: "HighPass",
61    description: "A second-order High Pass filter that rejects low-frequency components.",
62    params: &[ParamDef {
63        name: "period",
64        default: "20",
65        description: "Critical period (wavelength)",
66    }],
67    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/implemented/UltimateSmoother.pdf",
68    formula_latex: r#"
69\[
70a_1 = \exp\left(-\frac{1.414\pi}{Period}\right)
71\]
72\[
73c_2 = 2a_1 \cos\left(\frac{1.414\pi}{Period}\right)
74\]
75\[
76c_3 = -a_1^2
77\]
78\[
79c_1 = (1 + c_2 - c_3) / 4
80\]
81\[
82HP = c_1 (Price - 2 Price_{t-1} + Price_{t-2}) + c_2 HP_{t-1} + c_3 HP_{t-2}
83\]
84"#,
85    gold_standard_file: "high_pass.json",
86    category: "Ehlers DSP",
87};
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::traits::Next;
93    use proptest::prelude::*;
94
95    #[test]
96    fn test_high_pass_basic() {
97        let mut hp = HighPass::new(20);
98        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
99        for input in inputs {
100            let res = hp.next(input);
101            println!("Input: {}, Output: {}", input, res);
102            assert!(!res.is_nan());
103        }
104    }
105
106    proptest! {
107        #[test]
108        fn test_high_pass_parity(
109            inputs in prop::collection::vec(1.0..100.0, 10..100),
110        ) {
111            let period = 20;
112            let mut hp = HighPass::new(period);
113            let streaming_results: Vec<f64> = inputs.iter().map(|&x| hp.next(x)).collect();
114
115            // Batch implementation
116            let mut batch_results = Vec::with_capacity(inputs.len());
117            let period_f = period as f64;
118            let a1 = (-1.414 * PI / period_f).exp();
119            let c2 = 2.0 * a1 * (1.414 * PI / period_f).cos();
120            let c3 = -a1 * a1;
121            let c1 = (1.0 + c2 - c3) / 4.0;
122
123            let mut hp_hist = [0.0; 2];
124            let mut price_hist = [0.0; 2];
125
126            for (i, &input) in inputs.iter().enumerate() {
127                let bar = i + 1;
128                let res = if bar < 4 {
129                    0.0
130                } else {
131                    c1 * (input - 2.0 * price_hist[0] + price_hist[1]) + c2 * hp_hist[0] + c3 * hp_hist[1]
132                };
133                hp_hist[1] = hp_hist[0];
134                hp_hist[0] = res;
135                price_hist[1] = price_hist[0];
136                price_hist[0] = input;
137                batch_results.push(res);
138            }
139
140            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
141                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
142            }
143        }
144    }
145}