use std::sync::{Arc, RwLock};
use optimization::{Minimizer, GradientDescent, NumericalDifferentiation, Func};
use polars::frame::DataFrame;
use crate::analytics::statistics::{mean_portfolio_return, portfolio_std_dev, rand_weights, maximum_drawdown,
value_at_risk, expected_shortfall, daily_portfolio_returns, efficient_frontier_points};
#[derive(Debug, Clone)]
pub struct OptResult {
pub optimal_weights: Vec<f64>,
pub efficient_frontier: Vec<Vec<f64>>,
}
#[derive(Debug, Clone, Copy)]
pub enum ObjectiveFunction {
MaxSharpe,
MinVol,
MaxReturn,
MinDrawdown,
MinVar,
MinCVaR,
}
impl ObjectiveFunction {
pub fn from_str(s: &str) -> ObjectiveFunction {
match s {
"max_sharpe" => ObjectiveFunction::MaxSharpe,
"min_vol" => ObjectiveFunction::MinVol,
"max_return" => ObjectiveFunction::MaxReturn,
"min_drawdown" => ObjectiveFunction::MinDrawdown,
"min_var" => ObjectiveFunction::MinVar,
"min_cvar" => ObjectiveFunction::MinCVaR,
_ => ObjectiveFunction::MaxSharpe,
}
}
}
pub fn portfolio_optimization(
mean_returns: &Vec<f64>,
cov_matrix: &ndarray::Array2<f64>,
portfolio_returns: &DataFrame,
risk_free_rate: f64,
confidence_level: f64,
max_iterations: u64,
objective: ObjectiveFunction
) -> OptResult {
let efficient_frontier: Arc<RwLock<Vec<Vec<f64>>>> = Arc::new(RwLock::new(Vec::new()));
let efficient_frontier_clone = Arc::clone(&efficient_frontier);
let function = NumericalDifferentiation::new(Func(|weights: &[f64]| {
let weights = enforce_constraints(weights);
let _return = mean_portfolio_return(&weights.to_vec(), mean_returns);
let std_dev = portfolio_std_dev(&weights.to_vec(), cov_matrix);
if let Ok(mut guard) = efficient_frontier_clone.write() {
guard.push(vec![_return, std_dev]);
}
let objective = match objective {
ObjectiveFunction::MaxSharpe => {
let sharpe = (_return - risk_free_rate) / std_dev;
-sharpe
},
ObjectiveFunction::MinVol => {
std_dev
},
ObjectiveFunction::MaxReturn => {
-_return
},
ObjectiveFunction::MinDrawdown => {
let returns = daily_portfolio_returns(&weights, portfolio_returns);
let drawdown = maximum_drawdown(&returns);
drawdown
},
ObjectiveFunction::MinVar => {
let returns = daily_portfolio_returns(&weights, portfolio_returns);
let var = value_at_risk(&returns, confidence_level);
var
},
ObjectiveFunction::MinCVaR => {
let returns = daily_portfolio_returns(&weights, portfolio_returns);
let es = expected_shortfall(&returns, confidence_level);
es
}
};
objective
}));
let minimizer = GradientDescent::new();
let minimizer = minimizer.max_iterations(Some(max_iterations)); let minimizer = minimizer.gradient_tolerance(1e-6); let num_assets = mean_returns.len();
let initial_weights = rand_weights(num_assets); let solution = minimizer.minimize(&function, initial_weights);
let constrained_solution = enforce_constraints(&solution.position);
let efficient_frontier = efficient_frontier_points(efficient_frontier.read().unwrap().clone());
let result = OptResult {
optimal_weights: constrained_solution,
efficient_frontier: efficient_frontier.clone(),
};
result
}
fn enforce_constraints(weights: &[f64]) -> Vec<f64> {
let mut constrained_weights: Vec<f64> = weights.to_vec();
constrained_weights.iter_mut().for_each(|w| if *w < 0.0 { *w = 0.0 });
let sum: f64 = constrained_weights.iter().sum();
constrained_weights.iter_mut().for_each(|w| *w /= sum);
constrained_weights
}