Risk for every level of return
The previous topic found, for one target return, the weights with the least variance. Do this for every target and plot volatility on the horizontal axis against return on the vertical axis: the points form a curve, the efficient frontier. Every portfolio of the assets lies on or inside it, to the right. A portfolio inside the curve is wasteful, since another has the same return with less risk or more return with the same risk. Markowitz's insight was that investors should only look at the curve.
import numpy as np
mu = np.array([0.08, 0.05, 0.10])
cov = np.array([[0.0400, 0.0060, 0.0100],
[0.0060, 0.0100, 0.0020],
[0.0100, 0.0020, 0.0225]])
def target_return_weights(mu, cov, target):
ones = np.ones(len(mu))
s1, sm = np.linalg.solve(cov, ones), np.linalg.solve(cov, mu)
a, b, c = ones @ s1, ones @ sm, mu @ sm
d = a * c - b ** 2
return ((c - b * target) * s1 + (a * target - b) * sm) / d
for target in (0.05, 0.06, 0.07, 0.08, 0.09, 0.10):
w = target_return_weights(mu, cov, target)
print(target, round(float(np.sqrt(w @ cov @ w)), 4))Its shape, and its tip
The frontier is a parabola in (return, variance), and a hyperbola if you plot volatility. The variance at target return t is (a * t**2 - 2 * b * t + c) / (a * c - b**2), from the same four numbers a, b, c as before. Its lowest point is the global minimum-variance portfolio, at return b / a: the tip of the curve, which is the minimum-variance portfolio of two topics ago. Only the part from the tip upward is the efficient frontier; the lower branch is a mirror image with less return for the same risk.
# The tip of the curve: the minimum-variance portfolio and its return
import numpy as np
mu = np.array([0.08, 0.05, 0.10])
cov = np.array([[0.0400, 0.0060, 0.0100],
[0.0060, 0.0100, 0.0020],
[0.0100, 0.0020, 0.0225]])
ones = np.ones(3)
x = np.linalg.solve(cov, ones)
w = x / x.sum()
print(np.round(w, 4), round(float(w @ mu), 4), round(float(np.sqrt(w @ cov @ w)), 4)) # the tip: return, volatility
def closed_form_vol(target):
s1, sm = x, np.linalg.solve(cov, mu)
a, b, c = ones @ s1, ones @ sm, mu @ sm
return float(np.sqrt((a * target ** 2 - 2 * b * target + c) / (a * c - b ** 2)))
for target in (0.05, 0.0642, 0.07):
print(target, round(closed_form_vol(target), 4))Efficient and inefficient points
The same risk appears at two returns on the curve, one on each branch, and only the higher is worth holding. So a target is efficient exactly when it is at least the minimum-variance return. The test is a single comparison, but it settles which targets belong on a chart of choices. Searching the frontier for the best trade-off then becomes a question of which point to pick, and a common answer is the one that earns the most return per unit of risk above the risk-free rate.
# The point with the highest Sharpe ratio among candidate targets
import numpy as np
mu = np.array([0.08, 0.05, 0.10])
cov = np.array([[0.0400, 0.0060, 0.0100],
[0.0060, 0.0100, 0.0020],
[0.0100, 0.0020, 0.0225]])
rf = 0.02
ones = np.ones(3)
s1, sm = np.linalg.solve(cov, ones), np.linalg.solve(cov, mu)
a, b, c = ones @ s1, ones @ sm, mu @ sm
d = a * c - b ** 2
targets = np.linspace(0.05, 0.10, 11)
vols = np.sqrt((a * targets ** 2 - 2 * b * targets + c) / d)
sharpe = (targets - rf) / vols
for t, v, s in zip(targets, vols, sharpe):
print(round(float(t), 3), round(float(v), 4), round(float(s), 4))
print("best target:", round(float(targets[np.argmax(sharpe)]), 3))# Efficient or not: compare a target with the return of the minimum-variance portfolio
import numpy as np
mu = np.array([0.08, 0.05, 0.10])
cov = np.array([[0.0400, 0.0060, 0.0100],
[0.0060, 0.0100, 0.0020],
[0.0100, 0.0020, 0.0225]])
x = np.linalg.solve(cov, np.ones(3))
tip = float((x / x.sum()) @ mu)
for target in (0.05, 0.06, 0.07, 0.08):
print(target, "efficient" if target >= tip else "inefficient")mu and cov, both estimated, so it is itself an estimate, and a flattering one: the optimiser picked, among all mixes, the ones whose estimated returns happen to be high and risks low. Real outcomes land to the right of and below the line you see. Resampling the data, or comparing the frontier on the first half of a sample with its performance on the second, shows how much of the curve was luck. Treat the frontier as a map of trade-offs, not a promise.- The efficient frontier gives, for every target return, the least volatility:
sqrt(w @ cov @ w)withw = target_return_weights(...); portfolios inside it are wasteful. - Its tip is the global minimum-variance portfolio, at return
b / a; only targets at or above that return are efficient. - Pick a point by a criterion such as the highest Sharpe ratio
(target - rf) / volatility; remember the curve is built from estimates and is optimistic.
Next up: a vectorised backtester, testing a trading rule on historical prices.
Lab · what's graded
- Trace the frontier with
frontier_vols25 pts - Find the tip of the curve with
min_variance_return25 pts - Tell the good half from the bad with
is_efficient25 pts - Choose the best point with
max_sharpe_target25 pts