Return and risk in one number
Minimum variance ignores returns. Mean-variance optimisation (Markowitz) uses both: an investor wants a high expected return w @ mu and a low variance w @ cov @ w. A simple way to trade them off is the utility w @ mu - 0.5 * lambda * w @ cov @ w, where lambda is the risk aversion: the higher it is, the more a unit of variance costs. Two investors with the same beliefs but different lambda choose different portfolios, and the best portfolio for each is the maximum of a smooth, concave function.
import numpy as np
mu = np.array([0.08, 0.05, 0.10]) # expected annual returns
cov = np.array([[0.0400, 0.0060, 0.0100],
[0.0060, 0.0100, 0.0020],
[0.0100, 0.0020, 0.0225]])
def utility(weights, mu, cov, risk_aversion):
return weights @ mu - 0.5 * risk_aversion * weights @ cov @ weights
equal = np.full(3, 1 / 3)
print(round(float(equal @ mu), 4), round(float(equal @ cov @ equal), 4)) # 0.0767 0.0121
print(round(float(utility(equal, mu, cov, 4.0)), 4)) # 0.0526The best position for a given risk aversion
With no budget constraint (the rest of the money earns the risk-free rate, taken here as zero in excess terms) the best position is where the gradient mu - lambda * cov @ w is zero: w = cov^-1 * mu / lambda. In code that is np.linalg.solve(cov, mu) / lambda. It scales like common sense says: twice the risk aversion, half the position. The sum of the weights is whatever it needs to be; the remainder, 1 - w.sum(), is cash.
# The best position for several risk aversions: more aversion, smaller bets
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]])
for aversion in (5.0, 10.0, 20.0):
w = np.linalg.solve(cov, mu) / aversion
print(aversion, np.round(w, 3), "invested", round(float(w.sum()), 3))The tangency portfolio
With a risk-free asset, every investor holds the same risky mix, only in different amounts. That mix is the one with the highest Sharpe ratio (w @ mu - rf) / sqrt(w @ cov @ w), the tangency portfolio: w proportional to cov^-1 * (mu - rf), rescaled to sum to 1. The cautious investor puts only part of the money in it and the rest in cash; the bold one borrows to hold more. Deciding how risky a portfolio to hold and deciding which risky assets to hold are two separate problems.
# The tangency portfolio: no other mix of the three assets has a higher Sharpe ratio
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
x = np.linalg.solve(cov, mu - rf)
best = x / x.sum()
def sharpe(w):
return (w @ mu - rf) / np.sqrt(w @ cov @ w)
rng = np.random.default_rng(1)
trials = rng.normal(size=(2000, 3))
trials = trials / trials.sum(axis=1, keepdims=True)
print(np.round(best, 4), round(float(sharpe(best)), 4))
print(round(max(float(sharpe(w)) for w in trials), 4)) # the best of 2000 random mixes comes close, never aboveHitting a target return
Another way to ask the question: among all portfolios that sum to 1 and have a given expected return, which has the least variance? With two constraints, the solution is a combination of the two solved vectors s1 = cov^-1 * 1 and sm = cov^-1 * mu, with coefficients from four numbers a, b, c, d that summarise the market. Each target return gives one portfolio; plotting the risk of all of them is the efficient frontier of the next topic.
# Minimum-variance portfolios for three target returns
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)
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
for target in (0.06, 0.08, 0.09):
w = ((c - b * target) * s1 + (a * target - b) * sm) / d
print(target, np.round(w, 3), round(float(w @ mu), 4), round(float(np.sqrt(w @ cov @ w)), 4))mu: a small change in one expected return can swing the portfolio from one asset to another. The optimiser trusts the inputs completely, so any overestimated return becomes a big position. In practice returns are shrunk, weights are bounded, and the inputs come from models or views rather than raw past averages. Mean-variance is a good tool for thinking and a dangerous one for blind use.- Utility is
w @ mu - 0.5 * lambda * w @ cov @ w; the best position without a budget issolve(cov, mu) / lambda, and doublinglambdahalves it. - With a risk-free asset the best risky mix is the tangency portfolio
solve(cov, mu - rf), rescaled to sum to 1: the highest Sharpe ratio. - For a target return, the minimum-variance weights are a combination of
solve(cov, ones)andsolve(cov, mu); the result is only as good as the estimatedmu.
Next up: the efficient frontier, the curve of the best risk for every level of return.
Lab · what's graded
- Score a portfolio with
utility25 pts - Find the best position with
utility_weights25 pts - Maximise the Sharpe ratio with
tangency_weights25 pts - Hit a return target with
target_return_weights25 pts