715 lines
22 KiB
Rust
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);
|
|
}
|
|
}
|