Portfolio Optimization
Convex optimisation wrappers live in pyvallocation.optimization. They
share a thin interface so frontiers can be swapped without changing constraint
plumbing:
pyvallocation.optimization.MeanVariance- quadratic risk/return trade-offs with optional quadratic turnover costs.pyvallocation.optimization.MeanCVaR- linear-program CVaR frontiers with proportional costs.pyvallocation.optimization.RelaxedRiskParity- Gambeta & Kwon’s relaxed risk parity SOCP implementation.pyvallocation.optimization.RobustOptimizer- Meucci-style robust optimiser with chance-constraint and penalised variants.
Portfolio-Optimisation Toolbox
This package groups three single-period convex allocation models and exposes an identical, numerically stable API for each of them. All back-end calls rely exclusively on CVXOPT 1.3+; hence global optimality is guaranteed provided the solver returns a status flag “optimal”.
Acronym |
Risk measure / Objective functional \(f(w)\) |
Cone class |
CVXOPT routine |
|---|---|---|---|
|
\(\tfrac12\,w^{\top}\Sigma w\) |
PSD |
|
|
\(\operatorname{CVaR}_{\alpha}\bigl(-R\,w\bigr)\) |
LP |
|
|
\(\displaystyle \max_{\mu\in\mathcal U}\bigl[-w^{\top}\mu + \lambda\|S^{1/2}w\|_2\bigr]\) |
SOC |
|
Global symbols
\(N\) – number of risky assets.
\(T\) – number of Monte-Carlo or historical scenarios.
\(R\in\mathbb R^{T\times N}\) – scenario matrix of excess returns.
\(p\in\Delta^{T}\) – probability vector, \(\mathbf1^{\top}p=1\).
\(\mu:=R^{\top}p\), \(\Sigma:=(R-\mu^{\top})^{\top}\!\mathrm{diag}(p)(R-\mu^{\top})\).
\(w\in\mathbb R^{N}\) – portfolio after re-balancing, \(\mathbf1^{\top}w=1\).
Optional affine trading rules
encode leverage caps, tracking constraints, minimum/maximum holdings, etc.
Transaction-cost primitives
Quadratic impact : \(\Lambda=\operatorname{diag}(\lambda)\) (QP only)
\[(w-w_0)^{\top}\Lambda\,(w-w_0).\]Proportional turnover : \(c^{+},c^{-}\ge0\) (LP/SOCP)
\[\sum_{i=1}^{N}\bigl(c^{+}_iu^{+}_i+c^{-}_iu^{-}_i\bigr), \qquad w = w_0 + u^{+}-u^{-},\;u^{+},u^{-}\ge0.\]
Primary references
Markowitz (1952); Rockafellar & Uryasev (2000); Meucci (2005); Lobo et al. (2007).
Credits
MeanVariance and MeanCVaR classes are adapted from fortitudo-tech package (https://github.com/fortitudo-tech/fortitudo.tech)
- exception pyvallocation.optimization.InfeasibleOptimizationError[source]
Bases:
RuntimeErrorRaised when a portfolio optimisation problem has no feasible solution.
- class pyvallocation.optimization.MeanCVaR(R, p, alpha, G=None, h=None, A=None, b=None, *, initial_weights=None, proportional_costs=None)[source]
Bases:
_FrontierMixin,_BaseOptimizationRockafellar-Uryasev CVaR optimisation with optional proportional costs.
Formulation
For level \(\alpha\in(0,1)\) define the conditional value-at-risk
\[\operatorname{CVaR}_{\alpha}(Z)= \min_{c\in\mathbb R}\; c+\frac1\alpha\,\mathbb E[(Z-c)_{+}].\]Substituting \(Z=-R\,w\) and applying the sample average approximation produces the LP
\[\begin{split}\begin{aligned} \min_{w,c,\xi}\quad & c + \tfrac1\alpha\,p^{\top}\xi + c^{\top}(u^{+}+u^{-}) \\[4pt] \text{s.t.}\quad & \xi \;\ge\; -R\,w - c\mathbf1, \\[2pt] & \xi \;\ge\; 0, \\ & w = w_0 + u^{+}-u^{-},\;u^{+},u^{-}\ge 0, \\ & \mathbf1^{\top}w = 1,\; G w \le h,\; A w = b. \end{aligned}\end{split}\]- Parameters:
R, p – Scenario matrix and probabilities.
alpha – Tail probability \(\\alpha\) (e.g.
0.05= 95% CVaR).initial_weights, proportional_costs – Activate linear turnover frictions only when both are provided.
- efficient_frontier(num_portfolios)[source]
Return the CVaR efficient frontier with
num_portfoliosvertices.- Parameters:
num_portfolios – Number of portfolios on the frontier.
- Returns:
np.ndarray – Weight matrix with shape
(N, num_portfolios).- Parameters:
num_portfolios (int)
- Return type:
numpy.ndarray
- efficient_portfolio(return_target=None)
Solve the CVaR LP for a given target return
\tau(or min-CVaR portfolio if\tau is None).- Parameters:
return_target (float | None)
- Return type:
np.ndarray
- Parameters:
R (npt.ArrayLike)
p (npt.ArrayLike)
alpha (float)
G (Optional[npt.ArrayLike])
h (Optional[npt.ArrayLike])
A (Optional[npt.ArrayLike])
b (Optional[npt.ArrayLike])
initial_weights (Optional[npt.NDArray[np.floating]])
proportional_costs (Optional[npt.NDArray[np.floating]])
- class pyvallocation.optimization.MeanVariance(mean, covariance, G=None, h=None, A=None, b=None, *, initial_weights=None, market_impact_costs=None)[source]
Bases:
_FrontierMixin,_BaseOptimizationClassic mean-variance programme a la Markowitz (1952).
Problem statement
\[\begin{split}\begin{aligned} \min_{w}\;&\tfrac12\,w^{\top}\Sigma w + \tfrac12\,(w-w_0)^{\top}\Lambda(w-w_0) \\[3pt] \text{s.t. }& \mu^{\top}w \;\ge\; \tau, \quad \mathbf1^{\top}w = 1, \quad G w \le h,\; A w = b. \end{aligned}\end{split}\]\tauis supplied on the fly viaefficient_portfolio().\Lambda(quadratic impact) is optional; if omitted the model degenerates to the textbook QP with no trading costs.
Notes
The QP is strictly convex whenever
\Sigma> 0 or at least one positive lambda_i is present, hence the solution is unique.- Parameters:
mean, covariance – Mean vector \(\\mu\) and covariance matrix \(\\Sigma\).
G, h, A, b – Optional affine constraints as defined in the module docstring.
initial_weights, market_impact_costs –
w0and diag-elements of \(\\Lambda\) – must be passed together.
- efficient_frontier(num_portfolios)[source]
Return an
(N, num_portfolios)array whose columns trace the Markowitz efficient set between the variance minimiser and the return maximiser.- Parameters:
num_portfolios – Number of portfolios on the frontier.
- Returns:
np.ndarray – Weight matrix with shape
(N, num_portfolios).- Parameters:
num_portfolios (int)
- Return type:
numpy.ndarray
- efficient_portfolio(return_target=None)
Solve the QP once for a given target \(\tau\).
Passing
Noneyields the minimum-variance solution, i.e. the left-most point on the efficient frontier.- Parameters:
return_target (float | None)
- Return type:
np.ndarray
- max_sharpe(risk_free_rate=0.0)[source]
Solve directly for the maximum Sharpe ratio portfolio.
Uses the Cornuejols-Tütüncü (2007) reformulation:
\[\min_{y}\; y^\top \Sigma\, y \quad\text{s.t.}\quad (\mu - r_f)^\top y = 1,\; G\,y \le 0,\; A\,y = 0\]The optimal weights are
w = y / \mathbf{1}^\top y.- Parameters:
risk_free_rate – Risk-free rate for Sharpe computation.
- Returns:
OptimizationResult – Optimal weights, return, and volatility.
- Parameters:
risk_free_rate (float)
- Return type:
OptimizationResult
- Parameters:
mean (npt.ArrayLike)
covariance (npt.ArrayLike)
G (Optional[npt.ArrayLike])
h (Optional[npt.ArrayLike])
A (Optional[npt.ArrayLike])
b (Optional[npt.ArrayLike])
initial_weights (Optional[npt.NDArray[np.floating]])
market_impact_costs (Optional[npt.NDArray[np.floating]])
- class pyvallocation.optimization.OptimizationResult(weights, nominal_return, risk)[source]
Bases:
objectImmutable result object returned by
RobustOptimizer.It stores the optimal post-trade weights, the associated point-estimate return and a model-specific risk proxy (sigma* for mean-variance, CVaR* for mean-CVaR, or the robust radius t*).
- Parameters:
weights (npt.NDArray[np.floating])
nominal_return (float)
risk (float)
- nominal_return: float
- risk: float
- class pyvallocation.optimization.RelaxedRiskParity(mean, covariance, G=None, h=None, A=None, b=None, *, risk_budgets=None)[source]
Bases:
_BaseOptimizationImplement the relaxed risk parity model of Gambeta & Kwon (2020).
The decision vector is ordered as
[ x (n) | \zeta (n) | \psi | \gamma | \rho | q ],
with
qacting as the auxiliary variable that nests the average-risk constraint into standard SOC blocks. Long-only holdings, non-negative marginal risks, and the regulator variable are enforced explicitly.The formulation minimises \(\\psi - \\gamma\) subject to:
marginal risk consistency \(\\zeta = \\Sigma x\);
budget constraint \(\\mathbf{1}^{\\top}x = 1\);
ARC floor \(x_i\\zeta_i \\ge \\gamma^2\) (realised via rotated second-order cones);
regulated average risk \(\\|Lx\\|_2 \\le q\), \(\\|(q,\\sqrt{n}\\rho)\\|_2 \\le \\sqrt{n}\\psi\);
diagonal penalty \(\\sqrt{\\lambda}\\,\\|D x\\|_2 \\le \\rho\) whenever \(\\lambda > 0\);
optional return target \(\\mu^{\\top}x \\ge R\).
All conic blocks are expressed in a solver-ready format compatible with
cvxopt.solvers.conelp.- Parameters:
mean (npt.ArrayLike)
covariance (npt.ArrayLike)
G (Optional[npt.ArrayLike])
h (Optional[npt.ArrayLike])
A (Optional[npt.ArrayLike])
b (Optional[npt.ArrayLike])
risk_budgets (Optional[npt.ArrayLike])
- solve(*, lambda_reg=0.0, return_target=None, min_target=None)[source]
Solve the relaxed risk parity SOCP for a given
lambda_regand optional target returnreturn_target.- Parameters:
lambda_reg (float)
return_target (float | None)
min_target (float | None)
- Return type:
RelaxedRiskParityResult
- class pyvallocation.optimization.RelaxedRiskParityResult(weights, marginal_risk, psi, gamma, rho, objective, target_return, max_return)[source]
Bases:
objectResult container for the relaxed risk parity SOCP.
- Parameters:
weights (npt.NDArray[np.floating])
marginal_risk (npt.NDArray[np.floating])
psi (float)
gamma (float)
rho (float)
objective (float)
target_return (float | None)
max_return (float | None)
- weights
Optimal long-only allocations \(x^{\\star}\) (shape
(n,)).- Type:
npt.NDArray[np.floating]
- marginal_risk
Gradient vector \(\\zeta^{\\star} = \\Sigma x^{\\star}\) (variance-scale, not Roncalli’s volatility-normalised marginal risk).
- Type:
npt.NDArray[np.floating]
- psi
Optimal average-risk proxy \(\\psi^{\\star}\).
- Type:
float
- gamma
Optimal ARC floor \(\\gamma^{\\star}\); enforces the lower risk contribution bound \(x_i \\zeta_i \\ge \\gamma^2\).
- Type:
float
- rho
Regulator slack variable \(\\rho^{\\star}\) that upper bounds the diagonal risk penalty \(\\lambda\\,x^{\\top}\\Theta x\).
- Type:
float
- objective
Optimal value of \(\\psi - \\gamma\), i.e. the gap between the average-risk ceiling and the ARC floor.
- Type:
float
- target_return
Effective return constraint \(\\mu^{\\top}x \\ge R\) imposed at optimality. May differ from the requested target when clipping was required.
- Type:
float | None
- max_return
Feasible bound on the return target under the supplied constraints.
Noneif no target was requested.- Type:
float | None
- class pyvallocation.optimization.RobustOptimizer(expected_return, uncertainty_cov, G=None, h=None, A=None, b=None, *, initial_weights=None, proportional_costs=None)[source]
Bases:
_BaseOptimization
Single-period mean-variance allocator that immunises the portfolio against estimation error in the expected-return vector while leaving the covariance matrix untouched. The model is the direct implementation of the ellipsoidal framework put forward in
Goldfarb & Iyengar - “Robust Portfolio Selection Problems”, Math. Oper. Res. 28 (1), 1-38 (2003)
Meucci - Risk & Asset Allocation, Ch. 9 (Springer, 2005) and the robust-Bayesian extension in SSRN 681553 (2011)
1 Ellipsoidal ambiguity set
We assume the unknown mean \(\mu\) lies in
\[\mathcal U(\hat\mu,S,q) := \bigl\{\mu\in\mathbb R^{N}\;|\; \lVert S^{-1/2}(\mu-\hat\mu)\rVert_2 \le q\bigr\},\]- where
\(\hat\mu\) – point estimate (MLE, posterior mean, …)
\(S\succ0\) – mean-uncertainty scatter matrix \(S_\mu\). For NIW posteriors this is \(S_\mu = \frac{1}{T_1}\frac{\nu_1}{\nu_1-2}\Sigma_1\) (Meucci Eq. 9.151), not the posterior covariance \(\Sigma_1\).
\(q\) – radius, usually \(\sqrt{\chi^2_N(1-\alpha)}\) for a \(100(1-\alpha)\%\) credible set.
2 SOCP reformulation
Goldfarb-Iyengar (Thm 3.1) show
\[\min_{\mu\in\mathcal U}w^{\top}\mu = \hat\mu^{\top}w-q\,\lVert S^{1/2}w\rVert_2,\]so the worst-case mean is a linear term minus a 2-norm penalty. Introducing an epigraph variable \(t\) gives the cone programme
\[\min_{w,t}\;t+\lambda\lVert S^{1/2}w\rVert_2 \;\;\text{s.t.}\;\;t\ge-\hat\mu^{\top}w,\;w\!\in\!C,\]which CVXOPT solves as a second-order cone programme (type “q”).
3. Two parameterisations
solve_lambda_variant(lam)Direct penalty \(\lambda\). Higher lambda => stronger shrinkage toward the minimum-uncertainty portfolio.
solve_gamma_variant(gamma_mu, gamma_sigma_sq)Uses
gamma_muas the penalty weight and optionally caps the uncertainty radius viat^2 \le \gamma_{\sigma}^{2}(implemented ast <= sqrt(gamma_sigma_sq)).
Both wrappers feed the same private routine
_solve_socp().4. Optional proportional turnover
Passing both
initial_weightsandproportional_costsactivates the linear-cost mechanism of Lobo et al. (2007). The decision vector becomes\[(w,\;t,\;u^{+},u^{-})\in\mathbb R^{\,3N+1},\]with inventory balance \(w=w_0+u^{+}-u^{-},\;u^{+},u^{-}\ge0\).
5. Limitations
Only mean uncertainty is modelled; covariance risk would require an SDP.
The ellipsoid is static; time-varying radii must be supplied upstream.
Extremely ill-conditioned
uncertainty_covcan trigger numerical warnings in CVXOPT.
- efficient_frontier(lambdas)[source]
Sweep a list of
lambdasand returnnominal returns,
robust radii (
t*),and a weight matrix
(N, len(lambdas)).
- Parameters:
lambdas – Sequence of penalty parameters.
- Returns:
tuple – Returns list, risk radius list, and weight matrix.
- Parameters:
lambdas (Sequence[float])
- Return type:
Tuple[list[float], list[float], npt.NDArray[np.floating]]
- solve_gamma_variant(gamma_mu, gamma_sigma_sq)[source]
Solve the gamma-variant with a penalty weight and an optional radius cap.
gamma_muacts as the penalty weight on the uncertainty radius andgamma_sigma_sqcaps \(t^2\) (implemented ast <= sqrt(gamma_sigma_sq)).- Parameters:
gamma_mu (float)
gamma_sigma_sq (float)
- Return type:
OptimizationResult
- solve_lambda_variant(lam)[source]
Solve the lambda-variant:
\[\min_{w,t}\;t + \lambda\|S^{1/2}w\|_2 \quad\text{s.t.}\; t \ge -\hat\mu^{\top}w,\;\ldots\]- Parameters:
lam (float)
- Return type:
OptimizationResult
- Parameters:
expected_return (npt.NDArray[np.floating])
uncertainty_cov (npt.NDArray[np.floating])
G (Optional[npt.ArrayLike])
h (Optional[npt.ArrayLike])
A (Optional[npt.ArrayLike])
b (Optional[npt.ArrayLike])
initial_weights (Optional[npt.NDArray[np.floating]])
proportional_costs (Optional[npt.NDArray[np.floating]])