mod cvar;
mod drawdown;
mod ratios;
mod var;
pub use cvar::CVaR;
pub use drawdown::{DrawdownAnalysis, DrawdownStatistics};
pub use ratios::{
calmar_ratio, gain_to_pain_ratio, information_ratio, jensens_alpha, omega_ratio, sharpe_ratio,
sharpe_ratio_annualized, sortino_ratio, treynor_ratio,
};
pub use var::VaR;
use crate::monte_carlo::engine::{SimulationPath, Statistics};
use crate::monte_carlo::error::{MonteCarloError, Result};
#[derive(Debug, Clone)]
pub struct RiskReport {
pub var_90: f64,
pub var_95: f64,
pub var_99: f64,
pub cvar_90: f64,
pub cvar_95: f64,
pub cvar_99: f64,
pub drawdown: DrawdownStatistics,
pub sharpe_ratio: f64,
pub sortino_ratio: f64,
pub calmar_ratio: f64,
pub return_statistics: Statistics,
}
impl RiskReport {
pub fn from_paths(paths: &[SimulationPath], risk_free_rate: f64) -> Result<Self> {
if paths.is_empty() {
return Err(MonteCarloError::EmptyData {
context: "Cannot generate risk report from empty paths".to_string(),
});
}
let returns: Vec<f64> = paths
.iter()
.filter_map(SimulationPath::total_return)
.collect();
if returns.is_empty() {
return Err(MonteCarloError::EmptyData {
context: "No valid returns in paths".to_string(),
});
}
let return_statistics = Statistics::from_values(&returns);
let var_90 = VaR::historical(&returns, 0.90);
let var_95 = VaR::historical(&returns, 0.95);
let var_99 = VaR::historical(&returns, 0.99);
let cvar_90 = CVaR::from_returns(&returns, 0.90);
let cvar_95 = CVaR::from_returns(&returns, 0.95);
let cvar_99 = CVaR::from_returns(&returns, 0.99);
let drawdown = DrawdownAnalysis::from_paths(paths);
let sharpe = sharpe_ratio(&returns, risk_free_rate);
let sortino = sortino_ratio(&returns, risk_free_rate, 0.0);
let calmar = calmar_ratio(return_statistics.mean, drawdown.median);
Ok(Self {
var_90,
var_95,
var_99,
cvar_90,
cvar_95,
cvar_99,
drawdown,
sharpe_ratio: sharpe,
sortino_ratio: sortino,
calmar_ratio: calmar,
return_statistics,
})
}
#[must_use]
pub fn summary(&self) -> String {
format!(
"Risk Report\n\
===========\n\
\n\
Returns:\n\
- Mean: {:.2}%\n\
- Std Dev: {:.2}%\n\
- Min: {:.2}%\n\
- Max: {:.2}%\n\
\n\
Value at Risk:\n\
- VaR(90%): {:.2}%\n\
- VaR(95%): {:.2}%\n\
- VaR(99%): {:.2}%\n\
\n\
Expected Shortfall (CVaR):\n\
- CVaR(90%): {:.2}%\n\
- CVaR(95%): {:.2}%\n\
- CVaR(99%): {:.2}%\n\
\n\
Drawdown:\n\
- Mean Max Drawdown: {:.2}%\n\
- Median Max Drawdown: {:.2}%\n\
- 95th Percentile: {:.2}%\n\
- Worst Case: {:.2}%\n\
\n\
Risk-Adjusted Returns:\n\
- Sharpe Ratio: {:.2}\n\
- Sortino Ratio: {:.2}\n\
- Calmar Ratio: {:.2}",
self.return_statistics.mean * 100.0,
self.return_statistics.std * 100.0,
self.return_statistics.min * 100.0,
self.return_statistics.max * 100.0,
self.var_90 * 100.0,
self.var_95 * 100.0,
self.var_99 * 100.0,
self.cvar_90 * 100.0,
self.cvar_95 * 100.0,
self.cvar_99 * 100.0,
self.drawdown.mean * 100.0,
self.drawdown.median * 100.0,
self.drawdown.p95 * 100.0,
self.drawdown.worst * 100.0,
self.sharpe_ratio,
self.sortino_ratio,
self.calmar_ratio,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::monte_carlo::engine::{MonteCarloRng, PathMetadata};
fn create_test_paths(n: usize, seed: u64) -> Vec<SimulationPath> {
let mut rng = MonteCarloRng::new(seed);
(0..n)
.map(|i| {
let returns: Vec<f64> = (0..12)
.map(|j| {
if j == 0 {
100.0
} else {
let prev = if j > 0 {
100.0 + (j as f64 - 1.0) * 2.0
} else {
100.0
};
prev * (1.0 + rng.normal(0.01, 0.05))
}
})
.collect();
let mut values = vec![100.0];
for r in returns.iter().skip(1) {
values.push(*r);
}
SimulationPath::new(
(0..12).map(|j| j as f64 / 12.0).collect(),
values,
PathMetadata {
path_id: i,
seed,
is_antithetic: false,
},
)
})
.collect()
}
#[test]
fn test_risk_report_from_paths() {
let paths = create_test_paths(1000, 42);
let report = RiskReport::from_paths(&paths, 0.02).unwrap();
assert!(report.var_90 <= report.var_95);
assert!(report.var_95 <= report.var_99);
assert!(report.cvar_90 >= report.var_90 - 0.001);
assert!(report.cvar_95 >= report.var_95 - 0.001);
assert!(report.cvar_99 >= report.var_99 - 0.001);
}
#[test]
fn test_risk_report_empty_paths() {
let result = RiskReport::from_paths(&[], 0.02);
assert!(result.is_err());
}
#[test]
fn test_risk_report_summary() {
let paths = create_test_paths(100, 42);
let report = RiskReport::from_paths(&paths, 0.02).unwrap();
let summary = report.summary();
assert!(summary.contains("Risk Report"));
assert!(summary.contains("Value at Risk"));
assert!(summary.contains("Expected Shortfall"));
assert!(summary.contains("Sharpe Ratio"));
}
}