1つの資産から多数の資産へ
ここまでは1本のリターン系列を見てきました。ポートフォリオは多くの資産を持ち、そのリスクは各資産のリスクだけでなく、それらが*一緒に*どう動くかにも左右されます。2つの資産の共分散は、平均からの偏差の積の平均で、一緒に上がり下がりしやすければ正、一方が上がるとき他方が下がるなら負です。n 個の資産には n * n 個の共分散があり、共分散行列として並べます。行列は対称で、対角には分散が入ります。
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))) # True標本共分散行列
形状が (days, assets) のリターン表なら、行列全体は線形代数の1行で書けます。各列の平均を引き(axis=0、日にわたる平均)、中心化した表の転置に自分自身を掛けます。centered.T @ centered の (i, j) 成分は、日にわたる x_i * x_j の合計です。標本分散と同じく days - 1 で割ります。これが np.cov(returns, rowvar=False) や DataFrame.cov() の計算で、np.cov は資産が行に並ぶことを想定しているため rowvar=False が必要です。
相関:単位のない共分散
共分散が 0.0004 と言われても、2つの資産のボラティリティ次第なので意味がよく分かりません。2つの標準偏差の積で割るとスケールが消え、常に-1から1の間の相関が得られます:corr[i, j] = cov[i, j] / (std[i] * std[j])、std は対角の平方根です。コードでは np.outer(std, std) で積の表を作り、1回の割り算で行列全体を直すと、対角は1になります。相関は2つの資産がどれだけ連動するかを示し、リスクの式に必要なのはスケールを保った共分散です。
# 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)この行列は本物たりうるか
適当な数表は共分散行列ではありません。対称で、かつ半正定値でなければなりません。任意のウェイト w について w' C w はポートフォリオの分散なので、負にはなりません。同じことですが、負の固有値がないということです。推定された行列は合格しますが、手で書き換えたものや、出所の違う相関を寄せ集めたもの(例:A~B = 0.9、B~C = 0.9、A~C = -0.9)は不合格です。対称行列用で実数の固有値を返す np.linalg.eigvalsh で確かめ、丸め誤差のため小さな許容値を設けます。
# 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(days, assets)の表の共分散行列はcentered.T @ centered / (days - 1)。対称で、対角は分散- 相関は
cov / np.outer(std, std)。単位がなく-1から1の間、対角は1。分散はperiods倍、ボラティリティはその平方根倍で年率化する - 有効な共分散行列は対称で負の固有値がない(
np.linalg.eigvalsh)。日数が少なすぎる推定や手作業の修正はこれを壊しうる
次はポートフォリオのリターンと分散です。ウェイトとこの行列からポートフォリオのリスクを求めます。
ラボ:採点される内容
cov_matrixで共分散の表を作る25点corr_from_covで相関に直す25点annualised_volsで各資産のボラティリティを読む25点is_valid_covで本物の共分散行列になりうるか確かめる25点