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:
| asset | down | middle | up | price today |
|---|---|---|---|---|
| bond | 1 | 1 | 1 | 0.97 |
| stock | 80 | 100 | 125 | 98.75 |
| call, strike 100 | 0 | 0 | 25 | 6.75 |
| put, strike 100 | 20 | 0 | 0 | 5 |
That case study found the state prices , today's prices of 1 paid in a single state. Each price is 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 , one row per state:
Column 3 has two zeros, so expand along it (lesson 3.4.1): . That is not 0, so 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 from its columns with np.column_stack. Its determinant is 500 and its rank 3.
The third line prices the four assets as payoff. The last solves for the holdings that copy the put: 100 bonds, share, 1 call.
A fund that adds nothing
Now put the fund of the hook in place of the call:
Along row 1 the three terms of the cofactor expansion are , and , so . 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 bonds and shares that pay what the fund pays in two of the states:
This system has determinant , so it has exactly one solution: and . The up state checks it: . 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: , 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: 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, , is 50 times the bond's plus half the stock's, so it says nothing new about .
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 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 , 0.27 and 0.47. Each prices the fund at 97.875, while the call costs .
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 . Its price is .
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:
| position | units | value today |
|---|---|---|
| second fund | 250 | 24,475.50 |
| bond | ||
| stock | ||
| total | 6.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 times, to 2,000,000 (lesson 3.4.1).
Try it
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.
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 , the correlation matrix is
| 0 | 0.5 | 0.9 | 0.99 | 0.999 | 1 | |
|---|---|---|---|---|---|---|
| 1 | 0.75 | 0.19 | 0.0199 | 0.001999 | 0 |
The columns of are and . As grows they close in on each other, and the parallelogram between them flattens (lesson 3.4.1). At the two assets are copies and is singular.
The unit square under for correlations 0.5, 0.9 and 0.999. The two columns close in on the diagonal, and the area, , shrinks to a sliver.
Case study 3.2.5 built the covariance matrix as , with the diagonal matrix of volatilities (a different from the payoff matrix above). By the product rule of lesson 3.4.1, .
Rule: Determinant of a covariance matrix
is the volatility of asset and the correlation matrix. 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 has determinant . That looks nearly singular, yet : these assets are far from copies.
The small number comes from the variances: , and times 0.68 that is . Change the units and moves by the cube of the scale, (lesson 3.4.1):
| units of | ||
|---|---|---|
| annual, decimals | 0.68 | |
| annual, percent () | 0.68 | |
| monthly () | 0.68 | |
| daily () | 0.68 |
The same three assets span 19 powers of ten in . Two futures with and volatilities 0.20 and 0.21 have , in the middle of that range. Only tells them apart.
Try it
is built as , as in case study 3.2.5. The loop rescales it; stays 0.68.
Sigma / np.outer(sd, sd) divides entry by , which gives back. The last lines are the table of and the two futures.
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 , where is the futures' correlation matrix and holds each future's correlation with the stock. The answer says how much of each future to sell per unit of stock.
The correlations in are estimated from data, so their second decimal is uncertain. Here is for three nearby estimates:
| , futures to sell | total | |
|---|---|---|
| 0.9055 | ||
| 0.9005 | ||
| 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 has entries of about 500, close to . With a correlation of 0.9 between the futures, the same change in moves only from to .
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 before solving.
Try it
For each correlation between the futures, 0.999 and then 0.9, the loop solves for the three estimates and prints and its total.
The last lines print for 0.999, and the condition number of lesson 3.3.2 for both matrices.
A scale-free check for near-copies
Write two functions.
correlation_det(cov)returns for the covariance matrixcov. Build by dividing entry by , where is diagonal entry . RaiseValueErrorunlesscovis square and symmetric, with every variance positive.nearly_redundant(cov, tol=0.01)returnsTruewhen is belowtol.
The tests use the three assets in annual, percent and daily units, and the two futures with . They also try a matrix in which one asset appears twice. A check on 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: means every payoff has exactly one copying portfolio, a complete market. 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 have . depends on units, so a small proves nothing; check , between 0 and 1.
- In a hedge, near-copies give positions that swing with tiny changes in the estimates, while their total stays put.