Skip to main content

kestrel_chartkit/indicator/
connors_rsi.rs

1use super::rsi::Rsi;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4use crate::stats::percent_rank;
5use std::collections::VecDeque;
6
7/// Connors RSI: the mean of a price RSI, a streak RSI and a percentile rank of the one-bar return.
8///
9/// As implemented, and in each point different from the usual definition:
10///
11/// - The price RSI and the streak RSI are the **lines** of [`super::rsi::Rsi`] (an `Ema(3)` over
12///   the raw RSI), not raw RSI values.
13/// - The streak counts consecutive higher (positive) or lower (negative) closes and is `0` on an
14///   unchanged close. Streak and one-bar return only start updating on the bar *after* the price
15///   RSI first exists; the streak RSI runs over the streak values from the price RSI's first bar.
16/// - The percentile rank is `100 * count(r <= last) / count` over all one-bar returns seen so far,
17///   at most the last 100. **`rank_len` has no effect.**
18///
19/// `CRSI = (rsi_close + rsi_streak + rank) / 3`, clamped to `0..=100`.
20///
21/// First output: once the streak RSI exists. [`Indicator::reset`] clears all state.
22#[derive(Debug, Clone)]
23pub struct ConnorsRsiEngine {
24    rsi_close: Rsi,
25    rsi_streak: Rsi,
26    prev_close: Option<f64>,
27    current_streak: f64,
28    roc_history: VecDeque<f64>,
29}
30
31impl ConnorsRsiEngine {
32    pub fn new(rsi_len: usize, streak_len: usize, rank_len: usize) -> Self {
33        Self {
34            rsi_close: Rsi::with_period(rsi_len),
35            rsi_streak: Rsi::with_period(streak_len),
36            prev_close: None,
37            current_streak: 0.0,
38            roc_history: VecDeque::with_capacity(rank_len),
39        }
40    }
41
42    pub fn with_defaults() -> Self {
43        Self::new(3, 2, 100)
44    }
45}
46
47impl Indicator for ConnorsRsiEngine {
48    fn name(&self) -> &str {
49        "connors_rsi"
50    }
51
52    fn warmup_period(&self) -> usize {
53        100
54    }
55
56    fn reset(&mut self) {
57        self.rsi_close.reset();
58        self.rsi_streak.reset();
59        self.prev_close = None;
60        self.current_streak = 0.0;
61        self.roc_history.clear();
62    }
63
64    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
65        let rsi_c = self.rsi_close.on_bar(bar)?.value;
66
67        if let Some(prev) = self.prev_close {
68            if bar.close > prev {
69                self.current_streak = if self.current_streak >= 0.0 {
70                    self.current_streak + 1.0
71                } else {
72                    1.0
73                };
74            } else if bar.close < prev {
75                self.current_streak = if self.current_streak <= 0.0 {
76                    self.current_streak - 1.0
77                } else {
78                    -1.0
79                };
80            } else {
81                self.current_streak = 0.0;
82            }
83
84            let roc = if prev > 0.0 {
85                (bar.close - prev) / prev
86            } else {
87                0.0
88            };
89            self.roc_history.push_back(roc);
90            if self.roc_history.len() > 100 {
91                self.roc_history.pop_front();
92            }
93        }
94        self.prev_close = Some(bar.close);
95
96        // Dummy bar wrapping streak as price
97        let streak_bar = Bar::new(
98            bar.timestamp,
99            self.current_streak,
100            self.current_streak,
101            self.current_streak,
102            self.current_streak,
103            1.0,
104        );
105        let rsi_s = self.rsi_streak.on_bar(&streak_bar)?.value;
106
107        let last_roc = self.roc_history.back().copied().unwrap_or(0.0);
108        let slice: Vec<f64> = self.roc_history.iter().copied().collect();
109        let prank = percent_rank(&slice, last_roc);
110
111        let crsi = (rsi_c + rsi_s + prank) / 3.0;
112        Some(IndicatorOutput::new(crsi.clamp(0.0, 100.0)))
113    }
114
115    fn alerts(&self) -> Vec<IndicatorAlert> {
116        Vec::new()
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_connors_rsi() {
126        let mut crsi = ConnorsRsiEngine::with_defaults();
127        let mut out = None;
128        for i in 0..120 {
129            let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + (i % 5) as f64, 1000.0);
130            out = crsi.on_bar(&b);
131        }
132        assert!(out.is_some());
133        let val = out.unwrap().value;
134        assert!((0.0..=100.0).contains(&val));
135    }
136}