wickra_core/indicators/
connors_rsi.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rsi::Rsi;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
40pub struct ConnorsRsi {
41 period_rsi: usize,
42 period_streak: usize,
43 period_rank: usize,
44 rsi_close: Rsi,
45 rsi_streak: Rsi,
46 prev_price: Option<f64>,
47 streak: f64,
48 rocs: VecDeque<f64>,
51 current: Option<f64>,
52}
53
54impl ConnorsRsi {
55 pub fn new(period_rsi: usize, period_streak: usize, period_rank: usize) -> Result<Self> {
58 if period_rsi == 0 || period_streak == 0 || period_rank == 0 {
59 return Err(Error::PeriodZero);
60 }
61 Ok(Self {
62 period_rsi,
63 period_streak,
64 period_rank,
65 rsi_close: Rsi::new(period_rsi)?,
66 rsi_streak: Rsi::new(period_streak)?,
67 prev_price: None,
68 streak: 0.0,
69 rocs: VecDeque::with_capacity(period_rank),
70 current: None,
71 })
72 }
73
74 pub fn classic() -> Self {
76 Self::new(3, 2, 100).expect("classic Connors RSI parameters are valid")
77 }
78
79 pub const fn periods(&self) -> (usize, usize, usize) {
81 (self.period_rsi, self.period_streak, self.period_rank)
82 }
83}
84
85impl Indicator for ConnorsRsi {
86 type Input = f64;
87 type Output = f64;
88
89 fn update(&mut self, input: f64) -> Option<f64> {
90 if !input.is_finite() {
91 return self.current;
92 }
93 let rsi_close = self.rsi_close.update(input);
96
97 let Some(prev) = self.prev_price else {
98 self.prev_price = Some(input);
99 return None;
100 };
101
102 self.streak = if input > prev {
104 self.streak.max(0.0) + 1.0
105 } else if input < prev {
106 self.streak.min(0.0) - 1.0
107 } else {
108 0.0
109 };
110 let rsi_streak = self.rsi_streak.update(self.streak);
111
112 if prev != 0.0 {
114 let roc = (input - prev) / prev;
115 if self.rocs.len() == self.period_rank {
116 self.rocs.pop_front();
117 }
118 self.rocs.push_back(roc);
119 }
120 self.prev_price = Some(input);
121
122 let percent_rank = if self.rocs.len() == self.period_rank {
124 let latest = *self.rocs.back().expect("non-empty window");
125 let below = self.rocs.iter().filter(|&&r| r < latest).count();
126 Some(100.0 * below as f64 / self.period_rank as f64)
127 } else {
128 None
129 };
130
131 let value = (rsi_close?, rsi_streak?, percent_rank?);
132 let crsi = (value.0 + value.1 + value.2) / 3.0;
133 self.current = Some(crsi);
134 Some(crsi)
135 }
136
137 fn reset(&mut self) {
138 self.rsi_close.reset();
139 self.rsi_streak.reset();
140 self.prev_price = None;
141 self.streak = 0.0;
142 self.rocs.clear();
143 self.current = None;
144 }
145
146 fn warmup_period(&self) -> usize {
147 let rsi_close = self.period_rsi + 1;
153 let rsi_streak = self.period_streak + 2;
154 let rank = self.period_rank + 1;
155 rsi_close.max(rsi_streak).max(rank)
156 }
157
158 fn is_ready(&self) -> bool {
159 self.current.is_some()
160 }
161
162 fn name(&self) -> &'static str {
163 "ConnorsRSI"
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::traits::BatchExt;
171 use approx::assert_relative_eq;
172
173 #[test]
174 fn rejects_zero_period() {
175 assert!(matches!(ConnorsRsi::new(0, 2, 100), Err(Error::PeriodZero)));
176 assert!(matches!(ConnorsRsi::new(3, 0, 100), Err(Error::PeriodZero)));
177 assert!(matches!(ConnorsRsi::new(3, 2, 0), Err(Error::PeriodZero)));
178 }
179
180 #[test]
181 fn accessors_and_metadata() {
182 let crsi = ConnorsRsi::classic();
183 assert_eq!(crsi.periods(), (3, 2, 100));
184 assert_eq!(crsi.name(), "ConnorsRSI");
185 assert_eq!(crsi.warmup_period(), 101);
187 }
188
189 #[test]
190 fn classic_factory() {
191 assert_eq!(ConnorsRsi::classic().periods(), (3, 2, 100));
192 }
193
194 #[test]
195 fn warmup_emits_first_value_at_warmup_period() {
196 let mut crsi = ConnorsRsi::new(3, 2, 5).unwrap();
198 assert_eq!(crsi.warmup_period(), 6);
200 let prices: Vec<f64> = (1..=8).map(f64::from).collect();
201 let out = crsi.batch(&prices);
202 for v in out.iter().take(5) {
203 assert!(v.is_none());
204 }
205 assert!(out[5].is_some());
206 }
207
208 #[test]
209 fn pure_uptrend_saturates_high() {
210 let mut crsi = ConnorsRsi::classic();
216 for i in 1..=200 {
217 crsi.update(f64::from(i));
218 }
219 let v = crsi.current.unwrap();
220 assert!(
221 v > 60.0,
222 "uptrend should drive Connors RSI well above 50: {v}"
223 );
224 }
225
226 #[test]
227 fn output_is_bounded() {
228 let mut crsi = ConnorsRsi::classic();
229 let prices: Vec<f64> = (0..300)
230 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 20.0)
231 .collect();
232 for v in crsi.batch(&prices).iter().flatten() {
233 assert!(
234 (0.0..=100.0).contains(v),
235 "Connors RSI out of [0, 100]: {v}"
236 );
237 }
238 }
239
240 #[test]
241 fn streak_resets_to_zero_on_unchanged_close() {
242 let mut crsi = ConnorsRsi::new(3, 2, 100).unwrap();
244 crsi.update(10.0);
245 crsi.update(11.0);
246 crsi.update(12.0);
247 assert_eq!(crsi.streak, 2.0);
248 crsi.update(12.0);
249 assert_relative_eq!(crsi.streak, 0.0, epsilon = 1e-12);
250 crsi.update(11.0);
251 assert_eq!(crsi.streak, -1.0);
252 crsi.update(10.0);
253 assert_eq!(crsi.streak, -2.0);
254 }
255
256 #[test]
257 fn batch_equals_streaming() {
258 let prices: Vec<f64> = (1..=200)
259 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0 + f64::from(i) * 0.1)
260 .collect();
261 let mut a = ConnorsRsi::classic();
262 let mut b = ConnorsRsi::classic();
263 assert_eq!(
264 a.batch(&prices),
265 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
266 );
267 }
268
269 #[test]
270 fn reset_clears_state() {
271 let mut crsi = ConnorsRsi::classic();
272 let prices: Vec<f64> = (1..=200).map(f64::from).collect();
273 crsi.batch(&prices);
274 assert!(crsi.is_ready());
275 crsi.reset();
276 assert!(!crsi.is_ready());
277 assert_eq!(crsi.streak, 0.0);
278 assert!(crsi.prev_price.is_none());
279 }
280
281 #[test]
282 fn ignores_non_finite_input() {
283 let mut crsi = ConnorsRsi::classic();
284 let prices: Vec<f64> = (1..=200).map(f64::from).collect();
285 crsi.batch(&prices);
286 let before = crsi.current;
287 assert_eq!(crsi.update(f64::NAN), before);
288 assert_eq!(crsi.update(f64::INFINITY), before);
289 }
290
291 #[test]
292 fn zero_prev_skips_roc_update() {
293 let mut crsi = ConnorsRsi::new(3, 2, 4).unwrap();
298 crsi.update(0.0);
300 let after = crsi.update(1.0);
305 assert!(after.is_none(), "CRSI cannot emit on bar 2: {after:?}");
306 }
307}