Files
rustytorch/demos/rtx-risk-analyzer/src/var.rs
T
2026-03-04 00:08:42 +00:00

346 lines
9.7 KiB
Rust

//! Value at Risk (`VaR`) calculation methods
//!
//! Implements Historical, Parametric, and Monte Carlo `VaR` methods.
use crate::error::{Result, RiskAnalyzerError};
use rand::SeedableRng;
use rand_distr::{Distribution, Normal};
/// Calculate historical `VaR` from a sample of returns
///
/// # Arguments
/// * `returns` - Historical returns (must be non-empty)
/// * `confidence` - Confidence level (e.g., 0.95 for 95%)
///
/// # Returns
/// `VaR` as a positive value (represents potential loss)
pub fn historical_var(returns: &[f64], confidence: f64) -> Result<f64> {
if returns.is_empty() {
return Err(RiskAnalyzerError::InsufficientData(
"Returns array is empty".to_string(),
));
}
if !(0.0..=1.0).contains(&confidence) {
return Err(RiskAnalyzerError::InvalidConfig(format!(
"Confidence level must be between 0 and 1, got {confidence}"
)));
}
let mut sorted = returns.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let index = ((1.0 - confidence) * sorted.len() as f64).floor() as usize;
let index = index.min(sorted.len() - 1);
Ok(-sorted[index])
}
/// Calculate parametric `VaR` assuming normal distribution
///
/// # Arguments
/// * `mean` - Mean return
/// * `std_dev` - Standard deviation of returns
/// * `confidence` - Confidence level (e.g., 0.95 for 95%)
///
/// # Returns
/// `VaR` as a positive value (represents potential loss)
pub fn parametric_var(mean: f64, std_dev: f64, confidence: f64) -> Result<f64> {
if std_dev < 0.0 {
return Err(RiskAnalyzerError::InvalidConfig(format!(
"Standard deviation must be non-negative, got {std_dev}"
)));
}
if !(0.0..=1.0).contains(&confidence) {
return Err(RiskAnalyzerError::InvalidConfig(format!(
"Confidence level must be between 0 and 1, got {confidence}"
)));
}
let z_score = normal_quantile(1.0 - confidence)?;
Ok(-(mean + z_score * std_dev))
}
/// Calculate Monte Carlo `VaR` by simulating future returns
///
/// # Arguments
/// * `mean` - Mean return per period
/// * `std_dev` - Standard deviation per period
/// * `horizon` - Time horizon in periods
/// * `num_simulations` - Number of Monte Carlo paths
/// * `confidence` - Confidence level (e.g., 0.95 for 95%)
///
/// # Returns
/// `VaR` as a positive value (represents potential loss)
pub fn monte_carlo_var(
mean: f64,
std_dev: f64,
horizon: u32,
num_simulations: u32,
confidence: f64,
) -> Result<f64> {
if num_simulations == 0 {
return Err(RiskAnalyzerError::InvalidConfig(
"Number of simulations must be positive".to_string(),
));
}
if horizon == 0 {
return Err(RiskAnalyzerError::InvalidConfig(
"Time horizon must be positive".to_string(),
));
}
if std_dev < 0.0 {
return Err(RiskAnalyzerError::InvalidConfig(format!(
"Standard deviation must be non-negative, got {std_dev}"
)));
}
if !(0.0..=1.0).contains(&confidence) {
return Err(RiskAnalyzerError::InvalidConfig(format!(
"Confidence level must be between 0 and 1, got {confidence}"
)));
}
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let normal = Normal::new(mean, std_dev).map_err(|e| {
RiskAnalyzerError::CalculationError(format!("Failed to create normal distribution: {e}"))
})?;
let mut final_returns = Vec::with_capacity(num_simulations as usize);
for _ in 0..num_simulations {
let mut cumulative_return = 0.0;
for _ in 0..horizon {
cumulative_return += normal.sample(&mut rng);
}
final_returns.push(cumulative_return);
}
historical_var(&final_returns, confidence)
}
/// Calculate the quantile of a standard normal distribution
///
/// Uses Beasley-Springer-Moro algorithm for inverse normal CDF
fn normal_quantile(p: f64) -> Result<f64> {
if !(0.0..=1.0).contains(&p) {
return Err(RiskAnalyzerError::InvalidConfig(format!(
"Probability must be between 0 and 1, got {p}"
)));
}
if p == 0.0 {
return Ok(f64::NEG_INFINITY);
}
if p == 1.0 {
return Ok(f64::INFINITY);
}
let a = [
-3.969_683_028_665_376e1,
2.209_460_984_245_205e2,
-2.759_285_104_469_687e2,
1.383_577_518_672_69e2,
-3.066_479_806_614_716e1,
2.506_628_277_459_239,
];
let b = [
-5.447_609_879_822_406e1,
1.615_858_368_580_409e2,
-1.556_989_798_598_866e2,
6.680_131_188_771_972e1,
-1.328_068_155_288_572e1,
];
let c = [
-7.784_894_002_430_293e-3,
-3.223_964_580_411_365e-1,
-2.400_758_277_161_838,
-2.549_732_539_343_734,
4.374_664_141_464_968,
2.938_163_982_698_783,
];
let d = [
7.784_695_709_041_462e-3,
3.224_671_290_700_398e-1,
2.445_134_137_142_996,
3.754_408_661_907_416,
];
let p_low = 0.02425;
let p_high = 1.0 - p_low;
let q = if p < p_low {
let q = (-2.0 * p.ln()).sqrt();
(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5])
/ ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0)
} else if p <= p_high {
let q = p - 0.5;
let r = q * q;
(((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q
/ (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1.0)
} else {
let q = (-2.0 * (1.0 - p).ln()).sqrt();
-(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5])
/ ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0)
};
Ok(q)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_historical_var_empty_returns() {
let result = historical_var(&[], 0.95);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
RiskAnalyzerError::InsufficientData(_)
));
}
#[test]
fn test_historical_var_invalid_confidence() {
let returns = vec![0.01, -0.02, 0.03];
let result = historical_var(&returns, 1.5);
assert!(result.is_err());
}
#[test]
fn test_historical_var_95_confidence() {
let returns = vec![
0.01, -0.02, 0.03, -0.01, 0.02, -0.03, 0.015, -0.025, 0.005, -0.015,
];
let var = historical_var(&returns, 0.95).unwrap();
assert!(var > 0.0);
assert!(var <= 0.03);
}
#[test]
fn test_historical_var_99_confidence() {
let returns = vec![
0.01, -0.02, 0.03, -0.01, 0.02, -0.03, 0.015, -0.025, 0.005, -0.015, -0.04, 0.025,
-0.035, 0.018, -0.028, 0.012, -0.022, 0.008, -0.018, 0.002,
];
let var = historical_var(&returns, 0.99).unwrap();
assert!(var > 0.0);
assert!(var >= 0.035);
}
#[test]
fn test_parametric_var_negative_std() {
let result = parametric_var(0.01, -0.1, 0.95);
assert!(result.is_err());
}
#[test]
fn test_parametric_var_invalid_confidence() {
let result = parametric_var(0.01, 0.1, 2.0);
assert!(result.is_err());
}
#[test]
fn test_parametric_var_positive_mean() {
let var = parametric_var(0.001, 0.02, 0.95).unwrap();
assert!(var > 0.0);
}
#[test]
fn test_parametric_var_negative_mean() {
let var = parametric_var(-0.001, 0.02, 0.95).unwrap();
assert!(var > 0.0);
}
#[test]
fn test_parametric_var_99_vs_95() {
let mean = 0.001;
let std = 0.02;
let var_95 = parametric_var(mean, std, 0.95).unwrap();
let var_99 = parametric_var(mean, std, 0.99).unwrap();
assert!(var_99 > var_95);
}
#[test]
fn test_monte_carlo_var_zero_simulations() {
let result = monte_carlo_var(0.001, 0.02, 1, 0, 0.95);
assert!(result.is_err());
}
#[test]
fn test_monte_carlo_var_zero_horizon() {
let result = monte_carlo_var(0.001, 0.02, 0, 1000, 0.95);
assert!(result.is_err());
}
#[test]
fn test_monte_carlo_var_negative_std() {
let result = monte_carlo_var(0.001, -0.02, 1, 1000, 0.95);
assert!(result.is_err());
}
#[test]
fn test_monte_carlo_var_basic() {
let var = monte_carlo_var(0.001, 0.02, 1, 10_000, 0.95).unwrap();
assert!(var > 0.0);
assert!(var < 0.1);
}
#[test]
fn test_monte_carlo_var_horizon_scaling() {
let var_1 = monte_carlo_var(0.001, 0.02, 1, 10_000, 0.95).unwrap();
let var_10 = monte_carlo_var(0.001, 0.02, 10, 10_000, 0.95).unwrap();
assert!(var_10 > var_1);
}
#[test]
fn test_monte_carlo_var_confidence_scaling() {
let var_95 = monte_carlo_var(0.001, 0.02, 1, 10_000, 0.95).unwrap();
let var_99 = monte_carlo_var(0.001, 0.02, 1, 10_000, 0.99).unwrap();
assert!(var_99 > var_95);
}
#[test]
fn test_normal_quantile_invalid() {
let result = normal_quantile(1.5);
assert!(result.is_err());
}
#[test]
fn test_normal_quantile_0_5() {
let q = normal_quantile(0.5).unwrap();
assert!((q.abs()) < 1e-10);
}
#[test]
fn test_normal_quantile_0_05() {
let q = normal_quantile(0.05).unwrap();
assert!((q - (-1.6449)).abs() < 0.01);
}
#[test]
fn test_normal_quantile_0_95() {
let q = normal_quantile(0.95).unwrap();
assert!((q - 1.6449).abs() < 0.01);
}
#[test]
fn test_normal_quantile_0_01() {
let q = normal_quantile(0.01).unwrap();
assert!((q - (-2.3263)).abs() < 0.01);
}
#[test]
fn test_normal_quantile_0_99() {
let q = normal_quantile(0.99).unwrap();
assert!((q - 2.3263).abs() < 0.01);
}
}