365 lines
11 KiB
Rust
365 lines
11 KiB
Rust
//! 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<String>,
|
|
/// 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<SectorLimit>,
|
|
/// Target return (for min variance objective)
|
|
pub target_return: Option<f64>,
|
|
/// Target volatility (for max return objective)
|
|
pub target_volatility: Option<f64>,
|
|
}
|
|
|
|
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<Asset>,
|
|
/// 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<f64>,
|
|
/// 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<f64>,
|
|
/// 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<f64>,
|
|
}
|
|
|
|
/// Efficient frontier curve
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct EfficientFrontier {
|
|
/// Points on the frontier
|
|
pub points: Vec<FrontierPoint>,
|
|
/// 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);
|
|
}
|
|
}
|