ファイナンス機械学習:機械学習によるアセットアロケーション 階層的リスクパリティアプローチ HRP

 ポートフォリオの構築は、最小分散を制約付きで可能にするCLAがある。

このCLAは共分散行列が逆行列を持つことが必要であり、得意行列である時には計算ができない。また投資ユニバース中の一資産に重みが集中する弱点がある。
 階層的リスクパリティアプローチ(HRP)は、共分散行列の正定値性や逆行列を必要とせず、共分散行列に含まれる情報を使い、グラフ理論と機械学習を通じて、ポートフォリオを構築する。
 アルゴリズムには、ツリークラスタリング、準対角化、再帰的二分の三段階がある。

ツリークラスタリング

 $${T\times N}$$の観測値行列$${{\bf X}}$$の、N列のベクトルとクラスターの階層的構造へ再構成する。

$${{\bf X}}$$から相関係数行列$${{\bf \rho}=\{\rho_{i,j}\}\ i,j=1,\cdots N }$$を作る。$${\rho_{i,j}=\rho[X_i,X_j]}$$
この$${\rho_{i,j}}$$を用いて$${X_i,X_j}$$間の距離$${d_{i,j}=\displaystyle{d[X_i,X_j]=\sqrt{\frac{1}{2}(1-\rho_{ij})} }}$$と定義する。
 ここで、$${d[X,Y]> 0 }$$,$${d[X,X]=0}$$、$${d[X,Y]=d[Y,X]}$$であり、
$${d[X,Z]\le d[X,Y] + d[Y,Z]}$$
$${N\times N}$$の距離行列$${{\bf D}=\{d_{i,j}\} i,j=1\cdots N}$$とする。
$${{\bf D}}$$の任意の二つの列ベクトルのユークリッド距離$${\tilde{d}}$$の行列$${\tilde{{\bf D}} = \{ \tilde{d}_{i,j} \} \ i,j=1\cdots N, \tilde{d}_{i,j}=\tilde{d}[D_i, D_j]= \displaystyle{\sqrt{\sum^{N}_{n=1}(d_{n,i}-d_{n,j})^2} }}$$を作る。当然ながら、$${ \tilde{d}_{i,j}= \tilde{d}_{j,i}}$$である。

このユークリッド距離行列$${\tilde{{\bf D}}}$$を使い、最小距離である成分ペア$${(i^\ast,j^\ast)}$$をクラスタ$${u[1]}$$とラベリングする。$${(i^\ast, j^\ast)=argmin(i,j)_{i\ne j}{\tilde{d}_{j,i}}}$$

 この$${u[1]}$$クラスターと他のクラスタ化されていない要素間のユークリッド距離を計算し、$${\tilde{d}_ij}$$を更新(リンケージ基準)し、元の$${\tilde{{\bf D}}}$$に加える。
 これからさらに最小距離にあるペアをクラスタとし、${\tilde{{\bf D}}}$$を更新する。
 最終的に$${N-1}$$個のクラスタが${\tilde{{\bf D}}}$$に追加される。

 Scipyを使用したこの手順はスニペット16.1で実装されている。

import numpy as np
import pandas as pd
import scipy.cluster.hierarchy as sch
corr, cov = pd.DataFrame(corr), pd.DataFrame(cov)
dist = ((1-corr)/2.)**.5
link = sch.linkage(dist, 'single')

ここでの出力は、クラスター構成要素のインデックス(0,1)と構成要素間の距離、とそのクラスターに含まれる数の4つの要素を持つクラスター数$${(N-1)}$$、$${(N-1)\times4}$$のリンケージ行列となる。

準対角化

 リンケージ行列$${(N-1) \times 4}$$の最大値が体格上に並ぶように共分散行列の行と列を入れ替え、準対角化を行う。これによって、同質の投資対象が近くに配置され、同質ではない投資対象は離れることになる。リンケージで各行は二つのエッジを一つに結合しているから、各クラスターをそれぞれの構成要素をソートして置き換えている
 この作業はスニペット16.2で実装されている。

