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 fn update(&mut self, input: f64) -> Option<f64> {
67 let rsi = self.rsi.update(input)?;
68 let x = ((rsi - 50.0) / 50.0).clamp(-0.999, 0.999);
69 Some(0.5 * ((1.0 + x) / (1.0 - x)).ln())
70 }
71
72 fn reset(&mut self) {
73 self.rsi.reset();
74 }
75
76 fn warmup_period(&self) -> usize {
77 self.rsi.warmup_period()
78 }
79
80 fn is_ready(&self) -> bool {
81 self.rsi.is_ready()
82 }
83
84 fn name(&self) -> &'static str {
85 "FisherRSI"
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::traits::BatchExt;
93 use approx::assert_relative_eq;
94
95 #[test]
96 fn rejects_zero_period() {
97 assert!(FisherRsi::new(0).is_err());
98 }
99
100 #[test]
103 fn accessors_and_metadata() {
104 let f = FisherRsi::new(9).unwrap();
105 assert_eq!(f.period(), 9);
106 assert_eq!(f.warmup_period(), 10);
108 assert_eq!(f.name(), "FisherRSI");
109 }
110
111 #[test]
112 fn warmup_matches_rsi() {
113 let mut f = FisherRsi::new(3).unwrap();
114 assert_eq!(f.update(1.0), None);
116 assert_eq!(f.update(2.0), None);
117 assert_eq!(f.update(3.0), None);
118 assert!(f.update(4.0).is_some());
119 }
120
121 #[test]
122 fn matches_fisher_of_rsi() {
123 let prices: Vec<f64> = (0..60)
125 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 8.0)
126 .collect();
127 let mut fr = FisherRsi::new(9).unwrap();
128 let mut rsi = Rsi::new(9).unwrap();
129 for (i, &p) in prices.iter().enumerate() {
130 let got = fr.update(p);
131 let want = rsi.update(p).map(|r| {
132 let x = ((r - 50.0) / 50.0).clamp(-0.999, 0.999);
133 0.5 * ((1.0 + x) / (1.0 - x)).ln()
134 });
135 assert_eq!(got.is_some(), want.is_some(), "readiness mismatch at {i}");
136 if let (Some(a), Some(b)) = (got, want) {
137 assert_relative_eq!(a, b, epsilon = 1e-12);
138 }
139 }
140 }
141
142 #[test]
143 fn strong_uptrend_is_positive() {
144 let prices: Vec<f64> = (1..=40).map(f64::from).collect();
146 let mut f = FisherRsi::new(9).unwrap();
147 let last = f.batch(&prices).into_iter().flatten().last().unwrap();
148 assert!(
149 last > 1.0,
150 "strong uptrend should give a large positive value, got {last}"
151 );
152 }
153
154 #[test]
155 fn clamp_keeps_output_finite_at_extremes() {
156 let prices: Vec<f64> = (1..=30).map(f64::from).collect();
158 let mut f = FisherRsi::new(5).unwrap();
159 for v in f.batch(&prices).into_iter().flatten() {
160 assert!(v.is_finite(), "Fisher RSI must stay finite, got {v}");
161 }
162 }
163
164 #[test]
165 fn reset_clears_state() {
166 let mut f = FisherRsi::new(5).unwrap();
167 f.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
168 assert!(f.is_ready());
169 f.reset();
170 assert!(!f.is_ready());
171 assert_eq!(f.update(1.0), None);
172 }
173
174 #[test]
175 fn batch_equals_streaming() {
176 let prices: Vec<f64> = (1..=40)
177 .map(|i| 50.0 + (f64::from(i) * 0.5).sin() * 10.0)
178 .collect();
179 let mut a = FisherRsi::new(9).unwrap();
180 let mut b = FisherRsi::new(9).unwrap();
181 assert_eq!(
182 a.batch(&prices),
183 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
184 );
185 }
186}