4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

QuantXでポートフォリオ管理

Last updated at Posted at 2019-05-31

#QuantXでポートフォリオ管理
#はじめに
どうもgeoha6です。smart tradeでインターンやってます。

#ポートフォリオ管理とは
ポートフォリオとは

ポートフォリオ(英語:portfolio)とは、安全資産と危険資産の最適保有率のことである。マクロ経済学の分野からの延長線上として、金融経済学(financial economics)や数理ファイナンスを金融工学と同様に理論的バックグラウンドとして持ち、貨幣市場において金融機関が事業活動を通じて取り扱う様々なリスクを計測し適切なマネージメントを考える上で重要な概念である。(引用元:Wikipediea)

このポートフォリオを適切に管理するのがポートフォリオ管理です。

#QuantXで行うポートフォリオ管理

QuantXは、アルゴリズムトレードのプラットフォーム。

テクニカル指標を用いて、シグナルを出して売り買いをします。

ただ、そのトレードだけではなくリスク管理も実装できるのでは...
と思い、今回実装しました。
本来は、株式以外の資産も入れると面白いですが、それはいずれやるとして...
作っていきましょう。

##コードの構造
大きく3パートに分かれています。
image.png

###1.使うデータ・シグナルの準備

import scipy.optimize as sco
import pandas as pd
import numpy as np

def initialize(ctx):
    ctx.logger.debug("initialize() called")
    ctx.configure(
      channels={       
        "jp.stock": {
          "symbols": [
            "銘柄",
          ],
          "columns": [
            "close_price_adj",   
          ]
        }
      }
    )
    ctx.period_dict = {
      "period":[]
    }
    
    def _my_signal(data):
      cp_df = data["close_price_adj"].fillna(method="ffill")
      #日毎のリターンを入れるデータフレーム
      daily_df = pd.DataFrame(data=0, index=cp_df.index, columns=cp_df.columns)
      #調整後の重みを入れるデータフレーム 
      w_posterior_df = pd.DataFrame(data=0, index=cp_df.index[245:],columns=cp_df.columns) 
      
      #アロケーションの間隔の設定
      period  = 30 #アロケーションの間隔
      
      #period日ごとに、最適な比率を計算し、フラグを立てる。
      for i in range(0,len(cp_df.index[245:]),period):

        #アロケーションする日にフラグを立てる
        ctx.period_dict["period"].append(cp_df.index[245+i])
        
        '''
        最適化をして、w_posterior_dfを取得
        '''
        
      return {
          "posterior weight:g2": w_posterior_df,
        }
    ctx.regist_signal("my_signal", _my_signal)
    

###2.最適化計算(一例)
最適化に使える、scipyのoptimizeというライブラリを使います。
この部分を目的に応じて変えます。以下は一例(分散最小化)です。

         #初期値の設定
          if i == 0:#1日目は、初期値を等分とする
            x0 = [1. / len(cp_df.columns)] * len(cp_df.columns)
          else:#そうでないなら、一つ前の期のウェイト比を初期値にする
            x0 = record_weight
              
        #目的関数に必要な定数の準備
          #分散共分散・リターンを計算するために、日毎に利益率の変化をデータフレームに入れていく。
          for sym in daily_df.columns:
            daily_df[sym] = cp_df[sym].pct_change()*1000
          #分散共分散行列を作成(過去1年分のヒストリカルデータから)
          V = daily_df[cp_df.index[i]:cp_df.index[245+i]].cov().as_matrix()
          
        #目的関数の設定
          def minimize_vol(w):
            return np.dot(w.T, np.dot(V, w)) #σ_p^2 = w'Vw
            
        #制約条件
          cons = [{'type': 'eq', 'fun': lambda x: np.sum(x) - 1}]
          bnds = [(0, None)] * len(cp_df.columns) 
          
        #最適化実行
          opts = sco.minimize(fun=minimize_vol, x0=x0, method='SLSQP', bounds=bnds, constraints=cons)
          
        #最適解の受け渡し
          #w_posterior_dfに、最適解を代入
          w_posterior_df.loc[cp_df.index[245+i]] = opts["x"]
          #来期の初期値として今期の割合を格納
          record_weight = opts["x"]

###3.割合を保持しながら取引する
上のコードで算出された保有割合の通りに注文を行います。

