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)]
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 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}