wickra_core/indicators/
realized_volatility.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rolling_moments::RollingSum;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
47pub struct RealizedVolatility {
48 period: usize,
49 prev_price: Option<f64>,
50 window: VecDeque<f64>,
52 sum_sq: RollingSum,
53 last: Option<f64>,
54}
55
56impl RealizedVolatility {
57 pub fn new(period: usize) -> Result<Self> {
64 if period == 0 {
65 return Err(Error::PeriodZero);
66 }
67 if period > crate::error::MAX_PERIOD {
68 return Err(Error::InvalidPeriod {
69 message: crate::error::PERIOD_ABOVE_MAX,
70 });
71 }
72 Ok(Self {
73 period,
74 prev_price: None,
75 window: VecDeque::with_capacity(period),
76 sum_sq: RollingSum::new(),
77 last: None,
78 })
79 }
80
81 pub const fn period(&self) -> usize {
83 self.period
84 }
85}
86
87impl Indicator for RealizedVolatility {
88 type Input = f64;
89 type Output = f64;
90
91 #[inline]
92 fn update(&mut self, input: f64) -> Option<f64> {
93 if !input.is_finite() || input <= 0.0 {
96 return None;
97 }
98 let Some(prev) = self.prev_price else {
99 self.prev_price = Some(input);
100 return None;
101 };
102 self.prev_price = Some(input);
103 let r = (input / prev).ln();
106 if self.window.len() == self.period {
107 let old = self.window.pop_front().expect("window is non-empty");
108 self.sum_sq.evict(old * old);
109 }
110 self.window.push_back(r);
111 self.sum_sq.push(r * r);
112 if self.sum_sq.needs_reseed(self.period) {
113 self.sum_sq.reseed(self.window.iter().map(|v| v * v));
114 }
115 if self.window.len() < self.period {
116 return None;
117 }
118 let rv = self.sum_sq.value().max(0.0).sqrt();
121 self.last = Some(rv);
122 Some(rv)
123 }
124
125 fn reset(&mut self) {
126 self.prev_price = None;
127 self.window.clear();
128 self.sum_sq.reset();
129 self.last = None;
130 }
131
132 #[inline]
133 fn warmup_period(&self) -> usize {
134 self.period + 1
136 }
137
138 #[inline]
139 fn is_ready(&self) -> bool {
140 self.last.is_some()
141 }
142
143 #[inline]
144 fn name(&self) -> &'static str {
145 "RealizedVolatility"
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::traits::BatchExt;
153 use approx::assert_relative_eq;
154
155 #[test]
156 fn rejects_zero_period() {
157 assert!(matches!(RealizedVolatility::new(0), Err(Error::PeriodZero)));
158 }
159
160 #[test]
161 fn accessors_and_metadata() {
162 let rv = RealizedVolatility::new(20).unwrap();
163 assert_eq!(rv.period(), 20);
164 assert_eq!(rv.warmup_period(), 21);
165 assert_eq!(rv.name(), "RealizedVolatility");
166 assert!(!rv.is_ready());
167 }
168
169 #[test]
170 fn first_emission_at_warmup_period() {
171 let mut rv = RealizedVolatility::new(5).unwrap();
172 let out = rv.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
173 for v in out.iter().take(5) {
174 assert!(v.is_none());
175 }
176 assert!(out[5].is_some());
177 }
178
179 #[test]
180 fn known_value() {
181 let mut rv = RealizedVolatility::new(2).unwrap();
183 let out = rv.batch(&[100.0, 110.0, 121.0]);
184 let expected = (2.0 * (1.1_f64).ln().powi(2)).sqrt();
185 assert_relative_eq!(out[2].unwrap(), expected, epsilon = 1e-12);
186 }
187
188 #[test]
189 fn constant_series_yields_zero() {
190 let mut rv = RealizedVolatility::new(10).unwrap();
191 for v in rv.batch(&[100.0; 40]).into_iter().flatten() {
192 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
193 }
194 }
195
196 #[test]
197 fn output_is_non_negative() {
198 let mut rv = RealizedVolatility::new(20).unwrap();
199 let prices: Vec<f64> = (1..=200)
200 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 12.0)
201 .collect();
202 for v in rv.batch(&prices).into_iter().flatten() {
203 assert!(
204 v >= 0.0,
205 "realized volatility must be non-negative, got {v}"
206 );
207 }
208 }
209
210 #[test]
211 fn ignores_non_finite_input() {
212 let mut rv = RealizedVolatility::new(5).unwrap();
213 let out = rv.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
214 let last = *out.last().unwrap();
215 assert!(last.is_some());
216 assert_eq!(rv.update(f64::NAN), None);
217 assert_eq!(rv.update(f64::INFINITY), None);
218 }
219
220 #[test]
221 fn skips_non_positive_prices() {
222 let mut rv = RealizedVolatility::new(5).unwrap();
223 let warmup = rv.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
224 warmup.last().copied().flatten().expect("warmed up");
225 assert_eq!(rv.update(-5.0), None);
226 assert_eq!(rv.update(0.0), None);
227 let mut control = rv.clone();
229 let after = rv.update(21.0).expect("ready");
230 assert_eq!(control.update(21.0).expect("ready"), after);
231 }
232
233 #[test]
234 fn reset_clears_state() {
235 let mut rv = RealizedVolatility::new(5).unwrap();
236 rv.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
237 assert!(rv.is_ready());
238 rv.reset();
239 assert!(!rv.is_ready());
240 assert_eq!(rv.update(1.0), None);
241 }
242
243 #[test]
244 fn batch_equals_streaming() {
245 let prices: Vec<f64> = (1..=120)
246 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
247 .collect();
248 let batch = RealizedVolatility::new(20).unwrap().batch(&prices);
249 let mut b = RealizedVolatility::new(20).unwrap();
250 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
251 assert_eq!(batch, streamed);
252 }
253}