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 #[inline]
101 fn update(&mut self, input: f64) -> Option<f64> {
102 if !input.is_finite() {
103 return None;
104 }
105
106 if self.pending_anchor {
107 self.prev_close = None;
108 self.sum_gain = 0.0;
109 self.sum_loss = 0.0;
110 self.last_value = None;
111 self.pending_anchor = false;
112 }
113
114 let Some(prev) = self.prev_close else {
115 self.prev_close = Some(input);
116 return None;
117 };
118 self.prev_close = Some(input);
119
120 let diff = input - prev;
121 if diff > 0.0 {
122 self.sum_gain += diff;
123 } else if diff < 0.0 {
124 self.sum_loss -= diff;
125 }
126
127 let value = Self::rsi_from_sums(self.sum_gain, self.sum_loss);
128 self.last_value = Some(value);
129 Some(value)
130 }
131
132 fn reset(&mut self) {
133 self.prev_close = None;
134 self.sum_gain = 0.0;
135 self.sum_loss = 0.0;
136 self.last_value = None;
137 self.pending_anchor = false;
138 }
139
140 #[inline]
141 fn warmup_period(&self) -> usize {
142 2
143 }
144
145 #[inline]
146 fn is_ready(&self) -> bool {
147 self.last_value.is_some()
148 }
149
150 #[inline]
151 fn name(&self) -> &'static str {
152 "AnchoredRSI"
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::traits::BatchExt;
160 use approx::assert_relative_eq;
161
162 #[test]
163 fn accessors_and_metadata() {
164 let indicator = AnchoredRsi::new();
165 assert_eq!(indicator.name(), "AnchoredRSI");
166 assert_eq!(indicator.warmup_period(), 2);
167 assert_eq!(indicator.value(), None);
168 assert!(!indicator.is_ready());
169 }
170
171 #[test]
172 fn first_bar_seeds_and_returns_none() {
173 let mut indicator = AnchoredRsi::new();
174 assert_eq!(indicator.update(100.0), None);
175 assert!(!indicator.is_ready());
176 assert!(indicator.update(101.0).is_some());
178 assert!(indicator.is_ready());
179 }
180
181 #[test]
182 fn pure_uptrend_saturates_at_100() {
183 let mut indicator = AnchoredRsi::new();
184 let out = indicator.batch(&[10.0, 11.0, 12.0, 13.0]);
185 assert_relative_eq!(out[3].unwrap(), 100.0, epsilon = 1e-12);
186 }
187
188 #[test]
189 fn pure_downtrend_saturates_at_0() {
190 let mut indicator = AnchoredRsi::new();
191 let out = indicator.batch(&[13.0, 12.0, 11.0, 10.0]);
192 assert_relative_eq!(out[3].unwrap(), 0.0, epsilon = 1e-12);
193 }
194
195 #[test]
196 fn flat_window_reads_50() {
197 let mut indicator = AnchoredRsi::new();
198 let out = indicator.batch(&[42.0, 42.0, 42.0]);
199 assert_relative_eq!(out[2].unwrap(), 50.0, epsilon = 1e-12);
200 }
201
202 #[test]
203 fn cumulative_reference_values() {
204 let mut indicator = AnchoredRsi::new();
208 let out = indicator.batch(&[10.0, 11.0, 9.0, 12.0]);
209 assert_relative_eq!(out[1].unwrap(), 100.0, epsilon = 1e-9);
210 assert_relative_eq!(out[2].unwrap(), 33.333_333_333, epsilon = 1e-6);
211 assert_relative_eq!(out[3].unwrap(), 66.666_666_666, epsilon = 1e-6);
212 }
213
214 #[test]
215 fn set_anchor_clears_old_window() {
216 let mut indicator = AnchoredRsi::new();
219 indicator.batch(&[20.0, 19.0, 18.0, 17.0]);
220 assert_relative_eq!(indicator.value().unwrap(), 0.0, epsilon = 1e-12);
221 indicator.set_anchor();
222 assert_eq!(indicator.update(50.0), None);
224 let after = indicator.update(51.0).unwrap();
225 assert_relative_eq!(after, 100.0, epsilon = 1e-12);
226 }
227
228 #[test]
229 fn set_anchor_before_first_bar_acts_as_normal_start() {
230 let mut indicator = AnchoredRsi::new();
231 indicator.set_anchor();
232 assert_eq!(indicator.update(10.0), None);
233 assert_relative_eq!(indicator.update(11.0).unwrap(), 100.0, epsilon = 1e-12);
234 }
235
236 #[test]
237 fn ignores_non_finite_input() {
238 let mut indicator = AnchoredRsi::new();
239 indicator.batch(&[10.0, 11.0, 12.0]);
240 let before = indicator.value();
241 assert!(before.is_some());
242 assert_eq!(indicator.update(f64::NAN), None);
243 assert_eq!(indicator.update(f64::INFINITY), None);
244 assert_eq!(indicator.value(), before);
245 }
246
247 #[test]
248 fn non_finite_before_any_bar_returns_none() {
249 let mut indicator = AnchoredRsi::new();
250 assert_eq!(indicator.update(f64::NAN), None);
251 assert!(!indicator.is_ready());
252 }
253
254 #[test]
255 fn reset_clears_state() {
256 let mut indicator = AnchoredRsi::new();
257 indicator.batch(&[10.0, 11.0, 12.0]);
258 assert!(indicator.is_ready());
259 indicator.reset();
260 assert!(!indicator.is_ready());
261 assert_eq!(indicator.value(), None);
262 assert_eq!(indicator.update(50.0), None);
263 }
264
265 #[test]
266 fn stays_in_0_100_range() {
267 let prices: Vec<f64> = (0..200)
268 .map(|i| 100.0 + (f64::from(i) * 0.7).sin() * 10.0)
269 .collect();
270 let mut indicator = AnchoredRsi::new();
271 for value in indicator.batch(&prices).into_iter().flatten() {
272 assert!((0.0..=100.0).contains(&value), "RSI out of range: {value}");
273 }
274 }
275
276 #[test]
277 fn batch_equals_streaming() {
278 let prices: Vec<f64> = (1..=40)
279 .map(|i| (f64::from(i) * 0.3).sin() * 5.0 + f64::from(i))
280 .collect();
281 let mut a = AnchoredRsi::new();
282 let mut b = AnchoredRsi::new();
283 assert_eq!(
284 a.batch(&prices),
285 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
286 );
287 }
288}