wickra_core/indicators/
historical_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)]
38pub struct HistoricalVolatility {
39 period: usize,
40 trading_periods: usize,
41 prev_price: Option<f64>,
42 window: VecDeque<f64>,
44 moments: ShiftedMoments,
45 last: Option<f64>,
46}
47
48impl HistoricalVolatility {
49 pub fn new(period: usize, trading_periods: usize) -> Result<Self> {
61 if period == 0 || trading_periods == 0 {
62 return Err(Error::PeriodZero);
63 }
64 if period < 2 {
65 return Err(Error::InvalidPeriod {
66 message: "historical volatility period must be >= 2",
67 });
68 }
69 if period > crate::error::MAX_PERIOD {
70 return Err(Error::InvalidPeriod {
71 message: crate::error::PERIOD_ABOVE_MAX,
72 });
73 }
74 Ok(Self {
75 period,
76 trading_periods,
77 prev_price: None,
78 window: VecDeque::with_capacity(period),
79 moments: ShiftedMoments::new(),
80 last: None,
81 })
82 }
83
84 pub const fn periods(&self) -> (usize, usize) {
86 (self.period, self.trading_periods)
87 }
88
89 pub const fn value(&self) -> Option<f64> {
91 self.last
92 }
93}
94
95impl Indicator for HistoricalVolatility {
96 type Input = f64;
97 type Output = f64;
98
99 #[inline]
100 fn update(&mut self, input: f64) -> Option<f64> {
101 if !input.is_finite() || input <= 0.0 {
109 return None;
110 }
111 let Some(prev) = self.prev_price else {
112 self.prev_price = Some(input);
113 return None;
114 };
115 self.prev_price = Some(input);
119
120 let log_return = (input / prev).ln();
121 if self.window.len() == self.period {
122 let old = self.window.pop_front().expect("window is non-empty");
123 self.moments.evict(old);
124 }
125 self.window.push_back(log_return);
126 self.moments.push(log_return);
127 if self.moments.needs_reseed(self.period) {
128 self.moments.reseed(self.window.iter().copied());
129 }
130 if self.window.len() < self.period {
131 return None;
132 }
133 let variance = self.moments.sample_variance(self.period);
134 let hv = variance.sqrt() * (self.trading_periods as f64).sqrt() * 100.0;
135 self.last = Some(hv);
136 Some(hv)
137 }
138
139 fn reset(&mut self) {
140 self.prev_price = None;
141 self.window.clear();
142 self.moments.reset();
143 self.last = None;
144 }
145
146 #[inline]
147 fn warmup_period(&self) -> usize {
148 self.period + 1
150 }
151
152 #[inline]
153 fn is_ready(&self) -> bool {
154 self.last.is_some()
155 }
156
157 #[inline]
158 fn name(&self) -> &'static str {
159 "HistoricalVolatility"
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::traits::BatchExt;
167 use approx::assert_relative_eq;
168
169 #[test]
170 fn new_rejects_zero_period() {
171 assert!(matches!(
172 HistoricalVolatility::new(0, 252),
173 Err(Error::PeriodZero)
174 ));
175 assert!(matches!(
176 HistoricalVolatility::new(20, 0),
177 Err(Error::PeriodZero)
178 ));
179 }
180
181 #[test]
185 fn accessors_and_metadata() {
186 let mut hv = HistoricalVolatility::new(20, 252).unwrap();
187 assert_eq!(hv.periods(), (20, 252));
188 assert_eq!(hv.name(), "HistoricalVolatility");
189 assert_eq!(hv.value(), None);
190 for i in 1..=hv.warmup_period() {
191 hv.update(100.0 + f64::from(u32::try_from(i).unwrap()));
192 }
193 assert!(hv.value().is_some());
194 }
195
196 #[test]
197 fn new_rejects_period_one() {
198 assert!(matches!(
199 HistoricalVolatility::new(1, 252),
200 Err(Error::InvalidPeriod { .. })
201 ));
202 }
203
204 #[test]
205 fn first_emission_at_warmup_period() {
206 let mut hv = HistoricalVolatility::new(5, 252).unwrap();
207 assert_eq!(hv.warmup_period(), 6);
208 let out = hv.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
209 for v in out.iter().take(5) {
210 assert!(v.is_none());
211 }
212 assert!(out[5].is_some());
213 }
214
215 #[test]
216 fn constant_series_yields_zero() {
217 let mut hv = HistoricalVolatility::new(10, 252).unwrap();
219 let out = hv.batch(&[100.0; 40]);
220 for v in out.iter().skip(10).flatten() {
221 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
222 }
223 }
224
225 #[test]
226 fn geometric_series_yields_zero() {
227 let mut hv = HistoricalVolatility::new(10, 252).unwrap();
235 let prices: Vec<f64> = (0..40).map(|i| 100.0 * 1.01_f64.powi(i)).collect();
236 let out = hv.batch(&prices);
237 for v in out.iter().skip(10).flatten() {
238 assert_relative_eq!(*v, 0.0, epsilon = 1e-6);
239 }
240 }
241
242 #[test]
243 fn output_is_non_negative() {
244 let mut hv = HistoricalVolatility::new(20, 252).unwrap();
245 let prices: Vec<f64> = (1..=200)
246 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 12.0)
247 .collect();
248 for v in hv.batch(&prices).into_iter().flatten() {
249 assert!(v >= 0.0, "volatility must be non-negative, got {v}");
250 }
251 }
252
253 #[test]
254 fn ignores_non_finite_input() {
255 let mut hv = HistoricalVolatility::new(5, 252).unwrap();
256 let out = hv.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
257 let last = *out.last().unwrap();
258 assert!(last.is_some());
259 assert_eq!(hv.update(f64::NAN), None);
260 assert_eq!(hv.update(f64::INFINITY), None);
261 }
262
263 #[test]
268 fn skips_non_positive_prices() {
269 let mut hv = HistoricalVolatility::new(5, 252).unwrap();
270 let warmup_prices = (1..=20).map(f64::from).collect::<Vec<_>>();
272 let warmup = hv.batch(&warmup_prices);
273 let _baseline = warmup
274 .last()
275 .copied()
276 .flatten()
277 .expect("warmed up by index 5");
278
279 assert_eq!(hv.update(-5.0), None);
284 assert_eq!(hv.update(0.0), None);
285
286 let mut control = hv.clone();
290 let after_real = hv.update(21.0).expect("ready");
291 assert_eq!(control.update(21.0).expect("ready"), after_real);
292 }
293
294 #[test]
295 fn reset_clears_state() {
296 let mut hv = HistoricalVolatility::new(5, 252).unwrap();
297 hv.batch(&(1..=20).map(f64::from).collect::<Vec<_>>());
298 assert!(hv.is_ready());
299 hv.reset();
300 assert!(!hv.is_ready());
301 assert_eq!(hv.update(1.0), None);
302 }
303
304 #[test]
305 fn batch_equals_streaming() {
306 let prices: Vec<f64> = (1..=120)
307 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 9.0)
308 .collect();
309 let batch = HistoricalVolatility::new(20, 252).unwrap().batch(&prices);
310 let mut b = HistoricalVolatility::new(20, 252).unwrap();
311 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
312 assert_eq!(batch, streamed);
313 }
314}