The two-asset minimum by Lagrange
A portfolio puts a fraction of the money in asset . Its variance is , with the covariance matrix (case study 3.2.5).
The weights must add up to 1, so that all the money is invested: , where is the vector of ones. A negative weight is a short sale (case study 1.2.4).
So the safest portfolio solves: minimise subject to . Lesson 4.4.1's quadratic has gradient , and here , so . The budget has .
Lagrange's condition reads : every entry of is the same number, . For case study 1.2.4's two assets, with , the two entries are
Subtracting gives . With , that is , so . That is case study 1.2.4's vertex, , the same number found a second way.
Rule: The minimum-variance portfolio
Every entry of is equal, and the common value is the variance: . The minimum variance is .
Case study 3.2.5 noticed that at case study 1.2.4's weight both entries of equal 0.0326, the variance itself. The rule says why: it holds at every minimum-variance portfolio, with any number of assets.
The second line follows from the first. gives , and the budget fixes so that the weights add up to 1. In code, solve with np.linalg.solve (lesson 3.2.3) and divide by the sum of its entries.
Why the entries must agree
Entry of is the covariance of asset with the portfolio (case study 3.2.5). Move a little money from asset to asset , along (standard basis vectors, lesson 3.5.1): the budget stays the same. The variance changes at the rate (lesson 4.2.1).
If two entries differ, moving money towards the smaller one lowers the variance. Only when all entries agree does no move help.
Try it
np.linalg.solve(S2, np.ones(2)) is , found without forming the inverse. Dividing by its sum gives the weights, next to case study 1.2.4's .
The second line prints , the variance and the volatility: both entries are 0.0326.
Three assets
The three assets of case study 3.2.5 have expected returns , volatilities 12%, 18% and 25%, and correlations , , . The same rule gives the minimum-variance portfolio:
| A | B | C | |
|---|---|---|---|
| weight | 74.65% | 15.11% | 10.23% |
| 0.012036 | 0.012036 | 0.012036 |
Its variance is 0.012036, the common value, so its volatility is 10.97%. Its expected return is .
The 50/30/20 portfolio of case study 3.2.5 has . The entries differ, so it is not the minimum. Moving money from C to A lowers the variance at the rate per unit.
Case study 3.2.5's risk contribution of asset is . At the minimum every equals the variance, so each asset's share of the risk equals its share of the money. The shares are 74.65%, 15.11% and 10.23%.
Is this the lowest variance, or only a candidate? is positive definite, with eigenvalues 0.011741, 0.022502 and 0.075058 (lesson 3.7.1). So is strictly convex and the budget is linear: lesson 4.5.3 says the Lagrange point is the one global minimum.
Try it
The cell repeats case study 3.2.5's , then the rule. The third line prints the risk shares in percent.
Next, for 50/30/20, the rate of moving money from C to A, and a check that by central differences (lesson 4.1.2). Last, SciPy's answer (lesson 4.5.2) and the eigenvalues of .
A target return
The minimum-variance portfolio expects only 6.81%. An investor who wants more return asks for the safest portfolio that expects a target : a second constraint, .
Two equality constraints bring two multipliers, as in lesson 4.5.2's rule for two constraints:
That is five linear equations in , , , and , solved as one system like lesson 4.5.1's. For the answer is 29.87% in A, 16.88% in B and 53.25% in C, with volatility 15.99%.
The multiplier is a rate, as in lesson 4.5.1: the variance per unit of target return. From 9% to 9.1% the variance rises by 0.001268, close to . Extra return costs risk, and says how much.
Try it
solve_target builds the system: the first three rows are , the last two the constraints.
The last line compares the change in variance from 9% to 9.1% with .
The efficient frontier
Solving for every target at once gives a formula, with and . The first equation becomes , so . Every frontier portfolio mixes the same two vectors.
Put that into the two constraints. With three numbers from the data,
they read and . This system (lesson 3.2.3) gives and , with .
The variance is short to find too: , since and .
Rule: The minimum-variance frontier
The lowest variance for each target return : a parabola in . Its vertex, at , is the minimum-variance portfolio, with variance .
Here , , and . The vertex is at , with variance : the minimum-variance portfolio again.
| target | A | B | C | volatility |
|---|---|---|---|---|
| 5% | 111.81% | 13.64% | 14.62% | |
| 6% | 91.33% | 14.45% | 11.80% | |
| 6.81% | 74.65% | 15.11% | 10.23% | 10.97% |
| 8% | 50.36% | 16.07% | 33.57% | 12.66% |
| 9% | 29.87% | 16.88% | 53.25% | 15.99% |
| 10% | 9.39% | 17.69% | 72.92% | 20.20% |
| 11% | 18.50% | 92.60% | 24.84% | |
| 12% | 19.30% | 112.28% | 29.71% |
Plotted with volatility across and expected return up, the frontier is a hyperbola, the shape lesson 3.7.1 met as a level curve. Only its upper half is worth holding: the efficient frontier. A 6% target needs a volatility of 11.80%, and so does 7.63% on the upper half: same risk, more return.
The 50/30/20 portfolio lies inside the curve. For its 7.6% the frontier needs only 11.74%, against its 11.94%.
The frontier and its weights. With short sales the weights are straight lines in the target; long-only weights follow them inside the shaded band and bend away outside it.
Try it
The cell computes , , and , then and . frontier(m) is .
The loop prints the table. Its last two columns are the volatility from the weights and from the rule: they agree.
Two funds are enough
and are straight-line functions of , so the frontier weights are too. Each extra point of target return moves the weights by the same amounts: points in A, in B and in C. In the figure they are three straight lines.
Rule: The two-fund theorem
Any frontier portfolio is a mix of any two others. So two funds on the frontier are enough to build every frontier portfolio, short sales allowed.
Take the 8% and 12% frontier portfolios as the two funds. Then 75% of the first and 25% of the second gives exactly the 9% portfolio, (29.87%, 16.88%, 53.25%). The minimum-variance portfolio and the 11% portfolio, mixed with , give it too.
Try it
The first line is the change in the weights for one extra point of target. Then the two mixes, each next to the 9% portfolio.
The last line checks the inefficient half: 6% and 7.63% need the same volatility, and the frontier at 7.6% needs 11.74%.
No short sales
Many funds may not sell short, so every weight must satisfy . Each is an inequality constraint of lesson 4.5.2, with its own multiplier (nu). A constraint has .
Rule: Long-only KKT conditions
For each asset held, : the frontier condition holds for it. For each asset left out, is at least . Without a target return, drop : the assets held share one value of , and any asset left out has at least that.
Two assets. Case study 1.2.4 met , where the rule gives 123.53% in A and in B. Long-only, try all in A, where .
B is left out, and its entry 0.048 is above A's 0.04, so the conditions hold. The problem is convex (lesson 4.5.3), so all in A is the best long-only portfolio: case study 1.2.4's "end nearest the vertex".
Three assets, no target. The minimum-variance weights are all positive, so the ban changes nothing.
Three assets, with a target. The frontier weights are all positive from 6.29% to 10.46%, the band in the figure. There the long-only frontier is the same.
Above 10.46%, the formula would sell A short. Hold A at 0 instead: B and C alone must meet both constraints, which fixes their weights.
At 10.8% that gives 6.67% in B and 93.33% in C, with volatility 23.96%, against 23.89% with short sales. Its , so the conditions hold.
The sign test also catches a wrong guess. At 10%, holding A at 0 gives , the wrong sign: A should be held. There the short-sales portfolio, (9.39%, 17.69%, 72.92%), is already long-only.
Below 6.29% it is C that would be sold short; at 6.2% the long-only portfolio is 90% A and 10% B. Long-only targets run only from 6% (all A) to 11% (all C), the lowest and highest expected returns. With these assets the ban costs at most 0.20 points of volatility, at the 6% end: 12.00% against 11.80%.
Try it
The first line is the two-asset case: the rule's short sale of B, then for all in A.
without_A holds A at 0 and finds B and C from the two constraints. It then solves for and from the rows of B and C, and returns from A's row.
With more assets, guessing which weights are 0 gets slow, and a solver does the search. SLSQP (lesson 4.5.2) takes the two equations as "eq" constraints and as bounds. Check that the target lies between the lowest and highest expected return first, and check r.success after.
Try it
long_only(m) minimises with bounds=[(0, 1)] * 3, which keeps each weight between 0 and 1. With the budget, already forces .
The answers match the cases above. No long-only portfolio expects 11.5%: every asset expects at most 11%.
The explorer draws the frontier for the three assets. Set the target to 10.8% and read the weights with short sales allowed; then turn short sales off and watch A drop to 0. Move the target below 6.29% to see C do the same.
Explore
Every mix of the three assets is a point: its volatility across, its expected return up. Move the target return and read the cheapest mix in risk that earns it.
Grey dots are the assets, the open circle the minimum-variance portfolio.
- Frontier portfolio
- A 29.9%, B 16.9%, C 53.2%
- Return and risk
- expected return 9%, volatility 15.99%
- Minimum variance
- A 74.7%, B 15.1%, C 10.2%: return 6.81%, volatility 10.97%
On the desk
Try it
frontier is this case study's formula as a function of and . The first line compares the 9% weights from in decimals and in percent.
The loop adds the near-copy D and prints the smallest eigenvalue of and the 9% weights. The last line is the impossible : its eigenvalues, the rule's "variance", and the "variance" after moving along by 1 and by 10.
Minimum-variance and frontier portfolios
Write two functions, with NumPy, short sales allowed.
min_variance(cov)returns the weights . Usenp.linalg.solve, not the inverse.frontier_portfolio(mu, cov, m)returns the frontier weights for the target returnm.
Both raise ValueError unless cov is square, symmetric and positive definite. frontier_portfolio also raises it if mu has the wrong length, or if every expected return is the same.
The tests use case study 1.2.4's two assets and these three, percent units, and the bad inputs from the desk notes. They check that each entry of equals the variance, and the two-fund theorem. They also check that no other portfolio with the same budget and return has less variance.
See the tests
import numpy as np
MU = np.array([0.06, 0.08, 0.11])
SIGMA = np.array([0.12, 0.18, 0.25])
R = np.array([[1.0, 0.3, 0.1], [0.3, 1.0, 0.5], [0.1, 0.5, 1.0]])
COV = np.diag(SIGMA) @ R @ np.diag(SIGMA)
TWO = np.array([[0.04, 0.012], [0.012, 0.09]])
def _weights(value, what):
assert value is not None, f"{what} returned None: return the array of weights"
return np.asarray(value, dtype=float)
def _raises_value_error(fn, *args):
try:
fn(*args)
except ValueError:
return True
return False
def test_two_assets():
"""Case study 1.2.4's two assets: 73.58% in A and 26.42% in B"""
w = _weights(min_variance(TWO), "min_variance")
expect_close(w, [0.156 / 0.212, 1 - 0.156 / 0.212], what="min_variance for case study 1.2.4", rel=1e-9)
def test_three_assets():
"""The three assets: about 74.65% in A, 15.11% in B and 10.23% in C"""
w = _weights(min_variance(COV), "min_variance")
expect_close(w, [0.7465402, 0.1511109, 0.1023489], what="min_variance for A, B and C", rel=1e-5)
expect_close(w.sum(), 1.0, what="sum of the weights")
def test_equal_entries():
"""At the minimum, every entry of Σw equals the variance, for a random 4-asset Σ too"""
rng = np.random.default_rng(455)
M = rng.normal(size=(6, 4))
cov = M.T @ M / 20
w = _weights(min_variance(cov), "min_variance")
expect_close(cov @ w, np.full(4, w @ cov @ w), what="Σw against wᵀΣw in every entry", rel=1e-9)
def test_target_return():
"""The 9% frontier portfolio is about (29.87%, 16.88%, 53.25%), with volatility 15.99%"""
w = _weights(frontier_portfolio(MU, COV, 0.09), "frontier_portfolio")
expect_close(w, [0.2987268, 0.1687887, 0.5324845], what="frontier_portfolio(mu, cov, 0.09)", rel=1e-5)
expect_close(np.sqrt(w @ COV @ w), 0.1599427, what="volatility at 9%", rel=1e-5)
def test_constraints_hold():
"""A frontier portfolio sums to 1 and earns its target, here 12%"""
w = _weights(frontier_portfolio(MU, COV, 0.12), "frontier_portfolio")
expect_close(w.sum(), 1.0, what="sum of the weights")
expect_close(w @ MU, 0.12, what="expected return")
def test_no_lower_variance():
"""No other portfolio with the same budget and return has a lower variance"""
w = _weights(frontier_portfolio(MU, COV, 0.09), "frontier_portfolio")
# (3, −5, 2) keeps both the budget and the return: 3 − 5 + 2 = 0 and 0.18 − 0.40 + 0.22 = 0
d = np.array([3.0, -5.0, 2.0])
for step in [-0.01, 0.01, 0.1]:
other = w + step * d
assert other @ COV @ other > w @ COV @ w, f"moving by {step} × (3, −5, 2) lowered the variance: not the minimum"
def test_minimum_on_frontier():
"""The frontier portfolio at the minimum-variance return is the minimum-variance portfolio"""
w0 = _weights(min_variance(COV), "min_variance")
w = _weights(frontier_portfolio(MU, COV, float(w0 @ MU)), "frontier_portfolio")
expect_close(w, w0, what="frontier_portfolio at 6.81%", rel=1e-8)
def test_two_funds():
"""Two-fund theorem: 75% of the 8% portfolio plus 25% of the 12% portfolio is the 9% portfolio"""
w8 = _weights(frontier_portfolio(MU, COV, 0.08), "frontier_portfolio")
w12 = _weights(frontier_portfolio(MU, COV, 0.12), "frontier_portfolio")
w9 = _weights(frontier_portfolio(MU, COV, 0.09), "frontier_portfolio")
expect_close(0.75 * w8 + 0.25 * w12, w9, what="the mix of two frontier portfolios", rel=1e-9)
def test_percent_units():
"""Volatilities typed in percent give the same weights"""
pct = np.diag(100 * SIGMA) @ R @ np.diag(100 * SIGMA)
expect_close(_weights(min_variance(pct), "min_variance"), _weights(min_variance(COV), "min_variance"), what="weights from Σ in percent", rel=1e-9)
def test_not_symmetric():
"""A covariance matrix with a typo in one triangle raises ValueError"""
typo = COV.copy()
typo[0, 1] = 0.0648
assert _raises_value_error(min_variance, typo), "a matrix that is not symmetric should raise ValueError"
def test_not_positive_definite():
"""Impossible correlations, with a negative eigenvalue, raise ValueError"""
bad = np.array([[1.0, 0.9, 0.9], [0.9, 1.0, -0.9], [0.9, -0.9, 1.0]])
assert _raises_value_error(min_variance, np.diag(SIGMA) @ bad @ np.diag(SIGMA)), "Σ with a negative eigenvalue has no minimum: raise ValueError"
def test_equal_returns():
"""Equal expected returns leave no frontier: ValueError"""
assert _raises_value_error(frontier_portfolio, [0.08, 0.08, 0.08], COV, 0.09), "with every μᵢ = 8%, no portfolio earns 9%: raise ValueError"
def test_wrong_length():
"""Two expected returns for three assets raise ValueError"""
assert _raises_value_error(frontier_portfolio, [0.06, 0.08], COV, 0.07), "three assets need three expected returns"
Summary card
- Minimising subject to : Lagrange gives , so every entry of is equal, and equal to the variance. ; for case study 1.2.4, .
- For A, B and C: 74.65%, 15.11% and 10.23%, expected return 6.81%, volatility 10.97%. is positive definite, so it is the one global minimum.
- A target return adds : . The frontier variance is a parabola in , a hyperbola in volatility; its upper half is efficient.
- The frontier weights are straight lines in : any two frontier portfolios build all the others (the two-fund theorem).
- Long-only, by KKT: each asset held has , each asset left out at least that. Check 's eigenvalues, units, near-copies and the solver's
successbefore trusting weights.