pub mod garch;
use crate::error::FinError;
#[derive(Debug, Clone, Copy)]
pub struct OhlcBar {
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
}
fn sample_variance(values: &[f64]) -> f64 {
let n = values.len() as f64;
if n < 2.0 {
return 0.0;
}
let mean = values.iter().sum::<f64>() / n;
values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n - 1.0)
}
fn annualise(variance_per_bar: f64, bars_per_year: f64) -> f64 {
(variance_per_bar * bars_per_year).sqrt()
}
#[derive(Debug, Clone)]
pub struct CloseToClose {
window: usize,
bars_per_year: f64,
closes: Vec<f64>,
}
impl CloseToClose {
pub fn new(window: usize, bars_per_year: f64) -> Result<Self, FinError> {
if window == 0 {
return Err(FinError::InvalidPeriod(window));
}
Ok(Self { window, bars_per_year, closes: Vec::with_capacity(window + 1) })
}
pub fn update(&mut self, close: f64) -> Option<f64> {
self.closes.push(close);
if self.closes.len() > self.window + 1 {
self.closes.remove(0);
}
if self.closes.len() <= self.window {
return None;
}
let returns: Vec<f64> = self
.closes
.windows(2)
.map(|w| (w[1] / w[0]).ln())
.collect();
let var = sample_variance(&returns);
Some(annualise(var, self.bars_per_year))
}
pub fn warmup_period(&self) -> usize {
self.window + 1
}
}
#[derive(Debug, Clone)]
pub struct Parkinson {
window: usize,
bars_per_year: f64,
hl_sq: Vec<f64>,
}
impl Parkinson {
pub fn new(window: usize, bars_per_year: f64) -> Result<Self, FinError> {
if window == 0 {
return Err(FinError::InvalidPeriod(window));
}
Ok(Self { window, bars_per_year, hl_sq: Vec::with_capacity(window) })
}
pub fn update(&mut self, bar: OhlcBar) -> Option<f64> {
if bar.high <= 0.0 || bar.low <= 0.0 || bar.high < bar.low {
return None;
}
let hl = (bar.high / bar.low).ln();
self.hl_sq.push(hl * hl);
if self.hl_sq.len() > self.window {
self.hl_sq.remove(0);
}
if self.hl_sq.len() < self.window {
return None;
}
let n = self.window as f64;
let var_per_bar = self.hl_sq.iter().sum::<f64>() / (4.0 * n * 2.0_f64.ln());
Some(annualise(var_per_bar, self.bars_per_year))
}
pub fn warmup_period(&self) -> usize {
self.window
}
}
#[derive(Debug, Clone)]
pub struct GarmanKlass {
window: usize,
bars_per_year: f64,
terms: Vec<f64>,
}
impl GarmanKlass {
pub fn new(window: usize, bars_per_year: f64) -> Result<Self, FinError> {
if window == 0 {
return Err(FinError::InvalidPeriod(window));
}
Ok(Self { window, bars_per_year, terms: Vec::with_capacity(window) })
}
pub fn update(&mut self, bar: OhlcBar) -> Option<f64> {
if bar.open <= 0.0 || bar.high <= 0.0 || bar.low <= 0.0 || bar.close <= 0.0 {
return None;
}
let hl = (bar.high / bar.low).ln();
let co = (bar.close / bar.open).ln();
let term = 0.5 * hl * hl - (2.0 * 2.0_f64.ln() - 1.0) * co * co;
self.terms.push(term);
if self.terms.len() > self.window {
self.terms.remove(0);
}
if self.terms.len() < self.window {
return None;
}
let var_per_bar = self.terms.iter().sum::<f64>() / self.window as f64;
let var_per_bar = var_per_bar.max(0.0);
Some(annualise(var_per_bar, self.bars_per_year))
}
pub fn warmup_period(&self) -> usize {
self.window
}
}
#[derive(Debug, Clone)]
pub struct RogersSatchell {
window: usize,
bars_per_year: f64,
terms: Vec<f64>,
}
impl RogersSatchell {
pub fn new(window: usize, bars_per_year: f64) -> Result<Self, FinError> {
if window == 0 {
return Err(FinError::InvalidPeriod(window));
}
Ok(Self { window, bars_per_year, terms: Vec::with_capacity(window) })
}
pub fn update(&mut self, bar: OhlcBar) -> Option<f64> {
if bar.open <= 0.0 || bar.high <= 0.0 || bar.low <= 0.0 || bar.close <= 0.0 {
return None;
}
let hc = (bar.high / bar.close).ln();
let ho = (bar.high / bar.open).ln();
let lc = (bar.low / bar.close).ln();
let lo = (bar.low / bar.open).ln();
let term = hc * ho + lc * lo;
self.terms.push(term);
if self.terms.len() > self.window {
self.terms.remove(0);
}
if self.terms.len() < self.window {
return None;
}
let var_per_bar = (self.terms.iter().sum::<f64>() / self.window as f64).max(0.0);
Some(annualise(var_per_bar, self.bars_per_year))
}
pub fn warmup_period(&self) -> usize {
self.window
}
}
#[derive(Debug, Clone)]
pub struct YangZhang {
window: usize,
bars_per_year: f64,
bars: Vec<OhlcBar>,
prev_close: Option<f64>,
}
impl YangZhang {
pub fn new(window: usize, bars_per_year: f64) -> Result<Self, FinError> {
if window < 2 {
return Err(FinError::InvalidInput(
"YangZhang window must be at least 2".to_owned(),
));
}
Ok(Self { window, bars_per_year, bars: Vec::with_capacity(window), prev_close: None })
}
pub fn update(&mut self, bar: OhlcBar) -> Option<f64> {
if bar.open <= 0.0 || bar.high <= 0.0 || bar.low <= 0.0 || bar.close <= 0.0 {
return None;
}
self.prev_close = Some(bar.close);
self.bars.push(bar);
if self.bars.len() > self.window {
self.bars.remove(0);
}
if self.bars.len() < self.window {
return None;
}
let n = self.window as f64;
let mut overnight_sq = Vec::with_capacity(self.window - 1);
let mut open_sq = Vec::with_capacity(self.window);
let mut rs_terms = Vec::with_capacity(self.window);
for i in 0..self.bars.len() {
let b = &self.bars[i];
let hc = (b.high / b.close).ln();
let ho = (b.high / b.open).ln();
let lc = (b.low / b.close).ln();
let lo = (b.low / b.open).ln();
rs_terms.push(hc * ho + lc * lo);
open_sq.push((b.close / b.open).ln());
if i > 0 {
let prev = &self.bars[i - 1];
overnight_sq.push((b.open / prev.close).ln());
}
}
let mean_overnight = overnight_sq.iter().sum::<f64>() / overnight_sq.len() as f64;
let var_overnight = overnight_sq
.iter()
.map(|x| (x - mean_overnight).powi(2))
.sum::<f64>()
/ (overnight_sq.len() as f64 - 1.0).max(1.0);
let mean_open = open_sq.iter().sum::<f64>() / n;
let var_open = open_sq
.iter()
.map(|x| (x - mean_open).powi(2))
.sum::<f64>()
/ (n - 1.0).max(1.0);
let var_rs = (rs_terms.iter().sum::<f64>() / n).max(0.0);
let k = 0.34 / (1.34 + (n + 1.0) / (n - 1.0));
let var_yz = (var_overnight + k * var_open + (1.0 - k) * var_rs).max(0.0);
Some(annualise(var_yz, self.bars_per_year))
}
pub fn warmup_period(&self) -> usize {
self.window
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rising_bar(base: f64) -> OhlcBar {
OhlcBar { open: base, high: base * 1.01, low: base * 0.99, close: base * 1.005 }
}
#[test]
fn test_close_to_close_warmup() {
let mut ctc = CloseToClose::new(5, 252.0).unwrap();
for i in 0..5 {
assert!(ctc.update(100.0 + i as f64).is_none());
}
assert!(ctc.update(106.0).is_some());
}
#[test]
fn test_close_to_close_positive_vol() {
let mut ctc = CloseToClose::new(10, 252.0).unwrap();
let mut vol = None;
for i in 0..20 {
vol = ctc.update(100.0 + (i as f64).sin() * 2.0);
}
assert!(vol.unwrap() > 0.0);
}
#[test]
fn test_parkinson_warmup_and_positive() {
let mut pk = Parkinson::new(5, 252.0).unwrap();
for i in 0..4 {
assert!(pk.update(rising_bar(100.0 + i as f64)).is_none());
}
let v = pk.update(rising_bar(105.0));
assert!(v.is_some());
assert!(v.unwrap() > 0.0);
}
#[test]
fn test_garman_klass_positive() {
let mut gk = GarmanKlass::new(10, 252.0).unwrap();
let mut vol = None;
for i in 0..10 {
vol = gk.update(rising_bar(100.0 + i as f64));
}
assert!(vol.is_some());
}
#[test]
fn test_rogers_satchell_positive() {
let mut rs = RogersSatchell::new(10, 252.0).unwrap();
let mut vol = None;
for i in 0..10 {
vol = rs.update(rising_bar(100.0 + i as f64));
}
assert!(vol.is_some());
}
#[test]
fn test_yang_zhang_warmup() {
let mut yz = YangZhang::new(5, 252.0).unwrap();
for i in 0..4 {
assert!(yz.update(rising_bar(100.0 + i as f64)).is_none());
}
assert!(yz.update(rising_bar(105.0)).is_some());
}
#[test]
fn test_yang_zhang_invalid_window() {
assert!(YangZhang::new(1, 252.0).is_err());
}
#[test]
fn test_invalid_period() {
assert!(CloseToClose::new(0, 252.0).is_err());
assert!(Parkinson::new(0, 252.0).is_err());
assert!(GarmanKlass::new(0, 252.0).is_err());
assert!(RogersSatchell::new(0, 252.0).is_err());
}
}