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