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