Mean-Variance Optimization, From Scratch
A full derivation of Markowitz (1952) mean-variance optimization. Inputs, outputs, the math, and why this 70-year-old idea still anchors every portfolio you see on a Bloomberg terminal.
Harry Markowitz published “Portfolio Selection” in the Journal of Finance in 1952. He was 24 (born August 24, 1927; the paper appeared in the March 1952 issue, before his 25th birthday). The paper is eight pages long, contains one quadratic program, and won him a Nobel Prize 38 years later.
Seventy-three years on, every allocator on earth is still solving some descendant of his problem. Black-Litterman, risk parity, CVaR optimization, robust MVO, hierarchical risk parity: all of them are footnotes. So if you don’t understand Markowitz’s original formulation, you don’t understand what’s sitting underneath any of those.
This article walks through that formulation from scratch. Inputs, math, the Lagrangian, the closed-form solution, and the unconstrained case. We’ll touch the constraints too, because nobody runs the unconstrained version in production.
The Setup
You have risky assets. You want to allocate capital across them. Let be the vector of portfolio weights. There are two things you care about:
- The expected return of the portfolio:
- The variance of the portfolio:
Here is your vector of expected returns and is the covariance matrix of asset returns. Both are inputs. Both are estimates. (That second part will matter in a later article when we talk about estimation error. For now, pretend you know them exactly.)
You can also allocate to a risk-free asset with return , but we’ll add that at the end. Start with risky-only.
What Markowitz Actually Did
Markowitz’s insight was embarrassingly simple. Don’t evaluate assets one at a time. Evaluate portfolios. The return of a portfolio is the weighted average of its components. The risk of a portfolio is not, because it is a function of how the components move together.
A portfolio of two perfectly correlated assets has variance equal to the weighted average of variances. A portfolio of two perfectly negatively correlated assets can have zero variance. Everything in between gives you a tradeoff curve. Markowitz called this the “efficient frontier”: the set of portfolios with minimum variance for each level of expected return.
Concretely, for a target return , solve:
The half in front of the variance is just for clean derivatives. The two constraints say: hit your return target, and fully invest your capital (no cash). The objective minimizes risk.
Notice what’s not there: short-sale constraints, leverage limits, sector bounds, transaction costs. The basic version is naked. We dress it up later.
The Lagrangian
This is a quadratic program with linear equality constraints. You can write the Lagrangian directly:
Take the derivative with respect to and set it to zero:
Solve for :
Plug back into the two equality constraints and you get a 2x2 linear system in . Solve that, substitute, and you get the closed-form weights.
Define the following scalars (the A, B, C, D notation follows the convention used in many textbooks on portfolio theory; Luenberger’s Investment Science uses different variable names):
Then the efficient portfolio weights are:
The global minimum-variance portfolio is the special case , which gives:
The tangency portfolio (maximum Sharpe ratio when a risk-free asset exists) comes from setting in the Lagrangian, which removes the budget constraint and just maximizes . The solution is:
This is the textbook answer. Boyd & Vandenberghe’s Convex Optimization (2004, Ch. 4) lays out the same derivation with different variable names. The geometry is identical.
The Two-Fund Theorem
Here’s the part Markowitz was proud of. The efficient frontier is a single curve in space, and every point on it is a combination of just two portfolios: any two efficient portfolios.
Pick the GMV portfolio and the tangency portfolio. Then any efficient portfolio with expected return can be written as:
for some chosen to hit . In practice this means: compute two portfolios, blend. The whole frontier falls out of one matrix inversion and some scalar arithmetic.
In production code nobody computes the whole frontier this way. They solve the QP directly for a single , or they solve the dual problem (mean-variance tracing: fix variance, maximize return). Same answer.
When the Risk-Free Asset Exists
If you can lend and borrow at the risk-free rate , the efficient frontier collapses. The new efficient set is the Capital Market Line, a single ray from through the tangency portfolio. Every efficient portfolio is:
where is 100% in cash and is your risk budget. is fully invested in the tangency portfolio. means you’re borrowing at to lever up the tangency.
Sharpe called this the “two-fund theorem” generalized. Tobin called it the “separation theorem”. Either way, it’s the math underneath every target-date fund and 60/40 portfolio you’ve ever seen.
Why You’d Ever Use This
Honestly? In its raw form, almost nobody does. The unconstrained Markowitz solution famously produces concentrated, levered portfolios with extreme short positions. If you give it 50 assets and 5 years of monthly data, it’ll happily put -180% in some random small-cap and +280% in a handful of others. This was the whole subject of Michaud (1989) calling MVO an “error maximizer” (see the next article).
But here’s the thing: every modern method is built on top of Markowitz. Black-Litterman adds a Bayesian prior on and re-solves the same QP. Robust MVO replaces with uncertainty sets and re-solves the same QP. CVaR optimization replaces the variance objective with conditional value-at-risk, but the structure is the same quadratic/linear program with the same constraints.
So you learn Markowitz first not because you’ll use it raw, but because every modification requires you to understand the base case.
A Working Example
Here’s the smallest useful implementation. Compute the GMV portfolio for a 4-asset universe:
import numpy as np
mu = np.array([0.08, 0.12, 0.15, 0.10])
Sigma = np.array([
[0.04, 0.01, 0.02, 0.005],
[0.01, 0.09, 0.03, 0.01],
[0.02, 0.03, 0.16, 0.02],
[0.005, 0.01, 0.02, 0.05]
])
# Closed-form global minimum-variance portfolio
Sigma_inv = np.linalg.inv(Sigma)
ones = np.ones(len(mu))
w_gmv = Sigma_inv @ ones / (ones @ Sigma_inv @ ones)
print("GMV weights:", w_gmv.round(4))
print("Sum:", w_gmv.sum().round(6))
This produces something like [0.42, 0.21, 0.10, 0.27] for the inputs above. The sum is exactly 1. None of the weights are negative, but that’s an accident of the covariance matrix. Pick a different and you’ll see shorts.
For the tangency portfolio with :
r_f = 0.02
excess = mu - r_f * ones
w_tan = Sigma_inv @ excess / (ones @ Sigma_inv @ excess)
print("Tangency weights:", w_tan.round(4))
Output will be heavily concentrated, probably 60%+ in the asset with the highest excess return per unit of variance contribution. That’s the problem Michaud was complaining about.
What the Literature Says
- Markowitz (1952). “Portfolio Selection.” Journal of Finance 7(1): 77-91. The original.
- Markowitz (1959). Portfolio Selection: Efficient Diversification of Investments. Wiley. The book form, with the full QP.
- Luenberger (2013). Investment Science. 2nd ed. Oxford University Press. Ch. 6 has the cleanest derivation I know of. ISBN: 978-0-19-974008-6.
- Boyd & Vandenberghe (2004). Convex Optimization. Cambridge University Press. Ch. 4 frames MVO as a QP and shows the dual. ISBN: 978-0-521-83378-3.
- Michaud (1989). “The Markowitz Optimization Enigma: Is ‘Optimized’ Optimal?” Financial Analysts Journal 45(1): 31-42. The “error maximizer” critique. We’ll cover this in detail next.
- Black & Litterman (1992). “Asset Allocation: Combining Investor Views with Market Equilibrium.” Goldman Sachs Fixed Income Research. The Bayesian upgrade to MVO.
A Full Frontier Computation
The closed-form expressions above give you one portfolio at a time. To draw the full efficient frontier, you sweep from the GMV return to the maximum feasible return and compute at each point.
In Python, the full frontier is short:
import numpy as np
import matplotlib.pyplot as plt
# Inputs as before
mu = np.array([0.08, 0.12, 0.15, 0.10])
Sigma = np.array([
[0.04, 0.01, 0.02, 0.005],
[0.01, 0.09, 0.03, 0.01],
[0.02, 0.03, 0.16, 0.02],
[0.005, 0.01, 0.02, 0.05]
])
r_f = 0.02
Sigma_inv = np.linalg.inv(Sigma)
ones = np.ones(len(mu))
# Sweep target returns
A = ones @ Sigma_inv @ ones
B = ones @ Sigma_inv @ mu
C = mu @ Sigma_inv @ mu
D = A * C - B**2
targets = np.linspace(0.085, 0.18, 50) # Min to max
risks, weights = [], []
for r_bar in targets:
w = ((A * r_bar - B) * Sigma_inv @ mu + (C - B * r_bar) * Sigma_inv @ ones) / D
risk = np.sqrt(w @ Sigma @ w)
risks.append(risk)
weights.append(w)
# Plot frontier
plt.plot(risks, targets, 'b-', label='Efficient Frontier')
plt.xlabel('Volatility')
plt.ylabel('Expected Return')
plt.title('Mean-Variance Efficient Frontier')
plt.grid(True)
plt.legend()
plt.show()
The frontier is a hyperbola in space. The vertex is the GMV portfolio. The asymptote is the tangency line (CML) when a risk-free asset is added. The shape is the same for any number of assets. What changes is the curvature and the range of attainable returns.
A useful diagnostic is the Sharpe ratio at each point on the frontier. It’s maximized at the tangency portfolio. As you move away from tangency toward GMV, Sharpe declines because you’re accepting more volatility for less incremental return.
Sensitivity Analysis: How Stable Are the Weights?
The original Markowitz weights are not stable under small perturbations of . To see this, perturb each input by 1% and recompute the weights:
The change in weights is proportional to on both terms. For ill-conditioned (concentrated risk, few assets), has large entries. The weights can swing wildly.
This sensitivity is the math behind Michaud’s “error maximizer” critique. We saw it in detail in the previous article on estimation error. The point here is just: it’s a direct consequence of the optimization, not a bug.
Dual Formulation: Mean-Variance Tracing
An equivalent formulation of Markowitz is the dual: fix variance, maximize return.
This is a quadratically-constrained linear program (QCLP). For a given target volatility , it finds the portfolio with the highest expected return. Sweep and you trace out the same frontier as the primal formulation.
The dual is computationally nicer for constrained problems because the convex hull of achievable pairs is easier to reason about. It’s also the form used in risk-budgeting and vol-targeting applications, where you fix vol and maximize return.
Adding Position Limits and Sector Bounds
Real portfolios have constraints. The standard set:
- Long-only:
- No leverage: (usually for long-only)
- Position cap: for each
- Sector cap:
- Turnover cap:
All of these are linear (except the leverage one, which becomes linear under long-only). The QP stays convex. Solve with any standard solver.
The position cap is the silent stabilizer. With (no asset more than 10% of portfolio), the optimizer cannot concentrate. The weights are forced to be diversified.
In practice, to is standard for institutional portfolios. Below 5%, you’re forcing over-diversification. Above 15%, you might as well not have the cap.
The Lagrangian, Once More, With Constraints
If you want the full KKT conditions for the constrained QP, here they are. The problem:
The Lagrangian with multipliers for equality and for inequality:
KKT conditions:
- Stationarity:
- Primal feasibility:
- Dual feasibility:
- Complementary slackness: for all
The complementary slackness condition is the key: if , then and the constraint is non-binding. If , then and the constraint is binding (the optimizer is being held back from going more negative on that asset).
In code, modern QP solvers handle all of this for you. You don’t need to write the KKT system by hand. But understanding it helps interpret solver output.
What to Remember
Three things.
One: Markowitz’s problem is minimize variance subject to hitting a return target and fully investing capital. The objective is , the constraints are linear, the solution is a closed-form expression involving .
Two: the efficient frontier is spanned by two portfolios. The GMV portfolio and the tangency portfolio. Every other efficient portfolio is a linear combination.
Three: nobody runs this in production. Everyone runs some descendant. Learn the base case first, then learn the descendants.
The frontier itself is a hyperbola in space. The dual formulation fixes vol and maximizes return, producing the same frontier from a different angle. Position limits and sector bounds are linear additions that keep the QP convex while preventing pathological weights. And the KKT system tells you which constraints are binding at the optimum, useful diagnostic information for understanding your portfolio.
A practical note: when implementing this in cvxpy, quadprog, or any other solver, always add a small regularization term to the covariance matrix. Numerical issues arise when is near-singular. Adding with keeps the solver stable. This is the same Ledoit-Wolf shrinkage idea, just for numerical rather than statistical reasons.
The Markowitz formulation has had an enormous second life through its descendants. Every modern portfolio optimization method is, in some sense, an answer to the question “how do we fix Markowitz?” The next eleven articles in this module are all about those fixes.
Next article: why the descendants exist at all. The estimation error problem.