ta_lib_in_rust/indicators/volatility/hist_volatility.rs
1use crate::util::dataframe_utils::check_window_size;
2use polars::prelude::*;
3
4/// Calculates Historical Volatility (annualized standard deviation of returns)
5///
6/// Historical volatility is a statistical measure of the dispersion of returns for a given security
7/// or market index over a given period of time. It is calculated by taking the standard deviation
8/// of log returns and then annualizing the result.
9///
10/// # Arguments
11///
12/// * `df` - DataFrame containing the price data
13/// * `window` - Window size for volatility calculation (typically 20 days)
14/// * `column` - Column to calculate volatility on (usually "close")
15/// * `trading_periods` - Number of trading periods in a year (252 for daily data,
16/// 52 for weekly, 12 for monthly, etc.)
17///
18/// # Returns
19///
20/// Returns a PolarsResult containing the Historical Volatility Series as a percentage
21///
22/// # Example
23///
24/// ```
25/// use polars::prelude::*;
26/// use ta_lib_in_rust::indicators::volatility::calculate_hist_volatility;
27///
28/// let close = Series::new("close".into(), &[100.0, 102.0, 104.0, 103.0, 105.0, 107.0]);
29/// let df = DataFrame::new(vec![close.into()]).unwrap();
30///
31/// // Calculate 5-day historical volatility on daily data (252 trading days per year)
32/// let hist_vol = calculate_hist_volatility(&df, 5, "close", 252).unwrap();
33/// ```
34pub fn calculate_hist_volatility(
35 df: &DataFrame,
36 window: usize,
37 column: &str,
38 trading_periods: usize,
39) -> PolarsResult<Series> {
40 // Check window size
41 check_window_size(df, window, "Historical Volatility")?;
42
43 // Check if the specified column exists
44 if !df.schema().contains(column) {
45 return Err(PolarsError::ShapeMismatch(
46 format!(
47 "DataFrame must contain '{}' column for Historical Volatility calculation",
48 column
49 )
50 .into(),
51 ));
52 }
53
54 // Get the column to calculate returns
55 let price = df.column(column)?.f64()?;
56
57 // Calculate log returns: ln(price_t / price_t-1)
58 let mut returns = Vec::with_capacity(df.height());
59
60 returns.push(f64::NAN); // First element has no return
61
62 for i in 1..df.height() {
63 let current = price.get(i).unwrap_or(f64::NAN);
64 let previous = price.get(i - 1).unwrap_or(f64::NAN);
65
66 if !current.is_nan() && !previous.is_nan() && previous > 0.0 {
67 let log_return = (current / previous).ln();
68 returns.push(log_return);
69 } else {
70 returns.push(f64::NAN);
71 }
72 }
73
74 // Calculate rolling standard deviation of returns
75 let mut volatility = Vec::with_capacity(df.height());
76
77 // Fill NaN for the first window elements
78 for _ in 0..window {
79 volatility.push(f64::NAN);
80 }
81
82 // Calculate rolling standard deviation and annualize
83 for i in window..df.height() {
84 let mut sum = 0.0;
85 let mut sum_sq = 0.0;
86 let mut count = 0;
87
88 // Calculate variance within the window
89 for ret in returns.iter().skip(i - window + 1).take(window) {
90 if !ret.is_nan() {
91 sum += ret;
92 sum_sq += ret * ret;
93 count += 1;
94 }
95 }
96
97 if count > 1 {
98 // Calculate variance: E[X²] - (E[X])²
99 let mean = sum / count as f64;
100 let variance = sum_sq / count as f64 - mean * mean;
101
102 // Annualize volatility and convert to percentage
103 // Formula: σ_annual = σ_daily * sqrt(trading_periods)
104 let annualized_vol = if variance > 0.0 {
105 variance.sqrt() * (trading_periods as f64).sqrt() * 100.0
106 } else {
107 0.0
108 };
109
110 volatility.push(annualized_vol);
111 } else {
112 volatility.push(f64::NAN);
113 }
114 }
115
116 Ok(Series::new("hist_volatility".into(), volatility))
117}