The factor model's implied correlation¶
What the factor model says returns share.
- Requires
- the
Loadingof each instrument on each factor, each factor's variance over the return's window, each instrument's own variance. - Provides
- the correlation matrix of the instruments' returns.
- Law
- C = L S L' + E, normalised by its diagonal: returns co-move exactly as far as they load on the same factors.
- Source
- Elton, Gruber, Brown & Goetzmann, Modern Portfolio Theory and Investment Analysis, ch. 8.
- Proof
- invariant, limit.
No Trait form: a tool, not a step law: a matrix over every pair of instruments, L S L' + E normalised, read off the factor model; it runs where a game prepares its data, never in a step, so it has no Trait form.
finance.implied_correlation(loadings, factor_variances, idiosyncratic_variances)In [1]:
Copied!
import os, sys
root = os.getcwd()
while not os.path.isfile(os.path.join(root, 'docsite.json')):
root = os.path.dirname(root)
sys.path.insert(0, os.path.join(root, 'tools'))
import numpy as np
import matplotlib.pyplot as plt
from lusine import dmath as D, library
from lusine.kernels.finance.implied_correlation import implied_correlation
import os, sys
root = os.getcwd()
while not os.path.isfile(os.path.join(root, 'docsite.json')):
root = os.path.dirname(root)
sys.path.insert(0, os.path.join(root, 'tools'))
import numpy as np
import matplotlib.pyplot as plt
from lusine import dmath as D, library
from lusine.kernels.finance.implied_correlation import implied_correlation
The reference¶
The kernel's reference, in numpy: the body under the declaration above, and what every section below runs. It stays as the kernel's test beside its Trait form.
In [2]:
Copied!
library.reference(implied_correlation)
library.reference(implied_correlation)
Out[2]:
the reference, implied_correlation.py
def implied_correlation(loadings, factor_variances, idiosyncratic_variances):
"""THE IMPLIED CORRELATION: what the factor model says returns share.
Requires: the `Loading` of each instrument on each factor, each factor's
variance over the return's window, each instrument's own variance.
Provides: the correlation matrix of the instruments' returns.
Law: C = L S L' + E, normalised by its diagonal: returns co-move exactly as
far as they load on the same factors.
Source: Elton, Gruber, Brown & Goetzmann, Modern Portfolio Theory and
Investment Analysis, ch. 8.
Proof: invariant, limit."""
L = np.asarray(loadings, dtype=np.float64)
C = L @ np.diag(np.asarray(factor_variances, dtype=np.float64)) @ L.T + np.diag(np.asarray(idiosyncratic_variances, dtype=np.float64))
s = np.sqrt(np.diag(C))
return C / np.outer(s, s)
Invariant: a correlation matrix¶
Unit diagonal, symmetric, positive semi-definite, every entry within [-1, 1], for random loadings and variances.
In [3]:
Copied!
rng = np.random.default_rng(6)
for _ in range(50):
L = rng.normal(size=(8, 5))
C = implied_correlation(L, rng.uniform(1e-6, 1e-3, 5), rng.uniform(1e-7, 1e-4, 8))
assert np.allclose(np.diag(C), 1) and np.allclose(C, C.T) and np.min(np.linalg.eigvalsh(C)) > -1e-12 and np.all(np.abs(C) <= 1 + 1e-12)
print('a correlation matrix every time')
rng = np.random.default_rng(6)
for _ in range(50):
L = rng.normal(size=(8, 5))
C = implied_correlation(L, rng.uniform(1e-6, 1e-3, 5), rng.uniform(1e-7, 1e-4, 8))
assert np.allclose(np.diag(C), 1) and np.allclose(C, C.T) and np.min(np.linalg.eigvalsh(C)) > -1e-12 and np.all(np.abs(C) <= 1 + 1e-12)
print('a correlation matrix every time')
a correlation matrix every time
Limit: what nothing is shared is uncorrelated; one factor alone is one¶
Two instruments loading on different factors are uncorrelated; two on one factor with no noise of their own move as one.
In [4]:
Copied!
C = implied_correlation(np.array([[1.0, 0.0], [0.0, 1.0]]), np.array([1e-4, 1e-4]), np.array([1e-5, 1e-5]))
assert abs(C[0, 1]) < 1e-15
C = implied_correlation(np.array([[1.0], [2.0]]), np.array([1e-4]), np.array([0.0, 0.0]))
assert abs(C[0, 1] - 1) < 1e-12
noise = np.logspace(-8, -3, 30)
rho = [implied_correlation(np.array([[1.0], [1.0]]), np.array([1e-4]), np.array([n, n]))[0, 1] for n in noise]
assert np.all(np.diff(rho) < 0)
fig, ax = plt.subplots(figsize=(7, 3))
ax.semilogx(noise, rho)
ax.set_xlabel('own variance')
ax.set_ylabel('correlation')
plt.show()
C = implied_correlation(np.array([[1.0, 0.0], [0.0, 1.0]]), np.array([1e-4, 1e-4]), np.array([1e-5, 1e-5]))
assert abs(C[0, 1]) < 1e-15
C = implied_correlation(np.array([[1.0], [2.0]]), np.array([1e-4]), np.array([0.0, 0.0]))
assert abs(C[0, 1] - 1) < 1e-12
noise = np.logspace(-8, -3, 30)
rho = [implied_correlation(np.array([[1.0], [1.0]]), np.array([1e-4]), np.array([n, n]))[0, 1] for n in noise]
assert np.all(np.diff(rho) < 0)
fig, ax = plt.subplots(figsize=(7, 3))
ax.semilogx(noise, rho)
ax.set_xlabel('own variance')
ax.set_ylabel('correlation')
plt.show()