wickra_core/indicators/
stoch_rsi.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8use super::Rsi;
9
10#[derive(Debug, Clone)]
40pub struct StochRsi {
41 rsi_period: usize,
42 stoch_period: usize,
43 rsi: Rsi,
44 window: VecDeque<f64>,
46 last: Option<f64>,
47}
48
49impl StochRsi {
50 pub fn new(rsi_period: usize, stoch_period: usize) -> Result<Self> {
56 if rsi_period == 0 || stoch_period == 0 {
57 return Err(Error::PeriodZero);
58 }
59 Ok(Self {
60 rsi_period,
61 stoch_period,
62 rsi: Rsi::new(rsi_period)?,
63 window: VecDeque::with_capacity(stoch_period),
64 last: None,
65 })
66 }
67
68 pub const fn periods(&self) -> (usize, usize) {
70 (self.rsi_period, self.stoch_period)
71 }
72
73 pub const fn value(&self) -> Option<f64> {
75 self.last
76 }
77}
78
79impl Indicator for StochRsi {
80 type Input = f64;
81 type Output = f64;
82
83 #[inline]
84 fn update(&mut self, input: f64) -> Option<f64> {
85 if !input.is_finite() {
86 return None;
88 }
89 let rsi_value = self.rsi.update(input)?;
90
91 if self.window.len() == self.stoch_period {
92 self.window.pop_front();
93 }
94 self.window.push_back(rsi_value);
95 if self.window.len() < self.stoch_period {
96 return None;
97 }
98
99 let max = self
100 .window
101 .iter()
102 .copied()
103 .fold(f64::NEG_INFINITY, f64::max);
104 let min = self.window.iter().copied().fold(f64::INFINITY, f64::min);
105 let range = max - min;
106 let stoch = if range == 0.0 {
107 50.0
109 } else {
110 100.0 * (rsi_value - min) / range
111 };
112 self.last = Some(stoch);
113 Some(stoch)
114 }
115
116 fn reset(&mut self) {
117 self.rsi.reset();
118 self.window.clear();
119 self.last = None;
120 }
121
122 #[inline]
123 fn warmup_period(&self) -> usize {
124 self.rsi_period + self.stoch_period
127 }
128
129 #[inline]
130 fn is_ready(&self) -> bool {
131 self.last.is_some()
132 }
133
134 #[inline]
135 fn name(&self) -> &'static str {
136 "StochRSI"
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::traits::BatchExt;
144 use approx::assert_relative_eq;
145
146 #[test]
147 fn new_rejects_zero_period() {
148 assert!(matches!(StochRsi::new(0, 14), Err(Error::PeriodZero)));
149 assert!(matches!(StochRsi::new(14, 0), Err(Error::PeriodZero)));
150 }
151
152 #[test]
156 fn accessors_and_metadata() {
157 let mut sr = StochRsi::new(14, 14).unwrap();
158 assert_eq!(sr.periods(), (14, 14));
159 assert_eq!(sr.name(), "StochRSI");
160 assert_eq!(sr.value(), None);
161 for i in 1..=sr.warmup_period() {
162 sr.update(100.0 + f64::from(u32::try_from(i).unwrap()));
163 }
164 assert!(sr.value().is_some());
165 }
166
167 #[test]
168 fn first_emission_at_warmup_period() {
169 let mut sr = StochRsi::new(5, 4).unwrap();
170 assert_eq!(sr.warmup_period(), 9);
171 let prices: Vec<f64> = (1..=40)
172 .map(|i| 100.0 + (f64::from(i) * 0.6).sin() * 8.0)
173 .collect();
174 let out = sr.batch(&prices);
175 for v in out.iter().take(8) {
176 assert!(v.is_none());
177 }
178 assert!(out[8].is_some());
179 }
180
181 #[test]
182 fn flat_rsi_window_yields_50() {
183 let mut sr = StochRsi::new(5, 4).unwrap();
186 let out = sr.batch(&[100.0; 40]);
187 for v in out.iter().skip(9).flatten() {
188 assert_relative_eq!(*v, 50.0, epsilon = 1e-12);
189 }
190 }
191
192 #[test]
193 fn pure_uptrend_yields_50() {
194 let mut sr = StochRsi::new(5, 4).unwrap();
196 let out = sr.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
197 for v in out.iter().skip(9).flatten() {
198 assert_relative_eq!(*v, 50.0, epsilon = 1e-12);
199 }
200 }
201
202 #[test]
203 fn output_stays_within_0_100() {
204 let mut sr = StochRsi::new(14, 14).unwrap();
205 let prices: Vec<f64> = (1..=200)
206 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 15.0 + (f64::from(i) * 0.07).cos() * 6.0)
207 .collect();
208 for v in sr.batch(&prices).into_iter().flatten() {
209 assert!((0.0..=100.0).contains(&v), "StochRSI out of range: {v}");
210 }
211 }
212
213 #[test]
214 fn ignores_non_finite_input() {
215 let mut sr = StochRsi::new(5, 4).unwrap();
216 let prices: Vec<f64> = (1..=40)
217 .map(|i| 100.0 + (f64::from(i) * 0.6).sin() * 8.0)
218 .collect();
219 let out = sr.batch(&prices);
220 let last = *out.last().unwrap();
221 assert!(last.is_some());
222 assert_eq!(sr.update(f64::NAN), None);
223 assert_eq!(sr.update(f64::INFINITY), None);
224 }
225
226 #[test]
227 fn reset_clears_state() {
228 let mut sr = StochRsi::new(5, 4).unwrap();
229 sr.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
230 assert!(sr.is_ready());
231 sr.reset();
232 assert!(!sr.is_ready());
233 assert_eq!(sr.update(1.0), None);
234 }
235
236 #[test]
237 fn batch_equals_streaming() {
238 let prices: Vec<f64> = (1..=120)
239 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 12.0)
240 .collect();
241 let batch = StochRsi::new(14, 14).unwrap().batch(&prices);
242 let mut b = StochRsi::new(14, 14).unwrap();
243 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
244 assert_eq!(batch, streamed);
245 }
246}