Files
rustytorch/demos/rtx-quantumport-demo/src/optimizer.rs
T
2026-03-04 00:08:42 +00:00

715 lines
22 KiB
Rust

//! 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<OptimizedPortfolio, QuantumPortError> {
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<PortfolioWeight> = 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<Vec<f64>, QuantumPortError> {
let n = request.assets.len();
let cov = self.moment_calc.calculate_covariance(&request.returns);
let means: Vec<f64> = request
.returns
.iter()
.map(|r| r.returns.iter().sum::<f64>() / 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<Vec<f64>, 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<Vec<f64>, 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<f64> = request
.returns
.iter()
.map(|r| r.returns.iter().sum::<f64>() / 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<Vec<f64>, 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<f64> = request
.returns
.iter()
.map(|r| r.returns.iter().sum::<f64>() / 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<Vec<f64>, 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<f64> = 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<f64> = (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<f64> = 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::<f64>()
/ 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<f64> = (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::<f64>()
/ var_95_idx.max(1) as f64
* (252.0_f64).sqrt();
let cvar_99: f64 = -sorted_returns[..var_99_idx.max(1)].iter().sum::<f64>()
/ 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::<f64>() / n;
let mkt_mean: f64 = market.iter().sum::<f64>() / 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);
}
}