use crate::error::FinError;
use crate::signals::{BarInput, Signal, SignalValue};
use rust_decimal::Decimal;
use std::collections::VecDeque;
pub struct ConnorsRsi {
name: String,
rsi_period: usize,
streak_period: usize,
rank_period: usize,
prev_close: Option<Decimal>,
rsi_count: usize,
rsi_avg_gain: Option<Decimal>,
rsi_avg_loss: Option<Decimal>,
rsi_seed_gains: Vec<Decimal>,
rsi_seed_losses: Vec<Decimal>,
streak: Decimal,
streak_rsi_count: usize,
streak_rsi_avg_gain: Option<Decimal>,
streak_rsi_avg_loss: Option<Decimal>,
streak_seed_gains: Vec<Decimal>,
streak_seed_losses: Vec<Decimal>,
prev_streak: Option<Decimal>,
rocs: VecDeque<Decimal>,
}
fn rsi_value(avg_gain: Decimal, avg_loss: Decimal) -> Decimal {
if avg_loss.is_zero() {
return Decimal::from(100u32);
}
let rs = avg_gain / avg_loss;
Decimal::from(100u32) - Decimal::from(100u32) / (Decimal::ONE + rs)
}
impl ConnorsRsi {
pub fn new(
name: impl Into<String>,
rsi_period: usize,
streak_period: usize,
rank_period: usize,
) -> Result<Self, FinError> {
if rsi_period == 0 { return Err(FinError::InvalidPeriod(rsi_period)); }
if streak_period == 0 { return Err(FinError::InvalidPeriod(streak_period)); }
if rank_period == 0 { return Err(FinError::InvalidPeriod(rank_period)); }
Ok(Self {
name: name.into(),
rsi_period,
streak_period,
rank_period,
prev_close: None,
rsi_count: 0,
rsi_avg_gain: None,
rsi_avg_loss: None,
rsi_seed_gains: Vec::with_capacity(rsi_period),
rsi_seed_losses: Vec::with_capacity(rsi_period),
streak: Decimal::ZERO,
streak_rsi_count: 0,
streak_rsi_avg_gain: None,
streak_rsi_avg_loss: None,
streak_seed_gains: Vec::with_capacity(streak_period),
streak_seed_losses: Vec::with_capacity(streak_period),
prev_streak: None,
rocs: VecDeque::with_capacity(rank_period),
})
}
fn update_rsi_component(
close: Decimal,
prev: Decimal,
count: &mut usize,
seed_gains: &mut Vec<Decimal>,
seed_losses: &mut Vec<Decimal>,
avg_gain: &mut Option<Decimal>,
avg_loss: &mut Option<Decimal>,
period: usize,
) -> Option<Decimal> {
let change = close - prev;
let gain = if change > Decimal::ZERO { change } else { Decimal::ZERO };
let loss = if change < Decimal::ZERO { -change } else { Decimal::ZERO };
*count += 1;
if avg_gain.is_none() {
seed_gains.push(gain);
seed_losses.push(loss);
if seed_gains.len() == period {
let ag = seed_gains.iter().sum::<Decimal>() / Decimal::from(period as u32);
let al = seed_losses.iter().sum::<Decimal>() / Decimal::from(period as u32);
*avg_gain = Some(ag);
*avg_loss = Some(al);
return Some(rsi_value(ag, al));
}
return None;
}
let k = Decimal::ONE / Decimal::from(period as u32);
let ag = avg_gain.unwrap() * (Decimal::ONE - k) + gain * k;
let al = avg_loss.unwrap() * (Decimal::ONE - k) + loss * k;
*avg_gain = Some(ag);
*avg_loss = Some(al);
Some(rsi_value(ag, al))
}
}
impl Signal for ConnorsRsi {
fn name(&self) -> &str { &self.name }
fn update(&mut self, bar: &BarInput) -> Result<SignalValue, FinError> {
let close = bar.close;
let rsi_c1 = if let Some(pc) = self.prev_close {
Self::update_rsi_component(
close, pc,
&mut self.rsi_count,
&mut self.rsi_seed_gains,
&mut self.rsi_seed_losses,
&mut self.rsi_avg_gain,
&mut self.rsi_avg_loss,
self.rsi_period,
)
} else {
None
};
let new_streak = if let Some(pc) = self.prev_close {
if close > pc {
if self.streak > Decimal::ZERO { self.streak + Decimal::ONE }
else { Decimal::ONE }
} else if close < pc {
if self.streak < Decimal::ZERO { self.streak - Decimal::ONE }
else { -Decimal::ONE }
} else {
Decimal::ZERO
}
} else {
Decimal::ZERO
};
self.prev_close = Some(close);
let rsi_c2 = if let Some(ps) = self.prev_streak {
Self::update_rsi_component(
new_streak, ps,
&mut self.streak_rsi_count,
&mut self.streak_seed_gains,
&mut self.streak_seed_losses,
&mut self.streak_rsi_avg_gain,
&mut self.streak_rsi_avg_loss,
self.streak_period,
)
} else {
None
};
self.prev_streak = Some(new_streak);
self.streak = new_streak;
let roc_val = new_streak;
self.rocs.push_back(roc_val);
if self.rocs.len() > self.rank_period { self.rocs.pop_front(); }
let rsi_c3 = if self.rocs.len() == self.rank_period {
let count_below = self.rocs.iter()
.filter(|&&r| r < roc_val)
.count();
#[allow(clippy::cast_possible_truncation)]
Some(Decimal::from(count_below as u32)
/ Decimal::from(self.rank_period as u32)
* Decimal::from(100u32))
} else {
None
};
match (rsi_c1, rsi_c2, rsi_c3) {
(Some(c1), Some(c2), Some(c3)) => {
let crsi = (c1 + c2 + c3) / Decimal::from(3u32);
Ok(SignalValue::Scalar(crsi))
}
_ => Ok(SignalValue::Unavailable),
}
}
fn is_ready(&self) -> bool {
self.rsi_avg_gain.is_some()
&& self.streak_rsi_avg_gain.is_some()
&& self.rocs.len() >= self.rank_period
}
fn period(&self) -> usize { self.rank_period }
fn reset(&mut self) {
self.prev_close = None;
self.rsi_count = 0;
self.rsi_avg_gain = None;
self.rsi_avg_loss = None;
self.rsi_seed_gains.clear();
self.rsi_seed_losses.clear();
self.streak = Decimal::ZERO;
self.streak_rsi_count = 0;
self.streak_rsi_avg_gain = None;
self.streak_rsi_avg_loss = None;
self.streak_seed_gains.clear();
self.streak_seed_losses.clear();
self.prev_streak = None;
self.rocs.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ohlcv::OhlcvBar;
use crate::types::{NanoTimestamp, Price, Quantity, Symbol};
fn bar(c: &str) -> OhlcvBar {
let p = Price::new(c.parse().unwrap()).unwrap();
OhlcvBar {
symbol: Symbol::new("X").unwrap(),
open: p, high: p, low: p, close: p,
volume: Quantity::zero(),
ts_open: NanoTimestamp::new(0),
ts_close: NanoTimestamp::new(1),
tick_count: 1,
}
}
#[test]
fn test_crsi_invalid() {
assert!(ConnorsRsi::new("c", 0, 2, 100).is_err());
assert!(ConnorsRsi::new("c", 3, 0, 100).is_err());
assert!(ConnorsRsi::new("c", 3, 2, 0).is_err());
}
#[test]
fn test_crsi_unavailable_before_warmup() {
let mut c = ConnorsRsi::new("c", 3, 2, 10).unwrap();
assert_eq!(c.update_bar(&bar("100")).unwrap(), SignalValue::Unavailable);
assert!(!c.is_ready());
}
#[test]
fn test_crsi_ready_after_enough_bars() {
let mut c = ConnorsRsi::new("c", 3, 2, 10).unwrap();
let prices: Vec<u32> = (0..30).map(|i| 100 + (i % 3)).collect();
let mut ready = false;
for p in &prices {
let _ = c.update_bar(&bar(&p.to_string())).unwrap();
if c.is_ready() { ready = true; break; }
}
assert!(ready);
}
#[test]
fn test_crsi_reset() {
let mut c = ConnorsRsi::new("c", 3, 2, 10).unwrap();
let prices: Vec<u32> = (0..30).map(|i| 100 + (i % 3)).collect();
for p in &prices { let _ = c.update_bar(&bar(&p.to_string())).unwrap(); }
c.reset();
assert!(!c.is_ready());
}
#[test]
fn test_crsi_output_range() {
let mut c = ConnorsRsi::new("c", 3, 2, 10).unwrap();
let prices: Vec<u32> = (0..50).map(|i| 100 + (i % 5)).collect();
for p in &prices {
if let SignalValue::Scalar(v) = c.update_bar(&bar(&p.to_string())).unwrap() {
assert!(v >= Decimal::ZERO && v <= Decimal::from(100u32),
"CRSI out of range: {v}");
}
}
}
}