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 Engine.
8/// Connors RSI = (RSI(close, 3) + RSI(Streak, 2) + PercentRank(ROC(1), 100)) / 3
9#[derive(Debug, Clone)]
10pub struct ConnorsRsiEngine {
11    rsi_close: Rsi,
12    rsi_streak: Rsi,
13    prev_close: Option<f64>,
14    current_streak: f64,
15    roc_history: VecDeque<f64>,
16}
17
18impl ConnorsRsiEngine {
19    pub fn new(rsi_len: usize, streak_len: usize, rank_len: usize) -> Self {
20        Self {
21            rsi_close: Rsi::with_period(rsi_len),
22            rsi_streak: Rsi::with_period(streak_len),
23            prev_close: None,
24            current_streak: 0.0,
25            roc_history: VecDeque::with_capacity(rank_len),
26        }
27    }
28
29    pub fn with_defaults() -> Self {
30        Self::new(3, 2, 100)
31    }
32}
33
34impl Indicator for ConnorsRsiEngine {
35    fn name(&self) -> &str {
36        "connors_rsi"
37    }
38
39    fn warmup_period(&self) -> usize {
40        100
41    }
42
43    fn reset(&mut self) {
44        self.rsi_close.reset();
45        self.rsi_streak.reset();
46        self.prev_close = None;
47        self.current_streak = 0.0;
48        self.roc_history.clear();
49    }
50
51    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
52        let rsi_c = self.rsi_close.on_bar(bar)?.value;
53
54        if let Some(prev) = self.prev_close {
55            if bar.close > prev {
56                self.current_streak = if self.current_streak >= 0.0 {
57                    self.current_streak + 1.0
58                } else {
59                    1.0
60                };
61            } else if bar.close < prev {
62                self.current_streak = if self.current_streak <= 0.0 {
63                    self.current_streak - 1.0
64                } else {
65                    -1.0
66                };
67            } else {
68                self.current_streak = 0.0;
69            }
70
71            let roc = if prev > 0.0 {
72                (bar.close - prev) / prev
73            } else {
74                0.0
75            };
76            self.roc_history.push_back(roc);
77            if self.roc_history.len() > 100 {
78                self.roc_history.pop_front();
79            }
80        }
81        self.prev_close = Some(bar.close);
82
83        // Dummy bar wrapping streak as price
84        let streak_bar = Bar::new(
85            bar.timestamp,
86            self.current_streak,
87            self.current_streak,
88            self.current_streak,
89            self.current_streak,
90            1.0,
91        );
92        let rsi_s = self.rsi_streak.on_bar(&streak_bar)?.value;
93
94        let last_roc = self.roc_history.back().copied().unwrap_or(0.0);
95        let slice: Vec<f64> = self.roc_history.iter().copied().collect();
96        let prank = percent_rank(&slice, last_roc);
97
98        let crsi = (rsi_c + rsi_s + prank) / 3.0;
99        Some(IndicatorOutput::new(crsi.clamp(0.0, 100.0)))
100    }
101
102    fn alerts(&self) -> Vec<IndicatorAlert> {
103        Vec::new()
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_connors_rsi() {
113        let mut crsi = ConnorsRsiEngine::with_defaults();
114        let mut out = None;
115        for i in 0..120 {
116            let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + (i % 5) as f64, 1000.0);
117            out = crsi.on_bar(&b);
118        }
119        assert!(out.is_some());
120        let val = out.unwrap().value;
121        assert!((0.0..=100.0).contains(&val));
122    }
123}