1use crate::error::{Error, Result};
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone)]
26pub struct Rsi {
27 period: usize,
28 n_minus_1: f64,
30 inv_period: f64,
33 prev_close: f64,
36 has_prev: bool,
37 seed_buf_gains: Vec<f64>,
40 seed_buf_losses: Vec<f64>,
41 avg_gain: f64,
44 avg_loss: f64,
45 avgs_seeded: bool,
46 last_value: Option<f64>,
47}
48
49impl Rsi {
50 pub fn new(period: usize) -> Result<Self> {
56 if period == 0 {
57 return Err(Error::PeriodZero);
58 }
59 if period > crate::error::MAX_PERIOD {
60 return Err(Error::InvalidPeriod {
61 message: crate::error::PERIOD_ABOVE_MAX,
62 });
63 }
64 Ok(Self {
65 period,
66 n_minus_1: (period - 1) as f64,
67 inv_period: 1.0 / period as f64,
68 prev_close: 0.0,
69 has_prev: false,
70 seed_buf_gains: Vec::with_capacity(period),
71 seed_buf_losses: Vec::with_capacity(period),
72 avg_gain: 0.0,
73 avg_loss: 0.0,
74 avgs_seeded: false,
75 last_value: None,
76 })
77 }
78
79 pub const fn period(&self) -> usize {
81 self.period
82 }
83
84 pub const fn value(&self) -> Option<f64> {
86 self.last_value
87 }
88
89 pub fn batch_nan(&mut self, inputs: &[f64]) -> Vec<f64> {
102 let p = self.period;
103 let n = inputs.len();
104 if self.has_prev
105 || self.avgs_seeded
106 || !self.seed_buf_gains.is_empty()
107 || n <= p
108 || !inputs.iter().all(|x| x.is_finite())
109 {
110 return inputs
111 .iter()
112 .map(|&x| self.update(x).unwrap_or(f64::NAN))
113 .collect();
114 }
115
116 let mut out = vec![f64::NAN; p];
118 out.reserve(n - p);
119 let mut prev = inputs[0];
122 let (mut sum_gain, mut sum_loss) = (0.0_f64, 0.0_f64);
123 for &x in &inputs[1..=p] {
124 let diff = x - prev;
125 prev = x;
126 let gain = if diff > 0.0 { diff } else { 0.0 };
127 let loss = if diff < 0.0 { -diff } else { 0.0 };
128 self.seed_buf_gains.push(gain);
129 self.seed_buf_losses.push(loss);
130 sum_gain += gain;
131 sum_loss += loss;
132 }
133 let p_f64 = p as f64;
134 let mut ag = sum_gain / p_f64;
135 let mut al = sum_loss / p_f64;
136 out.push(Self::rsi_from_avgs(ag, al));
137
138 for &x in &inputs[p + 1..] {
140 let diff = x - prev;
141 prev = x;
142 let gain = if diff > 0.0 { diff } else { 0.0 };
143 let loss = if diff < 0.0 { -diff } else { 0.0 };
144 ag = ag.mul_add(self.n_minus_1, gain) * self.inv_period;
145 al = al.mul_add(self.n_minus_1, loss) * self.inv_period;
146 out.push(Self::rsi_from_avgs(ag, al));
147 }
148
149 self.prev_close = prev;
151 self.has_prev = true;
152 self.avg_gain = ag;
153 self.avg_loss = al;
154 self.avgs_seeded = true;
155 self.last_value = Some(out[n - 1]);
156 out
157 }
158
159 fn rsi_from_avgs(avg_gain: f64, avg_loss: f64) -> f64 {
160 let denom = avg_gain + avg_loss;
166 if denom == 0.0 {
167 50.0
168 } else {
169 100.0 * avg_gain / denom
170 }
171 }
172}
173
174impl Indicator for Rsi {
175 type Input = f64;
176 type Output = f64;
177
178 fn update(&mut self, input: f64) -> Option<f64> {
179 if !input.is_finite() {
180 return None;
181 }
182
183 if !self.has_prev {
184 self.prev_close = input;
185 self.has_prev = true;
186 return None;
187 }
188 let prev = self.prev_close;
189 self.prev_close = input;
190
191 let diff = input - prev;
192 let gain = if diff > 0.0 { diff } else { 0.0 };
193 let loss = if diff < 0.0 { -diff } else { 0.0 };
194
195 if self.avgs_seeded {
196 let new_ag = self.avg_gain.mul_add(self.n_minus_1, gain) * self.inv_period;
199 let new_al = self.avg_loss.mul_add(self.n_minus_1, loss) * self.inv_period;
200 self.avg_gain = new_ag;
201 self.avg_loss = new_al;
202 let v = Self::rsi_from_avgs(new_ag, new_al);
203 self.last_value = Some(v);
204 return Some(v);
205 }
206
207 self.seed_buf_gains.push(gain);
208 self.seed_buf_losses.push(loss);
209 if self.seed_buf_gains.len() == self.period {
210 let ag = self.seed_buf_gains.iter().sum::<f64>() / self.period as f64;
211 let al = self.seed_buf_losses.iter().sum::<f64>() / self.period as f64;
212 self.avg_gain = ag;
213 self.avg_loss = al;
214 self.avgs_seeded = true;
215 let v = Self::rsi_from_avgs(ag, al);
216 self.last_value = Some(v);
217 return Some(v);
218 }
219 None
220 }
221
222 fn reset(&mut self) {
223 self.prev_close = 0.0;
224 self.has_prev = false;
225 self.seed_buf_gains.clear();
226 self.seed_buf_losses.clear();
227 self.avg_gain = 0.0;
228 self.avg_loss = 0.0;
229 self.avgs_seeded = false;
230 self.last_value = None;
231 }
232
233 #[inline]
234 fn warmup_period(&self) -> usize {
235 self.period + 1
236 }
237
238 #[inline]
239 fn is_ready(&self) -> bool {
240 self.last_value.is_some()
241 }
242
243 #[inline]
244 fn name(&self) -> &'static str {
245 "RSI"
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use crate::traits::BatchExt;
253 use approx::assert_relative_eq;
254
255 fn rsi_naive(prices: &[f64], period: usize) -> Vec<Option<f64>> {
257 let n = period as f64;
258 let mut out = vec![None; prices.len()];
259 let mut gains: Vec<f64> = Vec::new();
260 let mut losses: Vec<f64> = Vec::new();
261 let mut avg_gain: Option<f64> = None;
262 let mut avg_loss: Option<f64> = None;
263 let rsi_val = |ag: f64, al: f64| -> f64 {
264 if al == 0.0 {
265 if ag == 0.0 {
266 50.0
267 } else {
268 100.0
269 }
270 } else {
271 100.0 - 100.0 / (1.0 + ag / al)
272 }
273 };
274 for i in 1..prices.len() {
275 let diff = prices[i] - prices[i - 1];
276 let gain = if diff > 0.0 { diff } else { 0.0 };
277 let loss = if diff < 0.0 { -diff } else { 0.0 };
278 if let (Some(ag), Some(al)) = (avg_gain, avg_loss) {
279 let nag = (ag * (n - 1.0) + gain) / n;
280 let nal = (al * (n - 1.0) + loss) / n;
281 avg_gain = Some(nag);
282 avg_loss = Some(nal);
283 out[i] = Some(rsi_val(nag, nal));
284 } else {
285 gains.push(gain);
286 losses.push(loss);
287 if gains.len() == period {
288 let ag = gains.iter().sum::<f64>() / n;
289 let al = losses.iter().sum::<f64>() / n;
290 avg_gain = Some(ag);
291 avg_loss = Some(al);
292 out[i] = Some(rsi_val(ag, al));
293 }
294 }
295 }
296 out
297 }
298
299 #[test]
300 fn new_rejects_zero_period() {
301 assert!(matches!(Rsi::new(0), Err(Error::PeriodZero)));
302 }
303
304 #[test]
308 fn accessors_and_metadata() {
309 let mut rsi = Rsi::new(14).unwrap();
310 assert_eq!(rsi.period(), 14);
311 assert_eq!(rsi.name(), "RSI");
312 assert_eq!(rsi.value(), None);
313 for i in 1..=15 {
314 rsi.update(100.0 + f64::from(i));
315 }
316 assert!(rsi.value().is_some());
317 }
318
319 #[test]
325 fn naive_helper_flat_series_yields_50() {
326 let ks = rsi_naive(&[42.0; 20], 5);
327 for r in ks.into_iter().skip(5) {
328 assert_eq!(r.expect("ready after period+1 inputs"), 50.0);
329 }
330 }
331
332 #[test]
338 fn naive_helper_monotone_up_yields_100() {
339 let prices: Vec<f64> = (1..=20).map(f64::from).collect();
340 let ks = rsi_naive(&prices, 5);
341 for r in ks.into_iter().skip(5) {
342 assert_eq!(r.expect("ready after period+1 inputs"), 100.0);
343 }
344 }
345
346 #[test]
347 fn warmup_period_is_period_plus_one() {
348 let rsi = Rsi::new(14).unwrap();
349 assert_eq!(rsi.warmup_period(), 15);
350 }
351
352 #[test]
353 fn first_emission_at_index_period() {
354 let prices: Vec<f64> = (1..=20).map(f64::from).collect();
356 let mut rsi = Rsi::new(14).unwrap();
357 let out = rsi.batch(&prices);
358 for x in &out[..14] {
360 assert!(x.is_none());
361 }
362 assert!(out[14].is_some());
363 }
364
365 #[test]
366 fn pure_uptrend_yields_rsi_100() {
367 let prices: Vec<f64> = (1..=20).map(f64::from).collect();
368 let mut rsi = Rsi::new(14).unwrap();
369 let out = rsi.batch(&prices);
370 for v in out.iter().filter_map(|x| x.as_ref()) {
372 assert_relative_eq!(*v, 100.0, epsilon = 1e-9);
373 }
374 }
375
376 #[test]
377 fn pure_downtrend_yields_rsi_0() {
378 let prices: Vec<f64> = (1..=20).rev().map(f64::from).collect();
379 let mut rsi = Rsi::new(14).unwrap();
380 let out = rsi.batch(&prices);
381 for v in out.iter().filter_map(|x| x.as_ref()) {
382 assert_relative_eq!(*v, 0.0, epsilon = 1e-9);
383 }
384 }
385
386 #[test]
387 fn flat_series_yields_rsi_50() {
388 let prices = [10.0_f64; 30];
389 let mut rsi = Rsi::new(14).unwrap();
390 let out = rsi.batch(&prices);
391 for v in out.iter().filter_map(|x| x.as_ref()) {
392 assert_relative_eq!(*v, 50.0, epsilon = 1e-12);
393 }
394 }
395
396 #[test]
397 fn classic_wilder_textbook_values() {
398 let prices = [
403 44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84, 46.08, 45.89, 46.03,
404 45.61, 46.28, 46.28,
405 ];
406 let mut rsi = Rsi::new(14).unwrap();
407 let out = rsi.batch(&prices);
408 let first = out[14].expect("first RSI emitted at index period");
409 assert_relative_eq!(first, 70.464, epsilon = 0.05);
410 }
411
412 #[test]
413 fn rsi_stays_in_0_100_range() {
414 let prices: Vec<f64> = (0..200)
415 .map(|i| 100.0 + (f64::from(i) * 0.7).sin() * 10.0)
416 .collect();
417 let mut rsi = Rsi::new(14).unwrap();
418 for x in rsi.batch(&prices).into_iter().flatten() {
419 assert!((0.0..=100.0).contains(&x), "RSI out of range: {x}");
420 }
421 }
422
423 #[test]
424 fn reset_clears_state() {
425 let mut rsi = Rsi::new(5).unwrap();
426 rsi.batch(&[1.0, 2.0, 3.0, 2.0, 4.0, 5.0, 6.0]);
427 assert!(rsi.is_ready());
428 rsi.reset();
429 assert!(!rsi.is_ready());
430 assert_eq!(rsi.update(1.0), None);
431 }
432
433 #[test]
434 fn batch_equals_streaming() {
435 let prices: Vec<f64> = (1..=40)
436 .map(|i| (f64::from(i) * 0.3).sin() * 5.0 + f64::from(i))
437 .collect();
438 let mut a = Rsi::new(7).unwrap();
439 let mut b = Rsi::new(7).unwrap();
440 assert_eq!(
441 a.batch(&prices),
442 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
443 );
444 }
445
446 #[test]
447 fn ignores_non_finite_input() {
448 let mut rsi = Rsi::new(3).unwrap();
449 rsi.batch(&[1.0, 2.0, 3.0, 4.0]);
450 let before = rsi.value();
451 assert!(before.is_some());
452 assert_eq!(rsi.update(f64::NAN), None);
453 assert_eq!(rsi.update(f64::INFINITY), None);
454 assert_eq!(rsi.value(), before);
455 }
456
457 fn bits_eq(a: &[f64], b: &[f64]) -> bool {
458 a.len() == b.len()
459 && a.iter()
460 .zip(b)
461 .all(|(x, y)| x == y || (x.is_nan() && y.is_nan()))
462 }
463
464 fn rsi_replay(period: usize, series: &[f64]) -> Vec<f64> {
465 let mut r = Rsi::new(period).unwrap();
466 series
467 .iter()
468 .map(|&x| r.update(x).unwrap_or(f64::NAN))
469 .collect()
470 }
471
472 #[test]
473 fn batch_nan_fast_path_is_bit_identical() {
474 let series: Vec<f64> = (0..300)
475 .map(|i| (f64::from(i) * 0.3).sin() * 5.0 + f64::from(i) * 0.1 + 100.0)
476 .collect();
477 let mut rsi = Rsi::new(14).unwrap();
478 let got = rsi.batch_nan(&series);
479 assert!(bits_eq(&got, &rsi_replay(14, &series)));
480 let mut ref_rsi = Rsi::new(14).unwrap();
481 for &x in &series {
482 ref_rsi.update(x);
483 }
484 assert_eq!(rsi.update(123.0), ref_rsi.update(123.0));
485 }
486
487 #[test]
488 fn batch_nan_falls_back_on_non_finite() {
489 let series = [10.0, 11.0, 9.0, f64::NAN, 12.0, 13.0, 8.0];
490 let mut rsi = Rsi::new(3).unwrap();
491 assert!(bits_eq(&rsi.batch_nan(&series), &rsi_replay(3, &series)));
492 }
493
494 #[test]
495 fn batch_nan_falls_back_when_not_fresh() {
496 let mut rsi = Rsi::new(3).unwrap();
497 rsi.update(50.0);
498 let series = [51.0, 49.0, 52.0, 53.0, 50.0];
499 let mut ref_rsi = Rsi::new(3).unwrap();
500 ref_rsi.update(50.0);
501 let want: Vec<f64> = series
502 .iter()
503 .map(|&x| ref_rsi.update(x).unwrap_or(f64::NAN))
504 .collect();
505 assert!(bits_eq(&rsi.batch_nan(&series), &want));
506 }
507
508 #[test]
509 fn batch_nan_too_short_to_seed_falls_back() {
510 let series = [10.0, 11.0, 12.0];
512 let mut rsi = Rsi::new(3).unwrap();
513 assert!(bits_eq(&rsi.batch_nan(&series), &rsi_replay(3, &series)));
514 }
515
516 proptest::proptest! {
517 #![proptest_config(proptest::test_runner::Config::with_cases(48))]
518 #[test]
519 fn rsi_matches_naive(
520 period in 1usize..20,
521 prices in proptest::collection::vec(1.0_f64..1000.0, 0..150),
522 ) {
523 let mut rsi = Rsi::new(period).unwrap();
524 let got = rsi.batch(&prices);
525 let want = rsi_naive(&prices, period);
526 proptest::prop_assert_eq!(got.len(), want.len());
527 for (g, w) in got.iter().zip(want.iter()) {
528 match (g, w) {
529 (None, None) => {}
530 (Some(a), Some(b)) => proptest::prop_assert!(
531 (a - b).abs() < 1e-7,
532 "got={a} want={b}"
533 ),
534 _ => proptest::prop_assert!(false, "warmup mismatch"),
535 }
536 }
537 }
538 }
539}