1use 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)]
11pub struct FisherTransform {
12 period: usize,
14 values: IndicatorValues,
15 stepper: Stepper<(std::collections::VecDeque<f64>, f64, f64, bool)>,
16 colors: Vec<Color32>,
18 visible: bool,
19}
20
21impl FisherTransform {
22 pub fn new(period: usize) -> Self {
24 Self {
25 period: period.max(1),
26 values: IndicatorValues::new(),
27 stepper: Stepper::default(),
28 colors: vec![
29 palette::BLUE, palette::ORANGE, ],
32 visible: true,
33 }
34 }
35
36 pub fn with_colors(mut self, colors: Vec<Color32>) -> Self {
38 self.colors = colors;
39 self
40 }
41}
42
43impl Default for FisherTransform {
45 fn default() -> Self {
46 Self::new(9)
47 }
48}
49
50impl Indicator for FisherTransform {
51 fn source(&self) -> Option<crate::studies::StudySource> {
52 Some(crate::studies::StudySource::Price(
53 crate::model::PriceSource::HL2,
54 ))
55 }
56
57 crate::studies::inputs::study_inputs!(self;
58 period: length("Period", 1, usize::MAX)
59 ; {});
60
61 fn name(&self) -> &str {
62 "Fisher Transform"
63 }
64
65 fn desc(&self) -> &str {
66 "Fisher Transform - Identifies price reversals with Gaussian distribution"
67 }
68
69 fn calculate(&mut self, data: &[Bar]) {
70 self.update(data, 0);
71 }
72
73 fn update(&mut self, data: &[Bar], from: usize) {
74 self.stepper
75 .run(data, from, &mut self.values, |state, bar| {
76 if !bar.high.is_finite() || !bar.low.is_finite() {
77 return IndicatorValue::None;
78 }
79 let price = (bar.high + bar.low) / 2.0;
80 state.0.push_back(price);
81 if state.0.len() > self.period {
82 state.0.pop_front();
83 }
84 if state.0.len() < self.period {
85 return IndicatorValue::None;
86 }
87 let high = state.0.iter().copied().fold(f64::NEG_INFINITY, f64::max);
88 let low = state.0.iter().copied().fold(f64::INFINITY, f64::min);
89 let range = high - low;
90 let normalized = if range > 0.0 {
91 2.0 * ((price - low) / range - 0.5)
92 } else {
93 0.0
94 };
95 let mut value = 0.33 * normalized + 0.67 * state.1;
96 if value > 0.99 {
97 value = 0.999;
98 } else if value < -0.99 {
99 value = -0.999;
100 }
101 let signal = if state.3 { state.2 } else { f64::NAN };
102 state.2 = 0.5 * ((1.0 + value) / (1.0 - value)).ln() + 0.5 * state.2;
103 state.1 = value;
104 state.3 = true;
105 if state.2.is_nan() {
106 IndicatorValue::None
107 } else {
108 IndicatorValue::Multiple(vec![state.2, signal])
109 }
110 });
111 }
112
113 fn is_incremental(&self) -> bool {
114 true
115 }
116
117 fn values(&self) -> &IndicatorValues {
118 &self.values
119 }
120
121 fn colors(&self) -> Vec<Color32> {
122 self.colors.clone()
123 }
124
125 fn set_colors(&mut self, colors: Vec<Color32>) {
126 if colors.len() >= 2 {
127 self.colors = colors;
128 }
129 }
130
131 fn is_overlay(&self) -> bool {
132 false }
134
135 fn is_visible(&self) -> bool {
136 self.visible
137 }
138
139 fn set_visible(&mut self, visible: bool) {
140 self.visible = visible;
141 }
142
143 fn clone_box(&self) -> Box<dyn Indicator> {
144 Box::new(self.clone())
145 }
146
147 fn line_names(&self) -> Vec<String> {
148 vec![format!("Fisher({})", self.period), "Signal".to_string()]
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::studies::test_support::make_range_bar as make_bar;
156
157 #[allow(clippy::type_complexity)]
162 const REFERENCE: &[(f64, f64, Option<(f64, f64)>)] = &[
163 (101.0, 99.0, None),
164 (102.0, 100.0, None),
165 (103.0, 101.0, None),
166 (104.0, 102.0, None),
167 (105.0, 103.0, Some((0.342828254415394, f64::NAN))),
168 (106.0, 104.0, Some((0.791373872129106, 0.342828254415394))),
169 (107.0, 105.0, Some((1.26149294935091, 0.791373872129106))),
170 (108.0, 106.0, Some((1.7251749834508, 1.26149294935091))),
171 (109.0, 107.0, Some((2.17540858067979, 1.7251749834508))),
172 (110.0, 108.0, Some((2.61256860142716, 2.17540858067979))),
173 (111.0, 109.0, Some((3.03914327264257, 2.61256860142716))),
174 (112.0, 110.0, Some((3.45779932767405, 3.03914327264257))),
175 (113.0, 111.0, Some((3.87077398411298, 3.45779932767405))),
176 (114.0, 112.0, Some((4.27977042664787, 3.87077398411298))),
177 (114.0, 112.0, Some((4.6860228060836, 4.27977042664787))),
178 (114.0, 112.0, Some((6.143212570292, 4.6860228060836))),
179 (114.0, 112.0, Some((6.8718074523962, 6.143212570292))),
180 (114.0, 112.0, Some((4.2454320902449, 6.8718074523962))),
181 (114.0, 112.0, Some((2.60547582289472, 4.2454320902449))),
182 (114.0, 112.0, Some((1.61276554585803, 2.60547582289472))),
183 (112.0, 110.0, Some((0.676974906197813, 1.61276554585803))),
184 (110.0, 108.0, Some((-0.104626787889589, 0.676974906197813))),
185 (108.0, 106.0, Some((-0.759435607263236, -0.104626787889589))),
186 (106.0, 104.0, Some((-1.32566396088824, -0.759435607263236))),
187 (104.0, 102.0, Some((-1.83329744102957, -1.32566396088824))),
188 ];
189
190 #[test]
191 fn matches_published_reference() {
192 let data: Vec<Bar> = REFERENCE
193 .iter()
194 .map(|&(h, l, _)| make_bar(h, l, (h + l) / 2.0))
195 .collect();
196 let mut ft = FisherTransform::new(5);
197 ft.calculate(&data);
198
199 assert_eq!(ft.values.len(), REFERENCE.len());
200 for (i, (value, &(_, _, expected))) in ft.values.to_rows().iter().zip(REFERENCE).enumerate()
201 {
202 let actual = match value {
203 IndicatorValue::Multiple(v) => Some((v[0], v[1])),
204 _ => None,
205 };
206 let near = |a: f64, b: f64| (a - b).abs() < 1e-9 || a.is_nan() && b.is_nan();
207 match (actual, expected) {
208 (None, None) => {}
209 (Some((f, t)), Some((ef, et))) => assert!(
210 near(f, ef) && near(t, et),
211 "bar {i}: got ({f}, {t}), expected ({ef}, {et})"
212 ),
213 _ => panic!("bar {i}: got {actual:?}, expected {expected:?}"),
214 }
215 }
216 }
217}