SigmaField
Mathematical Foundations for Quant Finance
0 of 256 lessons
      1. 3.1.1 Vectors and their arithmeticConcept · 30 min
      2. 3.1.2 The dot product, length and angleConcept · 35 min
      3. 3.1.3 Practice set: vectorsPractice set · 20 min
      4. 3.1.4 Case study: portfolio returns and correlation as an angleCase study · 35 min

Level 3, Linear algebra 3.1 Vectors Lesson 4 of 4

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

Case study: portfolio returns and correlation as an angle

A portfolio is a vector of weights

Number the assets A, B and C as 1, 2 and 3. The weights w=(w1,w2,w3)\mathbf{w} = (w_1, w_2, w_3) are the fractions of the money in each, as decimals: here w=(0.5,0.3,0.2)\mathbf{w} = (0.5, 0.3, 0.2). Lesson 0.2.4 worked out weights from dollar holdings.

Over a period, each asset earns a simple return (case study 1.4.7). Collect the three in a vector r=(r1,r2,r3)\mathbf{r} = (r_1, r_2, r_3); here bold r\mathbf{r} holds simple returns, not log returns.

Case study 1.4.7 found that the portfolio's return is the weighted sum of these. That is the dot product of lesson 3.1.2: Rp=w⋅rR_p = \mathbf{w} \cdot \mathbf{r}.

Suppose that in one year A returns 4%, B loses 10% and C gains 25%, so r=(0.04,−0.10,0.25)\mathbf{r} = (0.04, -0.10, 0.25). Then w⋅r=0.02−0.03+0.05=0.04\mathbf{w} \cdot \mathbf{r} = 0.02 - 0.03 + 0.05 = 0.04: the portfolio returns 4%.

The expected return

The same weighted sum of the assets' expected returns gives the portfolio's expected return. Write them as a vector μ=(0.06,0.08,0.11)\boldsymbol{\mu} = (0.06, 0.08, 0.11), a bold mu.

assetweight wiw_iexpected return μi\mu_iwiμiw_i \mu_i
A0.50.060.03
B0.30.080.024
C0.20.110.022
total10.076

Rule: Portfolio return

Rp=w⋅rE[Rp]=w⋅μ\begin{gathered} R_p = \mathbf{w} \cdot \mathbf{r} \\[4pt] \mathbb{E}[R_p] = \mathbf{w} \cdot \boldsymbol{\mu} \end{gathered}

E\mathbb{E} means expected value, as in case study 1.2.4. Weights have no units, so each answer is a return over the same period as r\mathbf{r} or μ\boldsymbol{\mu}: here, a year.

With dollar holdings in place of weights, the dot product is in dollars. The holdings are h=10,000,000 w\mathbf{h} = 10{,}000{,}000\,\mathbf{w}: $5 million, $3 million and $2 million. Then h⋅μ=760,000\mathbf{h} \cdot \boldsymbol{\mu} = 760{,}000 is the expected gain, and h⋅r=400,000\mathbf{h} \cdot \mathbf{r} = 400{,}000 the gain in the year above.

Try it

w @ r and w @ mu are the two dot products above. w * mu shows the three contributions before they are added.

The last line uses the dollar holdings instead of the weights.

⌘/Ctrl + Enter runs the cell

The budget: w⋅1=1\mathbf{w} \cdot \mathbf{1} = 1

Write 1=(1,1,1)\mathbf{1} = (1, 1, 1) for the vector of ones. Its dot product with any vector adds up the components, so w⋅1=w1+w2+w3\mathbf{w} \cdot \mathbf{1} = w_1 + w_2 + w_3, the total weight. A fully invested portfolio has w⋅1=1\mathbf{w} \cdot \mathbf{1} = 1: every dollar is placed somewhere.

Rule: Fully invested

w⋅1=1\mathbf{w} \cdot \mathbf{1} = 1

The weights add up to 1. Each weight may be negative or above 1; only the total is fixed.

Case study 1.2.4 put a weight ww in its first asset and 1−w1 - w in its second. As a vector that is (w,1−w)(w, 1 - w), whose components add up to 1 for every ww.

Its long-only portfolios run from (1,0)(1, 0) to (0,1)(0, 1). At ρ=0.8\rho = 0.8 the least risky mix was (1.2353,−0.2353)(1.2353, -0.2353), beyond that range: a short sale of the second asset.

