Files
rustytorch/demos/rtx-riskflow-demo/src/lib.rs
T
2026-03-04 00:08:42 +00:00

220 lines
5.8 KiB
Rust

//! 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<RiskAnalysisResult, RiskFlowError> {
// 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<RiskUpdate, RiskFlowError> {
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<RiskAnalysisResult, RiskFlowError> {
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());
}
}