Skip to main content

quantwave_core/indicators/
fisher_high_pass.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::indicators::high_pass::HighPass;
4use std::collections::VecDeque;
5
6/// Fisher HighPass Indicator
7///
8/// Based on John Ehlers' "Inferring Trading Strategies from Probability Distribution Functions".
9/// Applies a HighPass filter, normalizes the result to [-1, 1], smooths it with a 3-tap FIR,
10/// and then applies the Fisher Transform.
11#[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 { high = v; }
58            if v < low { low = v; }
59        }
60
61        let normalized = if high != low {
62            2.0 * (hp_val - low) / (high - low) - 1.0
63        } else {
64            0.0
65        };
66
67        // 3-tap FIR smoothing: (N + N[1] + N[2]) / 3
68        let smoothed = (normalized + self.smooth_history[0] + self.smooth_history[1]) / 3.0;
69        
70        self.smooth_history[1] = self.smooth_history[0];
71        self.smooth_history[0] = normalized;
72
73        // Fisher Transform
74        // y = 0.5 * ln((1+x)/(1-x))
75        // Clip to avoid log(0)
76        let x = smoothed.clamp(-0.999, 0.999);
77        0.5 * ((1.0 + x) / (1.0 - x)).ln()
78    }
79}
80
81pub const FISHER_HIGH_PASS_METADATA: IndicatorMetadata = IndicatorMetadata {
82    name: "FisherHighPass",
83    description: "Fisher Transform applied to normalized HighPass filtered prices.",
84    params: &[
85        ParamDef {
86            name: "hp_len",
87            default: "20",
88            description: "HighPass filter length",
89        },
90        ParamDef {
91            name: "norm_len",
92            default: "20",
93            description: "Normalization lookback period",
94        },
95    ],
96    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/InferringTradingStrategies.pdf",
97    formula_latex: r#"
98\[
99HP = \text{HighPass}(Price, hp\_len)
100\]
101\[
102N = 2 \cdot \frac{HP - Low(HP, norm\_len)}{High(HP, norm\_len) - Low(HP, norm\_len)} - 1
103\]
104\[
105S = \frac{N + N_{t-1} + N_{t-2}}{3}
106\]
107\[
108Fisher = 0.5 \cdot \ln\left(\frac{1+S}{1-S}\right)
109\]
110"#,
111    gold_standard_file: "fisher_high_pass.json",
112    category: "Ehlers DSP",
113};
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::traits::Next;
119    use proptest::prelude::*;
120
121    #[test]
122    fn test_fisher_hp_basic() {
123        let mut fhp = FisherHighPass::new(20, 20);
124        for i in 0..100 {
125            let val = fhp.next(100.0 + (i as f64 * 0.1).sin());
126            assert!(!val.is_nan());
127        }
128    }
129
130    proptest! {
131        #[test]
132        fn test_fisher_hp_parity(
133            inputs in prop::collection::vec(1.0..100.0, 100..200),
134        ) {
135            let hp_len = 20;
136            let norm_len = 20;
137            let mut fhp = FisherHighPass::new(hp_len, norm_len);
138            let streaming_results: Vec<f64> = inputs.iter().map(|&x| fhp.next(x)).collect();
139
140            // Batch implementation
141            let mut batch_results = Vec::with_capacity(inputs.len());
142            let mut hp = HighPass::new(hp_len);
143            let hp_vals: Vec<f64> = inputs.iter().map(|&x| hp.next(x)).collect();
144
145            let mut norm_vals = Vec::new();
146            for i in 0..hp_vals.len() {
147                let start = if i >= norm_len - 1 { i + 1 - norm_len } else { 0 };
148                let window = &hp_vals[start..i + 1];
149                
150                if window.len() < norm_len {
151                    batch_results.push(0.0);
152                    norm_vals.push(0.0);
153                    continue;
154                }
155
156                let mut high = f64::MIN;
157                let mut low = f64::MAX;
158                for &v in window {
159                    if v > high { high = v; }
160                    if v < low { low = v; }
161                }
162
163                let n = if high != low {
164                    2.0 * (hp_vals[i] - low) / (high - low) - 1.0
165                } else {
166                    0.0
167                };
168                norm_vals.push(n);
169
170                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;
171                let x = s.clamp(-0.999, 0.999);
172                batch_results.push(0.5 * ((1.0 + x) / (1.0 - x)).ln());
173            }
174
175            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
176                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
177            }
178        }
179    }
180}