Initial commit
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
//! 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user