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}