Portfolio Optimization

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)

class pyvallocation.optimization.MeanCVaR(R: npt.ArrayLike, p: npt.ArrayLike, alpha: float, G: npt.ArrayLike | None = None, h: npt.ArrayLike | None = None, A: npt.ArrayLike | None = None, b: npt.ArrayLike | None = None, *, initial_weights: npt.NDArray[np.floating] | None = None, proportional_costs: npt.NDArray[np.floating] | None = 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}\]
param R:

Scenario matrix and probabilities.

param p:

Scenario matrix and probabilities.

param alpha:

Tail probability \(\alpha\) (e.g. 0.05 = 95 % CVaR).

param initial_weights:

Activate linear turnover frictions iff both are given.

param proportional_costs:

Activate linear turnover frictions iff both are given.

efficient_frontier(num_portfolios: int) numpy.ndarray[source]

Return the CVaR efficient frontier with num_portfolios vertices.

efficient_portfolio(return_target: float | None = None) np.ndarray

Solve the CVaR LP for a given target return τ (or min-CVaR portfolio if τ is None).

class pyvallocation.optimization.MeanVariance(mean: npt.ArrayLike, covariance: npt.ArrayLike, G: npt.ArrayLike | None = None, h: npt.ArrayLike | None = None, A: npt.ArrayLike | None = None, b: npt.ArrayLike | None = None, *, initial_weights: npt.NDArray[np.floating] | None = None, market_impact_costs: npt.NDArray[np.floating] | None = None)[source]

Bases: _FrontierMixin, _BaseOptimization

Classic mean–variance programme à 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}\]
  • τ is supplied on the fly via efficient_portfolio().

  • Λ (quadratic impact) is optional; if omitted the model degenerates to the textbook QP with no trading costs.

Notes

The QP is strictly convex whenever Σ ≻ 0 or at least one positive λᵢ is present, hence the solution is unique.

param mean:

Mean vector \(\mu\) and covariance matrix \(\Sigma\).

param covariance:

Mean vector \(\mu\) and covariance matrix \(\Sigma\).

param G:

Optional affine constraints as defined in the module docstring.

param h:

Optional affine constraints as defined in the module docstring.

param A:

Optional affine constraints as defined in the module docstring.

param b:

Optional affine constraints as defined in the module docstring.

param initial_weights:

w0 and diag-elements of Λ – must be passed together.

param market_impact_costs:

w0 and diag-elements of Λ – must be passed together.

efficient_frontier(num_portfolios: int) numpy.ndarray[source]

Return an (N, num_portfolios) array whose columns trace the Markowitz efficient set between the variance minimiser and the return maximiser.

efficient_portfolio(return_target: float | None = None) np.ndarray

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.

class pyvallocation.optimization.OptimizationResult(weights: npt.NDArray[np.floating], nominal_return: float, risk: float)[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 (σ⋆ for mean–variance, CVaR⋆ for mean-CVaR, or the robust radius t⋆).

nominal_return: float
risk: float
weights: numpy.typing.NDArray.numpy.floating
class pyvallocation.optimization.RobustOptimizer(expected_return: npt.NDArray[np.floating], uncertainty_cov: npt.NDArray[np.floating], G: npt.ArrayLike | None = None, h: npt.ArrayLike | None = None, A: npt.ArrayLike | None = None, b: npt.ArrayLike | None = None, *, initial_weights: npt.NDArray[np.floating] | None = None, proportional_costs: npt.NDArray[np.floating] | None = 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)

  • MeucciRisk & 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\) — scatter matrix (posterior covariance, shrinkage, …)

  • \(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=q\). Higher λ ⇒ stronger shrinkage towards the global minimum-variance portfolio.

solve_gamma_variant(gamma_mu, gamma_sigma_sq)

Chance-constraint form (Ben-Tal & Nemirovski 2001). For tolerance \(\gamma_\mu\) and radius cap \(\gamma_{\sigma}^{2}\) we enforce

\[\Pr(\mu^{\top}w\le -t)\le\gamma_\mu,\quad t^2\le\gamma_{\sigma}^{2},\]

implemented as a linear row t √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: Sequence[float]) Tuple[list[float], list[float], npt.NDArray[np.floating]][source]

Sweep a list of lambdas and return

  • nominal returns,

  • robust radii (t⋆),

  • and a weight matrix (N, len(lambdas)).

solve_gamma_variant(gamma_mu: float, gamma_sigma_sq: float) OptimizationResult[source]

Solve the chance-constraint form (γ-variant). Arguments map onto

\[\Pr\bigl(\mu^{\top}w\le -t\bigr)\;\le\; γ_{\mu}, \qquad t\;\le\;\sqrt{γ_{\sigma}^{2}}.\]
solve_lambda_variant(lam: float) OptimizationResult[source]

Solve the λ-variant:

\[\min_{w,t}\;t + λ\|S^{1/2}w\|_2 \quad\text{s.t.}\; t \ge -\hat\mu^{\top}w,\;\ldots\]