quantwave_core/indicators/
high_pass.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::f64::consts::PI;
4
5#[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 usage: "Apply to price to isolate the cyclical component by attenuating the low-frequency trend. Use as the first stage before an oscillator or spectrum analyser.",
63 keywords: &["filter", "ehlers", "dsp", "high-pass", "cycle"],
64 ehlers_summary: "Ehlers derives the one-pole high-pass filter in Cycle Analytics for Traders analogously to EMA derivation, but applied to price differences rather than levels. It removes the DC component and low-frequency trend, leaving the cyclical content for downstream analysis.",
65 params: &[ParamDef {
66 name: "period",
67 default: "20",
68 description: "Critical period (wavelength)",
69 }],
70 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/implemented/UltimateSmoother.pdf",
71 formula_latex: r#"
72\[
73a_1 = \exp\left(-\frac{1.414\pi}{Period}\right)
74\]
75\[
76c_2 = 2a_1 \cos\left(\frac{1.414\pi}{Period}\right)
77\]
78\[
79c_3 = -a_1^2
80\]
81\[
82c_1 = (1 + c_2 - c_3) / 4
83\]
84\[
85HP = c_1 (Price - 2 Price_{t-1} + Price_{t-2}) + c_2 HP_{t-1} + c_3 HP_{t-2}
86\]
87"#,
88 gold_standard_file: "high_pass.json",
89 category: "Ehlers DSP",
90};
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::traits::Next;
96 use proptest::prelude::*;
97
98 #[test]
99 fn test_high_pass_basic() {
100 let mut hp = HighPass::new(20);
101 let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
102 for input in inputs {
103 let res = hp.next(input);
104 println!("Input: {}, Output: {}", input, res);
105 assert!(!res.is_nan());
106 }
107 }
108
109 proptest! {
110 #[test]
111 fn test_high_pass_parity(
112 inputs in prop::collection::vec(1.0..100.0, 10..100),
113 ) {
114 let period = 20;
115 let mut hp = HighPass::new(period);
116 let streaming_results: Vec<f64> = inputs.iter().map(|&x| hp.next(x)).collect();
117
118 let mut batch_results = Vec::with_capacity(inputs.len());
120 let period_f = period as f64;
121 let a1 = (-1.414 * PI / period_f).exp();
122 let c2 = 2.0 * a1 * (1.414 * PI / period_f).cos();
123 let c3 = -a1 * a1;
124 let c1 = (1.0 + c2 - c3) / 4.0;
125
126 let mut hp_hist = [0.0; 2];
127 let mut price_hist = [0.0; 2];
128
129 for (i, &input) in inputs.iter().enumerate() {
130 let bar = i + 1;
131 let res = if bar < 4 {
132 0.0
133 } else {
134 c1 * (input - 2.0 * price_hist[0] + price_hist[1]) + c2 * hp_hist[0] + c3 * hp_hist[1]
135 };
136 hp_hist[1] = hp_hist[0];
137 hp_hist[0] = res;
138 price_hist[1] = price_hist[0];
139 price_hist[0] = input;
140 batch_results.push(res);
141 }
142
143 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
144 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
145 }
146 }
147 }
148}