markowitz.optimizer.cornuejols_tutuncu¶
markowitz.optimizer.cornuejols_tutuncu
¶
Cornuejols--Tutuncu reformulation of the maximum-Sharpe problem.
The vanilla maximise (mu^T w - rf) / sqrt(w^T Sigma w) is non-convex (a
fractional / quasi-concave objective). The Cornuejols--Tutuncu trick is to
substitute
.. math::
y = \kappa \, w, \qquad \kappa > 0,
\qquad (\mu - rf \cdot \mathbf{1})^T y = 1,
which turns the problem into the convex QP
.. math::
\min_{y \ge 0,\ \kappa > 0} \; y^T \Sigma y
\quad \text{s.t.} \quad (\mu - rf)^T y = 1, \; \mathbf{1}^T y = \kappa.
The optimal weights are then recovered by w = y / sum(y). The
construction is only well-posed if at least one asset has a positive
excess return; we expose :func:detect_degeneracy to catch that case
before the solver wastes time on it.
This module deliberately exposes only the reformulation and the
back-transform; the actual call into the solver lives in
:mod:markowitz.optimizer.solvers, and orchestration is the job of
:class:markowitz.optimizer.mean_variance.MeanVariance.
ReformulatedProblem(problem: cp.Problem, y: cp.Variable, kappa: cp.Variable)
dataclass
¶
Container for the CVXPY problem and its decision variables.
The caller is expected to solve problem and then read the optimal
values off y and kappa.
back_transform(y_value: np.ndarray) -> np.ndarray
¶
Recover the portfolio weights w = y / sum(y).
Raises:
| Type | Description |
|---|---|
NumericalError
|
If |
Source code in src/markowitz/optimizer/cornuejols_tutuncu.py
detect_degeneracy(mu: np.ndarray, risk_free_rate: float, weight_bounds: tuple[float | None, float | None] | None, long_only: bool) -> None
¶
Raise :class:InfeasibleError if no positive-Sharpe portfolio exists.
The reformulation requires that the equality (mu - rf)^T y = 1 is
achievable with the sign structure permitted by the bounds. In the
long-only / non-negative-weights case this simply means at least one
asset must have a strictly positive excess return.
Source code in src/markowitz/optimizer/cornuejols_tutuncu.py
reformulate_max_sharpe(mu: np.ndarray, Sigma: np.ndarray, risk_free_rate: float, weight_bounds: tuple[float | None, float | None] | None, *, long_only: bool = True, extra_constraint_builders: Sequence[Any] | None = None) -> ReformulatedProblem
¶
Build the CVXPY problem in (y, kappa) space.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mu
|
ndarray
|
Expected returns and covariance matrix. |
required |
Sigma
|
ndarray
|
Expected returns and covariance matrix. |
required |
risk_free_rate
|
float
|
Reference rate |
required |
weight_bounds
|
tuple[float | None, float | None] | None
|
Box |
required |
long_only
|
bool
|
If |
True
|
extra_constraint_builders
|
Sequence[Any] | None
|
Optional iterable of callables |
None
|