kestrel_chartkit/indicator/
connors_rsi.rs1use super::rsi::Rsi;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4use crate::stats::percent_rank;
5use std::collections::VecDeque;
6
7#[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 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}