Skip to main content

egui_charts/studies/builtin/
ehlers_fisher.rs

1use crate::model::Bar;
2use crate::studies::palette;
3use crate::studies::stepper::Stepper;
4use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
5use egui::Color32;
6
7/// Ehlers Fisher Transform (John Ehlers version)
8/// Converts prices into a Gaussian normal distribution
9/// More responsive than standard Fisher Transform
10///
11/// Related: [`FisherTransform`](super::FisherTransform) clamps the smoothed
12/// value instead of the normalized price.
13#[derive(Clone)]
14pub struct EhlersFisher {
15    /// Lookback period (typically 10)
16    period: usize,
17    values: IndicatorValues,
18    stepper: Stepper<(std::collections::VecDeque<f64>, f64, f64, bool)>,
19    /// Colors: [Fisher, Trigger]
20    colors: Vec<Color32>,
21    visible: bool,
22}
23
24impl EhlersFisher {
25    /// Create with a lookback of `period` bars, clamped to at least 1.
26    pub fn new(period: usize) -> Self {
27        Self {
28            period: period.max(1),
29            values: IndicatorValues::new(),
30            stepper: Stepper::default(),
31            colors: vec![
32                palette::BLUE,   // Blue - Fisher
33                palette::ORANGE, // Orange - Trigger
34            ],
35            visible: true,
36        }
37    }
38
39    /// Set plot colors in the order returned by [`Indicator::line_names`].
40    pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
41        self.colors = colors;
42        self
43    }
44}
45
46impl Default for EhlersFisher {
47    fn default() -> Self {
48        Self::new(10)
49    }
50}
51
52impl Indicator for EhlersFisher {
53    fn source(&self) -> Option<crate::studies::StudySource> {
54        Some(crate::studies::StudySource::Price(
55            crate::model::PriceSource::HL2,
56        ))
57    }
58
59    crate::studies::inputs::study_inputs!(self;
60        period: length("Period", 1, usize::MAX)
61    ; {});
62
63    fn name(&self) -> &str {
64        "Ehlers Fisher"
65    }
66
67    fn desc(&self) -> &str {
68        "Ehlers Fisher Transform - Enhanced price distribution"
69    }
70
71    fn calculate(&mut self, data: &[Bar]) {
72        self.update(data, 0);
73    }
74
75    fn update(&mut self, data: &[Bar], from: usize) {
76        self.stepper
77            .run(data, from, &mut self.values, |state, bar| {
78                if !bar.high.is_finite() || !bar.low.is_finite() {
79                    return IndicatorValue::None;
80                }
81                let price = (bar.high + bar.low) / 2.0;
82                state.0.push_back(price);
83                if state.0.len() > self.period {
84                    state.0.pop_front();
85                }
86                if state.0.len() < self.period {
87                    return IndicatorValue::None;
88                }
89                let high = state.0.iter().copied().fold(f64::NEG_INFINITY, f64::max);
90                let low = state.0.iter().copied().fold(f64::INFINITY, f64::min);
91                let range = high - low;
92                let normalized = if range != 0.0 {
93                    2.0 * (price - low) / range - 1.0
94                } else {
95                    0.0
96                };
97                let value = 0.33 * normalized.clamp(-0.999, 0.999) + 0.67 * state.1;
98                let signal = if state.3 { state.2 } else { f64::NAN };
99                state.2 = 0.5 * ((1.0 + value) / (1.0 - value)).ln() + 0.5 * state.2;
100                state.1 = value;
101                state.3 = true;
102                if state.2.is_nan() {
103                    IndicatorValue::None
104                } else {
105                    IndicatorValue::Multiple(vec![state.2, signal])
106                }
107            });
108    }
109
110    fn is_incremental(&self) -> bool {
111        true
112    }
113
114    fn values(&self) -> &IndicatorValues {
115        &self.values
116    }
117
118    fn colors(&self) -> Vec<Color32> {
119        self.colors.clone()
120    }
121
122    fn set_colors(&mut self, colors: Vec<Color32>) {
123        if colors.len() >= 2 {
124            self.colors = colors;
125        }
126    }
127
128    fn is_overlay(&self) -> bool {
129        false
130    }
131
132    fn is_visible(&self) -> bool {
133        self.visible
134    }
135
136    fn set_visible(&mut self, visible: bool) {
137        self.visible = visible;
138    }
139
140    fn clone_box(&self) -> Box<dyn Indicator> {
141        Box::new(self.clone())
142    }
143
144    fn line_names(&self) -> Vec<String> {
145        vec![format!("Fisher({})", self.period), "Trigger".to_string()]
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use crate::studies::test_support::make_range_bar as make_bar;
153
154    #[test]
155    fn test_ehlers_fisher_calculation() {
156        let mut ef = EhlersFisher::new(5);
157
158        let data = vec![
159            make_bar(102.0, 98.0, 100.0),
160            make_bar(104.0, 99.0, 103.0),
161            make_bar(106.0, 101.0, 105.0),
162            make_bar(108.0, 103.0, 107.0),
163            make_bar(110.0, 105.0, 109.0),
164            make_bar(112.0, 107.0, 111.0),
165            make_bar(114.0, 109.0, 113.0),
166        ];
167
168        ef.calculate(&data);
169
170        assert_eq!(ef.values.len(), 7);
171
172        // Should have Fisher values
173        let valid_cnt = ef
174            .values
175            .to_rows()
176            .iter()
177            .filter(|v| !matches!(v, IndicatorValue::None))
178            .count();
179
180        assert!(valid_cnt > 0);
181    }
182}