def handle_signals(ctx, date, df_current):
  #アロケーションのフラグが立っていたら、取引を実行する。
  if date in ctx.period_dict["period"]:
    #最適な割合を狙って注文をする。
    for sym in df_current.index:
      sec= ctx.getSecurity(sym)
      target_weight = df_current.loc[sym,"posterior weight:g2"]
      sec.order_target_percent(target_weight/1.005,comment="")
    pass

##分散最小化ポートフォリオ
ポートフォリオ全体のリターンの分散を最小にするように銘柄の比率を計算して保有する、というものです。

リスクのひとつにボラティリティがあります。簡単にいうと、価格の変動のボラティリティ=ばらつきが分散です。ばらつきが大きいと、資産としてリスクを持っているとみなされます。

###実際の計算
以下のように定式化されます。

\begin{align}
\text{minimize} \quad &\sigma_{portfolio}^2\\
\text{subject to} \quad & \sigma_{portfolio}^2 = w^T\Sigma w\\
& \sum_{i=1}^n w_i = 1 \\
& w_i \geq 0 &i=1,\ldots,n\\
\end{align}

$w$: 銘柄ごとのポートフォリオ内に占めるウェイト (nx1ベクトル)
$\Sigma$:リターンの共分散行列 (nxn行列)

\begin{align}

w = (w_1,w_2,\ldots,w_i,\ldots,w_n)^T\\
\\
\Sigma = \left(
\begin{array}{ccccc}
\sigma_1^2 & \cdots & \sigma_{1i} & \cdots & \sigma_{1n}\\
\vdots & \ddots & & & \vdots \\
\sigma_{i1} & & \sigma_i^2 & & \sigma_{in} \\
\vdots & & & \ddots & \vdots \\
\sigma_{n1} & \cdots & \sigma_{ni} & \cdots & \sigma_n^2
\end{array}
\right)
\end{align}

###実装コード

        #初期値の設定
          if i == 0:#1日目は、初期値を等分とする
            x0 = [1. / len(cp_df.columns)] * len(cp_df.columns)
          else:#そうでないなら、一つ前の期のウェイト比を初期値にする
            x0 = record_weight
              
        #目的関数に必要な定数の準備
          #分散共分散・リターンを計算するために、日毎に利益率の変化をデータフレームに入れていく。
          for sym in daily_df.columns:
            daily_df[sym] = cp_df[sym].pct_change()*1000
          #分散共分散行列を作成(過去1年分のヒストリカルデータから)
          Sigma = daily_df[cp_df.index[i]:cp_df.index[245+i]].cov().as_matrix()
          
        #目的関数の設定
          def minimize_vol(w):
            return np.dot(w.T, np.dot(Sigma, w)) #σ_p^2 = w'Vw
            
        #制約条件
          cons = [{'type': 'eq', 'fun': lambda x: np.sum(x) - 1}]
          bnds = [(0, None)] * len(cp_df.columns) 
          
        #最適化実行
          opts = sco.minimize(fun=minimize_vol, x0=x0, method='SLSQP', bounds=bnds, constraints=cons)
          
        #最適解の受け渡し
          #w_posterior_dfに、最適解を代入
          w_posterior_df.loc[cp_df.index[245+i]] = opts["x"]
          #来期の初期値として今期の割合を格納
          record_weight = opts["x"]

全体のコードはこちら

####コードの解説
#####初期値の設定
最適化問題を解く際、初期値というものが必要になります。
難しいことは考えずに設定していきます。

if i == 0:
  x0 = [1. / len(cp_df.columns)] * len(cp_df.columns)
else:
  x0 = record_weight

取引を始める初日は、初期値は全ての銘柄を等分で持つ、という状態にします。
それ以降は、前の最適解の値を初期値にします。

#####目的関数に必要な定数の準備
######分散共分散行列の計算
最適化において、動かす変数以外は定数です。先に求めておきます。
今回は、$\Sigma$(分散共分散行列)が定数なので、それを求めます。

for sym in daily_df.columns:
  daily_df[sym] = cp_df[sym].pct_change()*1000
Sigma = daily_df[cp_df.index[i]:cp_df.index[245+i]].cov().as_matrix()

日毎のリターンを計算して、分散に直します。リターンを1000倍している理由は、あまりに数字が小さいと、最適化をするとき、計算で0とみなされてしまうからです。

