610 lines
17 KiB
Rust
610 lines
17 KiB
Rust
//! Shared types for RiskFlow - Real-Time Risk Attribution Engine.
|
|
//!
|
|
//! This crate defines the IPC types for real-time portfolio risk analysis.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
// ============================================================================
|
|
// Portfolio Types
|
|
// ============================================================================
|
|
|
|
/// A portfolio position.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Position {
|
|
/// Asset symbol.
|
|
pub symbol: String,
|
|
/// Asset name.
|
|
pub name: String,
|
|
/// Number of shares/units.
|
|
pub quantity: f64,
|
|
/// Current price.
|
|
pub price: f64,
|
|
/// Market value (quantity * price).
|
|
pub market_value: f64,
|
|
/// Weight in portfolio (%).
|
|
pub weight: f64,
|
|
/// Asset beta.
|
|
pub beta: f64,
|
|
/// Sector classification.
|
|
pub sector: String,
|
|
}
|
|
|
|
/// A portfolio.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Portfolio {
|
|
/// Portfolio ID.
|
|
pub id: String,
|
|
/// Portfolio name.
|
|
pub name: String,
|
|
/// Positions.
|
|
pub positions: Vec<Position>,
|
|
/// Total market value.
|
|
pub total_value: f64,
|
|
/// Cash position.
|
|
pub cash: f64,
|
|
/// Base currency.
|
|
pub currency: String,
|
|
/// Last update timestamp.
|
|
pub timestamp: String,
|
|
}
|
|
|
|
impl Portfolio {
|
|
/// Calculate total market value from positions.
|
|
#[must_use]
|
|
pub fn calculate_total_value(&self) -> f64 {
|
|
self.positions.iter().map(|p| p.market_value).sum::<f64>() + self.cash
|
|
}
|
|
|
|
/// Recalculate position weights.
|
|
pub fn update_weights(&mut self) {
|
|
let total = self.calculate_total_value();
|
|
for pos in &mut self.positions {
|
|
pos.weight = if total > 0.0 {
|
|
pos.market_value / total * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Risk Factor Types
|
|
// ============================================================================
|
|
|
|
/// Standard risk factors (Fama-French style).
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct RiskFactors {
|
|
/// Market factor (excess market return).
|
|
pub market: f64,
|
|
/// Size factor (SMB - Small Minus Big).
|
|
pub size: f64,
|
|
/// Value factor (HML - High Minus Low).
|
|
pub value: f64,
|
|
/// Momentum factor.
|
|
pub momentum: f64,
|
|
/// Low volatility factor.
|
|
pub low_volatility: f64,
|
|
/// Quality factor.
|
|
pub quality: f64,
|
|
}
|
|
|
|
impl RiskFactors {
|
|
/// Get factor value by name.
|
|
#[must_use]
|
|
pub fn get(&self, name: &str) -> Option<f64> {
|
|
match name.to_lowercase().as_str() {
|
|
"market" | "mkt" => Some(self.market),
|
|
"size" | "smb" => Some(self.size),
|
|
"value" | "hml" => Some(self.value),
|
|
"momentum" | "mom" => Some(self.momentum),
|
|
"low_volatility" | "lowvol" => Some(self.low_volatility),
|
|
"quality" | "qual" => Some(self.quality),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Get all factor names.
|
|
#[must_use]
|
|
pub fn names() -> Vec<&'static str> {
|
|
vec![
|
|
"market",
|
|
"size",
|
|
"value",
|
|
"momentum",
|
|
"low_volatility",
|
|
"quality",
|
|
]
|
|
}
|
|
}
|
|
|
|
/// Factor exposures for an asset or portfolio.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct FactorExposures {
|
|
/// Market beta.
|
|
pub market: f64,
|
|
/// Size exposure.
|
|
pub size: f64,
|
|
/// Value exposure.
|
|
pub value: f64,
|
|
/// Momentum exposure.
|
|
pub momentum: f64,
|
|
/// Low volatility exposure.
|
|
pub low_volatility: f64,
|
|
/// Quality exposure.
|
|
pub quality: f64,
|
|
}
|
|
|
|
impl FactorExposures {
|
|
/// Convert to vector.
|
|
#[must_use]
|
|
pub fn to_vec(&self) -> Vec<f64> {
|
|
vec![
|
|
self.market,
|
|
self.size,
|
|
self.value,
|
|
self.momentum,
|
|
self.low_volatility,
|
|
self.quality,
|
|
]
|
|
}
|
|
|
|
/// Create from vector.
|
|
#[must_use]
|
|
pub fn from_vec(v: &[f64]) -> Self {
|
|
Self {
|
|
market: v.first().copied().unwrap_or(0.0),
|
|
size: v.get(1).copied().unwrap_or(0.0),
|
|
value: v.get(2).copied().unwrap_or(0.0),
|
|
momentum: v.get(3).copied().unwrap_or(0.0),
|
|
low_volatility: v.get(4).copied().unwrap_or(0.0),
|
|
quality: v.get(5).copied().unwrap_or(0.0),
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Risk Metrics Types
|
|
// ============================================================================
|
|
|
|
/// Comprehensive risk metrics.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RiskMetrics {
|
|
/// Portfolio volatility (annualized %).
|
|
pub volatility: f64,
|
|
/// Portfolio beta.
|
|
pub beta: f64,
|
|
/// Value at Risk (95%, 1-day, %).
|
|
pub var_95: f64,
|
|
/// Value at Risk (99%, 1-day, %).
|
|
pub var_99: f64,
|
|
/// Conditional VaR / Expected Shortfall (95%, %).
|
|
pub cvar_95: f64,
|
|
/// Maximum drawdown (%).
|
|
pub max_drawdown: f64,
|
|
/// Tracking error vs benchmark (%).
|
|
pub tracking_error: f64,
|
|
/// Active share (%).
|
|
pub active_share: f64,
|
|
}
|
|
|
|
impl Default for RiskMetrics {
|
|
fn default() -> Self {
|
|
Self {
|
|
volatility: 0.0,
|
|
beta: 1.0,
|
|
var_95: 0.0,
|
|
var_99: 0.0,
|
|
cvar_95: 0.0,
|
|
max_drawdown: 0.0,
|
|
tracking_error: 0.0,
|
|
active_share: 0.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Risk attribution to factors.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RiskAttribution {
|
|
/// Total portfolio risk (volatility).
|
|
pub total_risk: f64,
|
|
/// Factor risk contributions.
|
|
pub factor_contributions: FactorRiskContribution,
|
|
/// Specific (idiosyncratic) risk.
|
|
pub specific_risk: f64,
|
|
/// Interaction effects.
|
|
pub interaction_risk: f64,
|
|
}
|
|
|
|
/// Factor risk contributions.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct FactorRiskContribution {
|
|
/// Market risk contribution.
|
|
pub market: f64,
|
|
/// Size risk contribution.
|
|
pub size: f64,
|
|
/// Value risk contribution.
|
|
pub value: f64,
|
|
/// Momentum risk contribution.
|
|
pub momentum: f64,
|
|
/// Low volatility risk contribution.
|
|
pub low_volatility: f64,
|
|
/// Quality risk contribution.
|
|
pub quality: f64,
|
|
}
|
|
|
|
impl FactorRiskContribution {
|
|
/// Get total factor risk.
|
|
#[must_use]
|
|
pub fn total(&self) -> f64 {
|
|
self.market + self.size + self.value + self.momentum + self.low_volatility + self.quality
|
|
}
|
|
|
|
/// Convert to named vector for display.
|
|
#[must_use]
|
|
pub fn to_named_vec(&self) -> Vec<(String, f64)> {
|
|
vec![
|
|
("Market".to_string(), self.market),
|
|
("Size".to_string(), self.size),
|
|
("Value".to_string(), self.value),
|
|
("Momentum".to_string(), self.momentum),
|
|
("Low Vol".to_string(), self.low_volatility),
|
|
("Quality".to_string(), self.quality),
|
|
]
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Risk Analysis Request/Response
|
|
// ============================================================================
|
|
|
|
/// Request for risk analysis.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RiskAnalysisRequest {
|
|
/// Portfolio to analyze.
|
|
pub portfolio: Portfolio,
|
|
/// Historical returns (optional, for custom analysis).
|
|
pub historical_returns: Option<Vec<f64>>,
|
|
/// Benchmark to compare against.
|
|
pub benchmark: Option<String>,
|
|
/// Analysis date.
|
|
pub analysis_date: String,
|
|
/// Risk-free rate.
|
|
pub risk_free_rate: f64,
|
|
}
|
|
|
|
/// Complete risk analysis result.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RiskAnalysisResult {
|
|
/// Portfolio summary.
|
|
pub portfolio_id: String,
|
|
/// Analysis timestamp.
|
|
pub timestamp: String,
|
|
/// Overall risk metrics.
|
|
pub metrics: RiskMetrics,
|
|
/// Factor exposures.
|
|
pub exposures: FactorExposures,
|
|
/// Risk attribution.
|
|
pub attribution: RiskAttribution,
|
|
/// Position-level risk.
|
|
pub position_risk: Vec<PositionRisk>,
|
|
/// Sector risk breakdown.
|
|
pub sector_risk: Vec<SectorRisk>,
|
|
/// Stress test results.
|
|
pub stress_tests: Vec<StressTestResult>,
|
|
}
|
|
|
|
/// Position-level risk metrics.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PositionRisk {
|
|
/// Position symbol.
|
|
pub symbol: String,
|
|
/// Contribution to portfolio volatility (%).
|
|
pub risk_contribution: f64,
|
|
/// Marginal risk (%).
|
|
pub marginal_risk: f64,
|
|
/// Percentage of total risk.
|
|
pub risk_pct: f64,
|
|
/// VaR contribution.
|
|
pub var_contribution: f64,
|
|
}
|
|
|
|
/// Sector-level risk breakdown.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SectorRisk {
|
|
/// Sector name.
|
|
pub sector: String,
|
|
/// Weight in portfolio (%).
|
|
pub weight: f64,
|
|
/// Risk contribution (%).
|
|
pub risk_contribution: f64,
|
|
/// Number of positions.
|
|
pub position_count: usize,
|
|
}
|
|
|
|
/// Stress test scenario result.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct StressTestResult {
|
|
/// Scenario name.
|
|
pub scenario: String,
|
|
/// Scenario description.
|
|
pub description: String,
|
|
/// Portfolio P&L impact (%).
|
|
pub pnl_impact: f64,
|
|
/// Position-level impacts.
|
|
pub position_impacts: Vec<(String, f64)>,
|
|
}
|
|
|
|
// ============================================================================
|
|
// Real-Time Update Types
|
|
// ============================================================================
|
|
|
|
/// Real-time risk update.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RiskUpdate {
|
|
/// Update timestamp.
|
|
pub timestamp: String,
|
|
/// Current VaR.
|
|
pub current_var: f64,
|
|
/// VaR change from previous update.
|
|
pub var_change: f64,
|
|
/// Current volatility.
|
|
pub volatility: f64,
|
|
/// Volatility change.
|
|
pub vol_change: f64,
|
|
/// Alert level (0=normal, 1=warning, 2=critical).
|
|
pub alert_level: u8,
|
|
/// Alert message (if any).
|
|
pub alert_message: Option<String>,
|
|
}
|
|
|
|
/// Market data update.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MarketUpdate {
|
|
/// Update timestamp.
|
|
pub timestamp: String,
|
|
/// Price updates.
|
|
pub prices: Vec<PriceUpdate>,
|
|
/// Factor updates.
|
|
pub factors: RiskFactors,
|
|
}
|
|
|
|
/// Single price update.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PriceUpdate {
|
|
/// Symbol.
|
|
pub symbol: String,
|
|
/// New price.
|
|
pub price: f64,
|
|
/// Change (%).
|
|
pub change_pct: f64,
|
|
}
|
|
|
|
// ============================================================================
|
|
// Sample Data Functions
|
|
// ============================================================================
|
|
|
|
/// Get a sample portfolio.
|
|
#[must_use]
|
|
pub fn get_sample_portfolio() -> Portfolio {
|
|
Portfolio {
|
|
id: "DEMO_001".to_string(),
|
|
name: "Demo Growth Portfolio".to_string(),
|
|
positions: vec![
|
|
Position {
|
|
symbol: "AAPL".to_string(),
|
|
name: "Apple Inc.".to_string(),
|
|
quantity: 100.0,
|
|
price: 180.0,
|
|
market_value: 18000.0,
|
|
weight: 18.0,
|
|
beta: 1.2,
|
|
sector: "Technology".to_string(),
|
|
},
|
|
Position {
|
|
symbol: "MSFT".to_string(),
|
|
name: "Microsoft Corp.".to_string(),
|
|
quantity: 50.0,
|
|
price: 380.0,
|
|
market_value: 19000.0,
|
|
weight: 19.0,
|
|
beta: 1.1,
|
|
sector: "Technology".to_string(),
|
|
},
|
|
Position {
|
|
symbol: "GOOGL".to_string(),
|
|
name: "Alphabet Inc.".to_string(),
|
|
quantity: 30.0,
|
|
price: 140.0,
|
|
market_value: 4200.0,
|
|
weight: 4.2,
|
|
beta: 1.15,
|
|
sector: "Technology".to_string(),
|
|
},
|
|
Position {
|
|
symbol: "JPM".to_string(),
|
|
name: "JPMorgan Chase".to_string(),
|
|
quantity: 80.0,
|
|
price: 170.0,
|
|
market_value: 13600.0,
|
|
weight: 13.6,
|
|
beta: 1.3,
|
|
sector: "Financials".to_string(),
|
|
},
|
|
Position {
|
|
symbol: "JNJ".to_string(),
|
|
name: "Johnson & Johnson".to_string(),
|
|
quantity: 60.0,
|
|
price: 155.0,
|
|
market_value: 9300.0,
|
|
weight: 9.3,
|
|
beta: 0.65,
|
|
sector: "Healthcare".to_string(),
|
|
},
|
|
Position {
|
|
symbol: "PG".to_string(),
|
|
name: "Procter & Gamble".to_string(),
|
|
quantity: 70.0,
|
|
price: 160.0,
|
|
market_value: 11200.0,
|
|
weight: 11.2,
|
|
beta: 0.55,
|
|
sector: "Consumer Staples".to_string(),
|
|
},
|
|
Position {
|
|
symbol: "XOM".to_string(),
|
|
name: "Exxon Mobil".to_string(),
|
|
quantity: 100.0,
|
|
price: 105.0,
|
|
market_value: 10500.0,
|
|
weight: 10.5,
|
|
beta: 0.95,
|
|
sector: "Energy".to_string(),
|
|
},
|
|
Position {
|
|
symbol: "VZ".to_string(),
|
|
name: "Verizon".to_string(),
|
|
quantity: 150.0,
|
|
price: 42.0,
|
|
market_value: 6300.0,
|
|
weight: 6.3,
|
|
beta: 0.45,
|
|
sector: "Communications".to_string(),
|
|
},
|
|
],
|
|
total_value: 100000.0,
|
|
cash: 7900.0,
|
|
currency: "USD".to_string(),
|
|
timestamp: "2024-01-15T16:00:00Z".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Get sample risk factors.
|
|
#[must_use]
|
|
pub fn get_sample_risk_factors() -> RiskFactors {
|
|
RiskFactors {
|
|
market: 0.05,
|
|
size: -0.02,
|
|
value: 0.01,
|
|
momentum: 0.03,
|
|
low_volatility: -0.01,
|
|
quality: 0.02,
|
|
}
|
|
}
|
|
|
|
/// Get predefined stress test scenarios.
|
|
#[must_use]
|
|
pub fn get_stress_scenarios() -> Vec<(String, String, Vec<(&'static str, f64)>)> {
|
|
vec![
|
|
(
|
|
"2008 Financial Crisis".to_string(),
|
|
"Sharp market decline with credit stress".to_string(),
|
|
vec![
|
|
("market", -0.40),
|
|
("size", -0.15),
|
|
("value", -0.10),
|
|
("quality", 0.05),
|
|
],
|
|
),
|
|
(
|
|
"Tech Bubble Burst".to_string(),
|
|
"Technology sector correction".to_string(),
|
|
vec![("market", -0.20), ("momentum", -0.25), ("quality", 0.10)],
|
|
),
|
|
(
|
|
"Rising Rates".to_string(),
|
|
"Sharp increase in interest rates".to_string(),
|
|
vec![
|
|
("market", -0.10),
|
|
("value", 0.05),
|
|
("low_volatility", -0.15),
|
|
],
|
|
),
|
|
(
|
|
"Inflation Spike".to_string(),
|
|
"Unexpected inflation acceleration".to_string(),
|
|
vec![("market", -0.08), ("value", 0.08), ("quality", -0.05)],
|
|
),
|
|
]
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests
|
|
// ============================================================================
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_portfolio_value() {
|
|
let portfolio = get_sample_portfolio();
|
|
let calculated = portfolio.calculate_total_value();
|
|
assert!((calculated - portfolio.total_value).abs() < 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_risk_factors_get() {
|
|
let factors = get_sample_risk_factors();
|
|
assert_eq!(factors.get("market"), Some(0.05));
|
|
assert_eq!(factors.get("MKT"), Some(0.05));
|
|
assert_eq!(factors.get("unknown"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_factor_exposures_roundtrip() {
|
|
let exposures = FactorExposures {
|
|
market: 1.1,
|
|
size: 0.2,
|
|
value: -0.1,
|
|
momentum: 0.3,
|
|
low_volatility: 0.0,
|
|
quality: 0.15,
|
|
};
|
|
|
|
let vec = exposures.to_vec();
|
|
let restored = FactorExposures::from_vec(&vec);
|
|
|
|
assert!((restored.market - exposures.market).abs() < 0.001);
|
|
assert!((restored.momentum - exposures.momentum).abs() < 0.001);
|
|
}
|
|
|
|
#[test]
|
|
fn test_factor_contribution_total() {
|
|
let contrib = FactorRiskContribution {
|
|
market: 0.10,
|
|
size: 0.02,
|
|
value: 0.03,
|
|
momentum: 0.01,
|
|
low_volatility: 0.005,
|
|
quality: 0.015,
|
|
};
|
|
|
|
let total = contrib.total();
|
|
assert!((total - 0.18).abs() < 0.001);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sample_portfolio() {
|
|
let portfolio = get_sample_portfolio();
|
|
assert_eq!(portfolio.positions.len(), 8);
|
|
assert!(portfolio.total_value > 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_stress_scenarios() {
|
|
let scenarios = get_stress_scenarios();
|
|
assert!(scenarios.len() >= 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_serialization() {
|
|
let portfolio = get_sample_portfolio();
|
|
let json = serde_json::to_string(&portfolio).unwrap();
|
|
assert!(json.contains("AAPL"));
|
|
|
|
let parsed: Portfolio = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(parsed.id, portfolio.id);
|
|
}
|
|
}
|