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

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