#[derive(Debug, Clone)]
pub struct Asset {
pub name: String,
pub expected_return: f64,
pub weight: f64,
}
#[derive(Debug, Clone)]
pub enum OptimizationConstraint {
SumToOne,
LongOnly,
MaxWeight(f64),
MinWeight(f64),
MaxConcentration(f64),
}
#[derive(Debug, Clone)]
pub struct EfficientFrontierPoint {
pub expected_return: f64,
pub volatility: f64,
pub sharpe: f64,
pub weights: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct OptimizationResult {
pub weights: Vec<f64>,
pub expected_return: f64,
pub volatility: f64,
pub sharpe: f64,
pub iterations: usize,
}
pub struct MeanVarianceOptimizer;
impl MeanVarianceOptimizer {
pub fn portfolio_return(weights: &[f64], returns: &[f64]) -> f64 {
weights.iter().zip(returns.iter()).map(|(w, r)| w * r).sum()
}
pub fn portfolio_variance(weights: &[f64], cov: &[Vec<f64>]) -> f64 {
let n = weights.len();
let mut var = 0.0_f64;
for i in 0..n {
for j in 0..n {
var += weights[i] * weights[j] * cov[i][j];
}
}
var
}
pub fn portfolio_volatility(weights: &[f64], cov: &[Vec<f64>]) -> f64 {
Self::portfolio_variance(weights, cov).max(0.0).sqrt()
}
pub fn gradient_step(
weights: &mut Vec<f64>,
returns: &[f64],
cov: &[Vec<f64>],
target_return: f64,
lr: f64,
) {
let n = weights.len();
let mut grad_var = vec![0.0_f64; n];
for i in 0..n {
for j in 0..n {
grad_var[i] += 2.0 * cov[i][j] * weights[j];
}
}
let actual_return = Self::portfolio_return(weights, returns);
let lambda = 2.0 * (actual_return - target_return);
for i in 0..n {
weights[i] -= lr * (grad_var[i] - lambda * returns[i]);
}
}
pub fn apply_constraints(weights: &mut Vec<f64>, constraints: &[OptimizationConstraint]) {
let n = weights.len();
for c in constraints {
match c {
OptimizationConstraint::LongOnly => {
for w in weights.iter_mut() {
if *w < 0.0 {
*w = 0.0;
}
}
}
OptimizationConstraint::MaxWeight(max) => {
for w in weights.iter_mut() {
if *w > *max {
*w = *max;
}
}
}
OptimizationConstraint::MinWeight(min) => {
for w in weights.iter_mut() {
if *w < *min {
*w = *min;
}
}
}
OptimizationConstraint::MaxConcentration(max_conc) => {
for w in weights.iter_mut() {
if *w > *max_conc {
*w = *max_conc;
}
}
}
OptimizationConstraint::SumToOne => {
let sum: f64 = weights.iter().sum();
if sum.abs() > 1e-12 {
for w in weights.iter_mut() {
*w /= sum;
}
} else {
let eq = 1.0 / n as f64;
for w in weights.iter_mut() {
*w = eq;
}
}
}
}
}
}
pub fn maximize_sharpe(
returns: &[f64],
cov: &[Vec<f64>],
rf_rate: f64,
constraints: &[OptimizationConstraint],
) -> OptimizationResult {
let n = returns.len();
if n == 0 {
return OptimizationResult {
weights: vec![],
expected_return: 0.0,
volatility: 0.0,
sharpe: 0.0,
iterations: 0,
};
}
let mut weights = vec![1.0 / n as f64; n];
Self::apply_constraints(&mut weights, constraints);
let max_iter = 1000_usize;
let lr = 0.01_f64;
let tol = 1e-8_f64;
let mut prev_sharpe = f64::NEG_INFINITY;
let mut iters = 0_usize;
for iter in 0..max_iter {
iters = iter + 1;
let ret = Self::portfolio_return(&weights, returns);
let vol = Self::portfolio_volatility(&weights, cov);
if vol < 1e-12 {
break;
}
let excess = ret - rf_rate;
let sharpe = excess / vol;
let mut grad = vec![0.0_f64; n];
for i in 0..n {
let dcov_i: f64 = (0..n).map(|j| cov[i][j] * weights[j]).sum();
let dvol_i = dcov_i / vol;
grad[i] = (returns[i] * vol - excess * dvol_i) / (vol * vol);
}
for i in 0..n {
weights[i] += lr * grad[i];
}
Self::apply_constraints(&mut weights, constraints);
if (sharpe - prev_sharpe).abs() < tol {
break;
}
prev_sharpe = sharpe;
}
let ret = Self::portfolio_return(&weights, returns);
let vol = Self::portfolio_volatility(&weights, cov);
let sharpe = if vol > 1e-12 { (ret - rf_rate) / vol } else { 0.0 };
OptimizationResult {
weights,
expected_return: ret,
volatility: vol,
sharpe,
iterations: iters,
}
}
pub fn minimize_variance(
returns: &[f64],
cov: &[Vec<f64>],
target_return: f64,
constraints: &[OptimizationConstraint],
) -> OptimizationResult {
let n = returns.len();
if n == 0 {
return OptimizationResult {
weights: vec![],
expected_return: 0.0,
volatility: 0.0,
sharpe: 0.0,
iterations: 0,
};
}
let mut weights = vec![1.0 / n as f64; n];
Self::apply_constraints(&mut weights, constraints);
let max_iter = 2000_usize;
let lr = 0.005_f64;
let tol = 1e-10_f64;
let mut prev_var = f64::MAX;
let mut iters = 0_usize;
for iter in 0..max_iter {
iters = iter + 1;
Self::gradient_step(&mut weights, returns, cov, target_return, lr);
Self::apply_constraints(&mut weights, constraints);
let var = Self::portfolio_variance(&weights, cov);
if (var - prev_var).abs() < tol {
break;
}
prev_var = var;
}
let ret = Self::portfolio_return(&weights, returns);
let vol = Self::portfolio_volatility(&weights, cov);
let sharpe = if vol > 1e-12 { ret / vol } else { 0.0 };
OptimizationResult {
weights,
expected_return: ret,
volatility: vol,
sharpe,
iterations: iters,
}
}
pub fn efficient_frontier(
returns: &[f64],
cov: &[Vec<f64>],
n_points: usize,
rf_rate: f64,
) -> Vec<EfficientFrontierPoint> {
if returns.is_empty() || n_points == 0 {
return vec![];
}
let min_ret = returns.iter().cloned().fold(f64::MAX, f64::min);
let max_ret = returns.iter().cloned().fold(f64::MIN, f64::max);
if (max_ret - min_ret).abs() < 1e-12 {
return vec![];
}
let constraints = &[OptimizationConstraint::SumToOne, OptimizationConstraint::LongOnly];
let mut frontier = Vec::with_capacity(n_points);
for k in 0..n_points {
let t = k as f64 / (n_points - 1).max(1) as f64;
let target = min_ret + t * (max_ret - min_ret);
let result = Self::minimize_variance(returns, cov, target, constraints);
let sharpe = if result.volatility > 1e-12 {
(result.expected_return - rf_rate) / result.volatility
} else {
0.0
};
frontier.push(EfficientFrontierPoint {
expected_return: result.expected_return,
volatility: result.volatility,
sharpe,
weights: result.weights,
});
}
frontier
}
}
#[cfg(test)]
mod tests {
use super::*;
fn simple_cov() -> Vec<Vec<f64>> {
vec![
vec![0.04, 0.006],
vec![0.006, 0.09],
]
}
#[test]
fn portfolio_return_correct() {
let weights = vec![0.6, 0.4];
let returns = vec![0.10, 0.08];
let r = MeanVarianceOptimizer::portfolio_return(&weights, &returns);
assert!((r - 0.092).abs() < 1e-9);
}
#[test]
fn portfolio_variance_correct() {
let weights = vec![1.0, 0.0];
let cov = simple_cov();
let v = MeanVarianceOptimizer::portfolio_variance(&weights, &cov);
assert!((v - 0.04).abs() < 1e-9);
}
#[test]
fn apply_sum_to_one() {
let mut w = vec![2.0, 3.0];
MeanVarianceOptimizer::apply_constraints(&mut w, &[OptimizationConstraint::SumToOne]);
assert!((w[0] - 0.4).abs() < 1e-9);
assert!((w[1] - 0.6).abs() < 1e-9);
}
#[test]
fn maximize_sharpe_runs() {
let returns = vec![0.10, 0.08, 0.12];
let cov = vec![
vec![0.04, 0.006, 0.002],
vec![0.006, 0.09, 0.003],
vec![0.002, 0.003, 0.16],
];
let constraints = [OptimizationConstraint::SumToOne, OptimizationConstraint::LongOnly];
let result = MeanVarianceOptimizer::maximize_sharpe(&returns, &cov, 0.02, &constraints);
let sum: f64 = result.weights.iter().sum();
assert!((sum - 1.0).abs() < 1e-6);
assert!(result.weights.iter().all(|&w| w >= -1e-9));
}
#[test]
fn efficient_frontier_has_correct_length() {
let returns = vec![0.05, 0.10];
let cov = simple_cov();
let frontier = MeanVarianceOptimizer::efficient_frontier(&returns, &cov, 5, 0.02);
assert_eq!(frontier.len(), 5);
}
}