//! RiskFlow - Real-Time Risk Attribution Engine. //! //! This demo showcases real-time portfolio risk analysis with factor attribution. pub mod attribution; pub mod risk_model; pub mod sample_data; pub mod stress_test; use riskflow_shared::{Portfolio, RiskAnalysisRequest, RiskAnalysisResult, RiskUpdate}; use thiserror::Error; use attribution::RiskAttributor; use risk_model::RiskModel; use stress_test::StressTester; /// Errors that can occur during risk analysis. #[derive(Debug, Error)] pub enum RiskFlowError { /// Empty portfolio. #[error("Portfolio is empty")] EmptyPortfolio, /// Invalid position data. #[error("Invalid position data: {0}")] InvalidPosition(String), /// Risk calculation failed. #[error("Risk calculation failed: {0}")] CalculationFailed(String), /// Insufficient data. #[error("Insufficient data for analysis")] InsufficientData, } /// Main RiskFlow engine. #[derive(Debug)] pub struct RiskFlow { /// Risk model. risk_model: RiskModel, /// Risk attributor. attributor: RiskAttributor, /// Stress tester. stress_tester: StressTester, } impl Default for RiskFlow { fn default() -> Self { Self::new() } } impl RiskFlow { /// Create a new RiskFlow engine. #[must_use] pub fn new() -> Self { Self { risk_model: RiskModel::new(), attributor: RiskAttributor::new(), stress_tester: StressTester::new(), } } /// Analyze portfolio risk. pub fn analyze( &self, request: &RiskAnalysisRequest, ) -> Result { // Validate portfolio if request.portfolio.positions.is_empty() { return Err(RiskFlowError::EmptyPortfolio); } // Calculate risk metrics let metrics = self.risk_model.calculate_metrics(&request.portfolio)?; // Calculate factor exposures let exposures = self.risk_model.calculate_exposures(&request.portfolio); // Calculate risk attribution let attribution = self.attributor.attribute(&request.portfolio, &exposures)?; // Calculate position-level risk let position_risk = self.risk_model.position_risk(&request.portfolio)?; // Calculate sector risk let sector_risk = self.risk_model.sector_risk(&request.portfolio); // Run stress tests let stress_tests = self.stress_tester.run_all(&request.portfolio); Ok(RiskAnalysisResult { portfolio_id: request.portfolio.id.clone(), timestamp: request.analysis_date.clone(), metrics, exposures, attribution, position_risk, sector_risk, stress_tests, }) } /// Get real-time risk update. pub fn get_update(&self, portfolio: &Portfolio) -> Result { let metrics = self.risk_model.calculate_metrics(portfolio)?; // Determine alert level let alert_level = if metrics.var_95 > 3.0 { 2 // Critical } else if metrics.var_95 > 2.0 { 1 // Warning } else { 0 // Normal }; let alert_message = match alert_level { 2 => Some("VaR exceeds risk limit".to_string()), 1 => Some("Elevated risk detected".to_string()), _ => None, }; Ok(RiskUpdate { timestamp: chrono_stub(), current_var: metrics.var_95, var_change: 0.0, // Would require previous state volatility: metrics.volatility, vol_change: 0.0, alert_level, alert_message, }) } /// Get the risk model. #[must_use] pub fn risk_model(&self) -> &RiskModel { &self.risk_model } } /// Simple timestamp stub (no chrono dependency). fn chrono_stub() -> String { "2024-01-15T16:00:00Z".to_string() } /// Run the full demo. pub fn run_demo() -> Result { let engine = RiskFlow::new(); let request = sample_data::create_sample_request(); engine.analyze(&request) } #[cfg(test)] mod tests { use super::*; use riskflow_shared::get_sample_portfolio; #[test] fn test_riskflow_creation() { let engine = RiskFlow::new(); assert!(std::mem::size_of_val(&engine) > 0); } #[test] fn test_analyze() { let engine = RiskFlow::new(); let request = RiskAnalysisRequest { portfolio: get_sample_portfolio(), historical_returns: None, benchmark: Some("SPY".to_string()), analysis_date: "2024-01-15".to_string(), risk_free_rate: 0.04, }; let result = engine.analyze(&request); assert!(result.is_ok()); let analysis = result.unwrap(); assert!(!analysis.position_risk.is_empty()); assert!(!analysis.sector_risk.is_empty()); } #[test] fn test_empty_portfolio_error() { let engine = RiskFlow::new(); let mut portfolio = get_sample_portfolio(); portfolio.positions.clear(); let request = RiskAnalysisRequest { portfolio, historical_returns: None, benchmark: None, analysis_date: "2024-01-15".to_string(), risk_free_rate: 0.04, }; let result = engine.analyze(&request); assert!(matches!(result, Err(RiskFlowError::EmptyPortfolio))); } #[test] fn test_get_update() { let engine = RiskFlow::new(); let portfolio = get_sample_portfolio(); let result = engine.get_update(&portfolio); assert!(result.is_ok()); let update = result.unwrap(); assert!(update.current_var > 0.0); } #[test] fn test_run_demo() { let result = run_demo(); assert!(result.is_ok()); } }