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