SigmaField
Mathematical Foundations for Quant Finance
0 of 256 lessons
      1. 3.4.1 Determinants as volume scalingConcept · 35 min
      2. 3.4.2 Practice set: determinantsPractice set · 20 min
      3. 3.4.3 Case study: redundant assets in a payoff matrixCase study · 30 min

Level 3, Linear algebra 3.4 Determinants Lesson 3 of 3

Case studyLesson 3.4.3 in Level 3, Linear algebra. About 30 minutes.

Case study: redundant assets in a payoff matrix

The market so far

Case study 3.3.4 set up a market with three states of the world in one year: down, middle and up. The numbers are invented for illustration. Each asset has a payoff in each state and a price today:

assetdownmiddleupprice today
bond1110.97
stock8010012598.75
call, strike 10000256.75
put, strike 10020005

That case study found the state prices ψ=(0.25,0.45,0.27)\boldsymbol{\psi} = (0.25, 0.45, 0.27), today's prices of 1 paid in a single state. Each price is ψ⋅\boldsymbol{\psi} \cdot payoff, the dot product of lesson 3.1.2. It also copied the put with 100 bonds, short one share and long one call: put–call parity.

It wrote the payoffs of the bond, the stock and the call as the columns of its payoff matrix DD, one row per state:

D=(180011000112525)D = \begin{pmatrix} 1 & 80 & 0 \\ 1 & 100 & 0 \\ 1 & 125 & 25 \end{pmatrix}

Column 3 has two zeros, so expand along it (lesson 3.4.1): det⁡D=25×(1⋅100−80⋅1)=500\det D = 25 \times (1 \cdot 100 - 80 \cdot 1) = 500. That is not 0, so DD has an inverse. Every payoff has exactly one portfolio of these three assets that copies it: the market is complete, as case study 3.3.4 found from the rank.

Try it

The cell builds DD from its columns with np.column_stack. Its determinant is 500 and its rank 3.

The third line prices the four assets as ψ⋅\boldsymbol{\psi} \cdot payoff. The last solves Dh=(20,0,0)D\mathbf{h} = (20, 0, 0) for the holdings h\mathbf{h} that copy the put: 100 bonds, −1-1 share, 1 call.

⌘/Ctrl + Enter runs the cell

A fund that adds nothing

Now put the fund of the hook in place of the call:

F=(1809011001001125112.5)F = \begin{pmatrix} 1 & 80 & 90 \\ 1 & 100 & 100 \\ 1 & 125 & 112.5 \end{pmatrix}

Along row 1 the three terms of the cofactor expansion are −1,250-1{,}250, −1,000-1{,}000 and 2,2502{,}250, so det⁡F=0\det F = 0. The box spanned by the three payoffs is flat (lesson 3.4.1): the fund's payoff lies in the plane of the bond's and the stock's. So it is a combination of them.

To find the combination, ask for aa bonds and bb shares that pay what the fund pays in two of the states:

a+80b=90(down)a+100b=100(middle)\begin{aligned} a + 80b &= 90 \quad \text{(down)} \\ a + 100b &= 100 \quad \text{(middle)} \end{aligned}

This 2×22 \times 2 system has determinant 1⋅100−80⋅1=201 \cdot 100 - 80 \cdot 1 = 20, so it has exactly one solution: b=0.5b = 0.5 and a=50a = 50. The up state checks it: 50+0.5×125=112.550 + 0.5 \times 125 = 112.5. The fund is 50 bonds and half a share.

The price follows. By the law of one price (case study 3.3.4), the fund must cost what its parts cost: 50×0.97+0.5×98.75=97.87550 \times 0.97 + 0.5 \times 98.75 = 97.875, its quoted price. Quoted at 98.50, it could be sold and its parts bought for 97.875, leaving 0.625 today and payoffs that cancel in every state: an arbitrage.

The call is still out of reach. The bond, the stock and the fund have rank 2, and adding the call's payoff as a fourth column raises it to 3. So no portfolio of the three copies the call: Fh=(0,0,25)F\mathbf{h} = (0, 0, 25) has no solution (lesson 3.3.2), and np.linalg.solve stops with an error.

A redundant asset never completes a market, and this one does not even narrow the call's price. Its pricing equation, ψ⋅(90,100,112.5)=97.875\boldsymbol{\psi} \cdot (90, 100, 112.5) = 97.875, is 50 times the bond's plus half the stock's, so it says nothing new about ψ\boldsymbol{\psi}.

Every state price on case study 3.3.4's line of solutions prices the fund at 97.875. So the call can still cost anything from 1.75 to 11.75, the gap that case study left open.

