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 fn update(&mut self, input: f64) -> Option<f64> {
84 if !input.is_finite() {
85 return self.last;
87 }
88 let rsi_value = self.rsi.update(input)?;
89
90 if self.window.len() == self.stoch_period {
91 self.window.pop_front();
92 }
93 self.window.push_back(rsi_value);
94 if self.window.len() < self.stoch_period {
95 return None;
96 }
97
98 let max = self
99 .window
100 .iter()
101 .copied()
102 .fold(f64::NEG_INFINITY, f64::max);
103 let min = self.window.iter().copied().fold(f64::INFINITY, f64::min);
104 let range = max - min;
105 let stoch = if range == 0.0 {
106 50.0
108 } else {
109 100.0 * (rsi_value - min) / range
110 };
111 self.last = Some(stoch);
112 Some(stoch)
113 }
114
115 fn reset(&mut self) {
116 self.rsi.reset();
117 self.window.clear();
118 self.last = None;
119 }
120
121 fn warmup_period(&self) -> usize {
122 self.rsi_period + self.stoch_period
125 }
126
127 fn is_ready(&self) -> bool {
128 self.last.is_some()
129 }
130
131 fn name(&self) -> &'static str {
132 "StochRSI"
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::traits::BatchExt;
140 use approx::assert_relative_eq;
141
142 #[test]
143 fn new_rejects_zero_period() {
144 assert!(matches!(StochRsi::new(0, 14), Err(Error::PeriodZero)));
145 assert!(matches!(StochRsi::new(14, 0), Err(Error::PeriodZero)));
146 }
147
148 #[test]
152 fn accessors_and_metadata() {
153 let mut sr = StochRsi::new(14, 14).unwrap();
154 assert_eq!(sr.periods(), (14, 14));
155 assert_eq!(sr.name(), "StochRSI");
156 assert_eq!(sr.value(), None);
157 for i in 1..=sr.warmup_period() {
158 sr.update(100.0 + f64::from(u32::try_from(i).unwrap()));
159 }
160 assert!(sr.value().is_some());
161 }
162
163 #[test]
164 fn first_emission_at_warmup_period() {
165 let mut sr = StochRsi::new(5, 4).unwrap();
166 assert_eq!(sr.warmup_period(), 9);
167 let prices: Vec<f64> = (1..=40)
168 .map(|i| 100.0 + (f64::from(i) * 0.6).sin() * 8.0)
169 .collect();
170 let out = sr.batch(&prices);
171 for v in out.iter().take(8) {
172 assert!(v.is_none());
173 }
174 assert!(out[8].is_some());
175 }
176
177 #[test]
178 fn flat_rsi_window_yields_50() {
179 let mut sr = StochRsi::new(5, 4).unwrap();
182 let out = sr.batch(&[100.0; 40]);
183 for v in out.iter().skip(9).flatten() {
184 assert_relative_eq!(*v, 50.0, epsilon = 1e-12);
185 }
186 }
187
188 #[test]
189 fn pure_uptrend_yields_50() {
190 let mut sr = StochRsi::new(5, 4).unwrap();
192 let out = sr.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
193 for v in out.iter().skip(9).flatten() {
194 assert_relative_eq!(*v, 50.0, epsilon = 1e-12);
195 }
196 }
197
198 #[test]
199 fn output_stays_within_0_100() {
200 let mut sr = StochRsi::new(14, 14).unwrap();
201 let prices: Vec<f64> = (1..=200)
202 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 15.0 + (f64::from(i) * 0.07).cos() * 6.0)
203 .collect();
204 for v in sr.batch(&prices).into_iter().flatten() {
205 assert!((0.0..=100.0).contains(&v), "StochRSI out of range: {v}");
206 }
207 }
208
209 #[test]
210 fn ignores_non_finite_input() {
211 let mut sr = StochRsi::new(5, 4).unwrap();
212 let prices: Vec<f64> = (1..=40)
213 .map(|i| 100.0 + (f64::from(i) * 0.6).sin() * 8.0)
214 .collect();
215 let out = sr.batch(&prices);
216 let last = *out.last().unwrap();
217 assert!(last.is_some());
218 assert_eq!(sr.update(f64::NAN), last);
219 assert_eq!(sr.update(f64::INFINITY), last);
220 }
221
222 #[test]
223 fn reset_clears_state() {
224 let mut sr = StochRsi::new(5, 4).unwrap();
225 sr.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
226 assert!(sr.is_ready());
227 sr.reset();
228 assert!(!sr.is_ready());
229 assert_eq!(sr.update(1.0), None);
230 }
231
232 #[test]
233 fn batch_equals_streaming() {
234 let prices: Vec<f64> = (1..=120)
235 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 12.0)
236 .collect();
237 let batch = StochRsi::new(14, 14).unwrap().batch(&prices);
238 let mut b = StochRsi::new(14, 14).unwrap();
239 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
240 assert_eq!(batch, streamed);
241 }
242}