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