fin_primitives/volatility/
mod.rs1pub mod garch;
26
27use crate::error::FinError;
28
29#[derive(Debug, Clone, Copy)]
31pub struct OhlcBar {
32 pub open: f64,
34 pub high: f64,
36 pub low: f64,
38 pub close: f64,
40}
41
42fn sample_variance(values: &[f64]) -> f64 {
45 let n = values.len() as f64;
46 if n < 2.0 {
47 return 0.0;
48 }
49 let mean = values.iter().sum::<f64>() / n;
50 values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n - 1.0)
51}
52
53fn annualise(variance_per_bar: f64, bars_per_year: f64) -> f64 {
54 (variance_per_bar * bars_per_year).sqrt()
55}
56
57#[derive(Debug, Clone)]
64pub struct CloseToClose {
65 window: usize,
66 bars_per_year: f64,
67 closes: Vec<f64>,
68}
69
70impl CloseToClose {
71 pub fn new(window: usize, bars_per_year: f64) -> Result<Self, FinError> {
79 if window == 0 {
80 return Err(FinError::InvalidPeriod(window));
81 }
82 Ok(Self { window, bars_per_year, closes: Vec::with_capacity(window + 1) })
83 }
84
85 pub fn update(&mut self, close: f64) -> Option<f64> {
87 self.closes.push(close);
88 if self.closes.len() > self.window + 1 {
89 self.closes.remove(0);
90 }
91 if self.closes.len() <= self.window {
92 return None;
93 }
94 let returns: Vec<f64> = self
95 .closes
96 .windows(2)
97 .map(|w| (w[1] / w[0]).ln())
98 .collect();
99 let var = sample_variance(&returns);
100 Some(annualise(var, self.bars_per_year))
101 }
102
103 pub fn warmup_period(&self) -> usize {
105 self.window + 1
106 }
107}
108
109#[derive(Debug, Clone)]
118pub struct Parkinson {
119 window: usize,
120 bars_per_year: f64,
121 hl_sq: Vec<f64>,
122}
123
124impl Parkinson {
125 pub fn new(window: usize, bars_per_year: f64) -> Result<Self, FinError> {
130 if window == 0 {
131 return Err(FinError::InvalidPeriod(window));
132 }
133 Ok(Self { window, bars_per_year, hl_sq: Vec::with_capacity(window) })
134 }
135
136 pub fn update(&mut self, bar: OhlcBar) -> Option<f64> {
138 if bar.high <= 0.0 || bar.low <= 0.0 || bar.high < bar.low {
139 return None;
140 }
141 let hl = (bar.high / bar.low).ln();
142 self.hl_sq.push(hl * hl);
143 if self.hl_sq.len() > self.window {
144 self.hl_sq.remove(0);
145 }
146 if self.hl_sq.len() < self.window {
147 return None;
148 }
149 let n = self.window as f64;
150 let var_per_bar = self.hl_sq.iter().sum::<f64>() / (4.0 * n * 2.0_f64.ln());
151 Some(annualise(var_per_bar, self.bars_per_year))
152 }
153
154 pub fn warmup_period(&self) -> usize {
156 self.window
157 }
158}
159
160#[derive(Debug, Clone)]
168pub struct GarmanKlass {
169 window: usize,
170 bars_per_year: f64,
171 terms: Vec<f64>,
172}
173
174impl GarmanKlass {
175 pub fn new(window: usize, bars_per_year: f64) -> Result<Self, FinError> {
180 if window == 0 {
181 return Err(FinError::InvalidPeriod(window));
182 }
183 Ok(Self { window, bars_per_year, terms: Vec::with_capacity(window) })
184 }
185
186 pub fn update(&mut self, bar: OhlcBar) -> Option<f64> {
188 if bar.open <= 0.0 || bar.high <= 0.0 || bar.low <= 0.0 || bar.close <= 0.0 {
189 return None;
190 }
191 let hl = (bar.high / bar.low).ln();
192 let co = (bar.close / bar.open).ln();
193 let term = 0.5 * hl * hl - (2.0 * 2.0_f64.ln() - 1.0) * co * co;
194 self.terms.push(term);
195 if self.terms.len() > self.window {
196 self.terms.remove(0);
197 }
198 if self.terms.len() < self.window {
199 return None;
200 }
201 let var_per_bar = self.terms.iter().sum::<f64>() / self.window as f64;
202 let var_per_bar = var_per_bar.max(0.0);
203 Some(annualise(var_per_bar, self.bars_per_year))
204 }
205
206 pub fn warmup_period(&self) -> usize {
208 self.window
209 }
210}
211
212#[derive(Debug, Clone)]
220pub struct RogersSatchell {
221 window: usize,
222 bars_per_year: f64,
223 terms: Vec<f64>,
224}
225
226impl RogersSatchell {
227 pub fn new(window: usize, bars_per_year: f64) -> Result<Self, FinError> {
232 if window == 0 {
233 return Err(FinError::InvalidPeriod(window));
234 }
235 Ok(Self { window, bars_per_year, terms: Vec::with_capacity(window) })
236 }
237
238 pub fn update(&mut self, bar: OhlcBar) -> Option<f64> {
240 if bar.open <= 0.0 || bar.high <= 0.0 || bar.low <= 0.0 || bar.close <= 0.0 {
241 return None;
242 }
243 let hc = (bar.high / bar.close).ln();
244 let ho = (bar.high / bar.open).ln();
245 let lc = (bar.low / bar.close).ln();
246 let lo = (bar.low / bar.open).ln();
247 let term = hc * ho + lc * lo;
248 self.terms.push(term);
249 if self.terms.len() > self.window {
250 self.terms.remove(0);
251 }
252 if self.terms.len() < self.window {
253 return None;
254 }
255 let var_per_bar = (self.terms.iter().sum::<f64>() / self.window as f64).max(0.0);
256 Some(annualise(var_per_bar, self.bars_per_year))
257 }
258
259 pub fn warmup_period(&self) -> usize {
261 self.window
262 }
263}
264
265#[derive(Debug, Clone)]
275pub struct YangZhang {
276 window: usize,
277 bars_per_year: f64,
278 bars: Vec<OhlcBar>,
280 prev_close: Option<f64>,
281}
282
283impl YangZhang {
284 pub fn new(window: usize, bars_per_year: f64) -> Result<Self, FinError> {
289 if window < 2 {
290 return Err(FinError::InvalidInput(
291 "YangZhang window must be at least 2".to_owned(),
292 ));
293 }
294 Ok(Self { window, bars_per_year, bars: Vec::with_capacity(window), prev_close: None })
295 }
296
297 pub fn update(&mut self, bar: OhlcBar) -> Option<f64> {
299 if bar.open <= 0.0 || bar.high <= 0.0 || bar.low <= 0.0 || bar.close <= 0.0 {
300 return None;
301 }
302 self.prev_close = Some(bar.close);
303 self.bars.push(bar);
304 if self.bars.len() > self.window {
305 self.bars.remove(0);
306 }
307 if self.bars.len() < self.window {
308 return None;
309 }
310
311 let n = self.window as f64;
312
313 let mut overnight_sq = Vec::with_capacity(self.window - 1);
316 let mut open_sq = Vec::with_capacity(self.window);
317 let mut rs_terms = Vec::with_capacity(self.window);
318
319 for i in 0..self.bars.len() {
320 let b = &self.bars[i];
321 let hc = (b.high / b.close).ln();
323 let ho = (b.high / b.open).ln();
324 let lc = (b.low / b.close).ln();
325 let lo = (b.low / b.open).ln();
326 rs_terms.push(hc * ho + lc * lo);
327
328 open_sq.push((b.close / b.open).ln());
330
331 if i > 0 {
333 let prev = &self.bars[i - 1];
334 overnight_sq.push((b.open / prev.close).ln());
335 }
336 }
337
338 let mean_overnight = overnight_sq.iter().sum::<f64>() / overnight_sq.len() as f64;
339 let var_overnight = overnight_sq
340 .iter()
341 .map(|x| (x - mean_overnight).powi(2))
342 .sum::<f64>()
343 / (overnight_sq.len() as f64 - 1.0).max(1.0);
344
345 let mean_open = open_sq.iter().sum::<f64>() / n;
346 let var_open = open_sq
347 .iter()
348 .map(|x| (x - mean_open).powi(2))
349 .sum::<f64>()
350 / (n - 1.0).max(1.0);
351
352 let var_rs = (rs_terms.iter().sum::<f64>() / n).max(0.0);
353
354 let k = 0.34 / (1.34 + (n + 1.0) / (n - 1.0));
355 let var_yz = (var_overnight + k * var_open + (1.0 - k) * var_rs).max(0.0);
356
357 Some(annualise(var_yz, self.bars_per_year))
358 }
359
360 pub fn warmup_period(&self) -> usize {
362 self.window
363 }
364}
365
366#[cfg(test)]
369mod tests {
370 use super::*;
371
372 fn rising_bar(base: f64) -> OhlcBar {
373 OhlcBar { open: base, high: base * 1.01, low: base * 0.99, close: base * 1.005 }
374 }
375
376 #[test]
377 fn test_close_to_close_warmup() {
378 let mut ctc = CloseToClose::new(5, 252.0).unwrap();
379 for i in 0..5 {
380 assert!(ctc.update(100.0 + i as f64).is_none());
381 }
382 assert!(ctc.update(106.0).is_some());
383 }
384
385 #[test]
386 fn test_close_to_close_positive_vol() {
387 let mut ctc = CloseToClose::new(10, 252.0).unwrap();
388 let mut vol = None;
389 for i in 0..20 {
390 vol = ctc.update(100.0 + (i as f64).sin() * 2.0);
391 }
392 assert!(vol.unwrap() > 0.0);
393 }
394
395 #[test]
396 fn test_parkinson_warmup_and_positive() {
397 let mut pk = Parkinson::new(5, 252.0).unwrap();
398 for i in 0..4 {
399 assert!(pk.update(rising_bar(100.0 + i as f64)).is_none());
400 }
401 let v = pk.update(rising_bar(105.0));
402 assert!(v.is_some());
403 assert!(v.unwrap() > 0.0);
404 }
405
406 #[test]
407 fn test_garman_klass_positive() {
408 let mut gk = GarmanKlass::new(10, 252.0).unwrap();
409 let mut vol = None;
410 for i in 0..10 {
411 vol = gk.update(rising_bar(100.0 + i as f64));
412 }
413 assert!(vol.is_some());
414 }
415
416 #[test]
417 fn test_rogers_satchell_positive() {
418 let mut rs = RogersSatchell::new(10, 252.0).unwrap();
419 let mut vol = None;
420 for i in 0..10 {
421 vol = rs.update(rising_bar(100.0 + i as f64));
422 }
423 assert!(vol.is_some());
424 }
425
426 #[test]
427 fn test_yang_zhang_warmup() {
428 let mut yz = YangZhang::new(5, 252.0).unwrap();
429 for i in 0..4 {
430 assert!(yz.update(rising_bar(100.0 + i as f64)).is_none());
431 }
432 assert!(yz.update(rising_bar(105.0)).is_some());
433 }
434
435 #[test]
436 fn test_yang_zhang_invalid_window() {
437 assert!(YangZhang::new(1, 252.0).is_err());
438 }
439
440 #[test]
441 fn test_invalid_period() {
442 assert!(CloseToClose::new(0, 252.0).is_err());
443 assert!(Parkinson::new(0, 252.0).is_err());
444 assert!(GarmanKlass::new(0, 252.0).is_err());
445 assert!(RogersSatchell::new(0, 252.0).is_err());
446 }
447}