feat(portfolio): add CARA, convex, mean-variance & ERC portfolio optimization module

New Rust portfolio_optimization module with PyO3 bindings:
- CARA/CRRA utility maximization via projected gradient descent
- General-purpose convex objective solver on simplex (ProjectedGradientSolver)
- Mean-variance optimization (max Sharpe, target return, min variance)
- Equal Risk Contribution (ERC) portfolio allocation
- Python bindings: cara_optimal_weights, mean_variance_optimal_weights,
  min_variance_weights, erc_weights
- 6/6 unit tests passing

Convergence fix: removed gradient-norm criterion on simplex boundary
(projected gradient never vanishes at constrained optimum).
Default learning rate increased from 0.005 to 0.1.
This commit is contained in:
ThotDjehuty
2026-04-15 03:08:08 +02:00
parent 58c3793b66
commit e67b0f8376
8 changed files with 889 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
//! Core traits for portfolio utility and convex optimisation.
use crate::core::OptimizrError;
/// A utility function U(x) mapping wealth → utility.
pub trait UtilityFunction: Send + Sync {
/// U(x) — the utility of wealth level x.
fn utility(&self, x: f64) -> f64;
/// U'(x) — first derivative (marginal utility).
fn marginal_utility(&self, x: f64) -> f64;
/// (U')^{-1}(y) — inverse marginal utility (used in duality).
fn inverse_marginal(&self, y: f64) -> f64;
/// Risk-aversion coefficient A(x) = -U''(x) / U'(x).
fn risk_aversion(&self, x: f64) -> f64;
/// Name identifier for logging / serialisation.
fn name(&self) -> &str;
}
/// Convex objective f(w) over portfolio weights w ∈ ^n.
///
/// Used by convex solvers (projected gradient, ADMM, etc.).
pub trait ConvexObjective: Send + Sync {
/// f(w) — objective value.
fn value(&self, w: &[f64]) -> f64;
/// ∇f(w) — gradient vector.
fn gradient(&self, w: &[f64]) -> Vec<f64>;
/// Dimension of the weight vector.
fn dim(&self) -> usize;
}
/// Convex constraint g(w) ≤ 0.
pub trait ConvexConstraint: Send + Sync {
/// g(w) — constraint value (feasible when ≤ 0).
fn value(&self, w: &[f64]) -> f64;
/// ∇g(w) — gradient of constraint function.
fn gradient(&self, w: &[f64]) -> Vec<f64>;
}
/// Result of a portfolio optimisation.
#[derive(Debug, Clone)]
pub struct PortfolioResult {
pub weights: Vec<f64>,
pub utility: f64,
pub expected_return: f64,
pub portfolio_variance: f64,
pub iterations: usize,
pub converged: bool,
}
impl PortfolioResult {
pub fn sharpe_ratio(&self, risk_free: f64) -> f64 {
let vol = self.portfolio_variance.sqrt();
if vol < 1e-15 {
return 0.0;
}
(self.expected_return - risk_free) / vol
}
}
/// Generic portfolio optimiser trait.
pub trait PortfolioOptimizer: Send + Sync {
fn optimize(
&self,
mu: &[f64],
cov: &[Vec<f64>],
max_weight: f64,
) -> Result<PortfolioResult, OptimizrError>;
}