quant-metrics 0.7.0

Pure performance statistics library for trading — Sharpe, Sortino, drawdown, VaR, portfolio composition
Documentation
//! Rolling window calculations.

use rust_decimal::Decimal;

use crate::MetricsError;

/// Rolling window calculator for time-series metrics.
///
/// # Example
/// ```
/// use quant_metrics::RollingWindow;
/// use rust_decimal_macros::dec;
///
/// let equity = vec![dec!(100), dec!(101), dec!(102), dec!(101), dec!(103), dec!(105)];
/// let rolling = RollingWindow::new(3);
///
/// let returns = rolling.returns(&equity).unwrap();
/// let volatility = rolling.volatility(&equity).unwrap();
/// ```
pub struct RollingWindow {
    window_size: usize,
}

impl RollingWindow {
    /// Create a new rolling window calculator.
    ///
    /// # Arguments
    /// * `window_size` - Number of periods in the window
    pub fn new(window_size: usize) -> Self {
        Self { window_size }
    }

    /// Calculate rolling returns.
    ///
    /// Returns the period return for each window.
    pub fn returns(&self, equity: &[Decimal]) -> Result<Vec<Decimal>, MetricsError> {
        if equity.len() < self.window_size {
            return Err(MetricsError::InsufficientData {
                required: self.window_size,
                actual: equity.len(),
            });
        }

        let mut results = Vec::with_capacity(equity.len() - self.window_size + 1);

        for window in equity.windows(self.window_size) {
            let start = window[0];
            let end = window[self.window_size - 1];

            if start == Decimal::ZERO {
                results.push(Decimal::ZERO);
            } else {
                let ret = ((end - start) / start) * Decimal::from(100);
                results.push(ret);
            }
        }

        Ok(results)
    }

    /// Calculate rolling volatility (standard deviation of returns).
    pub fn volatility(&self, equity: &[Decimal]) -> Result<Vec<Decimal>, MetricsError> {
        if equity.len() < self.window_size + 1 {
            return Err(MetricsError::InsufficientData {
                required: self.window_size + 1,
                actual: equity.len(),
            });
        }

        // First compute period returns
        let returns: Vec<Decimal> = equity
            .windows(2)
            .filter_map(|w| {
                if w[0] == Decimal::ZERO {
                    None
                } else {
                    Some((w[1] - w[0]) / w[0])
                }
            })
            .collect();

        if returns.len() < self.window_size {
            return Err(MetricsError::InsufficientData {
                required: self.window_size,
                actual: returns.len(),
            });
        }

        let mut results = Vec::with_capacity(returns.len() - self.window_size + 1);

        for window in returns.windows(self.window_size) {
            let std = std_deviation(window);
            results.push(std);
        }

        Ok(results)
    }

    /// Calculate rolling Sharpe ratio.
    ///
    /// # Arguments
    /// * `equity` - Equity curve
    /// * `risk_free_rate` - Annual risk-free rate
    /// * `periods_per_year` - Trading periods per year
    pub fn sharpe(
        &self,
        equity: &[Decimal],
        risk_free_rate: Decimal,
        periods_per_year: u32,
    ) -> Result<Vec<Decimal>, MetricsError> {
        if equity.len() < self.window_size + 1 {
            return Err(MetricsError::InsufficientData {
                required: self.window_size + 1,
                actual: equity.len(),
            });
        }

        let returns: Vec<Decimal> = equity
            .windows(2)
            .filter_map(|w| {
                if w[0] == Decimal::ZERO {
                    None
                } else {
                    Some((w[1] - w[0]) / w[0])
                }
            })
            .collect();

        if returns.len() < self.window_size {
            return Err(MetricsError::InsufficientData {
                required: self.window_size,
                actual: returns.len(),
            });
        }

        let period_rf = risk_free_rate / Decimal::from(periods_per_year);
        let sqrt_periods = decimal_sqrt(Decimal::from(periods_per_year));

        let mut results = Vec::with_capacity(returns.len() - self.window_size + 1);

        for window in returns.windows(self.window_size) {
            let mean_ret = mean(window);
            let std = std_deviation(window);

            if std == Decimal::ZERO {
                results.push(Decimal::ZERO);
            } else {
                let sharpe = ((mean_ret - period_rf) / std) * sqrt_periods;
                results.push(sharpe);
            }
        }

        Ok(results)
    }

    /// Calculate rolling maximum drawdown.
    pub fn max_drawdown(&self, equity: &[Decimal]) -> Result<Vec<Decimal>, MetricsError> {
        if equity.len() < self.window_size {
            return Err(MetricsError::InsufficientData {
                required: self.window_size,
                actual: equity.len(),
            });
        }

        let mut results = Vec::with_capacity(equity.len() - self.window_size + 1);

        for window in equity.windows(self.window_size) {
            let max_dd = window_max_drawdown(window);
            results.push(max_dd);
        }

        Ok(results)
    }
}

/// Calculate max drawdown for a window.
fn window_max_drawdown(equity: &[Decimal]) -> Decimal {
    if equity.is_empty() {
        return Decimal::ZERO;
    }

    let mut peak = equity[0];
    let mut max_dd = Decimal::ZERO;

    for &value in equity {
        if value > peak {
            peak = value;
        }

        if peak != Decimal::ZERO {
            let dd = (value - peak) / peak;
            if dd < max_dd {
                max_dd = dd;
            }
        }
    }

    max_dd * Decimal::from(100)
}

use crate::math::{decimal_sqrt, mean, std_deviation};

#[cfg(test)]
#[path = "rolling_tests.rs"]
mod tests;