SigmaField
Mathematical Foundations for Quant Finance
0 of 256 lessons
      1. 3.2.1 Matrices and matrix multiplicationConcept · 30 min
      2. 3.2.2 Matrices as linear mapsConcept · 30 min
      3. 3.2.3 Transpose, identity and inverseConcept · 35 min
      4. 3.2.4 Practice set: matricesPractice set · 25 min
      5. 3.2.5 Case study: the covariance matrix and portfolio varianceCase study · 35 min

Level 3, Linear algebra 3.2 Matrices Lesson 5 of 5

Case studyLesson 3.2.5 in Level 3, Linear algebra. About 35 minutes.

Case study: the covariance matrix and portfolio variance

The covariance matrix

The data are invented but typical, in annual decimals: 0.12 means 12% a year.

assetABC
expected return μi\mu_i0.060.080.11
volatility σi\sigma_i0.120.180.25
weight wiw_i0.50.30.2

The correlations are ρAB=0.3\rho_{AB} = 0.3, ρAC=0.1\rho_{AC} = 0.1 and ρBC=0.5\rho_{BC} = 0.5. As in case study 1.2.4, the volatility σi\sigma_i is how widely asset ii's yearly return typically swings. The correlation ρij\rho_{ij}, between −1-1 and 1, says how closely two assets move together.

The covariance of two assets is their correlation times both volatilities, ρijσiσj\rho_{ij}\sigma_i\sigma_j. Module 5.5 defines it from probability; here this product is all you need. An asset's covariance with itself is its variance σi2\sigma_i^2, because ρii=1\rho_{ii} = 1.

Rule: The covariance matrix

Σij=ρij σi σjΣ=DRD\begin{gathered} \Sigma_{ij} = \rho_{ij}\,\sigma_i\,\sigma_j \\[4pt] \Sigma = D R D \end{gathered}

Σ\Sigma (capital sigma) holds every variance on its diagonal and every covariance off it. RR is the correlation matrix, with entries ρij\rho_{ij} and 1 on its diagonal, and DD is the diagonal matrix of the volatilities.

Multiplying by DD on the left scales row ii of RR by σi\sigma_i; multiplying by DD on the right scales column jj by σj\sigma_j (lesson 3.2.1). So entry (i,j)(i, j) of DRDDRD is σi ρij σj\sigma_i\,\rho_{ij}\,\sigma_j. For the three assets:

Σ=(0.01440.006480.0030.006480.03240.02250.0030.02250.0625)\Sigma = \begin{pmatrix} 0.0144 & 0.00648 & 0.003 \\ 0.00648 & 0.0324 & 0.0225 \\ 0.003 & 0.0225 & 0.0625 \end{pmatrix}

The diagonal holds 0.1220.12^2, 0.1820.18^2 and 0.2520.25^2. The entry ΣAB=0.3×0.12×0.18=0.00648\Sigma_{AB} = 0.3 \times 0.12 \times 0.18 = 0.00648 appears twice, in row A and in row B.

Σ\Sigma is symmetric (lesson 3.2.3), because ρij=ρji\rho_{ij} = \rho_{ji}. The transpose rule says the same: (DRD)⊤=D⊤R⊤D⊤=DRD(DRD)^\top = D^\top R^\top D^\top = DRD, since DD and RR are symmetric. Its entries are annual variances and covariances, in squared decimals.

Try it

np.diag(sigma) builds DD. The second line checks Σ\Sigma against np.outer(sigma, sigma) * R, the products σiσj\sigma_i\sigma_j times ρij\rho_{ij} entry by entry. It also checks that Σ\Sigma is symmetric.

The last line repeats case study 3.1.4: w⋅μ=0.076\mathbf{w} \cdot \boldsymbol{\mu} = 0.076, and the weights add up to 1, w⋅1=1\mathbf{w} \cdot \mathbf{1} = 1, where 1\mathbf{1} is the vector of ones.

⌘/Ctrl + Enter runs the cell

Portfolio variance, three ways

First way: the double sum

The portfolio's return is Rp=w⋅r=wArA+wBrB+wCrCR_p = \mathbf{w} \cdot \mathbf{r} = w_A r_A + w_B r_B + w_C r_C, where rAr_A, rBr_B and rCr_C are the assets' returns over the year (case study 3.1.4). Its variance adds one term for every pair of assets, an asset paired with itself included:

σp2=∑i∑jwi wj Σij\sigma_p^2 = \sum_{i}\sum_{j} w_i\, w_j\, \Sigma_{ij}