Try it

The determinant of FF is exactly 0 here, and its rank is 2. The 2 × 2 solve finds the fund's recipe, checked in the up state.

cost is the price of the recipe, and 0.625 the arbitrage profit at a quote of 98.50. Then come the rank with the call added, and the attempt to copy it.

The last loop takes case study 3.3.4's line of state prices at t=0.07t = 0.07, 0.27 and 0.47. Each prices the fund at 97.875, while the call costs 25t25t.

⌘/Ctrl + Enter runs the cell
Check yourself
The fund is quoted at 97.50 instead of 97.875. What locks in a profit?

Nearly redundant: a fund with a sliver more

Exact redundancy is easy to spot: the determinant is 0. Near redundancy is harder.

A second fund pays 112.6 in the up state instead of 112.5, and otherwise the same. It holds 50 bonds, half a share and 0.004 calls, since 0.004×25=0.10.004 \times 25 = 0.1. Its price is ψ⋅(90,100,112.6)=97.902\boldsymbol{\psi} \cdot (90, 100, 112.6) = 97.902.

With the bond, the stock and this fund, the determinant is 2. That is not 0, so the market is complete and the call can be copied. But here is the portfolio that copies it:

positionunitsvalue today
second fund25024,475.50
bond−12,500-12{,}500−12,125.00-12{,}125.00
stock−125-125−12,343.75-12{,}343.75
total6.75

To copy a call worth 6.75, the desk holds 24,475.50 of the fund against short positions of about 12,000 in each of the others. Together, 250 funds contain exactly one call, and the bonds and shares cancel the rest.

So every error in the fund is multiplied by 250. Quote its price one cent too high, at 97.912, and the copied call costs 9.25 instead of 6.75. This is lesson 3.3.2's ill-conditioned system: the fund lies almost in the plane of the bond and the stock.

Is a determinant of 2 small? On its own it says nothing. With payoffs in cents every entry grows 100 times, and the determinant grows 1003100^3 times, to 2,000,000 (lesson 3.4.1).

Try it

GG has the second fund in place of the call. The solve gives the copying portfolio, and the next line prices it at the fair price and with the fund one cent dearer.

The last line is the determinant with every payoff in cents.

⌘/Ctrl + Enter runs the cell

Near-copies in a covariance matrix

Covariance matrices meet the same problem when two assets move almost together, such as two futures on the same index. For two assets with correlation ρ\rho, the correlation matrix is

R=(1ρρ1),det⁡R=1−ρ2R = \begin{pmatrix} 1 & \rho \\ \rho & 1 \end{pmatrix}, \qquad \det R = 1 - \rho^2
ρ\rho00.50.90.990.9991
det⁡R\det R10.750.190.01990.0019990

The columns of RR are (1,ρ)(1, \rho) and (ρ,1)(\rho, 1). As ρ\rho grows they close in on each other, and the parallelogram between them flattens (lesson 3.4.1). At ρ=1\rho = 1 the two assets are copies and RR is singular.

The unit square under RR for correlations 0.5, 0.9 and 0.999. The two columns close in on the diagonal, and the area, det⁡R=1−ρ2\det R = 1 - \rho^2, shrinks to a sliver.

Case study 3.2.5 built the covariance matrix as Σ=DRD\Sigma = DRD, with DD the diagonal matrix of volatilities (a different DD from the payoff matrix above). By the product rule of lesson 3.4.1, det⁡Σ=det⁡D⋅det⁡R⋅det⁡D\det \Sigma = \det D \cdot \det R \cdot \det D.

Rule: Determinant of a covariance matrix

det⁡Σ=(σ1σ2⋯σn)2det⁡R0≤det⁡R≤1\begin{gathered} \det \Sigma = (\sigma_1 \sigma_2 \cdots \sigma_n)^2 \det R \\[4pt] 0 \le \det R \le 1 \end{gathered}

σi\sigma_i is the volatility of asset ii and RR the correlation matrix. det⁡R\det R is 1 when the assets are uncorrelated and 0 when one is a combination of the others. It does not depend on units, so it is the number to check.

A small determinant proves nothing

The three assets of case study 3.2.5 have volatilities 0.12, 0.18 and 0.25, and correlations 0.3, 0.1 and 0.5. Their Σ\Sigma has determinant 1.9829×10−51.9829 \times 10^{-5}. That looks nearly singular, yet det⁡R=0.68\det R = 0.68: these assets are far from copies.

