Initial commit
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
//! 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user