Here ii and jj each run over A, B and C, and σp\sigma_p is the portfolio's volatility. Module 5.5 derives the formula from the rules of variance; take it as given, as case study 1.2.4 did for two assets. With three assets there are nine terms wiwjΣijw_iw_j\Sigma_{ij}:

ABC
A0.00360.0009720.0003
B0.0009720.0029160.00135
C0.00030.001350.0025

The three diagonal terms, each asset on its own, add up to 0.009016. The six off-diagonal terms, the pairs, add 0.005244. The total is σp2=0.01426\sigma_p^2 = 0.01426, a volatility of 0.01426≈0.119415\sqrt{0.01426} \approx 0.119415, or 11.94%.

Second way: the matrix product

The double sum is a row times a matrix times a column, as in lesson 3.2.3. First Σw\Sigma\mathbf{w}: each entry is a row of Σ\Sigma dotted with w\mathbf{w}, giving (0.009744,0.01746,0.02075)(0.009744, 0.01746, 0.02075). Then w⊤(Σw)=0.5×0.009744+0.3×0.01746+0.2×0.02075=0.01426\mathbf{w}^\top(\Sigma\mathbf{w}) = 0.5 \times 0.009744 + 0.3 \times 0.01746 + 0.2 \times 0.02075 = 0.01426.

Rule: Portfolio variance

σp2=w⊤Σ w=∑i∑jwiwjΣij\begin{gathered} \sigma_p^2 = \mathbf{w}^\top\Sigma\,\mathbf{w} \\[4pt] = \sum_i\sum_j w_i w_j \Sigma_{ij} \end{gathered}

The shapes are (1×n)(n×n)(n×1)(1 \times n)(n \times n)(n \times 1), so the answer is a single number. It is in the units of Σ\Sigma: an annual variance. Its square root is the volatility.

The correlations decide where between two extremes the answer lands.

If every correlation were 0, only the diagonal would count: a variance of 0.009016 and a volatility of 9.50%. If every correlation were 1, the volatility would be the weighted average w⋅σ=0.164\mathbf{w} \cdot \boldsymbol{\sigma} = 0.164 from the hook. The actual 11.94% sits between: that gap is diversification.

Try it

The double loop adds the nine terms one at a time; np.outer(w, w) * Sigma prints them as the table above.

Then Σw\Sigma\mathbf{w}, w⊤Σw\mathbf{w}^\top\Sigma\mathbf{w} and its square root. The last two lines are the extremes: the diagonal alone, and the weighted average of the volatilities.

⌘/Ctrl + Enter runs the cell

Two assets: case study 1.2.4 again

Case study 1.2.4 had two assets with σA=0.2\sigma_A = 0.2, σB=0.3\sigma_B = 0.3 and ρ=0.2\rho = 0.2, and weights ww in A and 1−w1 - w in B. Its covariance matrix is 2×22 \times 2:

Σ=(0.040.0120.0120.09)w=(w1−w)\begin{gathered} \Sigma = \begin{pmatrix} 0.04 & 0.012 \\ 0.012 & 0.09 \end{pmatrix} \\[6pt] \mathbf{w} = \begin{pmatrix} w \\ 1 - w \end{pmatrix} \end{gathered}

Multiplying out w⊤Σw\mathbf{w}^\top\Sigma\mathbf{w} gives back case study 1.2.4's formula. The two off-diagonal entries, both ρσAσB\rho\sigma_A\sigma_B, are where its factor 2 comes from:

w⊤Σw=w2σA2+(1−w)2σB2+2w(1−w)ρ σAσB\begin{aligned} \mathbf{w}^\top\Sigma\mathbf{w} = {} & w^2\sigma_A^2 + (1 - w)^2\sigma_B^2 \\ & + 2w(1 - w)\rho\,\sigma_A\sigma_B \end{aligned}

The numbers match case study 1.2.4's table:

ww00.250.50.751
w⊤Σw\mathbf{w}^\top\Sigma\mathbf{w}0.090.05760.03850.03260.04
volatility0.30.24010.19620.18060.2

At its minimum-variance weight w∗≈0.7358w^* \approx 0.7358, the product gives a variance of 0.0326 and a volatility of 0.1806, as before. The parabola of case study 1.2.4 is the two-asset case of w⊤Σw\mathbf{w}^\top\Sigma\mathbf{w}.

Try it