Long and short weights

A negative weight is a short sale: the fund borrows an asset, sells it, and spends the cash on the others. A "130/30" portfolio is long 130% of its capital and short 30%. For example, w=(−0.3,0.6,0.7)\mathbf{w} = (-0.3, 0.6, 0.7) is short A and long B and C.

Its weights still add up to 1, and its expected return is w⋅μ=0.107\mathbf{w} \cdot \boldsymbol{\mu} = 0.107. The sizes of the weights add up to 1.6: 160% of the capital is at work, which is also more risk.

Trades that keep the budget

Changing the weights by a vector d\mathbf{d} gives new weights w+d\mathbf{w} + \mathbf{d} (lesson 3.1.1). They still add up to 1 exactly when d⋅1=0\mathbf{d} \cdot \mathbf{1} = 0, that is, when d\mathbf{d} is orthogonal to 1\mathbf{1} (lesson 3.1.2). Whatever is sold pays for what is bought.

Moving 10% from A into C is d=(−0.1,0,0.1)\mathbf{d} = (-0.1, 0, 0.1). The new weights are (0.4,0.3,0.3)(0.4, 0.3, 0.3), and the expected return rises by d⋅μ=0.005\mathbf{d} \cdot \boldsymbol{\mu} = 0.005, to 0.081.

A zero-cost portfolio has w⋅1=0\mathbf{w} \cdot \mathbf{1} = 0, for example long $1 million of C and short $1 million of A. Nothing is invested, so a return per dollar invested would divide by 0. Desks measure it in dollars instead: in the year above, h⋅r=−40,000+250,000=210,000\mathbf{h} \cdot \mathbf{r} = -40{,}000 + 250{,}000 = 210{,}000.

Try it

np.ones(3) is the vector 1\mathbf{1}. The first line checks the budget of the original weights and of the 130/30 portfolio, then that portfolio's expected return.

The next lines make the trade d\mathbf{d}, then price the zero-cost portfolio in dollars.

⌘/Ctrl + Enter runs the cell
Check yourself
A manager changes the weights (0.5,0.3,0.2)(0.5, 0.3, 0.2) by d=(0.2,−0.1,−0.1)\mathbf{d} = (0.2, -0.1, -0.1). What happens?

Correlation as an angle

A portfolio's risk depends on how its assets move together. The usual measure is the correlation ρ\rho (rho), a number from −1-1 to 1 (case study 1.2.4). It is the cosine of an angle between two vectors.

Here are ten months of returns for two stocks, X and Y. They are invented: each month both follow a common market move plus a move of their own. The moves are drawn at random with a fixed seed and rounded to 0.1%.

Each stock's returns form a vector in R10\mathbb{R}^{10}, x=(x1,…,x10)\mathbf{x} = (x_1, \ldots, x_{10}) and y\mathbf{y}, one component per month.

The means are xˉ=1.37%\bar{x} = 1.37\% and yˉ=1.19%\bar{y} = 1.19\% (xˉ\bar{x} is read "x bar"). Centring subtracts each mean from every month:

x~=x−xˉ 1\tilde{\mathbf{x}} = \mathbf{x} - \bar{x}\,\mathbf{1}

This is a scaling and a subtraction from lesson 3.1.1, with 1\mathbf{1} now ten ones. x~\tilde{\mathbf{x}} is read "x tilde", and y~\tilde{\mathbf{y}} is made the same way. A centred vector's components add up to 0, so x~⋅1=0\tilde{\mathbf{x}} \cdot \mathbf{1} = 0.

monthxtx_t (%)yty_t (%)x~t\tilde{x}_t (%)y~t\tilde{y}_t (%)
1−0.8-0.8−1.9-1.9−2.17-2.17−3.09-3.09
25.52.34.131.11
38.73.87.332.61
42.53.91.132.71
5−6.0-6.01.7−7.37-7.370.51
6−2.2-2.21.0−3.57-3.57−0.19-0.19
7−5.1-5.1−1.3-1.3−6.47-6.47−2.49-2.49
80.4−1.7-1.7−0.97-0.97−2.89-2.89
93.20.91.83−0.29-0.29
107.53.26.132.01

