//! 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 { // 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, 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, 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()); } } }