The loop computes w⊤Σw\mathbf{w}^\top\Sigma\mathbf{w} for five weights, then at case study 1.2.4's w∗w^*.

⌘/Ctrl + Enter runs the cell
Check yourself
Two assets both have volatility 0.2, and their correlation is −1-1. What is the variance of the 50/50 portfolio?

Third way: from a sample of returns

On a desk, Σ\Sigma is not handed to you: it is estimated from past returns. Here is an invented sample of 120 monthly returns, ten years' worth, drawn with a fixed seed from the model above. Its monthly means are μ/12\boldsymbol{\mu}/12 and its monthly covariance matrix is Σ/12\Sigma/12.

Module 3.7 shows how such samples are made. They are illustrative, not market data.

Put the returns in a 120×3120 \times 3 matrix XX: row tt holds the returns of A, B and C in month tt. Then XwX\mathbf{w} dots each row with w\mathbf{w}: the 120 monthly returns of the portfolio. The first three are 0.0095, −0.0369-0.0369 and 0.0311.

The sample covariance of two columns is the dot product of their centred vectors divided by n−1n - 1, for nn months, as in case study 3.1.4. The sample covariance matrix SS collects these for every pair of columns: it does for the sample what Σ\Sigma does for the model. np.cov computes it.

The sample variance of the portfolio's returns then equals w⊤Sw\mathbf{w}^\top S\mathbf{w}, exactly, for any sample: both are 0.000973 a month.

Times 12, that is 0.011679 a year, a volatility of 10.81%: not 11.94%. The sample is only an estimate of the model, and ten years are not enough to pin Σ\Sigma down. Compare the annualised SS with Σ\Sigma:

12S≈(0.012380.005780.004140.005780.028580.01460.004140.01460.04241)12S \approx \begin{pmatrix} 0.01238 & 0.00578 & 0.00414 \\ 0.00578 & 0.02858 & 0.0146 \\ 0.00414 & 0.0146 & 0.04241 \end{pmatrix}

The sample volatilities are 11.13%, 16.91% and 20.59%, against 12%, 18% and 25%. np.corrcoef gives correlations 0.308, 0.181 and 0.419, against 0.3, 0.1 and 0.5; each is the cosine of an angle between centred return vectors (case study 3.1.4).

Try it

rowvar=False tells np.cov that the assets are the columns; by default it treats each row as one variable. ddof=1 makes np.var divide by n−1n - 1, as np.cov does; module 5.8 explains why.

Multiplying a monthly variance by 12 gives an annual one, if months are independent of each other (module 5.5 treats independence).

⌘/Ctrl + Enter runs the cell

Risk contributions

Which asset carries the risk? The last step of w⊤(Σw)\mathbf{w}^\top(\Sigma\mathbf{w}) is a dot product, so the variance splits into one term per asset.

Rule: Risk contributions

contributioni=wi (Σw)i∑iwi (Σw)i=w⊤Σw\begin{gathered} \text{contribution}_i = w_i\,(\Sigma\mathbf{w})_i \\[4pt] \textstyle\sum_i w_i\,(\Sigma\mathbf{w})_i = \mathbf{w}^\top\Sigma\mathbf{w} \end{gathered}

(Σw)i(\Sigma\mathbf{w})_i, row ii of Σ\Sigma dotted with w\mathbf{w}, is the covariance of asset ii with the whole portfolio. Times the weight, it is that asset's share of the portfolio's variance.

Here Σw=(0.009744,0.01746,0.02075)\Sigma\mathbf{w} = (0.009744, 0.01746, 0.02075), so the contributions are 0.004872, 0.005238 and 0.00415, which add up to 0.01426. As shares of the variance: 34.17%, 36.73% and 29.10%.

How the money is split, and how the variance is split. A holds half the money but about a third of the risk; C holds a fifth of the money but 29.1% of the risk.

Divided by σp\sigma_p, the contributions add up to the volatility instead: 4.08, 4.39 and 3.48 percentage points of the 11.94%.

A contribution can be negative. Sell C short, with w=(0.7,0.5,−0.2)\mathbf{w} = (0.7, 0.5, -0.2), which still adds up to 1. The variance is then 0.016852, a volatility of 12.98%.

C still moves with the rest of the portfolio, (Σw)C=0.00085(\Sigma\mathbf{w})_C = 0.00085, but its weight is negative. So its contribution is −0.00017-0.00017: the short position offsets part of the risk of A and B.