The subscript tt numbers the months. In 8 of the 10, X and Y sit on the same side of their means, both above or both below. Each of those months adds a positive product x~ty~t\tilde{x}_t\tilde{y}_t to the dot product x~⋅y~\tilde{\mathbf{x}} \cdot \tilde{\mathbf{y}}; months 5 and 9 subtract a little.

Rule: Correlation is a cosine

ρ=x~⋅y~∥x~∥ ∥y~∥=cos⁡θ\rho = \frac{\tilde{\mathbf{x}} \cdot \tilde{\mathbf{y}}}{\lVert \tilde{\mathbf{x}} \rVert\,\lVert \tilde{\mathbf{y}} \rVert} = \cos\theta

θ\theta is the angle between the centred return vectors. The Cauchy–Schwarz inequality of lesson 3.1.2 is why ρ\rho always lies between −1-1 and 1.

Here x~⋅y~=0.0061107\tilde{\mathbf{x}} \cdot \tilde{\mathbf{y}} = 0.0061107, ∥x~∥=0.1509\lVert \tilde{\mathbf{x}} \rVert = 0.1509 and ∥y~∥=0.0663\lVert \tilde{\mathbf{y}} \rVert = 0.0663, in decimals. So ρ=0.6113\rho = 0.6113, and the angle is 52.32°. NumPy's np.corrcoef gives the same 0.6113.

Two vectors in R10\mathbb{R}^{10} still lie in one flat plane through the origin, so the angle between them can be drawn.

Ten months as points, and the same months as two centred vectors scaled to length 1. The ticks mark the angles for correlations 1, 0.5, 0, −0.5-0.5 and −1-1. The vectors are 52.3° apart, and cos⁡52.3°=0.61\cos 52.3° = 0.61 is the correlation.

Divide x~⋅y~\tilde{\mathbf{x}} \cdot \tilde{\mathbf{y}} by n−1=9n - 1 = 9, with nn the number of months, and you get the sample covariance, 0.000679. Divide each length by 9=3\sqrt{9} = 3 and you get the sample standard deviations, X's being 0.050284; lesson 5.5.2 defines both. The factors cancel in the cosine, and case study 3.2.5 gathers covariances into a matrix.

The level's correlations as angles

The three assets A, B and C have correlations ρAB=0.3\rho_{AB} = 0.3, ρAC=0.1\rho_{AC} = 0.1 and ρBC=0.5\rho_{BC} = 0.5; case study 3.2.5 uses them. As angles:

ρ\rhoanglewhere it appears
10°
0.560°ρBC\rho_{BC}
0.372.54°ρAB\rho_{AB}
0.278.46°case study 1.2.4
0.184.26°ρAC\rho_{AC}
090°
−0.5-0.5120°
−1-1180°

Around 90°, where ρ\rho is near 0, the cosine changes fastest: a small turn moves ρ\rho the most. Near 0° and 180° it changes slowest: from 0° to 25.84°, ρ\rho only falls from 1 to 0.9.

Try it

The first lines invent the data: rng.normal(mean, size, 10) draws ten random numbers around mean, of typical size size (Level 5 explains it). Rounding to 3 decimals keeps 0.1%.

Then the cell centres both vectors and prints them in percent, with x~⋅1\tilde{\mathbf{x}} \cdot \mathbf{1} and the count of months on the same side. Next come the dot product and lengths, the cosine and its angle, and np.corrcoef for comparison. The last line is the table of angles above, with 0.9 added.

⌘/Ctrl + Enter runs the cell

Correlations of 1, 0 and −1

The extreme correlations are the extreme angles of lesson 3.1.2.

  • ρ=1\rho = 1: 0°. A fund holding X with double exposure, less a fee of 0.5% a month, returns y=2x−0.005 1\mathbf{y} = 2\mathbf{x} - 0.005\,\mathbf{1}. Centring removes the fee, and scaling by 2 keeps the direction (lesson 3.1.1). So y~=2x~\tilde{\mathbf{y}} = 2\tilde{\mathbf{x}}, the angle is 0° and ρ=1\rho = 1.
  • ρ=−1\rho = -1: 180°. A fund that returns −x-\mathbf{x} every month has y~=−x~\tilde{\mathbf{y}} = -\tilde{\mathbf{x}}, pointing the opposite way.
  • ρ=0\rho = 0: 90°. The centred vectors are orthogonal.

