wickra_core/indicators/
fisher_rsi.rs1use crate::error::Result;
4use crate::indicators::rsi::Rsi;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
38pub struct FisherRsi {
39 period: usize,
40 rsi: Rsi,
41}
42
43impl FisherRsi {
44 pub fn new(period: usize) -> Result<Self> {
50 Ok(Self {
51 period,
52 rsi: Rsi::new(period)?,
53 })
54 }
55
56 pub const fn period(&self) -> usize {
58 self.period
59 }
60}
61
62impl Indicator for FisherRsi {
63 type Input = f64;
64 type Output = f64;
65
66 #[inline]
67 fn update(&mut self, input: f64) -> Option<f64> {
68 let rsi = self.rsi.update(input)?;
69 let x = ((rsi - 50.0) / 50.0).clamp(-0.999, 0.999);
70 Some(0.5 * ((1.0 + x) / (1.0 - x)).ln())
71 }
72
73 fn reset(&mut self) {
74 self.rsi.reset();
75 }
76
77 #[inline]
78 fn warmup_period(&self) -> usize {
79 self.rsi.warmup_period()
80 }
81
82 #[inline]
83 fn is_ready(&self) -> bool {
84 self.rsi.is_ready()
85 }
86
87 #[inline]
88 fn name(&self) -> &'static str {
89 "FisherRSI"
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::traits::BatchExt;
97 use approx::assert_relative_eq;
98
99 #[test]
100 fn rejects_zero_period() {
101 assert!(FisherRsi::new(0).is_err());
102 }
103
104 #[test]
107 fn accessors_and_metadata() {
108 let f = FisherRsi::new(9).unwrap();
109 assert_eq!(f.period(), 9);
110 assert_eq!(f.warmup_period(), 10);
112 assert_eq!(f.name(), "FisherRSI");
113 }
114
115 #[test]
116 fn warmup_matches_rsi() {
117 let mut f = FisherRsi::new(3).unwrap();
118 assert_eq!(f.update(1.0), None);
120 assert_eq!(f.update(2.0), None);
121 assert_eq!(f.update(3.0), None);
122 assert!(f.update(4.0).is_some());
123 }
124
125 #[test]
126 fn matches_fisher_of_rsi() {
127 let prices: Vec<f64> = (0..60)
129 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 8.0)
130 .collect();
131 let mut fr = FisherRsi::new(9).unwrap();
132 let mut rsi = Rsi::new(9).unwrap();
133 for (i, &p) in prices.iter().enumerate() {
134 let got = fr.update(p);
135 let want = rsi.update(p).map(|r| {
136 let x = ((r - 50.0) / 50.0).clamp(-0.999, 0.999);
137 0.5 * ((1.0 + x) / (1.0 - x)).ln()
138 });
139 assert_eq!(got.is_some(), want.is_some(), "readiness mismatch at {i}");
140 if let (Some(a), Some(b)) = (got, want) {
141 assert_relative_eq!(a, b, epsilon = 1e-12);
142 }
143 }
144 }
145
146 #[test]
147 fn strong_uptrend_is_positive() {
148 let prices: Vec<f64> = (1..=40).map(f64::from).collect();
150 let mut f = FisherRsi::new(9).unwrap();
151 let last = f.batch(&prices).into_iter().flatten().last().unwrap();
152 assert!(
153 last > 1.0,
154 "strong uptrend should give a large positive value, got {last}"
155 );
156 }
157
158 #[test]
159 fn clamp_keeps_output_finite_at_extremes() {
160 let prices: Vec<f64> = (1..=30).map(f64::from).collect();
162 let mut f = FisherRsi::new(5).unwrap();
163 for v in f.batch(&prices).into_iter().flatten() {
164 assert!(v.is_finite(), "Fisher RSI must stay finite, got {v}");
165 }
166 }
167
168 #[test]
169 fn reset_clears_state() {
170 let mut f = FisherRsi::new(5).unwrap();
171 f.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
172 assert!(f.is_ready());
173 f.reset();
174 assert!(!f.is_ready());
175 assert_eq!(f.update(1.0), None);
176 }
177
178 #[test]
179 fn batch_equals_streaming() {
180 let prices: Vec<f64> = (1..=40)
181 .map(|i| 50.0 + (f64::from(i) * 0.5).sin() * 10.0)
182 .collect();
183 let mut a = FisherRsi::new(9).unwrap();
184 let mut b = FisherRsi::new(9).unwrap();
185 assert_eq!(
186 a.batch(&prices),
187 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
188 );
189 }
190}