//! Shared IPC types for the Portfolio Optimizer demo //! //! This crate defines the data structures shared between the Rust backend //! and the TypeScript frontend for the portfolio optimization demo. use serde::{Deserialize, Serialize}; /// Asset metadata for portfolio construction #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Asset { /// Asset ticker or identifier pub ticker: String, /// Asset name pub name: String, /// Asset class (equity, bond, commodity, etc.) pub asset_class: String, /// Sector classification pub sector: Option, /// Expected annual return (decimal, e.g., 0.08 for 8%) pub expected_return: f64, /// Annual volatility (decimal, e.g., 0.15 for 15%) pub volatility: f64, } /// Portfolio optimization objective #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum OptimizationObjective { /// Minimize variance for a target return MinVariance, /// Maximize Sharpe ratio MaxSharpe, /// Risk parity (equal risk contribution) RiskParity, /// Maximum return for target risk MaxReturn, } impl OptimizationObjective { /// Get display name pub fn display_name(&self) -> &'static str { match self { Self::MinVariance => "Minimum Variance", Self::MaxSharpe => "Maximum Sharpe Ratio", Self::RiskParity => "Risk Parity", Self::MaxReturn => "Maximum Return", } } /// Get description pub fn description(&self) -> &'static str { match self { Self::MinVariance => "Minimize portfolio variance for a target return", Self::MaxSharpe => "Maximize risk-adjusted return (Sharpe ratio)", Self::RiskParity => "Equal risk contribution from all assets", Self::MaxReturn => "Maximize expected return for a target risk level", } } } /// Portfolio constraints #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct PortfolioConstraints { /// Enforce long-only positions (no short selling) pub long_only: bool, /// Minimum weight per asset (0.0 - 1.0) pub min_weight: f64, /// Maximum weight per asset (0.0 - 1.0) pub max_weight: f64, /// Sector exposure limits (sector -> max weight) pub sector_limits: Vec, /// Target return (for min variance objective) pub target_return: Option, /// Target volatility (for max return objective) pub target_volatility: Option, } impl Default for PortfolioConstraints { fn default() -> Self { Self { long_only: true, min_weight: 0.0, max_weight: 1.0, sector_limits: Vec::new(), target_return: None, target_volatility: None, } } } /// Sector exposure limit #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SectorLimit { /// Sector name pub sector: String, /// Maximum total weight for this sector pub max_weight: f64, } /// Configuration for portfolio optimization #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct PortfolioConfig { /// List of assets pub assets: Vec, /// Optimization objective pub objective: OptimizationObjective, /// Portfolio constraints pub constraints: PortfolioConstraints, /// Risk-free rate for Sharpe ratio calculation (annual, decimal) pub risk_free_rate: f64, /// Use GPU for covariance computation pub use_gpu: bool, } impl Default for PortfolioConfig { fn default() -> Self { Self { assets: Vec::new(), objective: OptimizationObjective::MaxSharpe, constraints: PortfolioConstraints::default(), risk_free_rate: 0.02, use_gpu: true, } } } /// Optimized portfolio weights and metrics #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct OptimizationResult { /// Asset weights (same order as input assets) pub weights: Vec, /// Expected portfolio return (annual, decimal) pub expected_return: f64, /// Portfolio volatility (annual, decimal) pub volatility: f64, /// Sharpe ratio pub sharpe_ratio: f64, /// Risk contribution by asset pub risk_contributions: Vec, /// Optimization status message pub status: String, /// Whether optimization succeeded pub success: bool, /// Processing time in milliseconds pub processing_time_ms: f64, } /// Point on the efficient frontier #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct FrontierPoint { /// Expected return pub expected_return: f64, /// Volatility pub volatility: f64, /// Sharpe ratio pub sharpe_ratio: f64, /// Portfolio weights pub weights: Vec, } /// Efficient frontier curve #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct EfficientFrontier { /// Points on the frontier pub points: Vec, /// Index of maximum Sharpe ratio portfolio pub max_sharpe_index: usize, /// Index of minimum variance portfolio pub min_variance_index: usize, } /// Portfolio optimizer status #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OptimizerStatus { /// Whether optimizer is initialized pub initialized: bool, /// Number of assets in current portfolio pub num_assets: usize, /// Compute device being used pub device: String, /// Number of optimizations performed pub optimization_count: u64, /// Average optimization time in ms pub avg_optimization_time_ms: f64, } /// Sample portfolio presets #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum PortfolioPreset { /// Conservative portfolio (bonds heavy) Conservative, /// Balanced portfolio (60/40 stocks/bonds) Balanced, /// Aggressive portfolio (stocks heavy) Aggressive, /// Global diversified portfolio GlobalDiversified, /// Tech-focused portfolio TechFocus, } impl PortfolioPreset { /// Get display name pub fn display_name(&self) -> &'static str { match self { Self::Conservative => "Conservative", Self::Balanced => "Balanced", Self::Aggressive => "Aggressive", Self::GlobalDiversified => "Global Diversified", Self::TechFocus => "Tech Focus", } } /// Get description pub fn description(&self) -> &'static str { match self { Self::Conservative => "Low risk, bond-heavy portfolio for capital preservation", Self::Balanced => "Moderate risk with 60/40 stock/bond allocation", Self::Aggressive => "High risk, equity-focused portfolio for growth", Self::GlobalDiversified => "Globally diversified across asset classes and regions", Self::TechFocus => "Technology sector concentrated portfolio", } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_asset_serialization() { let asset = Asset { ticker: "AAPL".to_string(), name: "Apple Inc.".to_string(), asset_class: "equity".to_string(), sector: Some("Technology".to_string()), expected_return: 0.12, volatility: 0.20, }; let json = serde_json::to_string(&asset).unwrap(); let deserialized: Asset = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.ticker, "AAPL"); assert_eq!(deserialized.expected_return, 0.12); assert_eq!(deserialized.volatility, 0.20); } #[test] fn test_optimization_objective_display() { assert_eq!( OptimizationObjective::MaxSharpe.display_name(), "Maximum Sharpe Ratio" ); assert_eq!( OptimizationObjective::RiskParity.display_name(), "Risk Parity" ); } #[test] fn test_portfolio_constraints_default() { let constraints = PortfolioConstraints::default(); assert!(constraints.long_only); assert_eq!(constraints.min_weight, 0.0); assert_eq!(constraints.max_weight, 1.0); assert!(constraints.sector_limits.is_empty()); } #[test] fn test_portfolio_config_serialization() { let config = PortfolioConfig { assets: vec![Asset { ticker: "SPY".to_string(), name: "S&P 500 ETF".to_string(), asset_class: "equity".to_string(), sector: None, expected_return: 0.10, volatility: 0.15, }], objective: OptimizationObjective::MaxSharpe, constraints: PortfolioConstraints::default(), risk_free_rate: 0.02, use_gpu: true, }; let json = serde_json::to_string(&config).unwrap(); let deserialized: PortfolioConfig = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.assets.len(), 1); assert_eq!(deserialized.risk_free_rate, 0.02); assert!(deserialized.use_gpu); } #[test] fn test_optimization_result_serialization() { let result = OptimizationResult { weights: vec![0.6, 0.4], expected_return: 0.08, volatility: 0.12, sharpe_ratio: 0.5, risk_contributions: vec![0.072, 0.048], status: "Converged".to_string(), success: true, processing_time_ms: 15.5, }; let json = serde_json::to_string(&result).unwrap(); let deserialized: OptimizationResult = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.weights.len(), 2); assert!(deserialized.success); assert_eq!(deserialized.sharpe_ratio, 0.5); } #[test] fn test_efficient_frontier_serialization() { let frontier = EfficientFrontier { points: vec![ FrontierPoint { expected_return: 0.06, volatility: 0.08, sharpe_ratio: 0.5, weights: vec![0.3, 0.7], }, FrontierPoint { expected_return: 0.10, volatility: 0.15, sharpe_ratio: 0.53, weights: vec![0.7, 0.3], }, ], max_sharpe_index: 1, min_variance_index: 0, }; let json = serde_json::to_string(&frontier).unwrap(); let deserialized: EfficientFrontier = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.points.len(), 2); assert_eq!(deserialized.max_sharpe_index, 1); } #[test] fn test_portfolio_preset_display() { assert_eq!(PortfolioPreset::Conservative.display_name(), "Conservative"); assert_eq!(PortfolioPreset::Balanced.display_name(), "Balanced"); } #[test] fn test_sector_limit() { let limit = SectorLimit { sector: "Technology".to_string(), max_weight: 0.3, }; let json = serde_json::to_string(&limit).unwrap(); let deserialized: SectorLimit = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.sector, "Technology"); assert_eq!(deserialized.max_weight, 0.3); } }