Skip to main content

quantwave_core/indicators/
fisher.rs

1use crate::indicators::metadata::IndicatorMetadata;
2use crate::traits::Next;
3
4/// Fisher Transform
5///
6/// Based on John Ehlers' "Using The Fisher Transform".
7/// The Fisher Transform changes the Probability Density Function (PDF) of any
8/// waveform so that the transformed output has an approximately Gaussian PDF.
9/// This accentuates the largest deviations from the mean, providing sharp
10/// and easy to identify turning points.
11#[derive(Debug, Clone, Default)]
12pub struct FisherTransform;
13
14impl FisherTransform {
15    pub fn new() -> Self {
16        Self
17    }
18}
19
20impl Next<f64> for FisherTransform {
21    type Output = f64;
22
23    fn next(&mut self, input: f64) -> Self::Output {
24        // y = 0.5 * ln((1 + x) / (1 - x))
25        // This is exactly atanh(x)
26        // input must be in range (-1, 1)
27        let x = input.clamp(-0.999, 0.999);
28        0.5 * ((1.0 + x) / (1.0 - x)).ln()
29    }
30}
31
32pub const FISHER_METADATA: IndicatorMetadata = IndicatorMetadata {
33    name: "Fisher Transform",
34    description: "Converts inputs to a nearly Gaussian probability distribution, creating sharp peaks at turning points.",
35    usage: "Apply to normalized prices or oscillators to sharpen turning-point signals. The near-Gaussian output makes extreme values statistically significant and easy to trade.",
36    keywords: &["oscillator", "ehlers", "normalization", "momentum"],
37    ehlers_summary: "Ehlers introduces the Fisher Transform in Cybernetic Analysis (2004) to convert any bounded indicator into a Gaussian normal distribution. Values beyond ±1.5 signal statistically significant price extremes, sharper than raw oscillators.",
38    params: &[],
39    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/UsingTheFisherTransform.pdf",
40    formula_latex: r#"
41\[
42Fish(x) = 0.5 \times \ln\left(\frac{1 + x}{1 - x}\right) = \text{atanh}(x)
43\]
44"#,
45    gold_standard_file: "fisher.json",
46    category: "Ehlers DSP",
47};
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use crate::traits::Next;
53    use proptest::prelude::*;
54
55    #[test]
56    fn test_fisher_basic() {
57        let mut fish = FisherTransform::new();
58        // Values close to 1 should be large positive
59        assert!(fish.next(0.9) > 1.0);
60        // Values close to -1 should be large negative
61        assert!(fish.next(-0.9) < -1.0);
62        // Value 0 should be 0
63        approx::assert_relative_eq!(fish.next(0.0), 0.0, epsilon = 1e-6);
64    }
65
66    proptest! {
67        #[test]
68        fn test_fisher_parity(input in prop::collection::vec(-0.99..0.99, 1..100)) {
69            let mut fish = FisherTransform::new();
70            for &val in &input {
71                let s = fish.next(val);
72                let b = 0.5 * ((1.0 + val) / (1.0 - val)).ln();
73                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
74            }
75        }
76    }
77}