A portfolio is a vector of weights
A portfolio is described by a weight per asset: the fraction of its value invested there. The weights sum to 1, and a negative weight is a short position that finances the others. Its return over a period is the weighted average of the asset returns, so the expected return is a dot product w @ mu. Return is the easy half: it is linear, with no interaction between assets. Risk is not.
import numpy as np
weights = np.array([0.6, 0.4]) # 60% in asset A, 40% in asset B
mu = np.array([0.08, 0.05]) # expected annual returns
print(weights.sum()) # 1.0
print(weights @ mu) # 0.068 = 0.6 * 0.08 + 0.4 * 0.05Variance is a quadratic form
For two assets the variance of w1 * A + w2 * B is w1**2 * var(A) + w2**2 * var(B) + 2 * w1 * w2 * cov(A, B): each asset's own variance, plus a cross term. With n assets there are n * n terms, w_i * w_j * cov[i, j], and the whole sum is the quadratic form w @ cov @ w, one line in NumPy. The cross terms are the point: when two assets are not perfectly correlated they partly cancel, so the portfolio is less risky than the weighted average of the risks.
# The variance two ways: the explicit formula and the matrix form
import numpy as np
weights = np.array([0.6, 0.4])
cov = np.array([[0.04, 0.006], [0.006, 0.01]]) # variances 0.04 and 0.01 (volatilities 20% and 10%), covariance 0.006
explicit = weights[0] ** 2 * cov[0, 0] + weights[1] ** 2 * cov[1, 1] + 2 * weights[0] * weights[1] * cov[0, 1]
matrix_form = weights @ cov @ weights
print(round(explicit, 6), round(matrix_form, 6)) # 0.01888 0.01888
print(round(float(np.sqrt(matrix_form)), 4)) # 0.1374 a 13.7% volatility, below 0.6*20% + 0.4*10% = 16%# Diversification: 50/50 in two assets of 20% volatility, for several correlations
import numpy as np
weights = np.array([0.5, 0.5])
for rho in (1.0, 0.5, 0.0, -0.5):
cov = 0.2 * 0.2 * np.array([[1.0, rho], [rho, 1.0]])
print(rho, round(float(np.sqrt(weights @ cov @ weights)), 4)) # 1.0: 0.2, 0.5: 0.1732, 0.0: 0.1414, -0.5: 0.1Where does the risk come from?
A manager wants to know which positions drive the risk. The risk contribution of asset i is w_i * (cov @ w)_i / sigma_p: its weight times how strongly it moves with the portfolio as a whole, scaled by the portfolio volatility sigma_p. The contributions add up exactly to sigma_p, so they split the total into parts. An asset with a small weight but a high covariance with the rest can contribute far more than its weight suggests, and a hedge can contribute negatively.
# Risk contributions of a three-asset portfolio: they add up to the volatility
import numpy as np
weights = np.array([0.5, 0.3, 0.2])
cov = np.array([[0.0400, 0.0060, 0.0100],
[0.0060, 0.0100, 0.0020],
[0.0100, 0.0020, 0.0225]])
variance = weights @ cov @ weights
contributions = weights * (cov @ weights) / np.sqrt(variance)
print(np.round(contributions, 4), round(float(contributions.sum()), 4))
print(round(float(np.sqrt(variance)), 4))mu and cov are estimates, and the variance is the more reliable of the two: expected returns are very noisy, covariances far less so. The weights, mu and cov must also share a period: a daily covariance with annual returns is a common bug; scale the variance by the number of periods, the volatility by its square root.- Portfolio return is linear:
w @ mu. Variance is the quadratic formw @ cov @ w, withn * nterms and cross terms that let correlation below 1 reduce risk. - Volatility is
sqrt(variance * periods); the annualisation must match the period ofcov. - Risk contributions
w_i * (cov @ w)_i / sigma_psplit the volatility exactly by asset; a hedge can contribute a negative amount.
Next up: the minimum-variance portfolio, choosing the weights that make this risk as small as possible.
Lab · what's graded
- Add up the expected return with
portfolio_return25 pts - Measure the risk with
portfolio_variance25 pts - Quote the volatility with
portfolio_vol25 pts - Split the risk by asset with
risk_contributions25 pts