A short position turns returns round, so it turns the vector round too. −y-\mathbf{y} has correlation −0.6113-0.6113 with x\mathbf{x}, an angle of 127.68°=180°−52.32°127.68° = 180° - 52.32°. Writing returns in percent scales both vectors by 100 and leaves ρ\rho at 0.6113.

Zero correlation, and why centring matters

Take four months with returns (1,2,3,4)(1, 2, 3, 4) for one asset and (4,2,2,4)(4, 2, 2, 4) for another, in percent. Centred, they are (−1.5,−0.5,0.5,1.5)(-1.5, -0.5, 0.5, 1.5) and (1,−1,−1,1)(1, -1, -1, 1), and their dot product is −1.5+0.5−0.5+1.5=0-1.5 + 0.5 - 0.5 + 1.5 = 0. So ρ=0\rho = 0.

Yet the second asset depends on the first: it is high exactly when the first is extreme. Correlation measures only how closely two series follow a straight-line relation, not every link between them.

Skip the centring, and the cosine of the raw vectors is 0.866, an angle of 30°. Both series are positive every month, so the raw arrows point roughly the same way. For X and Y the raw cosine is 0.6472 rather than 0.6113, so always subtract the means first.

Try it

The first line checks the three extremes on the sample: double exposure, the opposite fund and the short position. The second shows that percent units change nothing.

The last lines are the four months: centred, they are orthogonal, while the raw cosine says 0.866.

⌘/Ctrl + Enter runs the cell
Check yourself
Stock Y's returns are written in percent, and then replaced by those of a fund with three times Y's exposure. What happens to the correlation of X and Y, 0.6113?

Try it

The first line pairs each month of X with the previous month of Y. The second correlates X with a constant series; np.errstate hides NumPy's warning, as in case study 1.4.7.

The loop drops each month in turn and prints the ten estimates that are left.

⌘/Ctrl + Enter runs the cell
Exercise

Portfolio return and correlation

Write two functions. Both take NumPy arrays or lists; start with np.asarray(x, dtype=float).

  1. portfolio_return(w, r) returns w⋅r\mathbf{w} \cdot \mathbf{r}. Raise ValueError if the sizes differ, or if the weights do not add up to 1, allowing a rounding error of 10−910^{-9}.
  2. correlation(x, y) returns x~⋅y~∥x~∥ ∥y~∥\frac{\tilde{\mathbf{x}} \cdot \tilde{\mathbf{y}}}{\lVert \tilde{\mathbf{x}} \rVert\,\lVert \tilde{\mathbf{y}} \rVert}, without calling np.corrcoef. Raise ValueError if the sizes differ, if there are fewer than two values, or if either series is constant.

The tests compare your correlation with np.corrcoef on the ten months above. They also try the cases on this page: double exposure, a short position, percent units and the four months with ρ=0\rho = 0.

A constant series of 0.01 centres to about 10−1810^{-18}, not exactly 0. So test whether every value is the same, not whether the length is 0.

See the tests
import numpy as np


def _sample():
    """The lesson's ten invented months for stocks X and Y."""
    rng = np.random.default_rng(449)
    market = rng.normal(0.008, 0.04, 10)
    x = np.round(market + rng.normal(0.002, 0.025, 10), 3)
    y = np.round(0.6 * market + rng.normal(0.003, 0.02, 10), 3)
    return x, y


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


def test_portfolio_return():
    """Weights (0.5, 0.3, 0.2) and returns (4%, −10%, 25%) give 4%"""
    expect_close(portfolio_return(np.array([0.5, 0.3, 0.2]), np.array([0.04, -0.10, 0.25])), 0.04, what="w · r")


def test_expected_return():
    """With expected returns (6%, 8%, 11%) the portfolio expects 7.6%"""
    expect_close(portfolio_return(np.array([0.5, 0.3, 0.2]), np.array([0.06, 0.08, 0.11])), 0.076, what="w · μ")


def test_short_weights():
    """A 130/30 portfolio, (−0.3, 0.6, 0.7), expects 10.7%: negative weights are allowed"""
    expect_close(portfolio_return([-0.3, 0.6, 0.7], [0.06, 0.08, 0.11]), 0.107, what="w · μ for (−0.3, 0.6, 0.7)")


