quantwave_core/indicators/
fisher_high_pass.rs1use crate::indicators::high_pass::HighPass;
2use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
3use crate::traits::Next;
4use crate::utils::RingBuffer as VecDeque;
5
6#[derive(Debug, Clone)]
12pub struct FisherHighPass {
13 hp: HighPass,
14 period: usize,
15 hp_window: VecDeque<f64>,
16 smooth_history: [f64; 2],
17 count: usize,
18}
19
20impl FisherHighPass {
21 pub fn new(hp_len: usize, norm_len: usize) -> Self {
22 Self {
23 hp: HighPass::new(hp_len),
24 period: norm_len,
25 hp_window: VecDeque::with_capacity(norm_len),
26 smooth_history: [0.0; 2],
27 count: 0,
28 }
29 }
30}
31
32impl Default for FisherHighPass {
33 fn default() -> Self {
34 Self::new(20, 20)
35 }
36}
37
38impl Next<f64> for FisherHighPass {
39 type Output = f64;
40
41 fn next(&mut self, input: f64) -> Self::Output {
42 self.count += 1;
43 let hp_val = self.hp.next(input);
44
45 self.hp_window.push_front(hp_val);
46 if self.hp_window.len() > self.period {
47 self.hp_window.pop_back();
48 }
49
50 if self.hp_window.len() < self.period {
51 return 0.0;
52 }
53
54 let mut high = f64::MIN;
55 let mut low = f64::MAX;
56 for &v in &self.hp_window {
57 if v > high {
58 high = v;
59 }
60 if v < low {
61 low = v;
62 }
63 }
64
65 let normalized = if high != low {
66 2.0 * (hp_val - low) / (high - low) - 1.0
67 } else {
68 0.0
69 };
70
71 let smoothed = (normalized + self.smooth_history[0] + self.smooth_history[1]) / 3.0;
73
74 self.smooth_history[1] = self.smooth_history[0];
75 self.smooth_history[0] = normalized;
76
77 let x = smoothed.clamp(-0.999, 0.999);
81 0.5 * ((1.0 + x) / (1.0 - x)).ln()
82 }
83}
84
85pub const FISHER_HIGH_PASS_METADATA: IndicatorMetadata = IndicatorMetadata {
86 name: "FisherHighPass",
87 description: "Fisher Transform applied to normalized HighPass filtered prices.",
88 usage: "Use to isolate high-frequency momentum from the cyclical component of price after trend removal. Provides a purer momentum signal than standard Fisher Transform applied to raw price.",
89 keywords: &["oscillator", "ehlers", "dsp", "high-pass", "momentum"],
90 ehlers_summary: "FisherHighPass applies the Fisher Transform to the high-pass filtered price rather than raw price. By first removing the low-frequency trend component with a high-pass filter, the resulting Fisher output captures only the cycle-domain momentum, producing an oscillator that is unaffected by the prevailing trend direction.",
91 params: &[
92 ParamDef {
93 name: "hp_len",
94 default: "20",
95 description: "HighPass filter length",
96 },
97 ParamDef {
98 name: "norm_len",
99 default: "20",
100 description: "Normalization lookback period",
101 },
102 ],
103 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/InferringTradingStrategies.pdf",
104 formula_latex: r#"
105\[
106HP = \text{HighPass}(Price, hp\_len)
107\]
108\[
109N = 2 \cdot \frac{HP - Low(HP, norm\_len)}{High(HP, norm\_len) - Low(HP, norm\_len)} - 1
110\]
111\[
112S = \frac{N + N_{t-1} + N_{t-2}}{3}
113\]
114\[
115Fisher = 0.5 \cdot \ln\left(\frac{1+S}{1-S}\right)
116\]
117"#,
118 gold_standard_file: "fisher_high_pass.json",
119 category: "Ehlers DSP",
120};
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::traits::Next;
126 use proptest::prelude::*;
127
128 #[test]
129 fn test_fisher_hp_basic() {
130 let mut fhp = FisherHighPass::new(20, 20);
131 for i in 0..100 {
132 let val = fhp.next(100.0 + (i as f64 * 0.1).sin());
133 assert!(!val.is_nan());
134 }
135 }
136
137 proptest! {
138 #[test]
139 fn test_fisher_hp_parity(
140 inputs in prop::collection::vec(1.0..100.0, 100..200),
141 ) {
142 let hp_len = 20;
143 let norm_len = 20;
144 let mut fhp = FisherHighPass::new(hp_len, norm_len);
145 let streaming_results: Vec<f64> = inputs.iter().map(|&x| fhp.next(x)).collect();
146
147 let mut batch_results = Vec::with_capacity(inputs.len());
149 let mut hp = HighPass::new(hp_len);
150 let hp_vals: Vec<f64> = inputs.iter().map(|&x| hp.next(x)).collect();
151
152 let mut norm_vals = Vec::new();
153 for i in 0..hp_vals.len() {
154 let start = if i >= norm_len - 1 { i + 1 - norm_len } else { 0 };
155 let window = &hp_vals[start..i + 1];
156
157 if window.len() < norm_len {
158 batch_results.push(0.0);
159 norm_vals.push(0.0);
160 continue;
161 }
162
163 let mut high = f64::MIN;
164 let mut low = f64::MAX;
165 for &v in window {
166 if v > high { high = v; }
167 if v < low { low = v; }
168 }
169
170 let n = if high != low {
171 2.0 * (hp_vals[i] - low) / (high - low) - 1.0
172 } else {
173 0.0
174 };
175 norm_vals.push(n);
176
177 let s = (norm_vals[i] + (if i > 0 { norm_vals[i-1] } else { 0.0 }) + (if i > 1 { norm_vals[i-2] } else { 0.0 })) / 3.0;
178 let x = s.clamp(-0.999, 0.999);
179 batch_results.push(0.5 * ((1.0 + x) / (1.0 - x)).ln());
180 }
181
182 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
183 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
184 }
185 }
186 }
187}