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

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);
}
}