//! 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, /// 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::() + 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 { 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 { 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>, /// Benchmark to compare against. pub benchmark: Option, /// 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, /// Sector risk breakdown. pub sector_risk: Vec, /// Stress test results. pub stress_tests: Vec, } /// 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, } /// Market data update. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MarketUpdate { /// Update timestamp. pub timestamp: String, /// Price updates. pub prices: Vec, /// 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); } }