def test_weights_must_add_to_one():
    """Weights adding to 0.999, dollar holdings or percentages: ValueError"""
    mu = np.array([0.06, 0.08, 0.11])
    assert _raises_value_error(portfolio_return, np.array([0.333, 0.333, 0.333]), mu), "expected ValueError for weights adding to 0.999"
    assert _raises_value_error(portfolio_return, np.array([5e6, 3e6, 2e6]), mu), "expected ValueError for dollar holdings"
    assert _raises_value_error(portfolio_return, np.array([50, 30, 20]), mu), "expected ValueError for weights in percent"


def test_sizes_must_match():
    """Three weights and two returns: ValueError"""
    assert _raises_value_error(portfolio_return, np.array([0.5, 0.3, 0.2]), np.array([0.06, 0.08])), "expected ValueError for sizes 3 and 2"


def test_correlation_sample():
    """The ten months of X and Y have correlation about 0.6113, as np.corrcoef says"""
    x, y = _sample()
    expect_close(correlation(x, y), np.corrcoef(x, y)[0, 1], what="correlation of X and Y")


def test_centring():
    """(1, 2, 3, 4) and (4, 2, 2, 4) have correlation 0: centre before taking the cosine"""
    expect_close(correlation([1, 2, 3, 4], [4, 2, 2, 4]), 0.0, what="correlation of (1, 2, 3, 4) and (4, 2, 2, 4)", abs_tol=1e-12)


def test_scaling():
    """Percent units and double exposure leave the correlation alone; a short position flips its sign"""
    x, y = _sample()
    rho = np.corrcoef(x, y)[0, 1]
    expect_close(correlation(100 * x, 100 * y), rho, what="correlation with returns in percent")
    expect_close(correlation(x, 2 * x - 0.005), 1.0, what="correlation of X with 2X − 0.005")
    expect_close(correlation(x, -y), -rho, what="correlation of X with −Y")
    expect_close(correlation(x, -x), -1.0, what="correlation of X with −X")


def test_constant_series():
    """A constant series has no correlation: ValueError, even when centring leaves rounding dust"""
    x, _ = _sample()
    assert _raises_value_error(correlation, x, np.full(10, 0.003)), "expected ValueError for a series that is 0.003 every month"
    assert _raises_value_error(correlation, x, np.full(10, 0.01)), (
        "expected ValueError for a series that is 0.01 every month: centred, it is about 1e-18, not exactly 0"
    )


def test_bad_lengths():
    """Series of different lengths, or a single month: ValueError"""
    x, y = _sample()
    assert _raises_value_error(correlation, x, y[:-1]), "expected ValueError for 10 and 9 months"
    assert _raises_value_error(correlation, [0.01], [0.02]), "expected ValueError for a single month"

Summary card

  • A portfolio's return is a dot product: Rp=w⋅rR_p = \mathbf{w} \cdot \mathbf{r} for simple returns, and E[Rp]=w⋅μ\mathbb{E}[R_p] = \mathbf{w} \cdot \boldsymbol{\mu}. With dollar holdings h\mathbf{h}, h⋅r\mathbf{h} \cdot \mathbf{r} is the gain in dollars.
  • Fully invested means w⋅1=1\mathbf{w} \cdot \mathbf{1} = 1. Weights may be negative (short). A trade d\mathbf{d} keeps the budget when d⋅1=0\mathbf{d} \cdot \mathbf{1} = 0, and changes the expected return by d⋅μ\mathbf{d} \cdot \boldsymbol{\mu}.
  • Check the order of the assets, the units and w⋅1\mathbf{w} \cdot \mathbf{1} before trusting w⋅r\mathbf{w} \cdot \mathbf{r}.
  • Correlation is the cosine of the angle between centred return vectors: ρ=x~⋅y~∥x~∥ ∥y~∥\rho = \frac{\tilde{\mathbf{x}} \cdot \tilde{\mathbf{y}}}{\lVert \tilde{\mathbf{x}} \rVert\,\lVert \tilde{\mathbf{y}} \rVert}. Cauchy–Schwarz keeps it in [−1,1][-1, 1].
  • ρ=1\rho = 1, 0 and −1-1 are angles of 0°, 90° and 180°. Positive scaling leaves ρ\rho alone; a short position flips its sign.
  • Centre first, line up the dates, and treat a short sample's correlation as rough. A constant series has none.

Free further reading

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