wickra_core/indicators/
rvi_volatility.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rolling_moments::ShiftedMoments;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
43pub struct RviVolatility {
44 period: usize,
45 window: VecDeque<f64>,
47 moments: ShiftedMoments,
48 prev_close: Option<f64>,
50 seed_up: Vec<f64>,
52 seed_down: Vec<f64>,
53 avg_up: Option<f64>,
54 avg_down: Option<f64>,
55 last_value: Option<f64>,
56}
57
58impl RviVolatility {
59 pub fn new(period: usize) -> Result<Self> {
71 if period == 0 {
72 return Err(Error::PeriodZero);
73 }
74 if period > crate::error::MAX_PERIOD {
75 return Err(Error::InvalidPeriod {
76 message: crate::error::PERIOD_ABOVE_MAX,
77 });
78 }
79 if period < 2 {
80 return Err(Error::InvalidPeriod {
81 message: "RVI period must be >= 2",
82 });
83 }
84 Ok(Self {
85 period,
86 window: VecDeque::with_capacity(period),
87 moments: ShiftedMoments::new(),
88 prev_close: None,
89 seed_up: Vec::with_capacity(period),
90 seed_down: Vec::with_capacity(period),
91 avg_up: None,
92 avg_down: None,
93 last_value: None,
94 })
95 }
96
97 pub const fn period(&self) -> usize {
99 self.period
100 }
101
102 pub const fn value(&self) -> Option<f64> {
104 self.last_value
105 }
106
107 fn ratio(avg_up: f64, avg_down: f64) -> f64 {
108 let denom = avg_up + avg_down;
109 if denom == 0.0 {
110 50.0
112 } else {
113 100.0 * avg_up / denom
114 }
115 }
116}
117
118impl Indicator for RviVolatility {
119 type Input = f64;
120 type Output = f64;
121
122 fn update(&mut self, input: f64) -> Option<f64> {
123 if !input.is_finite() {
124 return None;
126 }
127
128 if self.window.len() == self.period {
130 let old = self.window.pop_front().expect("window is non-empty");
131 self.moments.evict(old);
132 }
133 self.window.push_back(input);
134 self.moments.push(input);
135 if self.moments.needs_reseed(self.period) {
136 self.moments.reseed(self.window.iter().copied());
137 }
138
139 if self.window.len() < self.period {
140 self.prev_close = Some(input);
143 return None;
144 }
145
146 let n = self.period as f64;
147 let sd = self.moments.std_dev(self.period);
148
149 let prev = self
151 .prev_close
152 .expect("prev_close is set on every input before this point");
153 let (up, down) = if input > prev {
154 (sd, 0.0)
155 } else if input < prev {
156 (0.0, sd)
157 } else {
158 (0.0, 0.0)
159 };
160 self.prev_close = Some(input);
161
162 if let (Some(au), Some(ad)) = (self.avg_up, self.avg_down) {
164 let new_au = au.mul_add(n - 1.0, up) / n;
165 let new_ad = ad.mul_add(n - 1.0, down) / n;
166 self.avg_up = Some(new_au);
167 self.avg_down = Some(new_ad);
168 let v = Self::ratio(new_au, new_ad);
169 self.last_value = Some(v);
170 return Some(v);
171 }
172
173 self.seed_up.push(up);
174 self.seed_down.push(down);
175 if self.seed_up.len() == self.period {
176 let au = self.seed_up.iter().sum::<f64>() / n;
177 let ad = self.seed_down.iter().sum::<f64>() / n;
178 self.avg_up = Some(au);
179 self.avg_down = Some(ad);
180 let v = Self::ratio(au, ad);
181 self.last_value = Some(v);
182 return Some(v);
183 }
184 None
185 }
186
187 fn reset(&mut self) {
188 self.window.clear();
189 self.moments.reset();
190 self.prev_close = None;
191 self.seed_up.clear();
192 self.seed_down.clear();
193 self.avg_up = None;
194 self.avg_down = None;
195 self.last_value = None;
196 }
197
198 #[inline]
199 fn warmup_period(&self) -> usize {
200 2 * self.period - 1
207 }
208
209 #[inline]
210 fn is_ready(&self) -> bool {
211 self.last_value.is_some()
212 }
213
214 #[inline]
215 fn name(&self) -> &'static str {
216 "RVIVolatility"
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::traits::BatchExt;
224 use approx::assert_relative_eq;
225
226 #[test]
227 fn rejects_zero_period() {
228 assert!(matches!(RviVolatility::new(0), Err(Error::PeriodZero)));
229 }
230
231 #[test]
232 fn rejects_period_one() {
233 assert!(matches!(
234 RviVolatility::new(1),
235 Err(Error::InvalidPeriod { .. })
236 ));
237 }
238
239 #[test]
240 fn accessors_and_metadata() {
241 let rvi = RviVolatility::new(14).unwrap();
242 assert_eq!(rvi.period(), 14);
243 assert_eq!(rvi.name(), "RVIVolatility");
244 assert_eq!(rvi.value(), None);
245 assert_eq!(rvi.warmup_period(), 27);
246 assert!(!rvi.is_ready());
247 }
248
249 #[test]
250 fn constant_series_yields_fifty() {
251 let mut rvi = RviVolatility::new(5).unwrap();
255 let out = rvi.batch(&[42.0; 40]);
256 for v in out.iter().skip(9).flatten() {
257 assert_relative_eq!(*v, 50.0, epsilon = 1e-12);
258 }
259 }
260
261 #[test]
262 fn pure_uptrend_saturates_to_one_hundred() {
263 let mut rvi = RviVolatility::new(5).unwrap();
266 let prices: Vec<f64> = (1..=40).map(f64::from).collect();
267 let out = rvi.batch(&prices);
268 for v in out.iter().skip(9).flatten() {
269 assert_relative_eq!(*v, 100.0, epsilon = 1e-9);
270 }
271 }
272
273 #[test]
274 fn pure_downtrend_saturates_to_zero() {
275 let mut rvi = RviVolatility::new(5).unwrap();
276 let prices: Vec<f64> = (1..=40).rev().map(f64::from).collect();
277 let out = rvi.batch(&prices);
278 for v in out.iter().skip(9).flatten() {
279 assert_relative_eq!(*v, 0.0, epsilon = 1e-9);
280 }
281 }
282
283 #[test]
284 fn output_is_bounded() {
285 let mut rvi = RviVolatility::new(10).unwrap();
286 let prices: Vec<f64> = (0..200)
287 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 12.0)
288 .collect();
289 for v in rvi.batch(&prices).into_iter().flatten() {
290 assert!((0.0..=100.0).contains(&v), "RVI out of range: {v}");
291 }
292 }
293
294 #[test]
295 fn first_emission_at_warmup_period() {
296 let mut rvi = RviVolatility::new(5).unwrap();
297 assert_eq!(rvi.warmup_period(), 9);
298 let prices: Vec<f64> = (0..30)
299 .map(|i| 100.0 + (f64::from(i) * 0.4).sin() * 3.0)
300 .collect();
301 let out = rvi.batch(&prices);
302 for v in out.iter().take(8) {
303 assert!(v.is_none(), "indicator must still be warming up");
304 }
305 assert!(
306 out[8].is_some(),
307 "first value lands at warmup_period - 1 = 8"
308 );
309 }
310
311 #[test]
312 fn batch_equals_streaming() {
313 let prices: Vec<f64> = (0..120)
314 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
315 .collect();
316 let batch = RviVolatility::new(10).unwrap().batch(&prices);
317 let mut streamer = RviVolatility::new(10).unwrap();
318 let streamed: Vec<_> = prices.iter().map(|p| streamer.update(*p)).collect();
319 assert_eq!(batch, streamed);
320 }
321
322 #[test]
323 fn reset_clears_state() {
324 let mut rvi = RviVolatility::new(5).unwrap();
325 rvi.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
326 assert!(rvi.is_ready());
327 rvi.reset();
328 assert!(!rvi.is_ready());
329 assert_eq!(rvi.value(), None);
330 assert_eq!(rvi.update(1.0), None);
331 }
332
333 #[test]
334 fn ignores_non_finite_input() {
335 let mut rvi = RviVolatility::new(5).unwrap();
336 let out = rvi.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
337 let last = *out.last().unwrap();
338 assert!(last.is_some());
339 assert_eq!(rvi.update(f64::NAN), None);
340 assert_eq!(rvi.update(f64::INFINITY), None);
341 }
342}