use polars::prelude::*;
use polars::frame::DataFrame;
use std::collections::HashMap;
pub fn calculate_iv_percentile(
df: &DataFrame,
iv_column: &str,
lookback_period: usize,
) -> PolarsResult<Series> {
let iv = df.column(iv_column)?.f64()?;
let len = df.height();
let mut iv_percentile = vec![f64::NAN; len];
for i in (lookback_period - 1)..len {
let mut iv_history = Vec::with_capacity(lookback_period);
let start_idx = i - (lookback_period - 1);
for j in start_idx..=i {
if let Some(val) = iv.get(j) {
if !val.is_nan() {
iv_history.push(val);
}
}
}
if iv_history.is_empty() {
continue;
}
iv_history.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let current_iv = iv.get(i).unwrap_or(f64::NAN);
if current_iv.is_nan() {
continue;
}
let count_below = iv_history.iter().filter(|&&x| x < current_iv).count();
iv_percentile[i] = 100.0 * (count_below as f64) / (iv_history.len() as f64);
}
Ok(Series::new("iv_percentile".into(), iv_percentile))
}
pub fn calculate_iv_term_structure(
df: &DataFrame,
iv_column: &str,
expiry_column: &str,
) -> PolarsResult<Series> {
let expiry = df.column(expiry_column)?;
let iv = df.column(iv_column)?.f64()?;
let mut expiry_to_iv: HashMap<String, Vec<f64>> = HashMap::new();
let mut expiry_days: HashMap<String, f64> = HashMap::new();
for i in 0..df.height() {
let exp_result = expiry.get(i);
if let Ok(exp_value) = exp_result {
let exp_str = exp_value.to_string();
let iv_val = iv.get(i).unwrap_or(f64::NAN);
if !iv_val.is_nan() {
expiry_to_iv.entry(exp_str.clone()).or_insert_with(Vec::new).push(iv_val);
if let Ok(days) = exp_str.parse::<f64>() {
expiry_days.insert(exp_str, days);
}
}
}
}
let mut expiry_avg_iv: Vec<(f64, f64)> = Vec::new();
for (exp, ivs) in expiry_to_iv {
if let Some(&days) = expiry_days.get(&exp) {
let avg_iv = ivs.iter().sum::<f64>() / ivs.len() as f64;
expiry_avg_iv.push((days, avg_iv));
}
}
expiry_avg_iv.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut term_structure = vec![f64::NAN; df.height()];
if expiry_avg_iv.len() >= 2 {
let n = expiry_avg_iv.len() as f64;
let sum_x = expiry_avg_iv.iter().map(|(days, _)| days).sum::<f64>();
let sum_y = expiry_avg_iv.iter().map(|(_, iv)| iv).sum::<f64>();
let sum_xy = expiry_avg_iv.iter().map(|(days, iv)| days * iv).sum::<f64>();
let sum_xx = expiry_avg_iv.iter().map(|(days, _)| days * days).sum::<f64>();
let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x);
for i in 0..term_structure.len() {
term_structure[i] = slope;
}
}
Ok(Series::new("iv_term_structure".into(), term_structure))
}
pub fn calculate_iv_forecast(
df: &DataFrame,
iv_column: &str,
forecast_period: usize,
) -> PolarsResult<Series> {
let iv = df.column(iv_column)?.f64()?;
let len = df.height();
let mut iv_forecast = vec![f64::NAN; len];
let alpha = 0.1; let beta = 0.8;
if len < 30 {
return Ok(Series::new("iv_forecast".into(), iv_forecast));
}
let mut valid_iv_sum = 0.0;
let mut valid_iv_count = 0;
for i in 0..len {
if let Some(val) = iv.get(i) {
if !val.is_nan() {
valid_iv_sum += val;
valid_iv_count += 1;
}
}
}
if valid_iv_count == 0 {
return Ok(Series::new("iv_forecast".into(), iv_forecast));
}
let long_term_iv = valid_iv_sum / valid_iv_count as f64;
for i in 29..len {
let current_iv = iv.get(i).unwrap_or(f64::NAN);
if current_iv.is_nan() {
continue;
}
let forecast = alpha * current_iv + beta * long_term_iv + (1.0 - alpha - beta) * iv.get(i-1).unwrap_or(current_iv);
iv_forecast[i] = forecast;
}
Ok(Series::new("iv_forecast".into(), iv_forecast))
}
pub fn add_volatility_indicators(df: &mut DataFrame) -> PolarsResult<()> {
if !df.schema().contains("iv") {
return Err(PolarsError::ComputeError(
"Required column 'iv' not found".into(),
));
}
let iv_percentile = calculate_iv_percentile(df, "iv", 252)?;
df.with_column(iv_percentile)?;
if df.schema().contains("expiry") {
let iv_term = calculate_iv_term_structure(df, "iv", "expiry")?;
df.with_column(iv_term)?;
}
let iv_forecast = calculate_iv_forecast(df, "iv", 5)?;
df.with_column(iv_forecast)?;
Ok(())
}