//! Portfolio optimizer implementing various optimization methods. //! //! Supports mean-variance, higher-order, and QAOA-inspired optimization. use crate::QuantumPortError; use crate::moments::MomentCalculator; use quantumport_shared::{ OptimizationMetadata, OptimizationMethod, OptimizationRequest, OptimizedPortfolio, PortfolioStatistics, PortfolioWeight, RiskMetrics, }; /// Portfolio optimizer. #[derive(Debug)] pub struct PortfolioOptimizer { /// Moment calculator moment_calc: MomentCalculator, /// Maximum iterations max_iterations: usize, /// Convergence tolerance (for early stopping) #[allow(dead_code)] tolerance: f64, /// Risk-free rate risk_free_rate: f64, } impl Default for PortfolioOptimizer { fn default() -> Self { Self::new() } } impl PortfolioOptimizer { /// Create a new optimizer. #[must_use] pub fn new() -> Self { Self { moment_calc: MomentCalculator::new(), max_iterations: 1000, tolerance: 1e-8, risk_free_rate: 0.04, // 4% risk-free rate } } /// Optimize portfolio. pub fn optimize( &self, request: &OptimizationRequest, ) -> Result { let start = std::time::Instant::now(); let weights = match request.method { OptimizationMethod::MeanVariance | OptimizationMethod::MaximumSharpe => { self.optimize_mean_variance(request)? } OptimizationMethod::MinimumVariance => self.optimize_minimum_variance(request)?, OptimizationMethod::HigherOrder | OptimizationMethod::MeanVarianceSkewness => { self.optimize_higher_order(request)? } OptimizationMethod::QAOAInspired => self.optimize_qaoa_inspired(request)?, OptimizationMethod::RiskParity => self.optimize_risk_parity(request)?, }; let time_seconds = start.elapsed().as_secs_f64(); // Calculate statistics let statistics = self.calculate_statistics(&weights, request); let risk_metrics = self.calculate_risk_metrics(&weights, request); // Build portfolio weights let portfolio_weights: Vec = request .assets .iter() .zip(weights.iter()) .map(|(asset, &w)| PortfolioWeight { symbol: asset.symbol.clone(), weight: w, value: None, }) .collect(); Ok(OptimizedPortfolio { weights: portfolio_weights, statistics, risk_metrics, metadata: OptimizationMetadata { method: request.method, iterations: self.max_iterations, converged: true, objective_value: self.calculate_objective(&weights, request), time_seconds, constraints_satisfied: self.check_constraints(&weights, &request.constraints), }, }) } /// Mean-variance optimization. fn optimize_mean_variance( &self, request: &OptimizationRequest, ) -> Result, QuantumPortError> { let n = request.assets.len(); let cov = self.moment_calc.calculate_covariance(&request.returns); let means: Vec = request .returns .iter() .map(|r| r.returns.iter().sum::() / r.returns.len() as f64 * 252.0) .collect(); // Simple gradient descent optimization let mut weights = vec![1.0 / n as f64; n]; let lambda = request.objectives.risk_aversion; for _ in 0..self.max_iterations { // Calculate gradient let mut gradient = vec![0.0; n]; for i in 0..n { // dReturn/dw_i - lambda * dVariance/dw_i gradient[i] = means[i]; for j in 0..n { gradient[i] -= lambda * 2.0 * cov.get(i, j).unwrap_or(0.0) * weights[j]; } } // Update weights let step_size = 0.01; for i in 0..n { weights[i] += step_size * gradient[i]; } // Project to constraints self.project_to_constraints(&mut weights, &request.constraints); } Ok(weights) } /// Minimum variance optimization. fn optimize_minimum_variance( &self, request: &OptimizationRequest, ) -> Result, QuantumPortError> { let n = request.assets.len(); let cov = self.moment_calc.calculate_covariance(&request.returns); // Simple gradient descent for minimum variance let mut weights = vec![1.0 / n as f64; n]; for _ in 0..self.max_iterations { // Calculate variance gradient let mut gradient = vec![0.0; n]; for i in 0..n { for j in 0..n { gradient[i] += 2.0 * cov.get(i, j).unwrap_or(0.0) * weights[j]; } } // Update weights (minimize variance) let step_size = 0.01; for i in 0..n { weights[i] -= step_size * gradient[i]; } // Project to constraints self.project_to_constraints(&mut weights, &request.constraints); } Ok(weights) } /// Higher-order optimization including skewness and kurtosis. fn optimize_higher_order( &self, request: &OptimizationRequest, ) -> Result, QuantumPortError> { let n = request.assets.len(); let cov = self.moment_calc.calculate_covariance(&request.returns); let coskew = self.moment_calc.calculate_coskewness(&request.returns); let means: Vec = request .returns .iter() .map(|r| r.returns.iter().sum::() / r.returns.len() as f64 * 252.0) .collect(); let mut weights = vec![1.0 / n as f64; n]; let lambda_var = request.objectives.risk_aversion; let lambda_skew = request.objectives.skewness_preference; let lambda_kurt = request.objectives.kurtosis_aversion; for _ in 0..self.max_iterations { // Calculate gradient including higher moments let mut gradient = vec![0.0; n]; for i in 0..n { // Return gradient gradient[i] = means[i]; // Variance gradient for j in 0..n { gradient[i] -= lambda_var * 2.0 * cov.get(i, j).unwrap_or(0.0) * weights[j]; } // Skewness gradient (simplified) for j in 0..n { for k in 0..n { let idx = i * n * n + j * n + k; if idx < coskew.data.len() { gradient[i] += lambda_skew * 3.0 * coskew.data[idx] * weights[j] * weights[k]; } } } // Kurtosis gradient (simplified - penalty for concentration) gradient[i] -= lambda_kurt * 4.0 * weights[i].powi(3); } // Update weights let step_size = 0.005; for i in 0..n { weights[i] += step_size * gradient[i]; } // Project to constraints self.project_to_constraints(&mut weights, &request.constraints); } Ok(weights) } /// QAOA-inspired optimization. fn optimize_qaoa_inspired( &self, request: &OptimizationRequest, ) -> Result, QuantumPortError> { let n = request.assets.len(); // QAOA-inspired approach: simulate quantum annealing with temperature schedule let mut weights = vec![1.0 / n as f64; n]; let mut temperature = 1.0; let cooling_rate = 0.995; let cov = self.moment_calc.calculate_covariance(&request.returns); let means: Vec = request .returns .iter() .map(|r| r.returns.iter().sum::() / r.returns.len() as f64 * 252.0) .collect(); let mut best_weights = weights.clone(); let mut best_objective = self.evaluate_objective(&weights, &means, &cov, request); for iteration in 0..self.max_iterations { // Random perturbation (simulating quantum fluctuation) let perturbation_scale = temperature * 0.1; let mut new_weights = weights.clone(); for i in 0..n { // Deterministic perturbation based on iteration let phase = (iteration * (i + 1)) as f64 * 0.1; let delta = perturbation_scale * phase.sin(); new_weights[i] += delta; } // Project to constraints self.project_to_constraints(&mut new_weights, &request.constraints); // Evaluate objective let new_objective = self.evaluate_objective(&new_weights, &means, &cov, request); // Accept with probability based on temperature (simulated annealing) let delta = new_objective - best_objective; let accept = if delta > 0.0 { true } else { // Accept worse solutions with decreasing probability let prob = (delta / temperature).exp(); prob > 0.5 // Simplified acceptance }; if accept { weights = new_weights.clone(); if new_objective > best_objective { best_weights = new_weights; best_objective = new_objective; } } // Cool down temperature *= cooling_rate; } Ok(best_weights) } /// Risk parity optimization. fn optimize_risk_parity( &self, request: &OptimizationRequest, ) -> Result, QuantumPortError> { let n = request.assets.len(); let cov = self.moment_calc.calculate_covariance(&request.returns); // Equal risk contribution let mut weights = vec![1.0 / n as f64; n]; for _ in 0..self.max_iterations { // Calculate marginal risk contributions let mut mrc = vec![0.0; n]; let mut total_risk = 0.0; for i in 0..n { for j in 0..n { mrc[i] += cov.get(i, j).unwrap_or(0.0) * weights[j]; total_risk += weights[i] * cov.get(i, j).unwrap_or(0.0) * weights[j]; } } total_risk = total_risk.sqrt(); // Calculate risk contributions let rc: Vec = weights .iter() .zip(mrc.iter()) .map(|(&w, &m)| w * m / total_risk) .collect(); // Target equal risk contribution let target_rc = total_risk / n as f64; // Adjust weights for i in 0..n { let adjustment = (target_rc - rc[i]) / (rc[i] + 1e-10); weights[i] *= 1.0 + 0.1 * adjustment; } // Normalize let sum: f64 = weights.iter().sum(); for w in &mut weights { *w /= sum; } // Apply constraints self.project_to_constraints(&mut weights, &request.constraints); } Ok(weights) } /// Evaluate objective function. fn evaluate_objective( &self, weights: &[f64], means: &[f64], cov: &quantumport_shared::CovarianceMatrix, request: &OptimizationRequest, ) -> f64 { let n = weights.len(); // Expected return let expected_return: f64 = weights.iter().zip(means.iter()).map(|(w, m)| w * m).sum(); // Variance let mut variance = 0.0; for i in 0..n { for j in 0..n { variance += weights[i] * weights[j] * cov.get(i, j).unwrap_or(0.0); } } // Objective: maximize return - lambda * variance expected_return - request.objectives.risk_aversion * variance } /// Project weights to satisfy constraints. fn project_to_constraints( &self, weights: &mut [f64], constraints: &quantumport_shared::PortfolioConstraints, ) { // Apply bounds for w in weights.iter_mut() { *w = w.max(constraints.min_weight).min(constraints.max_weight); } // Normalize to budget let sum: f64 = weights.iter().sum(); if sum > 0.0 { for w in weights.iter_mut() { *w *= constraints.budget / sum; } } } /// Calculate portfolio statistics. fn calculate_statistics( &self, weights: &[f64], request: &OptimizationRequest, ) -> PortfolioStatistics { let moments = self .moment_calc .calculate_portfolio_moments(weights, &request.returns); let volatility = moments.variance.sqrt(); let sharpe_ratio = if volatility > 0.0 { (moments.mean - self.risk_free_rate) / volatility } else { 0.0 }; // Sortino ratio (using downside deviation) let portfolio_returns: Vec = (0..request.returns[0].returns.len()) .map(|t| { request .returns .iter() .zip(weights.iter()) .map(|(r, w)| w * r.returns[t]) .sum() }) .collect(); let downside_returns: Vec = portfolio_returns .iter() .filter(|&&r| r < 0.0) .copied() .collect(); let downside_deviation = if !downside_returns.is_empty() { let dd: f64 = downside_returns.iter().map(|r| r.powi(2)).sum::() / downside_returns.len() as f64; (dd * 252.0).sqrt() } else { volatility }; let sortino_ratio = if downside_deviation > 0.0 { (moments.mean - self.risk_free_rate) / downside_deviation } else { 0.0 }; // Maximum drawdown (simplified) let max_drawdown = self.calculate_max_drawdown(&portfolio_returns); PortfolioStatistics { expected_return: moments.mean, volatility, skewness: moments.skewness, kurtosis: moments.kurtosis, sharpe_ratio, sortino_ratio, max_drawdown, } } /// Calculate maximum drawdown. fn calculate_max_drawdown(&self, returns: &[f64]) -> f64 { let mut cumulative: f64 = 1.0; let mut peak: f64 = 1.0; let mut max_dd: f64 = 0.0; for &r in returns { cumulative *= 1.0 + r; peak = peak.max(cumulative); let dd = (peak - cumulative) / peak; max_dd = max_dd.max(dd); } max_dd } /// Calculate risk metrics. fn calculate_risk_metrics( &self, weights: &[f64], request: &OptimizationRequest, ) -> RiskMetrics { // Calculate portfolio returns let portfolio_returns: Vec = (0..request.returns[0].returns.len()) .map(|t| { request .returns .iter() .zip(weights.iter()) .map(|(r, w)| w * r.returns[t]) .sum() }) .collect(); // Sort returns for VaR calculation (treat NaN as largest) let mut sorted_returns = portfolio_returns.clone(); sorted_returns.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); let n = sorted_returns.len(); // VaR (annualized) let var_95_idx = ((1.0 - 0.95) * n as f64).floor() as usize; let var_99_idx = ((1.0 - 0.99) * n as f64).floor() as usize; let var_95 = -sorted_returns[var_95_idx] * (252.0_f64).sqrt(); let var_99 = -sorted_returns[var_99_idx] * (252.0_f64).sqrt(); // CVaR (Expected Shortfall) let cvar_95: f64 = -sorted_returns[..var_95_idx.max(1)].iter().sum::() / var_95_idx.max(1) as f64 * (252.0_f64).sqrt(); let cvar_99: f64 = -sorted_returns[..var_99_idx.max(1)].iter().sum::() / var_99_idx.max(1) as f64 * (252.0_f64).sqrt(); // Beta (simplified - correlation with first asset as market proxy) let market_returns = &request.returns[0].returns; let beta = self.calculate_beta(&portfolio_returns, market_returns); RiskMetrics { var_95, var_99, cvar_95, cvar_99, beta, tracking_error: None, information_ratio: None, } } /// Calculate beta. fn calculate_beta(&self, portfolio: &[f64], market: &[f64]) -> f64 { let n = portfolio.len().min(market.len()) as f64; let port_mean: f64 = portfolio.iter().sum::() / n; let mkt_mean: f64 = market.iter().sum::() / n; let mut covariance = 0.0; let mut market_variance = 0.0; for i in 0..n as usize { let port_dev = portfolio[i] - port_mean; let mkt_dev = market[i] - mkt_mean; covariance += port_dev * mkt_dev; market_variance += mkt_dev * mkt_dev; } if market_variance > 0.0 { covariance / market_variance } else { 1.0 } } /// Calculate objective value. fn calculate_objective(&self, weights: &[f64], request: &OptimizationRequest) -> f64 { let moments = self .moment_calc .calculate_portfolio_moments(weights, &request.returns); moments.mean - request.objectives.risk_aversion * moments.variance + request.objectives.skewness_preference * moments.skewness - request.objectives.kurtosis_aversion * moments.kurtosis.abs() } /// Check if constraints are satisfied. fn check_constraints( &self, weights: &[f64], constraints: &quantumport_shared::PortfolioConstraints, ) -> bool { let sum: f64 = weights.iter().sum(); if (sum - constraints.budget).abs() > 1e-6 { return false; } for &w in weights { if w < constraints.min_weight - 1e-6 || w > constraints.max_weight + 1e-6 { return false; } } true } } #[cfg(test)] mod tests { use super::*; use quantumport_shared::{ Asset, AssetClass, AssetReturns, OptimizationObjectives, PortfolioConstraints, }; fn create_test_request() -> OptimizationRequest { OptimizationRequest { assets: vec![ Asset { symbol: "A".to_string(), name: "Asset A".to_string(), asset_class: AssetClass::Equity, sector: None, currency: "USD".to_string(), }, Asset { symbol: "B".to_string(), name: "Asset B".to_string(), asset_class: AssetClass::FixedIncome, sector: None, currency: "USD".to_string(), }, ], returns: vec![ AssetReturns { symbol: "A".to_string(), returns: vec![ 0.01, -0.02, 0.015, 0.005, -0.01, 0.02, -0.005, 0.01, 0.008, -0.012, ], start_date: "2024-01-01".to_string(), end_date: "2024-01-10".to_string(), }, AssetReturns { symbol: "B".to_string(), returns: vec![ 0.002, 0.001, 0.003, -0.001, 0.002, 0.001, 0.002, -0.001, 0.003, 0.001, ], start_date: "2024-01-01".to_string(), end_date: "2024-01-10".to_string(), }, ], objectives: OptimizationObjectives::default(), constraints: PortfolioConstraints::default(), method: OptimizationMethod::HigherOrder, } } #[test] fn test_optimizer_creation() { let opt = PortfolioOptimizer::new(); assert_eq!(opt.max_iterations, 1000); } #[test] fn test_mean_variance_optimization() { let opt = PortfolioOptimizer::new(); let mut request = create_test_request(); request.method = OptimizationMethod::MeanVariance; let result = opt.optimize(&request); assert!(result.is_ok()); let portfolio = result.unwrap(); let total: f64 = portfolio.weights.iter().map(|w| w.weight).sum(); assert!((total - 1.0).abs() < 0.01); } #[test] fn test_minimum_variance() { let opt = PortfolioOptimizer::new(); let mut request = create_test_request(); request.method = OptimizationMethod::MinimumVariance; let result = opt.optimize(&request); assert!(result.is_ok()); } #[test] fn test_higher_order_optimization() { let opt = PortfolioOptimizer::new(); let request = create_test_request(); let result = opt.optimize(&request); assert!(result.is_ok()); let portfolio = result.unwrap(); assert!(!portfolio.weights.is_empty()); } #[test] fn test_qaoa_inspired() { let opt = PortfolioOptimizer::new(); let mut request = create_test_request(); request.method = OptimizationMethod::QAOAInspired; let result = opt.optimize(&request); assert!(result.is_ok()); } #[test] fn test_risk_parity() { let opt = PortfolioOptimizer::new(); let mut request = create_test_request(); request.method = OptimizationMethod::RiskParity; let result = opt.optimize(&request); assert!(result.is_ok()); } #[test] fn test_statistics() { let opt = PortfolioOptimizer::new(); let request = create_test_request(); let result = opt.optimize(&request).unwrap(); assert!(result.statistics.volatility >= 0.0); } #[test] fn test_risk_metrics() { let opt = PortfolioOptimizer::new(); let request = create_test_request(); let result = opt.optimize(&request).unwrap(); assert!(result.risk_metrics.var_95 >= 0.0); } }