One more pattern links back to case study 1.2.4. At its minimum-variance weight, both entries of Σw\Sigma\mathbf{w} equal 0.0326, the variance itself. Module 4.5 shows that every minimum-variance portfolio has this property.

Try it

w * (Sigma @ w) multiplies entry by entry, which here is what we want. The next two lines give the shares in percent, and the contributions in volatility points.

Then the short position in C, and Σw\Sigma\mathbf{w} at case study 1.2.4's w∗=0.1560.212w^* = \frac{0.156}{0.212}.

⌘/Ctrl + Enter runs the cell
Check yourself
An asset's risk contribution wi(Σw)iw_i(\Sigma\mathbf{w})_i is negative. What must be true?

On the desk

Try it

cov builds DRDDRD. The lines follow the notes in order: percent units, a mixed entry, monthly volatility, the typo, and the impossible correlations.

⌘/Ctrl + Enter runs the cell

Try it

1,000 samples of 120 months, then 1,000 of 480 months, all drawn from the same model with a fixed seed. Each line prints the 5th, 50th and 95th percentiles of the estimated volatility.

For the ten-year samples, the next line gives the 5th and 95th percentiles of the estimated ρBC\rho_{BC}, 0.376 and 0.606 around 0.5, then of A's annual mean.

⌘/Ctrl + Enter runs the cell

So treat Σ\Sigma as an estimate with error bars, and a portfolio built on it as no more precise than its inputs. Module 3.5 uses covariances to find the minimum-variance hedge, and module 3.6 finds the main directions of risk in the covariance matrix of yield changes.

Exercise

Covariance matrix, variance and risk contributions

Write three functions, with NumPy.

  1. covariance_matrix(vols, corr) returns Σ=DRD\Sigma = DRD. Raise ValueError unless corr is square with one row per asset, symmetric, has 1 on its diagonal and every entry between −1-1 and 1, and no volatility is negative.
  2. portfolio_variance(w, cov) returns w⊤Σw\mathbf{w}^\top\Sigma\mathbf{w} as a number. Raise ValueError if cov is not symmetric or its size does not match w.
  3. risk_contributions(w, cov) returns the array of wi(Σw)iw_i(\Sigma\mathbf{w})_i.

The tests use the three assets of this case study, case study 1.2.4's two assets, a short position, and a sample covariance matrix from np.cov. They also try each bad input from the desk notes.

See the tests
import numpy as np

SIGMA = [0.12, 0.18, 0.25]
R = [[1.0, 0.3, 0.1], [0.3, 1.0, 0.5], [0.1, 0.5, 1.0]]
W = [0.5, 0.3, 0.2]
EXPECTED = [[0.0144, 0.00648, 0.003], [0.00648, 0.0324, 0.0225], [0.003, 0.0225, 0.0625]]


def _raises_value_error(fn, *args):
    try:
        fn(*args)
    except ValueError:
        return True
    return False


def test_covariance_matrix():
    """The three assets: Σ has variances 0.0144, 0.0324, 0.0625 and covariances 0.00648, 0.003, 0.0225"""
    expect_close(covariance_matrix(SIGMA, R), EXPECTED, what="covariance_matrix(sigma, R)")


def test_two_assets():
    """Case study 1.2.4's two assets give [[0.04, 0.012], [0.012, 0.09]]"""
    expect_close(covariance_matrix([0.2, 0.3], [[1, 0.2], [0.2, 1]]), [[0.04, 0.012], [0.012, 0.09]], what="two-asset covariance matrix")


def test_portfolio_variance():
    """The portfolio (0.5, 0.3, 0.2) has variance 0.01426"""
    expect_close(portfolio_variance(W, EXPECTED), 0.01426, what="portfolio_variance(w, Sigma)")


def test_case_study_124():
    """With σ_A = 0.2, σ_B = 0.3, ρ = 0.2: variance 0.032625 at w = 0.75, and case study 1.2.4's minimum at w*"""
    cov = [[0.04, 0.012], [0.012, 0.09]]
    expect_close(portfolio_variance([0.75, 0.25], cov), 0.032625, what="variance at w = 0.75")
    w_star = 0.156 / 0.212
    expect_close(portfolio_variance([w_star, 1 - w_star], cov), 0.09 - 0.156**2 / (4 * 0.106), what="variance at w*")


def test_contributions():
    """The risk contributions are 0.004872, 0.005238 and 0.00415, and add up to the variance"""
    rc = np.asarray(risk_contributions(W, EXPECTED), dtype=float)
    expect_close(rc, [0.004872, 0.005238, 0.00415], what="risk_contributions(w, Sigma)")
    expect_close(rc.sum(), 0.01426, what="sum of the risk contributions")


