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 None;
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 #[inline]
147 fn warmup_period(&self) -> usize {
148 let rsi_close = self.period_rsi + 1;
154 let rsi_streak = self.period_streak + 2;
155 let rank = self.period_rank + 1;
156 rsi_close.max(rsi_streak).max(rank)
157 }
158
159 #[inline]
160 fn is_ready(&self) -> bool {
161 self.current.is_some()
162 }
163
164 #[inline]
165 fn name(&self) -> &'static str {
166 "ConnorsRSI"
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::traits::BatchExt;
174 use approx::assert_relative_eq;
175
176 #[test]
177 fn rejects_zero_period() {
178 assert!(matches!(ConnorsRsi::new(0, 2, 100), Err(Error::PeriodZero)));
179 assert!(matches!(ConnorsRsi::new(3, 0, 100), Err(Error::PeriodZero)));
180 assert!(matches!(ConnorsRsi::new(3, 2, 0), Err(Error::PeriodZero)));
181 }
182
183 #[test]
184 fn accessors_and_metadata() {
185 let crsi = ConnorsRsi::classic();
186 assert_eq!(crsi.periods(), (3, 2, 100));
187 assert_eq!(crsi.name(), "ConnorsRSI");
188 assert_eq!(crsi.warmup_period(), 101);
190 }
191
192 #[test]
193 fn classic_factory() {
194 assert_eq!(ConnorsRsi::classic().periods(), (3, 2, 100));
195 }
196
197 #[test]
198 fn warmup_emits_first_value_at_warmup_period() {
199 let mut crsi = ConnorsRsi::new(3, 2, 5).unwrap();
201 assert_eq!(crsi.warmup_period(), 6);
203 let prices: Vec<f64> = (1..=8).map(f64::from).collect();
204 let out = crsi.batch(&prices);
205 for v in out.iter().take(5) {
206 assert!(v.is_none());
207 }
208 assert!(out[5].is_some());
209 }
210
211 #[test]
212 fn pure_uptrend_saturates_high() {
213 let mut crsi = ConnorsRsi::classic();
219 for i in 1..=200 {
220 crsi.update(f64::from(i));
221 }
222 let v = crsi.current.unwrap();
223 assert!(
224 v > 60.0,
225 "uptrend should drive Connors RSI well above 50: {v}"
226 );
227 }
228
229 #[test]
230 fn output_is_bounded() {
231 let mut crsi = ConnorsRsi::classic();
232 let prices: Vec<f64> = (0..300)
233 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 20.0)
234 .collect();
235 for v in crsi.batch(&prices).iter().flatten() {
236 assert!(
237 (0.0..=100.0).contains(v),
238 "Connors RSI out of [0, 100]: {v}"
239 );
240 }
241 }
242
243 #[test]
244 fn streak_resets_to_zero_on_unchanged_close() {
245 let mut crsi = ConnorsRsi::new(3, 2, 100).unwrap();
247 crsi.update(10.0);
248 crsi.update(11.0);
249 crsi.update(12.0);
250 assert_eq!(crsi.streak, 2.0);
251 crsi.update(12.0);
252 assert_relative_eq!(crsi.streak, 0.0, epsilon = 1e-12);
253 crsi.update(11.0);
254 assert_eq!(crsi.streak, -1.0);
255 crsi.update(10.0);
256 assert_eq!(crsi.streak, -2.0);
257 }
258
259 #[test]
260 fn batch_equals_streaming() {
261 let prices: Vec<f64> = (1..=200)
262 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 5.0 + f64::from(i) * 0.1)
263 .collect();
264 let mut a = ConnorsRsi::classic();
265 let mut b = ConnorsRsi::classic();
266 assert_eq!(
267 a.batch(&prices),
268 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
269 );
270 }
271
272 #[test]
273 fn reset_clears_state() {
274 let mut crsi = ConnorsRsi::classic();
275 let prices: Vec<f64> = (1..=200).map(f64::from).collect();
276 crsi.batch(&prices);
277 assert!(crsi.is_ready());
278 crsi.reset();
279 assert!(!crsi.is_ready());
280 assert_eq!(crsi.streak, 0.0);
281 assert!(crsi.prev_price.is_none());
282 }
283
284 #[test]
285 fn ignores_non_finite_input() {
286 let mut crsi = ConnorsRsi::classic();
287 let prices: Vec<f64> = (1..=200).map(f64::from).collect();
288 crsi.batch(&prices);
289 let before = crsi.current;
290 assert_eq!(crsi.update(f64::NAN), None);
291 assert_eq!(crsi.update(f64::INFINITY), None);
292 assert_eq!(crsi.current, before);
294 }
295
296 #[test]
297 fn zero_prev_skips_roc_update() {
298 let mut crsi = ConnorsRsi::new(3, 2, 4).unwrap();
303 crsi.update(0.0);
305 let after = crsi.update(1.0);
310 assert!(after.is_none(), "CRSI cannot emit on bar 2: {after:?}");
311 }
312}