D'un actif à plusieurs
Tout ce qui précède portait sur une seule série de rendements. Un portefeuille contient de nombreux actifs, et son risque dépend non seulement du risque de chacun, mais de la façon dont ils évoluent *ensemble*. La covariance de deux actifs est le produit moyen de leurs écarts à la moyenne : positive s'ils montent et baissent ensemble, négative si l'un monte quand l'autre baisse. Pour n actifs, il y a n * n covariances, rangées dans une matrice de covariance, symétrique, avec les variances sur la diagonale.
import numpy as np
# Five days of returns for two assets: shape (days, assets)
returns = np.array([[0.01, 0.02], [-0.02, -0.01], [0.03, 0.01], [0.00, -0.02], [0.01, 0.03]])
centered = returns - returns.mean(axis=0) # subtract each column's mean
cov = centered.T @ centered / (len(returns) - 1) # (assets x days) @ (days x assets) -> (assets x assets)
print(np.round(cov, 6))
print(np.allclose(cov, np.cov(returns, rowvar=False))) # TrueLa matrice de covariance empirique
Avec un tableau de rendements de forme (days, assets), toute la matrice tient en une ligne d'algèbre linéaire. On soustrait la moyenne de chaque colonne (axis=0, moyenne sur les jours), et on multiplie la transposée du tableau centré par lui-même : l'entrée (i, j) de centered.T @ centered est la somme sur les jours de x_i * x_j. On divise par days - 1, comme pour une variance d'échantillon. C'est ce que calculent np.cov(returns, rowvar=False) et DataFrame.cov() ; notez le rowvar=False, car np.cov attend les actifs en lignes.
La corrélation : la covariance sans unités
Une covariance de 0.0004 dit peu : elle dépend de la volatilité des deux actifs. Diviser par le produit des deux écarts-types enlève l'échelle et donne la corrélation, toujours entre -1 et 1 : corr[i, j] = cov[i, j] / (std[i] * std[j]), avec std la racine de la diagonale. En code, np.outer(std, std) construit le tableau des produits et une division remet toute la matrice à l'échelle, avec des 1 sur la diagonale. La corrélation dit à quel point deux actifs évoluent ensemble ; la covariance, qui garde l'échelle, est ce dont les formules de risque ont besoin.
# From covariance to correlation and annualised volatilities
import numpy as np
returns = np.array([[0.01, 0.02], [-0.02, -0.01], [0.03, 0.01], [0.00, -0.02], [0.01, 0.03]])
cov = np.cov(returns, rowvar=False)
std = np.sqrt(np.diag(cov))
corr = cov / np.outer(std, std)
print(np.round(corr, 4))
print(np.round(np.sqrt(np.diag(cov) * 252), 4)) # daily variances scaled to a year: volatility grows with sqrt(252)Cette matrice peut-elle être réelle ?
Un tableau de nombres quelconque n'est pas une matrice de covariance. Elle doit être symétrique et semi-définie positive : pour tout vecteur de poids w, le nombre w' C w est la variance d'un portefeuille, donc il ne peut pas être négatif. De façon équivalente, aucune valeur propre n'est négative. Les matrices estimées passent, mais celles modifiées à la main, ou un assemblage de corrélations de sources différentes (par exemple A~B = 0.9, B~C = 0.9, A~C = -0.9), échouent. On vérifie avec np.linalg.eigvalsh, pour les matrices symétriques, qui renvoie des valeurs propres réelles, avec une petite tolérance pour les arrondis.
# A validity check: symmetry and no negative eigenvalue
import numpy as np
def is_valid_cov(matrix, tol=1e-10):
symmetric = bool(np.all(np.abs(matrix - matrix.T) <= tol))
return symmetric and bool(np.linalg.eigvalsh(matrix).min() >= -tol)
good = np.array([[1.0, 0.5, 0.2], [0.5, 1.0, 0.3], [0.2, 0.3, 1.0]])
bad = np.array([[1.0, 0.9, -0.9], [0.9, 1.0, 0.9], [-0.9, 0.9, 1.0]])
print(is_valid_cov(good), np.round(np.linalg.eigvalsh(good), 3))
print(is_valid_cov(bad), np.round(np.linalg.eigvalsh(bad), 3))# Few days, many assets: the sample covariance matrix is singular
import numpy as np
rng = np.random.default_rng(0)
returns = rng.normal(0, 0.01, size=(5, 8)) # 5 days, 8 assets
cov = np.cov(returns, rowvar=False)
print(np.linalg.matrix_rank(cov), "of", cov.shape[0]) # 4 of 8: some portfolios seem to have zero variance- La matrice de covariance d'un tableau
(days, assets)estcentered.T @ centered / (days - 1): symétrique, variances sur la diagonale. - La corrélation est
cov / np.outer(std, std): sans unité, entre -1 et 1, des 1 sur la diagonale ; on annualise les variances parperiods, les volatilités par sa racine. - Une matrice de covariance valide est symétrique sans valeur propre négative (
np.linalg.eigvalsh) ; des estimations sur trop peu de jours, ou retouchées à la main, peuvent l'enfreindre.
À suivre : le rendement et la variance d'un portefeuille, transformer les poids et cette matrice en risque de portefeuille.
Lab : ce qui est noté
- Construire le tableau des covariances avec
cov_matrix25 pts - Le ramener en corrélations avec
corr_from_cov25 pts - Lire la volatilité de chaque actif avec
annualised_vols25 pts - Tester si elle peut être réelle avec
is_valid_cov25 pts