371 lines
12 KiB
Rust
371 lines
12 KiB
Rust
//! Sample data utilities for the QuantumPort demo.
|
|
|
|
use quantumport_shared::{
|
|
Asset, AssetClass, AssetReturns, OptimizationMethod, OptimizationObjectives,
|
|
OptimizationRequest, PortfolioConstraints,
|
|
};
|
|
|
|
/// Get a comprehensive sample portfolio request.
|
|
#[must_use]
|
|
pub fn get_comprehensive_request() -> OptimizationRequest {
|
|
OptimizationRequest {
|
|
assets: get_diversified_portfolio(),
|
|
returns: get_simulated_returns(),
|
|
objectives: OptimizationObjectives {
|
|
target_return: None,
|
|
minimize_variance: true,
|
|
maximize_skewness: true,
|
|
minimize_kurtosis: true,
|
|
risk_aversion: 2.0,
|
|
skewness_preference: 1.0,
|
|
kurtosis_aversion: 0.5,
|
|
},
|
|
constraints: PortfolioConstraints {
|
|
budget: 1.0,
|
|
allow_short: false,
|
|
max_weight: 0.30,
|
|
min_weight: 0.05,
|
|
max_assets: Some(8),
|
|
sector_constraints: vec![],
|
|
max_turnover: None,
|
|
},
|
|
method: OptimizationMethod::HigherOrder,
|
|
}
|
|
}
|
|
|
|
/// Get a diversified portfolio of assets.
|
|
#[must_use]
|
|
pub fn get_diversified_portfolio() -> Vec<Asset> {
|
|
vec![
|
|
// US Large Cap
|
|
Asset {
|
|
symbol: "SPY".to_string(),
|
|
name: "S&P 500 ETF".to_string(),
|
|
asset_class: AssetClass::Equity,
|
|
sector: Some("US Large Cap".to_string()),
|
|
currency: "USD".to_string(),
|
|
},
|
|
// US Tech
|
|
Asset {
|
|
symbol: "QQQ".to_string(),
|
|
name: "NASDAQ 100 ETF".to_string(),
|
|
asset_class: AssetClass::Equity,
|
|
sector: Some("US Technology".to_string()),
|
|
currency: "USD".to_string(),
|
|
},
|
|
// US Small Cap
|
|
Asset {
|
|
symbol: "IWM".to_string(),
|
|
name: "Russell 2000 ETF".to_string(),
|
|
asset_class: AssetClass::Equity,
|
|
sector: Some("US Small Cap".to_string()),
|
|
currency: "USD".to_string(),
|
|
},
|
|
// International Developed
|
|
Asset {
|
|
symbol: "EFA".to_string(),
|
|
name: "EAFE ETF".to_string(),
|
|
asset_class: AssetClass::Equity,
|
|
sector: Some("International Developed".to_string()),
|
|
currency: "USD".to_string(),
|
|
},
|
|
// Emerging Markets
|
|
Asset {
|
|
symbol: "EEM".to_string(),
|
|
name: "Emerging Markets ETF".to_string(),
|
|
asset_class: AssetClass::Equity,
|
|
sector: Some("Emerging Markets".to_string()),
|
|
currency: "USD".to_string(),
|
|
},
|
|
// US Bonds
|
|
Asset {
|
|
symbol: "AGG".to_string(),
|
|
name: "US Aggregate Bond ETF".to_string(),
|
|
asset_class: AssetClass::FixedIncome,
|
|
sector: None,
|
|
currency: "USD".to_string(),
|
|
},
|
|
// Long-term Treasury
|
|
Asset {
|
|
symbol: "TLT".to_string(),
|
|
name: "20+ Year Treasury Bond ETF".to_string(),
|
|
asset_class: AssetClass::FixedIncome,
|
|
sector: None,
|
|
currency: "USD".to_string(),
|
|
},
|
|
// High Yield
|
|
Asset {
|
|
symbol: "HYG".to_string(),
|
|
name: "High Yield Corporate Bond ETF".to_string(),
|
|
asset_class: AssetClass::FixedIncome,
|
|
sector: None,
|
|
currency: "USD".to_string(),
|
|
},
|
|
// Gold
|
|
Asset {
|
|
symbol: "GLD".to_string(),
|
|
name: "Gold ETF".to_string(),
|
|
asset_class: AssetClass::Commodity,
|
|
sector: None,
|
|
currency: "USD".to_string(),
|
|
},
|
|
// Real Estate
|
|
Asset {
|
|
symbol: "VNQ".to_string(),
|
|
name: "Real Estate ETF".to_string(),
|
|
asset_class: AssetClass::RealEstate,
|
|
sector: None,
|
|
currency: "USD".to_string(),
|
|
},
|
|
]
|
|
}
|
|
|
|
/// Get simulated historical returns.
|
|
#[must_use]
|
|
pub fn get_simulated_returns() -> Vec<AssetReturns> {
|
|
let num_days = 252; // One year of trading days
|
|
|
|
vec![
|
|
// SPY - moderate return, moderate vol
|
|
simulate_asset_returns("SPY", num_days, 0.0004, 0.011, -0.15, 3.2),
|
|
// QQQ - higher return, higher vol
|
|
simulate_asset_returns("QQQ", num_days, 0.0005, 0.014, -0.25, 3.8),
|
|
// IWM - moderate return, high vol
|
|
simulate_asset_returns("IWM", num_days, 0.0003, 0.015, -0.30, 4.0),
|
|
// EFA - lower return, moderate vol
|
|
simulate_asset_returns("EFA", num_days, 0.0002, 0.012, -0.10, 3.1),
|
|
// EEM - volatile emerging markets
|
|
simulate_asset_returns("EEM", num_days, 0.0003, 0.018, -0.35, 4.5),
|
|
// AGG - low return, low vol, positive skew
|
|
simulate_asset_returns("AGG", num_days, 0.0001, 0.004, 0.10, 2.8),
|
|
// TLT - negative correlation to equities
|
|
simulate_asset_returns("TLT", num_days, 0.0001, 0.012, 0.20, 3.0),
|
|
// HYG - bond-like but higher vol
|
|
simulate_asset_returns("HYG", num_days, 0.0002, 0.007, -0.20, 3.5),
|
|
// GLD - safe haven, positive skew
|
|
simulate_asset_returns("GLD", num_days, 0.0002, 0.009, 0.25, 3.2),
|
|
// VNQ - real estate, equity-like
|
|
simulate_asset_returns("VNQ", num_days, 0.0003, 0.015, -0.25, 4.0),
|
|
]
|
|
}
|
|
|
|
/// Simulate returns for a single asset.
|
|
fn simulate_asset_returns(
|
|
symbol: &str,
|
|
num_days: usize,
|
|
mean: f64,
|
|
std: f64,
|
|
_skew: f64,
|
|
_kurt: f64,
|
|
) -> AssetReturns {
|
|
let mut returns = Vec::with_capacity(num_days);
|
|
let mut seed = symbol
|
|
.bytes()
|
|
.fold(0u64, |acc, b| acc.wrapping_add(b as u64))
|
|
* 12345;
|
|
|
|
for _ in 0..num_days {
|
|
// LCG pseudo-random - use modulo to ensure [0, 1) range
|
|
seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
|
|
let u1 = ((seed >> 16) % 32768) as f64 / 32768.0 + 0.0001; // Avoid 0
|
|
seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
|
|
let u2 = ((seed >> 16) % 32768) as f64 / 32768.0;
|
|
|
|
// Box-Muller transform for normal distribution
|
|
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
|
|
// Clamp to reasonable daily return range (-15% to +15%)
|
|
let ret = (mean + std * z).clamp(-0.15, 0.15);
|
|
returns.push(ret);
|
|
}
|
|
|
|
AssetReturns {
|
|
symbol: symbol.to_string(),
|
|
returns,
|
|
start_date: "2023-01-01".to_string(),
|
|
end_date: "2023-12-31".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Create a request for aggressive growth portfolio.
|
|
#[must_use]
|
|
pub fn get_aggressive_growth_request() -> OptimizationRequest {
|
|
let assets = vec![
|
|
Asset {
|
|
symbol: "QQQ".to_string(),
|
|
name: "NASDAQ 100 ETF".to_string(),
|
|
asset_class: AssetClass::Equity,
|
|
sector: Some("US Technology".to_string()),
|
|
currency: "USD".to_string(),
|
|
},
|
|
Asset {
|
|
symbol: "ARKK".to_string(),
|
|
name: "ARK Innovation ETF".to_string(),
|
|
asset_class: AssetClass::Equity,
|
|
sector: Some("Innovation".to_string()),
|
|
currency: "USD".to_string(),
|
|
},
|
|
Asset {
|
|
symbol: "SOXX".to_string(),
|
|
name: "Semiconductor ETF".to_string(),
|
|
asset_class: AssetClass::Equity,
|
|
sector: Some("Semiconductors".to_string()),
|
|
currency: "USD".to_string(),
|
|
},
|
|
Asset {
|
|
symbol: "XBI".to_string(),
|
|
name: "Biotech ETF".to_string(),
|
|
asset_class: AssetClass::Equity,
|
|
sector: Some("Biotech".to_string()),
|
|
currency: "USD".to_string(),
|
|
},
|
|
];
|
|
|
|
let returns = vec![
|
|
simulate_asset_returns("QQQ", 252, 0.0006, 0.016, -0.20, 3.5),
|
|
simulate_asset_returns("ARKK", 252, 0.0008, 0.028, -0.40, 5.0),
|
|
simulate_asset_returns("SOXX", 252, 0.0007, 0.022, -0.30, 4.2),
|
|
simulate_asset_returns("XBI", 252, 0.0005, 0.025, -0.35, 4.8),
|
|
];
|
|
|
|
OptimizationRequest {
|
|
assets,
|
|
returns,
|
|
objectives: OptimizationObjectives {
|
|
target_return: Some(0.20),
|
|
minimize_variance: true,
|
|
maximize_skewness: true,
|
|
minimize_kurtosis: true,
|
|
risk_aversion: 0.5,
|
|
skewness_preference: 0.3,
|
|
kurtosis_aversion: 0.2,
|
|
},
|
|
constraints: PortfolioConstraints::default(),
|
|
method: OptimizationMethod::HigherOrder,
|
|
}
|
|
}
|
|
|
|
/// Create a request for conservative income portfolio.
|
|
#[must_use]
|
|
pub fn get_conservative_income_request() -> OptimizationRequest {
|
|
let assets = vec![
|
|
Asset {
|
|
symbol: "AGG".to_string(),
|
|
name: "US Aggregate Bond ETF".to_string(),
|
|
asset_class: AssetClass::FixedIncome,
|
|
sector: None,
|
|
currency: "USD".to_string(),
|
|
},
|
|
Asset {
|
|
symbol: "BND".to_string(),
|
|
name: "Total Bond Market ETF".to_string(),
|
|
asset_class: AssetClass::FixedIncome,
|
|
sector: None,
|
|
currency: "USD".to_string(),
|
|
},
|
|
Asset {
|
|
symbol: "VCIT".to_string(),
|
|
name: "Intermediate Corporate Bond ETF".to_string(),
|
|
asset_class: AssetClass::FixedIncome,
|
|
sector: None,
|
|
currency: "USD".to_string(),
|
|
},
|
|
Asset {
|
|
symbol: "SCHD".to_string(),
|
|
name: "Dividend ETF".to_string(),
|
|
asset_class: AssetClass::Equity,
|
|
sector: Some("Dividend".to_string()),
|
|
currency: "USD".to_string(),
|
|
},
|
|
];
|
|
|
|
let returns = vec![
|
|
simulate_asset_returns("AGG", 252, 0.0001, 0.004, 0.05, 2.8),
|
|
simulate_asset_returns("BND", 252, 0.0001, 0.004, 0.05, 2.7),
|
|
simulate_asset_returns("VCIT", 252, 0.00012, 0.005, 0.02, 2.9),
|
|
simulate_asset_returns("SCHD", 252, 0.0003, 0.008, -0.10, 3.0),
|
|
];
|
|
|
|
OptimizationRequest {
|
|
assets,
|
|
returns,
|
|
objectives: OptimizationObjectives {
|
|
target_return: Some(0.05),
|
|
minimize_variance: true,
|
|
maximize_skewness: true,
|
|
minimize_kurtosis: true,
|
|
risk_aversion: 3.0,
|
|
skewness_preference: 0.8,
|
|
kurtosis_aversion: 0.8,
|
|
},
|
|
constraints: PortfolioConstraints::default(),
|
|
method: OptimizationMethod::MinimumVariance,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_comprehensive_request() {
|
|
let request = get_comprehensive_request();
|
|
assert_eq!(request.assets.len(), 10);
|
|
assert_eq!(request.returns.len(), 10);
|
|
}
|
|
|
|
#[test]
|
|
fn test_diversified_portfolio() {
|
|
let assets = get_diversified_portfolio();
|
|
assert_eq!(assets.len(), 10);
|
|
|
|
// Check for diversity of asset classes
|
|
let equity_count = assets
|
|
.iter()
|
|
.filter(|a| a.asset_class == AssetClass::Equity)
|
|
.count();
|
|
let fixed_income_count = assets
|
|
.iter()
|
|
.filter(|a| a.asset_class == AssetClass::FixedIncome)
|
|
.count();
|
|
|
|
assert!(equity_count >= 3);
|
|
assert!(fixed_income_count >= 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_simulated_returns() {
|
|
let returns = get_simulated_returns();
|
|
assert_eq!(returns.len(), 10);
|
|
|
|
for ret in &returns {
|
|
assert_eq!(ret.returns.len(), 252);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_aggressive_growth() {
|
|
let request = get_aggressive_growth_request();
|
|
assert_eq!(request.assets.len(), 4);
|
|
assert!(request.objectives.risk_aversion < 1.0); // Low risk aversion = aggressive
|
|
}
|
|
|
|
#[test]
|
|
fn test_conservative_income() {
|
|
let request = get_conservative_income_request();
|
|
assert_eq!(request.assets.len(), 4);
|
|
assert!(request.objectives.risk_aversion > 2.0); // High risk aversion = conservative
|
|
}
|
|
|
|
#[test]
|
|
fn test_returns_reasonable_range() {
|
|
let returns = get_simulated_returns();
|
|
|
|
for asset_returns in &returns {
|
|
for &r in &asset_returns.returns {
|
|
// Daily returns should typically be within -10% to +10%
|
|
assert!(r > -0.2 && r < 0.2);
|
|
}
|
|
}
|
|
}
|
|
}
|