egui_charts/studies/builtin/
ehlers_fisher.rs1use crate::model::Bar;
2use crate::studies::palette;
3use crate::studies::stepper::Stepper;
4use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
5use egui::Color32;
6
7#[derive(Clone)]
14pub struct EhlersFisher {
15 period: usize,
17 values: IndicatorValues,
18 stepper: Stepper<(std::collections::VecDeque<f64>, f64, f64, bool)>,
19 colors: Vec<Color32>,
21 visible: bool,
22}
23
24impl EhlersFisher {
25 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, palette::ORANGE, ],
35 visible: true,
36 }
37 }
38
39 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 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}