#####目的関数
最適化問題において、最小にする関数のことを目的関数と言います。目的関数を設定します。

def minimize_vol(w):
  return np.dot(w.T, np.dot(Sigma, w))

$w$を動かしながら、$w^T\Sigma w$を最小にしたい、という目的関数。

#####制約条件
最適化問題において、ある条件のもとで変数を動かしてほしいです。定式化したときは $\text{subject to}$ 以下に書かれます。

cons = [{'type': 'eq', 'fun': lambda x: np.sum(x) - 1}]
bnds = [(0, None)] * len(cp_df.columns) 

今回は、$w$が銘柄の比率なので、すべての和が1、すべて0以上という制約をつけています。

#####最適化実行
最適化に使える、scipyのoptimizeというライブラリを使います。
引数に、目的関数・初期値・手法・制約を渡します。
手法はscipy.optimizeに用意されている'SLSQP'という非線形最小化問題を解く手法を用います。
書き方が直感的で嬉しいですね。

opts = sco.minimize(fun=minimize_vol, x0=x0, method='SLSQP', bounds=bnds, constraints=cons)

#####最適解の受け渡し
最後に、出てきた最適解をQuantXで取引できるようにデータフレームの形に直します。
また、次に最適化問題を解く時の初期値としてこの最適解を記録しておきます。

w_posterior_df.loc[cp_df.index[245+i]] = opts["x"]
record_weight = opts["x"]

##リスクパリティ戦略
リスク寄与度を等しく持つポートフォリオです。
要するに、資産ごとのリスクを同じ比率で持つということです。
分散最小化などと違い、理論に基づいて比率を求めているわけではありませんが、人気の戦略らしいです。
こちらも本来は、日本株式の他に、外国株やreitやを入れたほうが分散の効果が出やすいですが、まずは国内株式だけでやってみましょう。

###実際の計算
以下のように定式化されます。

\begin{align}
\text{minimize} \quad &\sum_{i=1}^n \bigr(RC_i\,-\,RC_t\bigr)^2 \\
\text{subject to} \quad & \sigma_{portfolio} = \sqrt{w^T\Sigma w}\\
& RC_i = w_i\frac{\partial\,\sigma_{portfolio}}{\partial\,w_i}&i=1\ldots,n \\
& RC_t = \frac{\sigma_{portfolio}}{N}\\
& \sum_{i=1}^n w_i = 1 \\
& w_i \geq 0 &i=1,\ldots,n\\
\end{align}

$w$: 銘柄ごとのポートフォリオ内に占めるウェイト (nx1ベクトル)
$\Sigma$:リターンの共分散行列 (nxn行列)
$N$:銘柄数
$RC_i$:銘柄$i$のリスク寄与率
$RC_t$:目標とするリスク寄与度

###実装コード

      #初期値の設定
        if i == 0:#1日目は、初期値を等分とする
          x0 = [1. / len(cp_df.columns)] * len(cp_df.columns)
        else:#そうでないなら、一つ前の期のウェイト比を初期値にする
          x0 = record_weight
        
      #目的関数に必要な定数の準備
        #リターンのデータフレームの準備
        for sym in daily_df.columns:
          daily_df[sym] = cp_df[sym].pct_change()*1000
        #分散共分散行列を作成(過去1年分のヒストリカルデータから)
        Sigma = daily_df[cp_df.index[i]:cp_df.index[245+i]].cov().as_matrix()
        #目標となるRCの比率
        rc_t = [1. / len(cp_df.columns)] * len(cp_df.columns)
        
      #目的関数に必要な計算の準備
        #ポートフォリオの分散を計算する関数
        def calculate_portfolio_var(w,Sigma):
          w = np.matrix(w)
          return (w*Sigma*w.T)

        #RCの計算
        def calculate_risk_contribution(w,Sigma):
          w = np.matrix(w)
          sigma_p = np.sqrt(calculate_portfolio_var(w,Sigma))
          RC_i = np.multiply(Sigma*w.T,w.T)/sigma_p
          return RC_i
        
      #目的関数の設定
        def risk_budget_objective(w):
          sigma_p =  np.sqrt(calculate_portfolio_var(w,Sigma))# σ_p = √σ_p^2
          risk_target = np.asmatrix(np.multiply(sigma_p,rc_t))
          RC_i = calculate_risk_contribution(w,Sigma)
          J = sum(np.square(RC_i-risk_target.T))
          return J
            
      #制約条件
        cons = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
        bnds = [(0, None)] * len(cp_df.columns) 

      #最適化実行
        opts = sco.minimize(risk_budget_objective, x0=x0, method='SLSQP', bounds=bnds, constraints=cons)
        
      #最適解の受け渡し
        #w_posterior_dfに、最適解を代入
        w_posterior_df.loc[cp_df.index[245+i]] = opts["x"]
        #来期の初期値として今期の割合を格納
        record_weight = opts["x"]

