wickra_core/indicators/
fisher_transform.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
34pub struct FisherTransform {
35 period: usize,
36 window: VecDeque<f64>,
37 smoothed: f64,
38 last_fisher: Option<f64>,
39}
40
41impl FisherTransform {
42 pub fn new(period: usize) -> Result<Self> {
48 if period == 0 {
49 return Err(Error::PeriodZero);
50 }
51 if period > crate::error::MAX_PERIOD {
52 return Err(Error::InvalidPeriod {
53 message: crate::error::PERIOD_ABOVE_MAX,
54 });
55 }
56 Ok(Self {
57 period,
58 window: VecDeque::with_capacity(period),
59 smoothed: 0.0,
60 last_fisher: None,
61 })
62 }
63
64 pub const fn period(&self) -> usize {
66 self.period
67 }
68
69 pub const fn value(&self) -> Option<f64> {
71 self.last_fisher
72 }
73}
74
75impl Indicator for FisherTransform {
76 type Input = f64;
77 type Output = f64;
78
79 #[inline]
80 fn update(&mut self, input: f64) -> Option<f64> {
81 if !input.is_finite() {
82 return None;
83 }
84 if self.window.len() == self.period {
85 self.window.pop_front();
86 }
87 self.window.push_back(input);
88 if self.window.len() < self.period {
89 return None;
90 }
91 let max = self
92 .window
93 .iter()
94 .copied()
95 .fold(f64::NEG_INFINITY, f64::max);
96 let min = self.window.iter().copied().fold(f64::INFINITY, f64::min);
97 let range = max - min;
98 let raw = if range > 0.0 {
100 ((input - min) / range).mul_add(2.0, -1.0)
101 } else {
102 0.0
103 };
104 self.smoothed = 0.33f64.mul_add(raw, 0.67 * self.smoothed);
106 let clamped = self.smoothed.clamp(-0.999, 0.999);
108 let fisher = 0.5 * ((1.0 + clamped) / (1.0 - clamped)).ln();
109 self.last_fisher = Some(fisher);
110 Some(fisher)
111 }
112
113 fn reset(&mut self) {
114 self.window.clear();
115 self.smoothed = 0.0;
116 self.last_fisher = None;
117 }
118
119 #[inline]
120 fn warmup_period(&self) -> usize {
121 self.period
122 }
123
124 #[inline]
125 fn is_ready(&self) -> bool {
126 self.last_fisher.is_some()
127 }
128
129 #[inline]
130 fn name(&self) -> &'static str {
131 "FisherTransform"
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::traits::BatchExt;
139
140 #[test]
141 fn new_rejects_zero_period() {
142 assert!(matches!(FisherTransform::new(0), Err(Error::PeriodZero)));
143 }
144
145 #[test]
146 fn accessors_and_metadata() {
147 let mut ft = FisherTransform::new(10).unwrap();
148 assert_eq!(ft.period(), 10);
149 assert_eq!(ft.warmup_period(), 10);
150 assert_eq!(ft.name(), "FisherTransform");
151 assert!(ft.value().is_none());
152 for i in 1..=10 {
153 ft.update(f64::from(i));
154 }
155 assert!(ft.value().is_some());
156 assert!(ft.is_ready());
157 }
158
159 #[test]
160 fn warmup_returns_none_until_seed() {
161 let mut ft = FisherTransform::new(5).unwrap();
162 for i in 1..=4 {
163 assert_eq!(ft.update(f64::from(i)), None);
164 }
165 assert!(ft.update(5.0).is_some());
166 }
167
168 #[test]
169 fn constant_series_zero_range_yields_zero() {
170 let mut ft = FisherTransform::new(5).unwrap();
171 let out = ft.batch(&[42.0_f64; 30]);
172 for x in out.iter().skip(5).flatten() {
173 assert!(x.abs() < 1e-6, "expected near-zero, got {x}");
174 }
175 }
176
177 #[test]
178 fn batch_equals_streaming() {
179 let prices: Vec<f64> = (0..60)
180 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 8.0)
181 .collect();
182 let mut a = FisherTransform::new(10).unwrap();
183 let mut b = FisherTransform::new(10).unwrap();
184 let batch = a.batch(&prices);
185 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
186 assert_eq!(batch, streamed);
187 }
188
189 #[test]
190 fn ignores_non_finite_input() {
191 let mut ft = FisherTransform::new(5).unwrap();
192 ft.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
193 let before = ft.value();
194 assert!(before.is_some());
195 assert_eq!(ft.update(f64::NAN), None);
196 assert_eq!(ft.update(f64::INFINITY), None);
197 }
198
199 #[test]
200 fn reset_clears_state() {
201 let mut ft = FisherTransform::new(5).unwrap();
202 ft.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
203 assert!(ft.is_ready());
204 ft.reset();
205 assert!(!ft.is_ready());
206 assert_eq!(ft.update(1.0), None);
207 }
208}