422 lines
13 KiB
Rust
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());
|
|
}
|
|
}
|