use crate::util::dataframe_utils::check_window_size;
use polars::prelude::*;
pub fn calculate_hist_volatility(
df: &DataFrame,
window: usize,
column: &str,
trading_periods: usize,
) -> PolarsResult<Series> {
check_window_size(df, window, "Historical Volatility")?;
if !df.schema().contains(column) {
return Err(PolarsError::ShapeMismatch(
format!(
"DataFrame must contain '{}' column for Historical Volatility calculation",
column
)
.into(),
));
}
let price = df.column(column)?.f64()?;
let mut returns = Vec::with_capacity(df.height());
returns.push(f64::NAN);
for i in 1..df.height() {
let current = price.get(i).unwrap_or(f64::NAN);
let previous = price.get(i - 1).unwrap_or(f64::NAN);
if !current.is_nan() && !previous.is_nan() && previous > 0.0 {
let log_return = (current / previous).ln();
returns.push(log_return);
} else {
returns.push(f64::NAN);
}
}
let mut volatility = Vec::with_capacity(df.height());
for _ in 0..window {
volatility.push(f64::NAN);
}
for i in window..df.height() {
let mut sum = 0.0;
let mut sum_sq = 0.0;
let mut count = 0;
for ret in returns.iter().skip(i - window + 1).take(window) {
if !ret.is_nan() {
sum += ret;
sum_sq += ret * ret;
count += 1;
}
}
if count > 1 {
let mean = sum / count as f64;
let variance = sum_sq / count as f64 - mean * mean;
let annualized_vol = if variance > 0.0 {
variance.sqrt() * (trading_periods as f64).sqrt() * 100.0
} else {
0.0
};
volatility.push(annualized_vol);
} else {
volatility.push(f64::NAN);
}
}
Ok(Series::new("hist_volatility".into(), volatility))
}