wickra_core/indicators/
adaptive_rsi.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
44pub struct AdaptiveRsi {
45 period: usize,
46 prices: VecDeque<f64>,
47 abs_changes: VecDeque<f64>,
48 abs_sum: f64,
49 prev: Option<f64>,
50 seed_gain: f64,
51 seed_loss: f64,
52 seed_count: usize,
53 avg_gain: Option<f64>,
54 avg_loss: Option<f64>,
55 last: Option<f64>,
56}
57
58impl AdaptiveRsi {
59 pub fn new(period: usize) -> Result<Self> {
65 if period == 0 {
66 return Err(Error::PeriodZero);
67 }
68 Ok(Self {
69 period,
70 prices: VecDeque::with_capacity(period + 1),
71 abs_changes: VecDeque::with_capacity(period),
72 abs_sum: 0.0,
73 prev: None,
74 seed_gain: 0.0,
75 seed_loss: 0.0,
76 seed_count: 0,
77 avg_gain: None,
78 avg_loss: None,
79 last: None,
80 })
81 }
82
83 pub const fn period(&self) -> usize {
85 self.period
86 }
87
88 pub const fn value(&self) -> Option<f64> {
90 self.last
91 }
92
93 fn rsi_from_avgs(avg_gain: f64, avg_loss: f64) -> f64 {
94 let denom = avg_gain + avg_loss;
95 if denom == 0.0 {
96 50.0
97 } else {
98 100.0 * (avg_gain / denom)
99 }
100 }
101
102 fn efficiency_ratio(&self, price: f64) -> f64 {
103 let oldest = *self.prices.front().expect("window non-empty");
104 let direction = (price - oldest).abs();
105 if self.abs_sum == 0.0 {
106 0.0
107 } else {
108 (direction / self.abs_sum).clamp(0.0, 1.0)
109 }
110 }
111}
112
113impl Indicator for AdaptiveRsi {
114 type Input = f64;
115 type Output = f64;
116
117 fn update(&mut self, price: f64) -> Option<f64> {
118 if !price.is_finite() {
119 return self.last;
120 }
121 let Some(prev) = self.prev else {
122 self.prev = Some(price);
123 self.prices.push_back(price);
124 return None;
125 };
126 let change = price - prev;
127 self.prev = Some(price);
128 let gain = if change > 0.0 { change } else { 0.0 };
129 let loss = if change < 0.0 { -change } else { 0.0 };
130
131 self.prices.push_back(price);
133 if self.prices.len() > self.period + 1 {
134 self.prices.pop_front();
135 }
136 if self.abs_changes.len() == self.period {
137 self.abs_sum -= self.abs_changes.pop_front().expect("non-empty");
138 }
139 self.abs_changes.push_back(change.abs());
140 self.abs_sum += change.abs();
141
142 if let (Some(ag), Some(al)) = (self.avg_gain, self.avg_loss) {
143 let er = self.efficiency_ratio(price);
144 let fast = 2.0 / 3.0;
145 let slow = 2.0 / 31.0;
146 let sc = (er * (fast - slow) + slow).powi(2);
147 let new_ag = ag + sc * (gain - ag);
148 let new_al = al + sc * (loss - al);
149 self.avg_gain = Some(new_ag);
150 self.avg_loss = Some(new_al);
151 let v = Self::rsi_from_avgs(new_ag, new_al);
152 self.last = Some(v);
153 return Some(v);
154 }
155
156 self.seed_gain += gain;
157 self.seed_loss += loss;
158 self.seed_count += 1;
159 if self.seed_count == self.period {
160 let ag = self.seed_gain / self.period as f64;
161 let al = self.seed_loss / self.period as f64;
162 self.avg_gain = Some(ag);
163 self.avg_loss = Some(al);
164 let v = Self::rsi_from_avgs(ag, al);
165 self.last = Some(v);
166 return Some(v);
167 }
168 None
169 }
170
171 fn reset(&mut self) {
172 self.prices.clear();
173 self.abs_changes.clear();
174 self.abs_sum = 0.0;
175 self.prev = None;
176 self.seed_gain = 0.0;
177 self.seed_loss = 0.0;
178 self.seed_count = 0;
179 self.avg_gain = None;
180 self.avg_loss = None;
181 self.last = None;
182 }
183
184 fn warmup_period(&self) -> usize {
185 self.period + 1
186 }
187
188 fn is_ready(&self) -> bool {
189 self.last.is_some()
190 }
191
192 fn name(&self) -> &'static str {
193 "AdaptiveRsi"
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::traits::BatchExt;
201 use approx::assert_relative_eq;
202
203 #[test]
204 fn rejects_zero_period() {
205 assert!(matches!(AdaptiveRsi::new(0), Err(Error::PeriodZero)));
206 }
207
208 #[test]
209 fn accessors_and_metadata() {
210 let r = AdaptiveRsi::new(14).unwrap();
211 assert_eq!(r.period(), 14);
212 assert_eq!(r.warmup_period(), 15);
213 assert_eq!(r.name(), "AdaptiveRsi");
214 assert!(!r.is_ready());
215 assert_eq!(r.value(), None);
216 }
217
218 #[test]
219 fn first_emission_at_warmup_period() {
220 let mut r = AdaptiveRsi::new(4).unwrap();
221 let out = r.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
222 for v in out.iter().take(4) {
223 assert!(v.is_none());
224 }
225 assert!(out[4].is_some());
226 }
227
228 #[test]
229 fn pure_uptrend_is_one_hundred() {
230 let mut r = AdaptiveRsi::new(5).unwrap();
231 let last = r
232 .batch(&(1..=40).map(f64::from).collect::<Vec<_>>())
233 .into_iter()
234 .flatten()
235 .last()
236 .unwrap();
237 assert_relative_eq!(last, 100.0, epsilon = 1e-9);
238 }
239
240 #[test]
241 fn flat_market_is_neutral() {
242 let mut r = AdaptiveRsi::new(4).unwrap();
243 let last = r.batch(&[7.0; 20]).into_iter().flatten().last().unwrap();
244 assert_relative_eq!(last, 50.0, epsilon = 1e-9);
245 }
246
247 #[test]
248 fn output_in_range() {
249 let mut r = AdaptiveRsi::new(14).unwrap();
250 for v in r
251 .batch(
252 &(0..200)
253 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 8.0)
254 .collect::<Vec<_>>(),
255 )
256 .into_iter()
257 .flatten()
258 {
259 assert!((0.0..=100.0).contains(&v));
260 }
261 }
262
263 #[test]
264 fn ignores_non_finite() {
265 let mut r = AdaptiveRsi::new(4).unwrap();
266 let ready = r
267 .batch(&[1.0, 2.0, 3.0, 4.0, 5.0])
268 .into_iter()
269 .flatten()
270 .last()
271 .unwrap();
272 assert_eq!(r.update(f64::NAN), Some(ready));
273 }
274
275 #[test]
276 fn reset_clears_state() {
277 let mut r = AdaptiveRsi::new(4).unwrap();
278 r.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
279 assert!(r.is_ready());
280 r.reset();
281 assert!(!r.is_ready());
282 assert_eq!(r.value(), None);
283 assert_eq!(r.update(1.0), None);
284 }
285
286 #[test]
287 fn batch_equals_streaming() {
288 let xs: Vec<f64> = (0..120)
289 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
290 .collect();
291 let batch = AdaptiveRsi::new(14).unwrap().batch(&xs);
292 let mut b = AdaptiveRsi::new(14).unwrap();
293 let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
294 assert_eq!(batch, streamed);
295 }
296}