wickra_core/indicators/
volatility_of_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)]
44pub struct VolatilityOfVolatility {
45 vol_window: usize,
46 vov_window: usize,
47 prev_price: Option<f64>,
48 returns: VecDeque<f64>,
50 ret_moments: ShiftedMoments,
51 vols: VecDeque<f64>,
53 vol_moments: ShiftedMoments,
54 last: Option<f64>,
55}
56
57impl VolatilityOfVolatility {
58 pub fn new(vol_window: usize, vov_window: usize) -> Result<Self> {
68 if vol_window == 0 || vov_window == 0 {
69 return Err(Error::PeriodZero);
70 }
71 if vol_window < 2 || vov_window < 2 {
72 return Err(Error::InvalidPeriod {
73 message: "vol-of-vol windows must both be >= 2",
74 });
75 }
76 Ok(Self {
77 vol_window,
78 vov_window,
79 prev_price: None,
80 returns: VecDeque::with_capacity(vol_window),
81 ret_moments: ShiftedMoments::new(),
82 vols: VecDeque::with_capacity(vov_window),
83 vol_moments: ShiftedMoments::new(),
84 last: None,
85 })
86 }
87
88 pub const fn windows(&self) -> (usize, usize) {
90 (self.vol_window, self.vov_window)
91 }
92
93 pub const fn value(&self) -> Option<f64> {
95 self.last
96 }
97}
98
99impl Indicator for VolatilityOfVolatility {
100 type Input = f64;
101 type Output = f64;
102
103 fn update(&mut self, input: f64) -> Option<f64> {
104 if !input.is_finite() || input <= 0.0 {
107 return None;
108 }
109 let Some(prev) = self.prev_price else {
110 self.prev_price = Some(input);
111 return None;
112 };
113 self.prev_price = Some(input);
114 let r = (input / prev).ln();
117
118 if self.returns.len() == self.vol_window {
120 let old = self.returns.pop_front().expect("returns window non-empty");
121 self.ret_moments.evict(old);
122 }
123 self.returns.push_back(r);
124 self.ret_moments.push(r);
125 if self.ret_moments.needs_reseed(self.vol_window) {
126 self.ret_moments.reseed(self.returns.iter().copied());
127 }
128 if self.returns.len() < self.vol_window {
129 return None;
130 }
131 let vol = self.ret_moments.sample_variance(self.vol_window).sqrt();
132
133 if self.vols.len() == self.vov_window {
135 let old = self.vols.pop_front().expect("vols window non-empty");
136 self.vol_moments.evict(old);
137 }
138 self.vols.push_back(vol);
139 self.vol_moments.push(vol);
140 if self.vol_moments.needs_reseed(self.vov_window) {
141 self.vol_moments.reseed(self.vols.iter().copied());
142 }
143 if self.vols.len() < self.vov_window {
144 return None;
145 }
146 let vov = self.vol_moments.sample_variance(self.vov_window).sqrt();
147 self.last = Some(vov);
148 Some(vov)
149 }
150
151 fn reset(&mut self) {
152 self.prev_price = None;
153 self.returns.clear();
154 self.ret_moments.reset();
155 self.vols.clear();
156 self.vol_moments.reset();
157 self.last = None;
158 }
159
160 #[inline]
161 fn warmup_period(&self) -> usize {
162 self.vol_window + self.vov_window
166 }
167
168 #[inline]
169 fn is_ready(&self) -> bool {
170 self.last.is_some()
171 }
172
173 #[inline]
174 fn name(&self) -> &'static str {
175 "VolatilityOfVolatility"
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::traits::BatchExt;
183 use crate::HistoricalVolatility;
184 use approx::assert_relative_eq;
185
186 #[test]
187 fn rejects_zero_window() {
188 assert!(matches!(
189 VolatilityOfVolatility::new(0, 10),
190 Err(Error::PeriodZero)
191 ));
192 assert!(matches!(
193 VolatilityOfVolatility::new(10, 0),
194 Err(Error::PeriodZero)
195 ));
196 }
197
198 #[test]
199 fn rejects_window_one() {
200 assert!(matches!(
201 VolatilityOfVolatility::new(1, 10),
202 Err(Error::InvalidPeriod { .. })
203 ));
204 assert!(matches!(
205 VolatilityOfVolatility::new(10, 1),
206 Err(Error::InvalidPeriod { .. })
207 ));
208 }
209
210 #[test]
211 fn accessors_and_metadata() {
212 let vov = VolatilityOfVolatility::new(20, 10).unwrap();
213 assert_eq!(vov.windows(), (20, 10));
214 assert_eq!(vov.warmup_period(), 30);
215 assert_eq!(vov.name(), "VolatilityOfVolatility");
216 assert!(!vov.is_ready());
217 assert_eq!(vov.value(), None);
218 }
219
220 #[test]
221 fn first_emission_at_warmup_period() {
222 let mut vov = VolatilityOfVolatility::new(3, 3).unwrap();
223 let prices: Vec<f64> = (1..=20)
224 .map(|i| 100.0 + (f64::from(i) * 0.7).sin() * 4.0)
225 .collect();
226 let out = vov.batch(&prices);
227 let warmup = vov.warmup_period(); for v in out.iter().take(warmup - 1) {
229 assert!(v.is_none());
230 }
231 assert!(out[warmup - 1].is_some());
232 }
233
234 #[test]
235 fn matches_two_stage_reference() {
236 let (vol_window, vov_window) = (3, 3);
239 let prices: Vec<f64> = [100.0, 102.0, 101.0, 104.0, 103.5, 106.0, 105.0, 108.0].to_vec();
240
241 let mut hv = HistoricalVolatility::new(vol_window, 1).unwrap();
242 let vol_series: Vec<f64> = hv
243 .batch(&prices)
244 .into_iter()
245 .flatten()
246 .map(|v| v / 100.0)
247 .collect();
248 let tail = &vol_series[vol_series.len() - vov_window..];
251 let n = vov_window as f64;
252 let mean = tail.iter().sum::<f64>() / n;
253 let expected =
254 (tail.iter().map(|v| (v - mean) * (v - mean)).sum::<f64>() / (n - 1.0)).sqrt();
255
256 let mut vov = VolatilityOfVolatility::new(vol_window, vov_window).unwrap();
257 let out = vov.batch(&prices);
258 assert_relative_eq!(out.last().unwrap().unwrap(), expected, epsilon = 1e-9);
259 }
260
261 #[test]
262 fn constant_series_yields_zero() {
263 let mut vov = VolatilityOfVolatility::new(5, 5).unwrap();
264 for v in vov.batch(&[100.0; 60]).into_iter().flatten() {
265 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
266 }
267 }
268
269 #[test]
270 fn output_is_non_negative() {
271 let mut vov = VolatilityOfVolatility::new(10, 10).unwrap();
272 let prices: Vec<f64> = (1..=300)
273 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 12.0)
274 .collect();
275 for v in vov.batch(&prices).into_iter().flatten() {
276 assert!(v >= 0.0, "vol-of-vol must be non-negative, got {v}");
277 }
278 }
279
280 #[test]
281 fn ignores_non_finite_input() {
282 let mut vov = VolatilityOfVolatility::new(3, 3).unwrap();
283 let out = vov.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
284 let last = *out.last().unwrap();
285 assert!(last.is_some());
286 assert_eq!(vov.update(f64::NAN), None);
287 assert_eq!(vov.update(f64::INFINITY), None);
288 }
289
290 #[test]
291 fn skips_non_positive_prices() {
292 let mut vov = VolatilityOfVolatility::new(3, 3).unwrap();
293 let warmup = vov.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
294 warmup.last().copied().flatten().expect("warmed up");
295 assert_eq!(vov.update(-5.0), None);
296 assert_eq!(vov.update(0.0), None);
297 let mut control = vov.clone();
299 let after = vov.update(41.0).expect("ready");
300 assert_eq!(control.update(41.0).expect("ready"), after);
301 }
302
303 #[test]
304 fn reset_clears_state() {
305 let mut vov = VolatilityOfVolatility::new(3, 3).unwrap();
306 vov.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
307 assert!(vov.is_ready());
308 vov.reset();
309 assert!(!vov.is_ready());
310 assert_eq!(vov.value(), None);
311 assert_eq!(vov.update(1.0), None);
312 }
313
314 #[test]
315 fn batch_equals_streaming() {
316 let prices: Vec<f64> = (1..=200)
317 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
318 .collect();
319 let batch = VolatilityOfVolatility::new(10, 10).unwrap().batch(&prices);
320 let mut b = VolatilityOfVolatility::new(10, 10).unwrap();
321 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
322 assert_eq!(batch, streamed);
323 }
324}