Modern Portfolio Theory and Mean-Variance Optimization › Quadratic Programming for Portfolio Optimization
Quantitative FinanceAll chapters Modern Portfolio Theory and Mean-Variance Optimization › Quadratic Programming for Portfolio OptimizationOne section · Sections in this chapter6 / 12 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
For practical applications with many assets and constraints, we use quadratic programming (QP). Quadratic programming is a mathematical optimization technique designed specifically for problems where the objective function is quadratic (involving squared terms) and the constraints are linear. Mean-variance optimization fits this structure perfectly because portfolio variance is a quadratic function of the weights.
The standard QP form is:
where:
- : vector of decision variables
- : symmetric matrix (quadratic term)
- : vector (linear term)
- : matrix and vector defining equality constraints
- : matrix and vector defining inequality constraints
The minimum-variance formulation uses and . For the defined utility , minimizing uses and . Equivalently, dividing the objective by positive gives and . The analytical exact-target formulation enforces both budget and target return as equalities; the utility formulation does not require a target-return equality. Inequalities can encode position limits and other practical requirements.
Adding Practical Constraints
Real portfolio optimization often includes additional constraints that reflect regulatory requirements, risk management policies, or investment mandates:
- No short selling: for all . This constraint prevents the portfolio from taking negative positions, which an investor cannot or chooses not to take.
- Maximum position size: (e.g., 10% per asset). This limits single-security concentration and can encourage diversification, but does not guarantee diversified sector or factor exposure.
- Sector constraints: Total exposure to a sector some limit. This prevents the portfolio from becoming overly exposed to a single industry or market segment.
- Turnover constraints: Changes from current portfolio are limited. This controls transaction costs and tax consequences by restricting how much trading occurs.
Let's implement a more realistic optimizer using SciPy's SLSQP method. Here target_return is a minimum return floor, imposed as an inequality, rather than the exact-target equality used in the analytical derivation. If the solver reports failure, the function raises an error instead of presenting a fallback allocation as optimal:
This implementation uses the ridge-regularized covariance , so its numerical candidate targets that model, not exactly the supplied unregularized covariance problem. The fixed ridge can matter substantially when covariance entries are small. For tiny objectives, we divide the entire chosen objective by a positive scale to avoid premature termination under an absolute stopping tolerance; this does not change its mathematical minimizer. A successful solver flag is not a proof of global optimality. After thresholding and normalization, we check finite weights, budget, bounds and the return floor to an absolute feasibility tolerance of ; this is a feasibility check, not an optimality certificate.
import numpy as np
from scipy.optimize import minimize
def mean_variance_optimize(
mu,
cov_matrix,
target_return=None,
risk_aversion=None,
max_weight=None,
min_weight=0.0,
allow_short=False,
):
"""
Find an approximate feasible candidate for a ridge-regularized model.
Requires finite, compatible inputs and a positive-semidefinite covariance;
the utility branch assumes positive risk_aversion. Uses Sigma + 1e-8 I.
Tiny entire objectives are positively rescaled; success is not an
optimality proof. Returned feasibility uses an absolute 1e-7 tolerance.
Parameters
----------
mu : ndarray
Expected returns
cov_matrix : ndarray
Covariance matrix
target_return : float, optional
Minimum portfolio return (a floor for min variance formulation)
risk_aversion : float, optional
Risk aversion parameter (for max utility formulation)
max_weight : float, optional
Maximum weight per asset
min_weight : float, optional
Minimum weight per asset when allow_short=False (default 0).
Ignored when allow_short=True, which sets the lower bound to -1.
allow_short : bool
Use lower bound -1 rather than min_weight; the default upper bound is 1.
Returns
-------
weights : ndarray
Approximate feasible weights for the ridge-regularized model
Raises
------
RuntimeError
If the optimizer reports failure or returned weights fail validation.
"""
# Convert to numpy arrays and ensure proper shape
mu_arr = np.asarray(mu).flatten()
cov_arr = np.asarray(cov_matrix)
n = len(mu_arr)
# Fixed ridge changes the supplied covariance model; see the stated scope.
cov_arr = cov_arr + np.eye(len(mu_arr)) * 1e-8
if target_return is not None:
objective = lambda w: float(w @ cov_arr @ w)
elif risk_aversion is not None:
objective = lambda w: (
-float(mu_arr @ w - (risk_aversion / 2) * (w @ cov_arr @ w))
)
else:
objective = lambda w: float(w @ cov_arr @ w)
objective_scale = float(np.max(np.abs(cov_arr)))
if target_return is None and risk_aversion is not None:
objective_scale = max(
float(np.max(np.abs(mu_arr))), abs(risk_aversion) * objective_scale
)
if 0 < objective_scale < 1e-6:
unscaled_objective = objective
objective = lambda w: unscaled_objective(w) / objective_scale
constraints = [{"type": "eq", "fun": lambda w: np.sum(w) - 1.0}]
if target_return is not None:
constraints.append(
{"type": "ineq", "fun": lambda w: float(mu_arr @ w - target_return)}
)
if allow_short:
lower = -1.0
else:
lower = min_weight
upper = 1.0 if max_weight is None else max_weight
bounds = [(lower, upper) for _ in range(n)]
x0 = np.full(n, 1.0 / n)
if not allow_short:
x0 = np.clip(x0, lower, upper)
x0 = x0 / x0.sum()
result = minimize(
objective,
x0,
method="SLSQP",
bounds=bounds,
constraints=constraints,
options={"maxiter": 500, "ftol": 1e-10},
)
if not result.success:
raise RuntimeError(f"Mean-variance optimization failed: {result.message}")
weights = result.x
weights = np.asarray(weights, dtype=float)
if not np.all(np.isfinite(weights)):
raise RuntimeError("Mean-variance optimization returned invalid weights")
weights[np.abs(weights) < 1e-10] = 0.0
total = weights.sum()
if not np.isfinite(total) or total == 0:
raise RuntimeError("Mean-variance optimization returned invalid weights")
weights = weights / total
feasibility_tolerance = 1e-7
if (
abs(weights.sum() - 1.0) > feasibility_tolerance
or np.any(weights < lower - feasibility_tolerance)
or np.any(weights > upper + feasibility_tolerance)
or (
target_return is not None
and mu_arr @ weights < target_return - feasibility_tolerance
)
):
raise RuntimeError("Mean-variance optimization returned infeasible weights")
return weights# Unconstrained GMV (allows short selling)
w_unconstrained = global_minimum_variance_portfolio(cov_matrix)
# Constrained: no short selling
w_long_only = mean_variance_optimize(mu, cov_matrix, allow_short=False)
# Constrained: max 40% per asset, no short selling
max_pos = 0.4
w_max_40 = mean_variance_optimize(mu, cov_matrix, allow_short=False, max_weight=max_pos)
# Calculate performance metrics
ret_unconstrained = w_unconstrained @ mu
risk_unconstrained = np.sqrt(w_unconstrained @ cov_matrix @ w_unconstrained)
ret_long_only = w_long_only @ mu
risk_long_only = np.sqrt(w_long_only @ cov_matrix @ w_long_only)
ret_max_40 = w_max_40 @ mu
risk_max_40 = np.sqrt(w_max_40 @ cov_matrix @ w_max_40)Portfolio Weights Comparison: ------------------------------------------------------------ Asset Unconstrained Long-Only Max 40% ------------------------------------------------------------ Stocks 2.95% 2.95% 33.78% Bonds 92.03% 92.03% 40.00% Commodities 5.02% 5.02% 26.22% ------------------------------------------------------------ Unconstrained: Return: 3.25%, Risk: 4.82% Long-Only: Return: 3.25%, Risk: 4.82% Max 40%: Return: 5.21%, Risk: 9.49%

In the original covariance model, the unrestricted GMV optimum is already long-only, so that constraint is nonbinding. The 40% cap raises volatility substantially, from 4.82% to 9.49%. Adding constraints cannot improve the minimum objective when the objective itself is unchanged. Here the numerical constrained helper also adds a small ridge, so exact equality with the unrestricted calculation is only expected to displayed precision.