The small number comes from the variances: (σAσBσC)2=2.9160×10−5(\sigma_A\sigma_B\sigma_C)^2 = 2.9160 \times 10^{-5}, and times 0.68 that is 1.9829×10−51.9829 \times 10^{-5}. Change the units and det⁡Σ\det \Sigma moves by the cube of the scale, det⁡(cΣ)=c3det⁡Σ\det(c\Sigma) = c^3 \det \Sigma (lesson 3.4.1):

units of Σ\Sigmadet⁡Σ\det \Sigmadet⁡R\det R
annual, decimals1.9829×10−51.9829 \times 10^{-5}0.68
annual, percent (Σ×104\Sigma \times 10^4)1.9829×1071.9829 \times 10^{7}0.68
monthly (Σ÷12\Sigma \div 12)1.1475×10−81.1475 \times 10^{-8}0.68
daily (Σ÷252\Sigma \div 252)1.2391×10−121.2391 \times 10^{-12}0.68

The same three assets span 19 powers of ten in det⁡Σ\det \Sigma. Two futures with ρ=0.999\rho = 0.999 and volatilities 0.20 and 0.21 have det⁡Σ=3.5262×10−6\det \Sigma = 3.5262 \times 10^{-6}, in the middle of that range. Only det⁡R\det R tells them apart.

Try it

Σ\Sigma is built as DRDDRD, as in case study 3.2.5. The loop rescales it; det⁡R\det R stays 0.68.

Sigma / np.outer(sd, sd) divides entry (i,j)(i, j) by σiσj\sigma_i\sigma_j, which gives RR back. The last lines are the table of 1−ρ21 - \rho^2 and the two futures.

⌘/Ctrl + Enter runs the cell
Check yourself
A risk system flags any covariance matrix whose determinant is below 10−610^{-6} as nearly singular. The desk switches the three assets of case study 3.2.5 from annual to daily returns. What happens?

What a near-copy does to a hedge

A nearly singular matrix magnifies errors in any system solved with it (lesson 3.3.2). Hedges are such systems.

A desk holds a stock and hedges it with two futures on the same index, one expiring in March and one in June. The two futures have correlation 0.999. Case study 3.5.5 shows that the hedge leaving the least risk solves a system built from these covariances.

To keep the numbers simple, give the stock and both futures the same volatility. Then the system is Rh=cR\mathbf{h} = \mathbf{c}, where RR is the futures' correlation matrix and c\mathbf{c} holds each future's correlation with the stock. The answer h\mathbf{h} says how much of each future to sell per unit of stock.

The correlations in c\mathbf{c} are estimated from data, so their second decimal is uncertain. Here is h\mathbf{h} for three nearby estimates:

c\mathbf{c}h\mathbf{h}, futures to selltotal
(0.90,0.91)(0.90, 0.91)(−4.5473,5.4527)(-4.5473, 5.4527)0.9055
(0.90,0.90)(0.90, 0.90)(0.4502,0.4502)(0.4502, 0.4502)0.9005
(0.90,0.89)(0.90, 0.89)(5.4477,−4.5523)(5.4477, -4.5523)0.8954

A change of 0.01 in one correlation swings each position by about 5: from buying 4.5 March futures to selling 5.4. The total hardly moves. The data fixes how much of the index to sell, but not how to split it between two near-copies.

The inverse of RR has entries of about 500, close to 1÷det⁡R=500.251 \div \det R = 500.25. With a correlation of 0.9 between the futures, the same change in c\mathbf{c} moves h\mathbf{h} only from (0.4737,0.4737)(0.4737, 0.4737) to (0.4263,0.5263)(0.4263, 0.5263).

The fix is to stop asking the data a question it cannot answer. Hedge with one of the two futures, or with both as one block, and check det⁡R\det R before solving.

Try it

For each correlation between the futures, 0.999 and then 0.9, the loop solves Rh=cR\mathbf{h} = \mathbf{c} for the three estimates and prints h\mathbf{h} and its total.

The last lines print R−1R^{-1} for 0.999, and the condition number of lesson 3.3.2 for both matrices.

⌘/Ctrl + Enter runs the cell
Exercise

A scale-free check for near-copies

Write two functions.

  1. correlation_det(cov) returns det⁡R\det R for the covariance matrix cov. Build RR by dividing entry (i,j)(i, j) by σiσj\sigma_i\sigma_j, where σi2\sigma_i^2 is diagonal entry ii. Raise ValueError unless cov is square and symmetric, with every variance positive.
  2. nearly_redundant(cov, tol=0.01) returns True when det⁡R\det R is below tol.

The tests use the three assets in annual, percent and daily units, and the two futures with ρ=0.999\rho = 0.999. They also try a matrix in which one asset appears twice. A check on det⁡Σ\det \Sigma itself fails them.

See the tests
import numpy as np

