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

422 lines
13 KiB
Rust

//! Shared IPC types for the Risk Analyzer demo
//!
//! This crate defines the data structures shared between the Rust backend
//! and the TypeScript frontend for the financial risk analysis demo.
use serde::{Deserialize, Serialize};
/// VaR calculation method
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VaRMethod {
/// Historical simulation using actual return distribution
Historical,
/// Parametric (variance-covariance) assuming normal distribution
Parametric,
/// Monte Carlo simulation with stochastic processes
MonteCarlo,
}
impl VaRMethod {
/// Get display name
pub fn display_name(&self) -> &'static str {
match self {
Self::Historical => "Historical Simulation",
Self::Parametric => "Parametric (Normal)",
Self::MonteCarlo => "Monte Carlo Simulation",
}
}
/// Get description
pub fn description(&self) -> &'static str {
match self {
Self::Historical => "Uses actual historical return distribution",
Self::Parametric => "Assumes normal distribution, uses mean and variance",
Self::MonteCarlo => "Simulates future paths using stochastic processes",
}
}
}
/// Configuration for risk analysis
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RiskConfig {
/// Confidence level (e.g., 0.95 for 95%, 0.99 for 99%)
pub confidence_level: f64,
/// Time horizon in days (e.g., 1, 10, 252)
pub time_horizon_days: u32,
/// Number of Monte Carlo simulations (if using MC method)
pub num_simulations: u32,
/// VaR calculation method
pub method: VaRMethod,
}
impl Default for RiskConfig {
fn default() -> Self {
Self {
confidence_level: 0.95,
time_horizon_days: 1,
num_simulations: 10_000,
method: VaRMethod::Historical,
}
}
}
/// Comprehensive risk metrics for a portfolio
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RiskMetrics {
/// Value at Risk (VaR) - maximum expected loss at confidence level
pub var: f64,
/// Conditional VaR (CVaR/Expected Shortfall) - expected loss beyond VaR
pub cvar: f64,
/// Annualized volatility (standard deviation)
pub volatility: f64,
/// Maximum drawdown (largest peak-to-trough decline)
pub max_drawdown: f64,
/// Sharpe ratio (risk-adjusted return)
pub sharpe_ratio: f64,
/// Sortino ratio (downside risk-adjusted return)
pub sortino_ratio: f64,
/// Market beta (if benchmark provided)
pub beta: Option<f64>,
/// Correlation with market (if benchmark provided)
pub correlation: Option<f64>,
/// Average return
pub mean_return: f64,
/// Skewness of returns
pub skewness: f64,
/// Kurtosis of returns (excess kurtosis)
pub kurtosis: f64,
}
/// Stress test scenario definition
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StressScenario {
/// Scenario name
pub name: String,
/// Scenario description
pub description: String,
/// Market shock percentage (e.g., -0.20 for 20% drop)
pub market_shock: f64,
/// Volatility spike multiplier (e.g., 2.0 for 2x normal vol)
pub volatility_spike: f64,
/// Correlation change (e.g., 0.2 means correlations increase by 0.2)
pub correlation_change: f64,
}
impl StressScenario {
/// Create 2008 Financial Crisis scenario
pub fn financial_crisis_2008() -> Self {
Self {
name: "2008 Financial Crisis".to_string(),
description: "Lehman Brothers collapse, credit freeze".to_string(),
market_shock: -0.45,
volatility_spike: 3.0,
correlation_change: 0.3,
}
}
/// Create COVID-19 March 2020 scenario
pub fn covid_march_2020() -> Self {
Self {
name: "COVID-19 March 2020".to_string(),
description: "Pandemic lockdowns, market crash".to_string(),
market_shock: -0.34,
volatility_spike: 2.5,
correlation_change: 0.25,
}
}
/// Create Dot-com Crash 2000 scenario
pub fn dotcom_crash_2000() -> Self {
Self {
name: "Dot-com Crash 2000".to_string(),
description: "Tech bubble burst".to_string(),
market_shock: -0.49,
volatility_spike: 2.2,
correlation_change: 0.15,
}
}
/// Create Black Monday 1987 scenario
pub fn black_monday_1987() -> Self {
Self {
name: "Black Monday 1987".to_string(),
description: "Largest single-day market crash".to_string(),
market_shock: -0.23,
volatility_spike: 4.0,
correlation_change: 0.35,
}
}
/// Get all predefined scenarios
pub fn all_predefined() -> Vec<Self> {
vec![
Self::financial_crisis_2008(),
Self::covid_march_2020(),
Self::dotcom_crash_2000(),
Self::black_monday_1987(),
]
}
}
/// Result of a stress test
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StressTestResult {
/// Scenario name
pub scenario: String,
/// Total portfolio loss under scenario
pub portfolio_loss: f64,
/// Portfolio loss percentage
pub portfolio_loss_pct: f64,
/// Whether loss exceeds VaR threshold
pub var_breach: bool,
/// Worst performing asset ticker
pub worst_asset: String,
/// Worst asset loss
pub worst_asset_loss: f64,
/// Best performing asset ticker (least loss or gain)
pub best_asset: String,
/// Best asset return
pub best_asset_return: f64,
}
/// Monte Carlo simulation result
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MonteCarloResult {
/// Simulated portfolio value paths (subset for visualization)
pub paths: Vec<Vec<f64>>,
/// Distribution of final values from all simulations
pub final_values: Vec<f64>,
/// Percentiles: (percentile, value) pairs
pub percentiles: Vec<(f64, f64)>,
/// Mean final value
pub mean_final_value: f64,
/// Median final value
pub median_final_value: f64,
/// Standard deviation of final values
pub std_final_value: f64,
}
/// Asset for risk analysis
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RiskAsset {
/// Asset ticker
pub ticker: String,
/// Asset name
pub name: String,
/// Portfolio weight (0.0 - 1.0)
pub weight: f64,
/// Historical returns
pub returns: Vec<f64>,
}
/// Portfolio for risk analysis
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RiskPortfolio {
/// Portfolio assets
pub assets: Vec<RiskAsset>,
/// Optional benchmark returns for beta calculation
pub benchmark_returns: Option<Vec<f64>>,
/// Risk-free rate (annualized)
pub risk_free_rate: f64,
}
/// Risk analysis request
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RiskAnalysisRequest {
/// Portfolio to analyze
pub portfolio: RiskPortfolio,
/// Risk configuration
pub config: RiskConfig,
}
/// Complete risk analysis result
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RiskAnalysisResult {
/// Risk metrics
pub metrics: RiskMetrics,
/// Monte Carlo result (if MC method used)
pub monte_carlo: Option<MonteCarloResult>,
/// Stress test results (if requested)
pub stress_tests: Vec<StressTestResult>,
/// Processing time in milliseconds
pub processing_time_ms: f64,
/// Method used for VaR calculation
pub method_used: VaRMethod,
}
/// Risk analyzer status
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RiskAnalyzerStatus {
/// Whether analyzer is initialized
pub initialized: bool,
/// Number of analyses performed
pub analysis_count: u64,
/// Average processing time in ms
pub avg_processing_time_ms: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_var_method_serialization() {
let method = VaRMethod::Historical;
let json = serde_json::to_string(&method).unwrap();
let deserialized: VaRMethod = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, VaRMethod::Historical);
}
#[test]
fn test_var_method_display() {
assert_eq!(
VaRMethod::Historical.display_name(),
"Historical Simulation"
);
assert_eq!(
VaRMethod::MonteCarlo.display_name(),
"Monte Carlo Simulation"
);
}
#[test]
fn test_risk_config_default() {
let config = RiskConfig::default();
assert_eq!(config.confidence_level, 0.95);
assert_eq!(config.time_horizon_days, 1);
assert_eq!(config.num_simulations, 10_000);
assert_eq!(config.method, VaRMethod::Historical);
}
#[test]
fn test_risk_config_serialization() {
let config = RiskConfig {
confidence_level: 0.99,
time_horizon_days: 10,
num_simulations: 5_000,
method: VaRMethod::MonteCarlo,
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: RiskConfig = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.confidence_level, 0.99);
assert_eq!(deserialized.method, VaRMethod::MonteCarlo);
}
#[test]
fn test_stress_scenario_predefined() {
let scenario = StressScenario::financial_crisis_2008();
assert_eq!(scenario.name, "2008 Financial Crisis");
assert_eq!(scenario.market_shock, -0.45);
assert_eq!(scenario.volatility_spike, 3.0);
}
#[test]
fn test_all_predefined_scenarios() {
let scenarios = StressScenario::all_predefined();
assert_eq!(scenarios.len(), 4);
assert!(scenarios.iter().any(|s| s.name.contains("2008")));
assert!(scenarios.iter().any(|s| s.name.contains("COVID")));
}
#[test]
fn test_risk_asset_serialization() {
let asset = RiskAsset {
ticker: "AAPL".to_string(),
name: "Apple Inc.".to_string(),
weight: 0.3,
returns: vec![0.01, -0.02, 0.015],
};
let json = serde_json::to_string(&asset).unwrap();
let deserialized: RiskAsset = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.ticker, "AAPL");
assert_eq!(deserialized.weight, 0.3);
assert_eq!(deserialized.returns.len(), 3);
}
#[test]
fn test_risk_metrics_serialization() {
let metrics = RiskMetrics {
var: 10000.0,
cvar: 12000.0,
volatility: 0.15,
max_drawdown: 0.25,
sharpe_ratio: 1.2,
sortino_ratio: 1.5,
beta: Some(0.9),
correlation: Some(0.85),
mean_return: 0.08,
skewness: -0.5,
kurtosis: 3.0,
};
let json = serde_json::to_string(&metrics).unwrap();
let deserialized: RiskMetrics = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.var, 10000.0);
assert_eq!(deserialized.beta, Some(0.9));
}
#[test]
fn test_stress_test_result_serialization() {
let result = StressTestResult {
scenario: "Test Scenario".to_string(),
portfolio_loss: 50000.0,
portfolio_loss_pct: 0.20,
var_breach: true,
worst_asset: "XYZ".to_string(),
worst_asset_loss: 0.35,
best_asset: "ABC".to_string(),
best_asset_return: -0.05,
};
let json = serde_json::to_string(&result).unwrap();
let deserialized: StressTestResult = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.scenario, "Test Scenario");
assert!(deserialized.var_breach);
}
#[test]
fn test_monte_carlo_result_serialization() {
let result = MonteCarloResult {
paths: vec![vec![100.0, 102.0, 105.0], vec![100.0, 98.0, 96.0]],
final_values: vec![105.0, 96.0, 110.0],
percentiles: vec![(0.05, 90.0), (0.50, 105.0), (0.95, 120.0)],
mean_final_value: 105.0,
median_final_value: 105.0,
std_final_value: 10.0,
};
let json = serde_json::to_string(&result).unwrap();
let deserialized: MonteCarloResult = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.paths.len(), 2);
assert_eq!(deserialized.percentiles.len(), 3);
}
#[test]
fn test_risk_portfolio_serialization() {
let portfolio = RiskPortfolio {
assets: vec![RiskAsset {
ticker: "SPY".to_string(),
name: "S&P 500 ETF".to_string(),
weight: 1.0,
returns: vec![0.01, -0.005],
}],
benchmark_returns: Some(vec![0.012, -0.004]),
risk_free_rate: 0.02,
};
let json = serde_json::to_string(&portfolio).unwrap();
let deserialized: RiskPortfolio = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.assets.len(), 1);
assert!(deserialized.benchmark_returns.is_some());
}
}