全体のコードはこちら

####コードの解説
#####初期値の設定
分散最小化と同じ

if i == 0:
  x0 = [1. / len(cp_df.columns)] * len(cp_df.columns)
else:
  x0 = record_weight

#####目的関数に必要な定数・関数の準備
######分散共分散行列の計算
分散最小化と同じ

for sym in daily_df.columns:
  daily_df[sym] = cp_df[sym].pct_change()*1000
Sigma = daily_df[cp_df.index[i]:cp_df.index[245+i]].cov().as_matrix()

######ポートフォリオの分散の計算
分散最小化と同じ

def calculate_portfolio_var(w,Sigma):
  w = np.matrix(w)
  return (w*Sigma*w.T)

######目標のリスク寄与度を計算

rc_t = [1. / len(cp_df.columns)] * len(cp_df.columns)

今回は、全体のリスク$\sigma_{portfolio}$の銘柄数分の1。

######RC(リスク寄与度)の計算
リスクパリティ戦略の一番の特徴である、リスク寄与度 $RC_i$
定義式は微分の形ですが、実際には

\begin{align}
RC_i = \Bigr(\frac{\Sigma w^T \circ w}{\sigma_{portfolio}}\Bigr)_i
\end{align}

と計算できます。

def calculate_risk_contribution(w,Sigma):
  w = np.matrix(w)
  sigma_p = np.sqrt(calculate_portfolio_var(w,Sigma))
  RC_i = np.multiply(Sigma*w.T,w.T)/sigma_p
  return RC_i

#####目的関数
目的関数は、$RC_i$と、$RC_t$の差の2乗を最小化したいという目的関数。変数はもちろん$w$。

def risk_budget_objective(w):
  sigma_p =  np.sqrt(calculate_portfolio_var(w,Sigma))
  risk_target = np.asmatrix(np.multiply(sigma_p,rc_t))
  RC_i = calculate_risk_contribution(w,Sigma)
  J = sum(np.square(RC_i-risk_target.T))[0,0] 
  return J

#####制約条件
分散最小化と同じ

cons = [{'type': 'eq', 'fun': lambda x: np.sum(x) - 1}]
bnds = [(0, None)] * len(cp_df.columns) 

#####最適化実行
分散最小化と同じ

opts = sco.minimize(risk_budget_objective, x0=x0, method='SLSQP', bounds=bnds, constraints=cons)

#####最適解の受け渡し
分散最小化と同じ

w_posterior_df.loc[cp_df.index[245+i]] = opts["x"]
record_weight = opts["x"]

##使い方
quantX的には毛色の違うアルゴリズムを作ってみました。最後に使い方の例を紹介したいと思います。
###そのまま使ってみる
リスクパリティ戦略の投資信託が世の中にはある。
QuantXで、信託手数料を浮かせることができるかも...
image.png

###テクニカルと合わせてみる
リーマンショック局面のバックテスト
とあるテクニカルトレードの成績
Pasted Graphic 4.png
このトレードに、リスクパリティ戦略を組み込んだ結果
Pasted Graphic 2.png

MaxDrawdwnもVolatilityも小さくなりました。
リスク(ばらつき)が大きくて困っているアルゴリズムに取り入れるのはアリかもしれませんね。

##最後に
いかがでしたでしょうか、世の中にはたくさんのポートフォリオ管理の仕方があるので、色々実装していけると思います。
それではこのあたりで失礼します。

###勉強会の宣伝
SmartTrade社では毎週水曜日18:00から勉強会を行っています。(https://python-algo.connpass.com/)

###免責注意事項
このコードや知識を使った実際の取引で生じた損益に関しては一切の責任を負いかねますので御了承下さい。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?