egui_charts/studies/builtin/
connors_rsi.rs1use crate::model::Bar;
2use crate::studies::palette;
3use crate::studies::stepper::Stepper;
4use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
5use egui::Color32;
6
7#[derive(Clone)]
16pub struct ConnorsRSI {
17 rsi_period: usize,
19 streak_period: usize,
21 rank_period: usize,
23 values: IndicatorValues,
24 stepper: Stepper<State>,
25 color: Color32,
26 visible: bool,
27}
28
29#[derive(Clone, Default)]
30struct State {
31 previous: Option<f64>,
32 streak: f64,
33 price_rsi: (usize, f64, f64),
34 streak_rsi: (usize, f64, f64),
35 roc: std::collections::VecDeque<f64>,
36}
37
38impl ConnorsRSI {
39 pub fn new() -> Self {
41 Self {
42 rsi_period: 3,
43 streak_period: 2,
44 rank_period: 100,
45 values: IndicatorValues::new(),
46 stepper: Stepper::default(),
47 color: palette::BLUE,
48 visible: true,
49 }
50 }
51
52 pub fn with_periods(mut self, rsi: usize, streak: usize, rank: usize) -> Self {
54 self.rsi_period = rsi.max(1);
55 self.streak_period = streak.max(1);
56 self.rank_period = rank.max(1);
57 self
58 }
59
60 pub fn with_color(mut self, color: Color32) -> Self {
62 self.color = color;
63 self
64 }
65
66 fn next_rsi(state: &mut (usize, f64, f64), change: f64, period: usize) -> f64 {
68 let gain = if change > 0.0 { change } else { 0.0 };
69 let loss = if change < 0.0 { -change } else { 0.0 };
70 if state.0 < period {
71 state.0 += 1;
72 state.1 += gain;
73 state.2 += loss;
74 if state.0 < period {
75 return f64::NAN;
76 }
77 state.1 /= period as f64;
78 state.2 /= period as f64;
79 } else {
80 state.1 = (state.1 * (period - 1) as f64 + gain) / period as f64;
81 state.2 = (state.2 * (period - 1) as f64 + loss) / period as f64;
82 }
83 super::rsi::from_averages(state.1, state.2)
84 }
85}
86
87impl Default for ConnorsRSI {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93impl Indicator for ConnorsRSI {
94 fn source(&self) -> Option<crate::studies::StudySource> {
95 Some(crate::studies::StudySource::Price(
96 crate::model::PriceSource::Close,
97 ))
98 }
99
100 crate::studies::inputs::study_inputs!(self;
101 rsi_period: length("RSI period", 1, usize::MAX),
102 streak_period: length("Streak period", 1, usize::MAX),
103 rank_period: length("Rank period", 1, usize::MAX)
104 ; {});
105
106 fn name(&self) -> &str {
107 "Connors RSI"
108 }
109
110 fn desc(&self) -> &str {
111 "Connors RSI - Composite momentum indicator"
112 }
113
114 fn calculate(&mut self, data: &[Bar]) {
115 self.update(data, 0);
116 }
117
118 fn update(&mut self, data: &[Bar], from: usize) {
119 self.stepper
120 .run(data, from, &mut self.values, |state, bar| {
121 if !bar.close.is_finite() {
122 return IndicatorValue::None;
123 }
124 let previous = state.previous.replace(bar.close);
125 let (mut price_rsi, mut streak_rsi, mut roc) = (f64::NAN, f64::NAN, f64::NAN);
126 if let Some(previous) = previous {
127 price_rsi =
128 Self::next_rsi(&mut state.price_rsi, bar.close - previous, self.rsi_period);
129 let streak = if bar.close > previous {
130 if state.streak > 0.0 {
131 state.streak + 1.0
132 } else {
133 1.0
134 }
135 } else if bar.close < previous {
136 if state.streak < 0.0 {
137 state.streak - 1.0
138 } else {
139 -1.0
140 }
141 } else {
142 0.0
143 };
144 streak_rsi = Self::next_rsi(
145 &mut state.streak_rsi,
146 streak - state.streak,
147 self.streak_period,
148 );
149 state.streak = streak;
150 if previous != 0.0 {
151 roc = (bar.close - previous) / previous * 100.0;
152 }
153 }
154 let rank = if state.roc.len() == self.rank_period && !roc.is_nan() {
155 state
156 .roc
157 .iter()
158 .filter(|&&v| !v.is_nan() && v < roc)
159 .count() as f64
160 / self.rank_period as f64
161 * 100.0
162 } else {
163 f64::NAN
164 };
165 state.roc.push_back(roc);
166 if state.roc.len() > self.rank_period {
167 state.roc.pop_front();
168 }
169 if price_rsi.is_nan() || streak_rsi.is_nan() || rank.is_nan() {
170 IndicatorValue::None
171 } else {
172 IndicatorValue::Single((price_rsi + streak_rsi + rank) / 3.0)
173 }
174 });
175 }
176
177 fn is_incremental(&self) -> bool {
178 true
179 }
180
181 fn values(&self) -> &IndicatorValues {
182 &self.values
183 }
184
185 fn colors(&self) -> Vec<Color32> {
186 vec![self.color]
187 }
188
189 fn set_colors(&mut self, colors: Vec<Color32>) {
190 if !colors.is_empty() {
191 self.color = colors[0];
192 }
193 }
194
195 fn is_overlay(&self) -> bool {
196 false }
198
199 fn is_visible(&self) -> bool {
200 self.visible
201 }
202
203 fn set_visible(&mut self, visible: bool) {
204 self.visible = visible;
205 }
206
207 fn clone_box(&self) -> Box<dyn Indicator> {
208 Box::new(self.clone())
209 }
210
211 fn line_names(&self) -> Vec<String> {
212 vec![format!(
213 "CRSI({},{},{})",
214 self.rsi_period, self.streak_period, self.rank_period
215 )]
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use chrono::Utc;
223
224 fn make_bar(close: f64) -> Bar {
225 Bar {
226 time: Utc::now(),
227 open: close,
228 high: close + 1.0,
229 low: close - 1.0,
230 close,
231 volume: 1000.0,
232 colors: None,
233 payload: None,
234 }
235 }
236
237 #[test]
238 fn test_streak_calculation() {
239 let data = [
240 make_bar(100.0),
241 make_bar(101.0), make_bar(102.0), make_bar(101.0), make_bar(100.0), make_bar(100.0), ];
247
248 let expected = [200.0 / 3.0, 25.0, 125.0 / 9.0, 1325.0 / 21.0];
251 let mut study = ConnorsRSI::new().with_periods(2, 2, 2);
252 study.calculate(&data);
253 for (bar, want) in (2..data.len()).zip(expected) {
254 let got = study.values().value(0, bar).expect("defined Connors RSI");
255 assert!((got - want).abs() < 1e-9, "bar {bar}: {got} != {want}");
256 }
257 }
258
259 #[test]
262 fn flat_series_uses_neutral_rsi() {
263 let mut crsi = ConnorsRSI::new().with_periods(3, 2, 5);
264 let data: Vec<Bar> = (0..20).map(|_| make_bar(100.0)).collect();
265 crsi.calculate(&data);
266 match crsi.values.to_rows()[19] {
267 IndicatorValue::Single(v) => assert!((v - 100.0 / 3.0).abs() < 1e-9),
268 _ => panic!("expected a value after warmup"),
269 }
270 }
271
272 #[test]
273 fn test_connors_rsi_range() {
274 let mut crsi = ConnorsRSI::new().with_periods(3, 2, 20);
275
276 let data: Vec<Bar> = (0..100).map(|i| make_bar(100.0 + i as f64)).collect();
277
278 crsi.calculate(&data);
279
280 let mut checked = 0;
282 for value in &crsi.values.to_rows() {
283 if let IndicatorValue::Single(v) = value {
284 assert!(*v >= 0.0 && *v <= 100.0, "CRSI {v} out of range");
285 checked += 1;
286 }
287 }
288 assert!(checked > 0, "no value to check");
289 }
290}