287 lines
9.5 KiB
Rust
287 lines
9.5 KiB
Rust
//! Efficient frontier generation.
|
|
//!
|
|
//! Generates mean-variance and higher-order efficient frontiers.
|
|
|
|
use crate::QuantumPortError;
|
|
use crate::optimizer::PortfolioOptimizer;
|
|
use quantumport_shared::{
|
|
EfficientFrontier, FrontierPoint, FrontierType, OptimizationMethod, OptimizationObjectives,
|
|
OptimizationRequest,
|
|
};
|
|
|
|
/// Efficient frontier generator.
|
|
#[derive(Debug)]
|
|
pub struct FrontierGenerator {
|
|
/// Portfolio optimizer
|
|
optimizer: PortfolioOptimizer,
|
|
}
|
|
|
|
impl Default for FrontierGenerator {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl FrontierGenerator {
|
|
/// Create a new frontier generator.
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
optimizer: PortfolioOptimizer::new(),
|
|
}
|
|
}
|
|
|
|
/// Generate efficient frontier.
|
|
pub fn generate(
|
|
&self,
|
|
request: &OptimizationRequest,
|
|
num_points: usize,
|
|
) -> Result<EfficientFrontier, QuantumPortError> {
|
|
// Determine frontier type
|
|
let frontier_type = match request.method {
|
|
OptimizationMethod::MeanVariance | OptimizationMethod::MinimumVariance => {
|
|
FrontierType::MeanVariance
|
|
}
|
|
OptimizationMethod::MeanVarianceSkewness => FrontierType::MeanVarianceSkewness,
|
|
_ => FrontierType::HigherOrder,
|
|
};
|
|
|
|
// Get minimum variance portfolio
|
|
let mut min_var_request = request.clone();
|
|
min_var_request.method = OptimizationMethod::MinimumVariance;
|
|
let min_variance_portfolio = self.optimizer.optimize(&min_var_request)?;
|
|
|
|
// Get maximum Sharpe portfolio
|
|
let mut max_sharpe_request = request.clone();
|
|
max_sharpe_request.method = OptimizationMethod::MaximumSharpe;
|
|
let max_sharpe_portfolio = self.optimizer.optimize(&max_sharpe_request)?;
|
|
|
|
// Generate frontier points
|
|
let min_return = min_variance_portfolio.statistics.expected_return;
|
|
let max_return = min_return + 0.20; // Extend frontier upward
|
|
|
|
let points =
|
|
self.generate_points(request, num_points, min_return, max_return, frontier_type)?;
|
|
|
|
Ok(EfficientFrontier {
|
|
points,
|
|
min_variance_portfolio,
|
|
max_sharpe_portfolio,
|
|
frontier_type,
|
|
})
|
|
}
|
|
|
|
/// Generate frontier points.
|
|
fn generate_points(
|
|
&self,
|
|
request: &OptimizationRequest,
|
|
num_points: usize,
|
|
min_return: f64,
|
|
max_return: f64,
|
|
frontier_type: FrontierType,
|
|
) -> Result<Vec<FrontierPoint>, QuantumPortError> {
|
|
let mut points = Vec::with_capacity(num_points);
|
|
|
|
for i in 0..num_points {
|
|
let target_return =
|
|
min_return + (max_return - min_return) * (i as f64 / (num_points - 1) as f64);
|
|
|
|
// Create request with target return
|
|
let mut point_request = request.clone();
|
|
point_request.objectives.target_return = Some(target_return);
|
|
|
|
// Optimize for this target
|
|
let portfolio = self.optimizer.optimize(&point_request)?;
|
|
|
|
let point = FrontierPoint {
|
|
expected_return: portfolio.statistics.expected_return,
|
|
volatility: portfolio.statistics.volatility,
|
|
skewness: if frontier_type != FrontierType::MeanVariance {
|
|
Some(portfolio.statistics.skewness)
|
|
} else {
|
|
None
|
|
},
|
|
kurtosis: if frontier_type == FrontierType::HigherOrder {
|
|
Some(portfolio.statistics.kurtosis)
|
|
} else {
|
|
None
|
|
},
|
|
sharpe_ratio: portfolio.statistics.sharpe_ratio,
|
|
weights: portfolio.weights,
|
|
};
|
|
|
|
points.push(point);
|
|
}
|
|
|
|
Ok(points)
|
|
}
|
|
|
|
/// Generate 3D frontier (return-volatility-skewness).
|
|
pub fn generate_3d_frontier(
|
|
&self,
|
|
request: &OptimizationRequest,
|
|
return_points: usize,
|
|
skew_points: usize,
|
|
) -> Result<Vec<FrontierPoint>, QuantumPortError> {
|
|
let mut points = Vec::new();
|
|
|
|
// Get range of returns
|
|
let mut min_var_request = request.clone();
|
|
min_var_request.method = OptimizationMethod::MinimumVariance;
|
|
let min_var = self.optimizer.optimize(&min_var_request)?;
|
|
|
|
let min_return = min_var.statistics.expected_return;
|
|
let max_return = min_return + 0.15;
|
|
|
|
// For each return level, vary skewness preference
|
|
for i in 0..return_points {
|
|
let target_return =
|
|
min_return + (max_return - min_return) * (i as f64 / (return_points - 1) as f64);
|
|
|
|
for j in 0..skew_points {
|
|
let skew_pref = j as f64 / (skew_points - 1) as f64 * 2.0; // 0 to 2
|
|
|
|
let mut point_request = request.clone();
|
|
point_request.objectives = OptimizationObjectives {
|
|
target_return: Some(target_return),
|
|
minimize_variance: true,
|
|
maximize_skewness: true,
|
|
minimize_kurtosis: true,
|
|
risk_aversion: 1.0,
|
|
skewness_preference: skew_pref,
|
|
kurtosis_aversion: 0.5,
|
|
};
|
|
|
|
let portfolio = self.optimizer.optimize(&point_request)?;
|
|
|
|
let point = FrontierPoint {
|
|
expected_return: portfolio.statistics.expected_return,
|
|
volatility: portfolio.statistics.volatility,
|
|
skewness: Some(portfolio.statistics.skewness),
|
|
kurtosis: Some(portfolio.statistics.kurtosis),
|
|
sharpe_ratio: portfolio.statistics.sharpe_ratio,
|
|
weights: portfolio.weights,
|
|
};
|
|
|
|
points.push(point);
|
|
}
|
|
}
|
|
|
|
Ok(points)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use quantumport_shared::{Asset, AssetClass, AssetReturns, 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_frontier_generator_creation() {
|
|
let generator = FrontierGenerator::new();
|
|
assert!(std::mem::size_of_val(&generator) > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_generate_frontier() {
|
|
let generator = FrontierGenerator::new();
|
|
let request = create_test_request();
|
|
|
|
let result = generator.generate(&request, 5);
|
|
assert!(result.is_ok());
|
|
|
|
let frontier = result.unwrap();
|
|
assert_eq!(frontier.points.len(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_frontier_has_min_variance() {
|
|
let generator = FrontierGenerator::new();
|
|
let request = create_test_request();
|
|
|
|
let frontier = generator.generate(&request, 5).unwrap();
|
|
assert!(!frontier.min_variance_portfolio.weights.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_frontier_has_max_sharpe() {
|
|
let generator = FrontierGenerator::new();
|
|
let request = create_test_request();
|
|
|
|
let frontier = generator.generate(&request, 5).unwrap();
|
|
assert!(!frontier.max_sharpe_portfolio.weights.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_frontier_points_ordered() {
|
|
let generator = FrontierGenerator::new();
|
|
let request = create_test_request();
|
|
|
|
let frontier = generator.generate(&request, 10).unwrap();
|
|
|
|
// Returns should generally increase along frontier
|
|
let first_return = frontier.points[0].expected_return;
|
|
let last_return = frontier.points[frontier.points.len() - 1].expected_return;
|
|
assert!(last_return >= first_return - 0.1); // Allow some tolerance
|
|
}
|
|
|
|
#[test]
|
|
fn test_3d_frontier() {
|
|
let generator = FrontierGenerator::new();
|
|
let request = create_test_request();
|
|
|
|
let result = generator.generate_3d_frontier(&request, 3, 3);
|
|
assert!(result.is_ok());
|
|
|
|
let points = result.unwrap();
|
|
assert_eq!(points.len(), 9); // 3 x 3
|
|
|
|
// All points should have skewness
|
|
for point in &points {
|
|
assert!(point.skewness.is_some());
|
|
}
|
|
}
|
|
}
|