diff --git a/src/panel/historic_vol.rs b/src/panel/historic_vol.rs new file mode 100644 index 0000000..50a72ad --- /dev/null +++ b/src/panel/historic_vol.rs @@ -0,0 +1,85 @@ +use ndarray::{Array, Array1, Zip}; +use polars::prelude::*; + +#[allow(dead_code)] +fn expo_weights(lback_periods: usize, half_life: f64) -> Array1 { + // Calculates exponential series weights for finite horizon, normalized to 1. + let decf = 2f64.powf(-1.0 / half_life); + let mut weights = Array::from_iter( + (0..lback_periods) + .map(|ii| (lback_periods - ii - 1) as f64) + .map(|exponent| (decf.powf(exponent)) * (1.0 - decf)), + ); + weights /= weights.sum(); + weights +} +#[allow(dead_code)] +fn expo_std(x: &Array1, w: &Array1, remove_zeros: bool) -> f64 { + assert_eq!(x.len(), w.len(), "weights and window must have same length"); + let (filtered_x, filtered_w) = if remove_zeros { + let indices: Vec = x + .iter() + .enumerate() + .filter_map(|(i, &val)| if val != 0.0 { Some(i) } else { None }) + .collect(); + ( + Array::from_iter(indices.iter().map(|&i| x[i])), + Array::from_iter(indices.iter().map(|&i| w[i])), + ) + } else { + (x.clone(), w.clone()) + }; + let filtered_w = &filtered_w / filtered_w.sum(); + Zip::from(&filtered_x) + .and(&filtered_w) + .fold(0.0, |acc, &x_val, &w_val| acc + w_val * x_val.abs()) +} + +#[allow(dead_code)] +fn flat_std(x: &Array1, remove_zeros: bool) -> f64 { + let filtered_x = if remove_zeros { + x.iter() + .filter(|&&val| val != 0.0) + .cloned() + .collect::>() + } else { + x.clone() + }; + filtered_x.mapv(f64::abs).mean().unwrap_or(0.0) +} + +/// Calculate historic volatility. +/// Arguments: +/// - `df`: A Quantamental DataFrame. +/// - `xcat`: The category to calculate the historic volatility for. +/// - `cids`: A list of cross-sections. If none are provided, all cross-sections available +/// in the DataFrame will be used. +/// - `lback_periods`: The number of lookback periods to use for the calculation. +/// - `lback_method`: The method to use for the lookback period calculation. Options are +/// 'ma' (moving average) and 'xma' (exponential moving average). +/// - `half_life`: The half-life of the exponential weighting function. +/// - `start`: Only include data after this date. Defaults to the earliest date available. +/// - `end`: Only include data before this date. Defaults to the latest date available. +/// - `est_freq`: The frequency of the data. Defaults to 'D' (daily). Options are 'D' (daily), +/// 'W' (weekly), 'M' (monthly), and 'Q' (quarterly). +/// - `remove_zeros`: Whether to remove zero values from the calculation. Defaults to False. +/// - `postfix`: A string to append to XCAT of the result series. +/// - `nan_tolerance`: The maximum proportion of NaN values allowed in the calculation. +pub fn historic_vol( + df: polars::prelude::DataFrame, + xcat: String, + cids: Option>, + lback_periods: Option, + lback_method: Option, + half_life: Option, + start: Option, + end: Option, + est_freq: Option, + remove_zeros: Option, + postfix: Option, + nan_tolerance: Option, +) -> Result> { + println!("Calculating historic volatility with the following parameters:"); + println!("xcat: {:?},\ncids: {:?},\nlback_periods: {:?},lback_method: {:?},\nhalf_life: {:?},\nstart: {:?},\nend: {:?},\nest_freq: {:?},\nremove_zeros: {:?},\npostfix: {:?},\nnan_tolerance: {:?}", xcat, cids, lback_periods,lback_method, half_life, start, end, est_freq, remove_zeros, postfix, nan_tolerance); + Ok(df.to_owned()) +}