quant-metrics 0.7.0

Pure performance statistics library for trading — Sharpe, Sortino, drawdown, VaR, portfolio composition
Documentation
//! Value at Risk and Conditional Value at Risk calculations.
//!
//! Historical VaR and CVaR (Expected Shortfall) for portfolio risk assessment.
//! Pure math, no I/O.

use rust_decimal::Decimal;

use crate::MetricsError;

/// Historical Value at Risk at given confidence level.
///
/// Sorts returns and picks the percentile. Returns the loss threshold
/// (negative value for a losing portfolio).
pub fn var(returns: &[Decimal], confidence_pct: u32) -> Decimal {
    match try_var(returns, confidence_pct) {
        Ok(v) => v,
        Err(_) => Decimal::ZERO,
    }
}

/// Historical VaR with error handling for insufficient data.
pub fn try_var(returns: &[Decimal], confidence_pct: u32) -> Result<Decimal, MetricsError> {
    if confidence_pct == 0 || confidence_pct >= 100 {
        return Err(MetricsError::InvalidParameter(
            "confidence_pct must be in (0, 100)".into(),
        ));
    }

    let n = returns.len();
    let min_observations = (100 / (100 - confidence_pct as usize)).max(10);

    if n < min_observations {
        return Err(MetricsError::InsufficientData {
            required: min_observations,
            actual: n,
        });
    }

    let mut sorted: Vec<Decimal> = returns.to_vec();
    sorted.sort();

    let index = ((100 - confidence_pct) as usize * n) / 100;
    let index = index.min(n - 1);

    Ok(sorted[index])
}

/// Conditional Value at Risk (Expected Shortfall) at given confidence level.
///
/// Mean of all returns at or below the VaR threshold.
pub fn cvar(returns: &[Decimal], confidence_pct: u32) -> Decimal {
    let n = returns.len();
    if n == 0 || confidence_pct == 0 || confidence_pct >= 100 {
        return Decimal::ZERO;
    }

    let mut sorted: Vec<Decimal> = returns.to_vec();
    sorted.sort();

    let cutoff = ((100 - confidence_pct) as usize * n) / 100;
    let cutoff = cutoff.max(1).min(n);

    let tail: &[Decimal] = &sorted[..cutoff];
    let sum: Decimal = tail.iter().sum();
    sum / Decimal::from(tail.len() as u32)
}

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