use crate::error::{Error, Result};
use crate::traits::Indicator;
#[derive(Debug, Clone)]
pub struct Rsi {
period: usize,
n_minus_1: f64,
inv_period: f64,
prev_close: f64,
has_prev: bool,
seed_buf_gains: Vec<f64>,
seed_buf_losses: Vec<f64>,
avg_gain: f64,
avg_loss: f64,
avgs_seeded: bool,
last_value: Option<f64>,
}
impl Rsi {
pub fn new(period: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
if period > crate::error::MAX_PERIOD {
return Err(Error::InvalidPeriod {
message: crate::error::PERIOD_ABOVE_MAX,
});
}
Ok(Self {
period,
n_minus_1: (period - 1) as f64,
inv_period: 1.0 / period as f64,
prev_close: 0.0,
has_prev: false,
seed_buf_gains: Vec::with_capacity(period),
seed_buf_losses: Vec::with_capacity(period),
avg_gain: 0.0,
avg_loss: 0.0,
avgs_seeded: false,
last_value: None,
})
}
pub const fn period(&self) -> usize {
self.period
}
pub const fn value(&self) -> Option<f64> {
self.last_value
}
pub fn batch_nan(&mut self, inputs: &[f64]) -> Vec<f64> {
let p = self.period;
let n = inputs.len();
if self.has_prev
|| self.avgs_seeded
|| !self.seed_buf_gains.is_empty()
|| n <= p
|| !inputs.iter().all(|x| x.is_finite())
{
return inputs
.iter()
.map(|&x| self.update(x).unwrap_or(f64::NAN))
.collect();
}
let mut out = vec![f64::NAN; p];
out.reserve(n - p);
let mut prev = inputs[0];
let (mut sum_gain, mut sum_loss) = (0.0_f64, 0.0_f64);
for &x in &inputs[1..=p] {
let diff = x - prev;
prev = x;
let gain = if diff > 0.0 { diff } else { 0.0 };
let loss = if diff < 0.0 { -diff } else { 0.0 };
self.seed_buf_gains.push(gain);
self.seed_buf_losses.push(loss);
sum_gain += gain;
sum_loss += loss;
}
let p_f64 = p as f64;
let mut ag = sum_gain / p_f64;
let mut al = sum_loss / p_f64;
out.push(Self::rsi_from_avgs(ag, al));
for &x in &inputs[p + 1..] {
let diff = x - prev;
prev = x;
let gain = if diff > 0.0 { diff } else { 0.0 };
let loss = if diff < 0.0 { -diff } else { 0.0 };
ag = ag.mul_add(self.n_minus_1, gain) * self.inv_period;
al = al.mul_add(self.n_minus_1, loss) * self.inv_period;
out.push(Self::rsi_from_avgs(ag, al));
}
self.prev_close = prev;
self.has_prev = true;
self.avg_gain = ag;
self.avg_loss = al;
self.avgs_seeded = true;
self.last_value = Some(out[n - 1]);
out
}
fn rsi_from_avgs(avg_gain: f64, avg_loss: f64) -> f64 {
let denom = avg_gain + avg_loss;
if denom == 0.0 {
50.0
} else {
100.0 * avg_gain / denom
}
}
}
impl Indicator for Rsi {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return None;
}
if !self.has_prev {
self.prev_close = input;
self.has_prev = true;
return None;
}
let prev = self.prev_close;
self.prev_close = input;
let diff = input - prev;
let gain = if diff > 0.0 { diff } else { 0.0 };
let loss = if diff < 0.0 { -diff } else { 0.0 };
if self.avgs_seeded {
let new_ag = self.avg_gain.mul_add(self.n_minus_1, gain) * self.inv_period;
let new_al = self.avg_loss.mul_add(self.n_minus_1, loss) * self.inv_period;
self.avg_gain = new_ag;
self.avg_loss = new_al;
let v = Self::rsi_from_avgs(new_ag, new_al);
self.last_value = Some(v);
return Some(v);
}
self.seed_buf_gains.push(gain);
self.seed_buf_losses.push(loss);
if self.seed_buf_gains.len() == self.period {
let ag = self.seed_buf_gains.iter().sum::<f64>() / self.period as f64;
let al = self.seed_buf_losses.iter().sum::<f64>() / self.period as f64;
self.avg_gain = ag;
self.avg_loss = al;
self.avgs_seeded = true;
let v = Self::rsi_from_avgs(ag, al);
self.last_value = Some(v);
return Some(v);
}
None
}
fn reset(&mut self) {
self.prev_close = 0.0;
self.has_prev = false;
self.seed_buf_gains.clear();
self.seed_buf_losses.clear();
self.avg_gain = 0.0;
self.avg_loss = 0.0;
self.avgs_seeded = false;
self.last_value = None;
}
#[inline]
fn warmup_period(&self) -> usize {
self.period + 1
}
#[inline]
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
#[inline]
fn name(&self) -> &'static str {
"RSI"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn rsi_naive(prices: &[f64], period: usize) -> Vec<Option<f64>> {
let n = period as f64;
let mut out = vec![None; prices.len()];
let mut gains: Vec<f64> = Vec::new();
let mut losses: Vec<f64> = Vec::new();
let mut avg_gain: Option<f64> = None;
let mut avg_loss: Option<f64> = None;
let rsi_val = |ag: f64, al: f64| -> f64 {
if al == 0.0 {
if ag == 0.0 {
50.0
} else {
100.0
}
} else {
100.0 - 100.0 / (1.0 + ag / al)
}
};
for i in 1..prices.len() {
let diff = prices[i] - prices[i - 1];
let gain = if diff > 0.0 { diff } else { 0.0 };
let loss = if diff < 0.0 { -diff } else { 0.0 };
if let (Some(ag), Some(al)) = (avg_gain, avg_loss) {
let nag = (ag * (n - 1.0) + gain) / n;
let nal = (al * (n - 1.0) + loss) / n;
avg_gain = Some(nag);
avg_loss = Some(nal);
out[i] = Some(rsi_val(nag, nal));
} else {
gains.push(gain);
losses.push(loss);
if gains.len() == period {
let ag = gains.iter().sum::<f64>() / n;
let al = losses.iter().sum::<f64>() / n;
avg_gain = Some(ag);
avg_loss = Some(al);
out[i] = Some(rsi_val(ag, al));
}
}
}
out
}
#[test]
fn new_rejects_zero_period() {
assert!(matches!(Rsi::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let mut rsi = Rsi::new(14).unwrap();
assert_eq!(rsi.period(), 14);
assert_eq!(rsi.name(), "RSI");
assert_eq!(rsi.value(), None);
for i in 1..=15 {
rsi.update(100.0 + f64::from(i));
}
assert!(rsi.value().is_some());
}
#[test]
fn naive_helper_flat_series_yields_50() {
let ks = rsi_naive(&[42.0; 20], 5);
for r in ks.into_iter().skip(5) {
assert_eq!(r.expect("ready after period+1 inputs"), 50.0);
}
}
#[test]
fn naive_helper_monotone_up_yields_100() {
let prices: Vec<f64> = (1..=20).map(f64::from).collect();
let ks = rsi_naive(&prices, 5);
for r in ks.into_iter().skip(5) {
assert_eq!(r.expect("ready after period+1 inputs"), 100.0);
}
}
#[test]
fn warmup_period_is_period_plus_one() {
let rsi = Rsi::new(14).unwrap();
assert_eq!(rsi.warmup_period(), 15);
}
#[test]
fn first_emission_at_index_period() {
let prices: Vec<f64> = (1..=20).map(f64::from).collect();
let mut rsi = Rsi::new(14).unwrap();
let out = rsi.batch(&prices);
for x in &out[..14] {
assert!(x.is_none());
}
assert!(out[14].is_some());
}
#[test]
fn pure_uptrend_yields_rsi_100() {
let prices: Vec<f64> = (1..=20).map(f64::from).collect();
let mut rsi = Rsi::new(14).unwrap();
let out = rsi.batch(&prices);
for v in out.iter().filter_map(|x| x.as_ref()) {
assert_relative_eq!(*v, 100.0, epsilon = 1e-9);
}
}
#[test]
fn pure_downtrend_yields_rsi_0() {
let prices: Vec<f64> = (1..=20).rev().map(f64::from).collect();
let mut rsi = Rsi::new(14).unwrap();
let out = rsi.batch(&prices);
for v in out.iter().filter_map(|x| x.as_ref()) {
assert_relative_eq!(*v, 0.0, epsilon = 1e-9);
}
}
#[test]
fn flat_series_yields_rsi_50() {
let prices = [10.0_f64; 30];
let mut rsi = Rsi::new(14).unwrap();
let out = rsi.batch(&prices);
for v in out.iter().filter_map(|x| x.as_ref()) {
assert_relative_eq!(*v, 50.0, epsilon = 1e-12);
}
}
#[test]
fn classic_wilder_textbook_values() {
let prices = [
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84, 46.08, 45.89, 46.03,
45.61, 46.28, 46.28,
];
let mut rsi = Rsi::new(14).unwrap();
let out = rsi.batch(&prices);
let first = out[14].expect("first RSI emitted at index period");
assert_relative_eq!(first, 70.464, epsilon = 0.05);
}
#[test]
fn rsi_stays_in_0_100_range() {
let prices: Vec<f64> = (0..200)
.map(|i| 100.0 + (f64::from(i) * 0.7).sin() * 10.0)
.collect();
let mut rsi = Rsi::new(14).unwrap();
for x in rsi.batch(&prices).into_iter().flatten() {
assert!((0.0..=100.0).contains(&x), "RSI out of range: {x}");
}
}
#[test]
fn reset_clears_state() {
let mut rsi = Rsi::new(5).unwrap();
rsi.batch(&[1.0, 2.0, 3.0, 2.0, 4.0, 5.0, 6.0]);
assert!(rsi.is_ready());
rsi.reset();
assert!(!rsi.is_ready());
assert_eq!(rsi.update(1.0), None);
}
#[test]
fn batch_equals_streaming() {
let prices: Vec<f64> = (1..=40)
.map(|i| (f64::from(i) * 0.3).sin() * 5.0 + f64::from(i))
.collect();
let mut a = Rsi::new(7).unwrap();
let mut b = Rsi::new(7).unwrap();
assert_eq!(
a.batch(&prices),
prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
);
}
#[test]
fn ignores_non_finite_input() {
let mut rsi = Rsi::new(3).unwrap();
rsi.batch(&[1.0, 2.0, 3.0, 4.0]);
let before = rsi.value();
assert!(before.is_some());
assert_eq!(rsi.update(f64::NAN), None);
assert_eq!(rsi.update(f64::INFINITY), None);
assert_eq!(rsi.value(), before);
}
fn bits_eq(a: &[f64], b: &[f64]) -> bool {
a.len() == b.len()
&& a.iter()
.zip(b)
.all(|(x, y)| x == y || (x.is_nan() && y.is_nan()))
}
fn rsi_replay(period: usize, series: &[f64]) -> Vec<f64> {
let mut r = Rsi::new(period).unwrap();
series
.iter()
.map(|&x| r.update(x).unwrap_or(f64::NAN))
.collect()
}
#[test]
fn batch_nan_fast_path_is_bit_identical() {
let series: Vec<f64> = (0..300)
.map(|i| (f64::from(i) * 0.3).sin() * 5.0 + f64::from(i) * 0.1 + 100.0)
.collect();
let mut rsi = Rsi::new(14).unwrap();
let got = rsi.batch_nan(&series);
assert!(bits_eq(&got, &rsi_replay(14, &series)));
let mut ref_rsi = Rsi::new(14).unwrap();
for &x in &series {
ref_rsi.update(x);
}
assert_eq!(rsi.update(123.0), ref_rsi.update(123.0));
}
#[test]
fn batch_nan_falls_back_on_non_finite() {
let series = [10.0, 11.0, 9.0, f64::NAN, 12.0, 13.0, 8.0];
let mut rsi = Rsi::new(3).unwrap();
assert!(bits_eq(&rsi.batch_nan(&series), &rsi_replay(3, &series)));
}
#[test]
fn batch_nan_falls_back_when_not_fresh() {
let mut rsi = Rsi::new(3).unwrap();
rsi.update(50.0);
let series = [51.0, 49.0, 52.0, 53.0, 50.0];
let mut ref_rsi = Rsi::new(3).unwrap();
ref_rsi.update(50.0);
let want: Vec<f64> = series
.iter()
.map(|&x| ref_rsi.update(x).unwrap_or(f64::NAN))
.collect();
assert!(bits_eq(&rsi.batch_nan(&series), &want));
}
#[test]
fn batch_nan_too_short_to_seed_falls_back() {
let series = [10.0, 11.0, 12.0];
let mut rsi = Rsi::new(3).unwrap();
assert!(bits_eq(&rsi.batch_nan(&series), &rsi_replay(3, &series)));
}
proptest::proptest! {
#![proptest_config(proptest::test_runner::Config::with_cases(48))]
#[test]
fn rsi_matches_naive(
period in 1usize..20,
prices in proptest::collection::vec(1.0_f64..1000.0, 0..150),
) {
let mut rsi = Rsi::new(period).unwrap();
let got = rsi.batch(&prices);
let want = rsi_naive(&prices, period);
proptest::prop_assert_eq!(got.len(), want.len());
for (g, w) in got.iter().zip(want.iter()) {
match (g, w) {
(None, None) => {}
(Some(a), Some(b)) => proptest::prop_assert!(
(a - b).abs() < 1e-7,
"got={a} want={b}"
),
_ => proptest::prop_assert!(false, "warmup mismatch"),
}
}
}
}
}