def test_short_position():
    """Short 20% of C: C's risk contribution is negative, about −0.00017"""
    rc = np.asarray(risk_contributions([0.7, 0.5, -0.2], EXPECTED), dtype=float)
    expect_close(rc, [0.008904, 0.008118, -0.00017], what="risk contributions with C sold short")


def test_sample():
    """For a sample of returns, the variance of the portfolio's returns equals wᵀSw with S from np.cov"""
    rng = np.random.default_rng(5)
    X = rng.normal(0.0, 0.05, size=(60, 3))
    w = np.array(W)
    expect_close(portfolio_variance(w, np.cov(X, rowvar=False)), np.var(X @ w, ddof=1), what="wᵀSw against the sample variance", rel=1e-9)


def test_correlation_above_one():
    """A correlation of 1.2 raises ValueError"""
    bad = [[1.0, 1.2, 0.1], [1.2, 1.0, 0.5], [0.1, 0.5, 1.0]]
    assert _raises_value_error(covariance_matrix, SIGMA, bad), "a correlation of 1.2 should raise ValueError"


def test_correlation_not_symmetric():
    """A correlation matrix with ρ_AB = 0.3 in one place and 0.03 in the other raises ValueError"""
    bad = [[1.0, 0.3, 0.1], [0.03, 1.0, 0.5], [0.1, 0.5, 1.0]]
    assert _raises_value_error(covariance_matrix, SIGMA, bad), "a correlation matrix that is not symmetric should raise ValueError"


def test_diagonal_not_one():
    """A correlation matrix with 0.9 on its diagonal raises ValueError"""
    bad = [[0.9, 0.3, 0.1], [0.3, 1.0, 0.5], [0.1, 0.5, 1.0]]
    assert _raises_value_error(covariance_matrix, SIGMA, bad), "a diagonal entry other than 1 should raise ValueError"


def test_sizes_differ():
    """Three volatilities with a 2 × 2 correlation matrix raise ValueError"""
    assert _raises_value_error(covariance_matrix, SIGMA, [[1, 0.3], [0.3, 1]]), "three volatilities need a 3 × 3 correlation matrix"


def test_negative_volatility():
    """A negative volatility raises ValueError"""
    assert _raises_value_error(covariance_matrix, [0.12, -0.18, 0.25], R), "a volatility of -0.18 should raise ValueError"


def test_covariance_not_symmetric():
    """portfolio_variance refuses a covariance matrix with a typo in one triangle"""
    typo = [row[:] for row in EXPECTED]
    typo[0][1] = 0.0648
    assert _raises_value_error(portfolio_variance, W, typo), "a covariance matrix that is not symmetric should raise ValueError"


def test_weights_wrong_length():
    """Two weights with a 3 × 3 covariance matrix raise ValueError"""
    assert _raises_value_error(portfolio_variance, [0.5, 0.5], EXPECTED), "two weights cannot go with a 3 × 3 matrix"

Summary card

  • The covariance matrix has Σij=ρijσiσj\Sigma_{ij} = \rho_{ij}\sigma_i\sigma_j: variances on the diagonal, covariances off it. Σ=DRD\Sigma = DRD, with DD the diagonal matrix of volatilities and RR the correlation matrix. It is symmetric.
  • Portfolio variance is σp2=w⊤Σw=∑i∑jwiwjΣij\sigma_p^2 = \mathbf{w}^\top\Sigma\mathbf{w} = \sum_i\sum_j w_iw_j\Sigma_{ij}; for the three assets, 0.01426, a volatility of 11.94%. With two assets it is case study 1.2.4's parabola.
  • For a sample of returns XX, the sample variance of XwX\mathbf{w} equals w⊤Sw\mathbf{w}^\top S\mathbf{w}, with SS the sample covariance matrix (np.cov(X, rowvar=False)).
  • Risk contributions wi(Σw)iw_i(\Sigma\mathbf{w})_i add up to w⊤Σw\mathbf{w}^\top\Sigma\mathbf{w}. They can differ a lot from the weights, and a hedge or short position can contribute a negative amount.
  • Check units (annual decimals), symmetry and valid correlations; estimates of Σ\Sigma from ten years of data are noisy.

Free further reading

Optional. This lesson covers what you need; these add more examples and practice. All are free to read.