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

MV

\(\tfrac12\,w^{\top}\Sigma w\)

PSD

  • cvxopt.solvers.qp()

CVaRalpha

\(\operatorname{CVaR}_{\alpha}\bigl(-R\,w\bigr)\)

LP

cvxopt.solvers.conelp()

RB

\(\displaystyle \max_{\mu\in\mathcal U}\bigl[-w^{\top}\mu + \lambda\|S^{1/2}w\|_2\bigr]\)

SOC

cvxopt.solvers.conelp()

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

\[G\,w\;\le\;h, \qquad A\,w\;=\;b,\]

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: RuntimeError

Raised 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, _BaseOptimization

Rockafellar-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_portfolios vertices.

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, _BaseOptimization

Classic 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}\]
  • \tau is supplied on the fly via efficient_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_costsw0 and 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 None yields 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: object

Immutable 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: _BaseOptimization

Implement the relaxed risk parity model of Gambeta & Kwon (2020).

The decision vector is ordered as

[ x (n) | \zeta (n) | \psi | \gamma | \rho | q ],

with q acting 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_reg and optional target return return_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: object

Result 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. None if 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_mu as the penalty weight and optionally caps the uncertainty radius via t^2 \le \gamma_{\sigma}^{2} (implemented as t <= sqrt(gamma_sigma_sq)).

Both wrappers feed the same private routine _solve_socp().

4. Optional proportional turnover

Passing both initial_weights and proportional_costs activates 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_cov can trigger numerical warnings in CVXOPT.

efficient_frontier(lambdas)[source]

Sweep a list of lambdas and return

  • nominal 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_mu acts as the penalty weight on the uncertainty radius and gamma_sigma_sq caps \(t^2\) (implemented as t <= 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]])