wickra_core/indicators/
anchored_rsi.rs1use crate::traits::Indicator;
4
5#[derive(Debug, Clone, Default)]
48pub struct AnchoredRsi {
49 prev_close: Option<f64>,
50 sum_gain: f64,
51 sum_loss: f64,
52 last_value: Option<f64>,
53 pending_anchor: bool,
54}
55
56impl AnchoredRsi {
57 pub const fn new() -> Self {
59 Self {
60 prev_close: None,
61 sum_gain: 0.0,
62 sum_loss: 0.0,
63 last_value: None,
64 pending_anchor: false,
65 }
66 }
67
68 pub fn set_anchor(&mut self) {
72 self.pending_anchor = true;
73 }
74
75 pub const fn value(&self) -> Option<f64> {
78 self.last_value
79 }
80
81 fn rsi_from_sums(sum_gain: f64, sum_loss: f64) -> f64 {
82 if sum_loss == 0.0 {
83 if sum_gain == 0.0 {
84 50.0
86 } else {
87 100.0
88 }
89 } else {
90 let rs = sum_gain / sum_loss;
91 100.0 - 100.0 / (1.0 + rs)
92 }
93 }
94}
95
96impl Indicator for AnchoredRsi {
97 type Input = f64;
98 type Output = f64;
99
100 fn update(&mut self, input: f64) -> Option<f64> {
101 if !input.is_finite() {
102 return self.last_value;
103 }
104
105 if self.pending_anchor {
106 self.prev_close = None;
107 self.sum_gain = 0.0;
108 self.sum_loss = 0.0;
109 self.last_value = None;
110 self.pending_anchor = false;
111 }
112
113 let Some(prev) = self.prev_close else {
114 self.prev_close = Some(input);
115 return None;
116 };
117 self.prev_close = Some(input);
118
119 let diff = input - prev;
120 if diff > 0.0 {
121 self.sum_gain += diff;
122 } else if diff < 0.0 {
123 self.sum_loss -= diff;
124 }
125
126 let value = Self::rsi_from_sums(self.sum_gain, self.sum_loss);
127 self.last_value = Some(value);
128 Some(value)
129 }
130
131 fn reset(&mut self) {
132 self.prev_close = None;
133 self.sum_gain = 0.0;
134 self.sum_loss = 0.0;
135 self.last_value = None;
136 self.pending_anchor = false;
137 }
138
139 fn warmup_period(&self) -> usize {
140 2
141 }
142
143 fn is_ready(&self) -> bool {
144 self.last_value.is_some()
145 }
146
147 fn name(&self) -> &'static str {
148 "AnchoredRSI"
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::traits::BatchExt;
156 use approx::assert_relative_eq;
157
158 #[test]
159 fn accessors_and_metadata() {
160 let indicator = AnchoredRsi::new();
161 assert_eq!(indicator.name(), "AnchoredRSI");
162 assert_eq!(indicator.warmup_period(), 2);
163 assert_eq!(indicator.value(), None);
164 assert!(!indicator.is_ready());
165 }
166
167 #[test]
168 fn first_bar_seeds_and_returns_none() {
169 let mut indicator = AnchoredRsi::new();
170 assert_eq!(indicator.update(100.0), None);
171 assert!(!indicator.is_ready());
172 assert!(indicator.update(101.0).is_some());
174 assert!(indicator.is_ready());
175 }
176
177 #[test]
178 fn pure_uptrend_saturates_at_100() {
179 let mut indicator = AnchoredRsi::new();
180 let out = indicator.batch(&[10.0, 11.0, 12.0, 13.0]);
181 assert_relative_eq!(out[3].unwrap(), 100.0, epsilon = 1e-12);
182 }
183
184 #[test]
185 fn pure_downtrend_saturates_at_0() {
186 let mut indicator = AnchoredRsi::new();
187 let out = indicator.batch(&[13.0, 12.0, 11.0, 10.0]);
188 assert_relative_eq!(out[3].unwrap(), 0.0, epsilon = 1e-12);
189 }
190
191 #[test]
192 fn flat_window_reads_50() {
193 let mut indicator = AnchoredRsi::new();
194 let out = indicator.batch(&[42.0, 42.0, 42.0]);
195 assert_relative_eq!(out[2].unwrap(), 50.0, epsilon = 1e-12);
196 }
197
198 #[test]
199 fn cumulative_reference_values() {
200 let mut indicator = AnchoredRsi::new();
204 let out = indicator.batch(&[10.0, 11.0, 9.0, 12.0]);
205 assert_relative_eq!(out[1].unwrap(), 100.0, epsilon = 1e-9);
206 assert_relative_eq!(out[2].unwrap(), 33.333_333_333, epsilon = 1e-6);
207 assert_relative_eq!(out[3].unwrap(), 66.666_666_666, epsilon = 1e-6);
208 }
209
210 #[test]
211 fn set_anchor_clears_old_window() {
212 let mut indicator = AnchoredRsi::new();
215 indicator.batch(&[20.0, 19.0, 18.0, 17.0]);
216 assert_relative_eq!(indicator.value().unwrap(), 0.0, epsilon = 1e-12);
217 indicator.set_anchor();
218 assert_eq!(indicator.update(50.0), None);
220 let after = indicator.update(51.0).unwrap();
221 assert_relative_eq!(after, 100.0, epsilon = 1e-12);
222 }
223
224 #[test]
225 fn set_anchor_before_first_bar_acts_as_normal_start() {
226 let mut indicator = AnchoredRsi::new();
227 indicator.set_anchor();
228 assert_eq!(indicator.update(10.0), None);
229 assert_relative_eq!(indicator.update(11.0).unwrap(), 100.0, epsilon = 1e-12);
230 }
231
232 #[test]
233 fn ignores_non_finite_input() {
234 let mut indicator = AnchoredRsi::new();
235 indicator.batch(&[10.0, 11.0, 12.0]);
236 let before = indicator.value();
237 assert!(before.is_some());
238 assert_eq!(indicator.update(f64::NAN), before);
239 assert_eq!(indicator.update(f64::INFINITY), before);
240 assert_eq!(indicator.value(), before);
241 }
242
243 #[test]
244 fn non_finite_before_any_bar_returns_none() {
245 let mut indicator = AnchoredRsi::new();
246 assert_eq!(indicator.update(f64::NAN), None);
247 assert!(!indicator.is_ready());
248 }
249
250 #[test]
251 fn reset_clears_state() {
252 let mut indicator = AnchoredRsi::new();
253 indicator.batch(&[10.0, 11.0, 12.0]);
254 assert!(indicator.is_ready());
255 indicator.reset();
256 assert!(!indicator.is_ready());
257 assert_eq!(indicator.value(), None);
258 assert_eq!(indicator.update(50.0), None);
259 }
260
261 #[test]
262 fn stays_in_0_100_range() {
263 let prices: Vec<f64> = (0..200)
264 .map(|i| 100.0 + (f64::from(i) * 0.7).sin() * 10.0)
265 .collect();
266 let mut indicator = AnchoredRsi::new();
267 for value in indicator.batch(&prices).into_iter().flatten() {
268 assert!((0.0..=100.0).contains(&value), "RSI out of range: {value}");
269 }
270 }
271
272 #[test]
273 fn batch_equals_streaming() {
274 let prices: Vec<f64> = (1..=40)
275 .map(|i| (f64::from(i) * 0.3).sin() * 5.0 + f64::from(i))
276 .collect();
277 let mut a = AnchoredRsi::new();
278 let mut b = AnchoredRsi::new();
279 assert_eq!(
280 a.batch(&prices),
281 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
282 );
283 }
284}