def get_quasi_diag(link):
   
    link = link.astype(int)
    sortIx = pd.Series([link[-1, 0], link[-1, 1]])
    numItems = link[-1, 3]    # number of original items
    while sortIx.max() >= numItems:
        sortIx.index = range(0, sortIx.shape[0] * 2, 2)    # make space
        df0 = sortIx[sortIx >= numItems]    # find clusters
        i = df0.index
        j = df0.values - numItems
        sortIx[i] = link[j, 0]    # item 1
        df0 = pd.Series(link[j, 1], index=i+1)
        sortIx = sortIx.append(df0)    # item 2
        sortIx = sortIx.sort_index()    # re-sort
        sortIx.index = range(sortIx.shape[0])    # re-index
    lst =  sortIx.tolist()
    return lst

再起的2分割 Recursive Bisection

 対角な共分散行列では、分散の逆数に比例した資産配分が最適解となる。
 この特質を使い、分割されたサブグループ内では分散の逆数を重みとし、次に、一つ上の階層で集約された分散に反比例して、隣接するサブセットの重みを使い調整する。
 このアルゴリズムは以下の手順となる。
初期化:全資産を含むリスト$${L=\{L_0\}, L_0=\{n\}, n=1,\cdots N}$$を持つ最初のリストを作り、全要素に単位1のウェイトを割り当てる。$${\omega_n=1, n=1,\cdots, N}$$
イテレーション$${i}$$の$${L_i}$$が要素数1になるまで、リストを以下に従い2分割する。$${|L_i|>1}$$を満たす$${L_i \in L}$$を$${L_i^{(1)} \capL_i^{(2)}=L_i }$$、$${|L_i^{(1)}|=int[\frac{1}{2}|L_i|]}$$。この其々の分割の持つ分散を、$${\tilde{{\bf V}}^{(j)}_i = \tilde{\bf{\omega}}_{i}^{(j)T} {\bf V}_{i}^{(j)} \tilde{\bf{\omega}}_{i}^{(j)} }$$とする。
 $${{\bf V}_{i}^{(j)}}$$は、$${L_{i}^{j}}$$に含まれる要素同士の共分散行列成分で、$${{\bf \omega}_{i}^{(j)}=\displaystyle{\frac{1}{tr(diag[{\bf V}_{i}^{(j)}] ) \cdot diag[{\bf V}_{i}^{(j)}] } j=1,2}}$$と与えられている。
 さらに、$${0\le \alpha_i \le 1}$$の$${\alpha_i=1-\displaystyle{\frac{\tilde{V}_i^{(1)}}{\tilde{V}_i^{(1)}+\tilde{V}_i^{(2)} } }}$$を使い、分割されたサブセットの重みを、
$${{\bf \omega}_{i}^{(1)}=\alpha_i \times {\bf \omega}_{i}^{(1)} }$$、
$${{\bf \omega}_{i}^{(2)}=(1-\alpha_i) \times {\bf \omega}_{i}^{(2)}}$$
とする。
 ポートフォリオの構築に制約を付け加えたい場合、$${\alpha_i}$$の等式を変更する。
 この再帰的2分割の実装はスニペット16.3で与えられている。

def get_ivp(cov, **kargs):
    ivp = 1.0 / np.diag(cov)
    ivp /= ivp.sum()
    return ivp
def getClusterVar(cov, cItems):
    cov_ = cov.loc[cItems, cItems]    # matrix slice
    w_ = get_ivp(cov_).reshape(-1, 1)
    cVar = np.dot(np.dot(w_.T, cov_), w_)[0, 0]
    return cVar

def getRecBipart(cov, sortIx) :    
    w = pd.Series([1] * len(sortIx), index=sortIx)
    cItems = [sortIx]    # initialize all items in one cluster
    while len(cItems) > 0:
        cItems = [i[j: k] for i in cItems
                  for j, k in ((0, int(len(i) / 2)), (int(len(i) / 2), len(i))) if len(i) > 1]    # bi-section
        for i in range(0, len(cItems), 2):    # parse in pairs
            cItems0 = cItems[i]    # cluster 1
            cItems1 = cItems[i+1]    # cluster 2
            cVar0 = get_cluster_var(cov, cItems0)
            cVar1 = get_cluster_var(cov, cItems1)
            alpha = 1 - cVar0 / (cVar0 + cVar1)
            w[cItems0] *= alpha    # weight 1
            w[cItems1] *= 1 - alpha    # weight 2
    return w


この記事が気に入ったらサポートをしてみませんか?