SIGMA = np.array([0.12, 0.18, 0.25])
R3 = np.array([[1, 0.3, 0.1], [0.3, 1, 0.5], [0.1, 0.5, 1]])
COV3 = np.diag(SIGMA) @ R3 @ np.diag(SIGMA)  # the three assets of case study 3.2.5, annual
FUTURES = np.diag([0.20, 0.21]) @ np.array([[1, 0.999], [0.999, 1]]) @ np.diag([0.20, 0.21])


def test_three_assets():
    """The three assets of case study 3.2.5 have a correlation determinant of 0.68"""
    expect_close(correlation_det(COV3), 0.68, what="correlation_det(three assets)", rel=1e-9)


def test_units_do_not_matter():
    """The same three assets in percent (× 10⁴) and daily (÷ 252) units still give 0.68"""
    expect_close(correlation_det(COV3 * 1e4), 0.68, what="correlation_det in percent units", rel=1e-9)
    expect_close(correlation_det(COV3 / 252), 0.68, what="correlation_det in daily units", rel=1e-9)


def test_futures():
    """Two futures with correlation 0.999 give 1 − 0.999² = 0.001999"""
    expect_close(correlation_det(FUTURES), 0.001999, what="correlation_det(two futures)", rel=1e-9)


def test_flags_near_copies():
    """The two futures are nearly redundant, in annual and in percent units"""
    expect_equal(nearly_redundant(FUTURES), True, what="nearly_redundant(two futures)")
    expect_equal(nearly_redundant(FUTURES * 1e4), True, what="nearly_redundant(two futures, percent units)")


def test_does_not_flag_small_units():
    """The three assets in daily units are not flagged, although their det Σ is about 1.2e-12"""
    expect_equal(nearly_redundant(COV3 / 252), False, what="nearly_redundant(three assets, daily units)")


def test_duplicate_asset():
    """An asset listed twice gives a correlation determinant of 0, and is flagged"""
    sd = np.array([0.12, 0.18, 0.12])
    R = np.array([[1, 0.3, 1], [0.3, 1, 0.3], [1, 0.3, 1]])
    cov = np.diag(sd) @ R @ np.diag(sd)
    expect_close(correlation_det(cov), 0.0, what="correlation_det with a duplicate asset", abs_tol=1e-12)
    expect_equal(nearly_redundant(cov), True, what="nearly_redundant with a duplicate asset")


def test_tolerance():
    """With correlation 0.9, det R = 0.19: not flagged at tol 0.01, flagged at tol 0.2"""
    cov = np.array([[0.04, 0.9 * 0.2 * 0.3], [0.9 * 0.2 * 0.3, 0.09]])
    expect_equal(nearly_redundant(cov), False, what="nearly_redundant(ρ = 0.9)")
    expect_equal(nearly_redundant(cov, tol=0.2), True, what="nearly_redundant(ρ = 0.9, tol=0.2)")


def _raises(cov, what):
    try:
        correlation_det(cov)
    except ValueError:
        return
    raise AssertionError(f"expected ValueError for {what}")


def test_not_square():
    """A 2 × 3 array is not a covariance matrix: ValueError"""
    _raises(np.ones((2, 3)), "a 2 × 3 array")


def test_not_symmetric():
    """A matrix that is not symmetric is not a covariance matrix: ValueError"""
    _raises(np.array([[0.04, 0.01], [0.02, 0.09]]), "a matrix that is not symmetric")


def test_zero_variance():
    """A variance of 0 leaves no correlation: ValueError"""
    _raises(np.array([[0.04, 0.0], [0.0, 0.0]]), "a zero variance")

Summary card

  • Square payoff matrix, one column per asset: det⁡≠0\det \neq 0 means every payoff has exactly one copying portfolio, a complete market. det⁡=0\det = 0 means some asset is a combination of the others: it is redundant.
  • A redundant asset must cost what its combination costs, or there is an arbitrage. It never completes a market, and its pricing equation adds nothing: the call's price can still be anything from 1.75 to 11.75.
  • A nearly redundant asset makes the market complete only on paper. Copying with it needs huge offsetting positions that multiply every price error.
  • Two assets with correlation ρ\rho have det⁡R=1−ρ2\det R = 1 - \rho^2. det⁡Σ=(σ1⋯σn)2det⁡R\det \Sigma = (\sigma_1 \cdots \sigma_n)^2 \det R depends on units, so a small det⁡Σ\det \Sigma proves nothing; check det⁡R\det R, between 0 and 1.
  • In a hedge, near-copies give positions that swing with tiny changes in the estimates, while their total stays put.

Free further reading

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