309 lines
8.6 KiB
Rust
309 lines
8.6 KiB
Rust
//! QuantumPort - Higher-Order Portfolio Optimization Demo.
|
|
//!
|
|
//! This demo showcases portfolio optimization using higher-order moments
|
|
//! (skewness, kurtosis) beyond traditional mean-variance optimization.
|
|
|
|
pub mod comparison;
|
|
pub mod frontier;
|
|
pub mod moments;
|
|
pub mod optimizer;
|
|
pub mod sample_data;
|
|
|
|
use quantumport_shared::{
|
|
EfficientFrontier, OptimizationRequest, OptimizedPortfolio, PortfolioComparison,
|
|
PortfolioConstraints,
|
|
};
|
|
use thiserror::Error;
|
|
|
|
use comparison::PortfolioComparer;
|
|
use frontier::FrontierGenerator;
|
|
use optimizer::PortfolioOptimizer;
|
|
|
|
/// Errors that can occur during portfolio optimization.
|
|
#[derive(Debug, Error)]
|
|
pub enum QuantumPortError {
|
|
/// No assets provided.
|
|
#[error("No assets provided for optimization")]
|
|
NoAssets,
|
|
|
|
/// No returns data provided.
|
|
#[error("No returns data provided")]
|
|
NoReturnsData,
|
|
|
|
/// Mismatched assets and returns.
|
|
#[error("Asset count ({0}) doesn't match returns count ({1})")]
|
|
MismatchedData(usize, usize),
|
|
|
|
/// Optimization failed.
|
|
#[error("Optimization failed: {0}")]
|
|
OptimizationFailed(String),
|
|
|
|
/// Invalid constraints.
|
|
#[error("Invalid constraints: {0}")]
|
|
InvalidConstraints(String),
|
|
|
|
/// Numerical error.
|
|
#[error("Numerical error: {0}")]
|
|
NumericalError(String),
|
|
}
|
|
|
|
/// Main QuantumPort system for higher-order portfolio optimization.
|
|
#[derive(Debug)]
|
|
pub struct QuantumPort {
|
|
optimizer: PortfolioOptimizer,
|
|
frontier_generator: FrontierGenerator,
|
|
comparer: PortfolioComparer,
|
|
}
|
|
|
|
impl Default for QuantumPort {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl QuantumPort {
|
|
/// Create a new QuantumPort system.
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
optimizer: PortfolioOptimizer::new(),
|
|
frontier_generator: FrontierGenerator::new(),
|
|
comparer: PortfolioComparer::new(),
|
|
}
|
|
}
|
|
|
|
/// Optimize a portfolio based on the request.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns error if optimization fails or input data is invalid.
|
|
pub fn optimize(
|
|
&self,
|
|
request: &OptimizationRequest,
|
|
) -> Result<OptimizedPortfolio, QuantumPortError> {
|
|
// Validate input
|
|
self.validate_request(request)?;
|
|
|
|
// Run optimization
|
|
self.optimizer.optimize(request)
|
|
}
|
|
|
|
/// Generate efficient frontier.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns error if frontier generation fails.
|
|
pub fn generate_frontier(
|
|
&self,
|
|
request: &OptimizationRequest,
|
|
num_points: usize,
|
|
) -> Result<EfficientFrontier, QuantumPortError> {
|
|
self.validate_request(request)?;
|
|
self.frontier_generator.generate(request, num_points)
|
|
}
|
|
|
|
/// Compare mean-variance vs higher-order optimization.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns error if comparison fails.
|
|
pub fn compare_methods(
|
|
&self,
|
|
request: &OptimizationRequest,
|
|
) -> Result<PortfolioComparison, QuantumPortError> {
|
|
self.validate_request(request)?;
|
|
self.comparer.compare(request)
|
|
}
|
|
|
|
/// Validate optimization request.
|
|
fn validate_request(&self, request: &OptimizationRequest) -> Result<(), QuantumPortError> {
|
|
if request.assets.is_empty() {
|
|
return Err(QuantumPortError::NoAssets);
|
|
}
|
|
|
|
if request.returns.is_empty() {
|
|
return Err(QuantumPortError::NoReturnsData);
|
|
}
|
|
|
|
if request.assets.len() != request.returns.len() {
|
|
return Err(QuantumPortError::MismatchedData(
|
|
request.assets.len(),
|
|
request.returns.len(),
|
|
));
|
|
}
|
|
|
|
self.validate_constraints(&request.constraints)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Validate portfolio constraints.
|
|
fn validate_constraints(
|
|
&self,
|
|
constraints: &PortfolioConstraints,
|
|
) -> Result<(), QuantumPortError> {
|
|
if constraints.budget <= 0.0 {
|
|
return Err(QuantumPortError::InvalidConstraints(
|
|
"Budget must be positive".to_string(),
|
|
));
|
|
}
|
|
|
|
if constraints.max_weight < constraints.min_weight {
|
|
return Err(QuantumPortError::InvalidConstraints(
|
|
"max_weight must be >= min_weight".to_string(),
|
|
));
|
|
}
|
|
|
|
if !constraints.allow_short && constraints.min_weight < 0.0 {
|
|
return Err(QuantumPortError::InvalidConstraints(
|
|
"min_weight must be >= 0 when short selling is not allowed".to_string(),
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Run a complete demo with sample data.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns error if demo fails.
|
|
pub fn run_demo() -> Result<PortfolioComparison, QuantumPortError> {
|
|
let request = quantumport_shared::get_sample_request();
|
|
let qp = QuantumPort::new();
|
|
qp.compare_methods(&request)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use quantumport_shared::{
|
|
Asset, AssetClass, AssetReturns, OptimizationMethod, OptimizationObjectives,
|
|
};
|
|
|
|
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],
|
|
start_date: "2024-01-01".to_string(),
|
|
end_date: "2024-01-05".to_string(),
|
|
},
|
|
AssetReturns {
|
|
symbol: "B".to_string(),
|
|
returns: vec![0.002, 0.001, 0.003, -0.001, 0.002],
|
|
start_date: "2024-01-01".to_string(),
|
|
end_date: "2024-01-05".to_string(),
|
|
},
|
|
],
|
|
objectives: OptimizationObjectives::default(),
|
|
constraints: PortfolioConstraints::default(),
|
|
method: OptimizationMethod::HigherOrder,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_quantumport_creation() {
|
|
let qp = QuantumPort::new();
|
|
assert!(std::mem::size_of_val(&qp) > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_optimize() {
|
|
let request = create_test_request();
|
|
let qp = QuantumPort::new();
|
|
|
|
let result = qp.optimize(&request);
|
|
assert!(result.is_ok());
|
|
|
|
let portfolio = result.unwrap();
|
|
assert!(!portfolio.weights.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_assets_error() {
|
|
let mut request = create_test_request();
|
|
request.assets.clear();
|
|
request.returns.clear();
|
|
|
|
let qp = QuantumPort::new();
|
|
let result = qp.optimize(&request);
|
|
assert!(matches!(result.unwrap_err(), QuantumPortError::NoAssets));
|
|
}
|
|
|
|
#[test]
|
|
fn test_mismatched_data_error() {
|
|
let mut request = create_test_request();
|
|
request.returns.pop();
|
|
|
|
let qp = QuantumPort::new();
|
|
let result = qp.optimize(&request);
|
|
assert!(matches!(
|
|
result.unwrap_err(),
|
|
QuantumPortError::MismatchedData(_, _)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_generate_frontier() {
|
|
let request = create_test_request();
|
|
let qp = QuantumPort::new();
|
|
|
|
let result = qp.generate_frontier(&request, 10);
|
|
assert!(result.is_ok());
|
|
|
|
let frontier = result.unwrap();
|
|
assert!(!frontier.points.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_compare_methods() {
|
|
let request = create_test_request();
|
|
let qp = QuantumPort::new();
|
|
|
|
let result = qp.compare_methods(&request);
|
|
assert!(result.is_ok());
|
|
|
|
let comparison = result.unwrap();
|
|
assert!(!comparison.mean_variance.weights.is_empty());
|
|
assert!(!comparison.higher_order.weights.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_run_demo() {
|
|
let result = run_demo();
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_constraints() {
|
|
let mut request = create_test_request();
|
|
request.constraints.max_weight = -1.0;
|
|
|
|
let qp = QuantumPort::new();
|
|
let result = qp.optimize(&request);
|
|
assert!(matches!(
|
|
result.unwrap_err(),
|
|
QuantumPortError::InvalidConstraints(_)
|
|
));
|
|
}
|
|
}
|