おめさんの備忘録

特定の金融商品/銘柄の売買の勧誘/推奨等を目的とするものではないです。市場全般の推奨や証券市場等の動向の上昇/下落を示唆するものでもないです。掲載の金融商品/資金運用等による投資によって生じたいかなる損失について、一切の責任を負いません。

ベイジアン多変量GARCH(BEKK)モデルを基に、時間変化する最適ポートフォリオをサンプリングする

これは何

「最適ポートフォリオの事後分布」を考えてみる


使うデータ

次の指数の日次リターン [%] を使用。

  • S&P500
  • 日経225
  • FTSE100

期間は2022-12-29 ~ 2023-03-09。

最もシンプルなモデル

次をDGPを仮定。

 \textbf{y}_t = (y_{1,t}, ..., y_{k,t})' \stackrel{\text{iid}}{\sim} \mathcal{N}(\textbf{y} \mid \textbf{m}, \textbf{S}). \qquad (1)(←お手製のナンバリング;普段overleafだから使い方がわからない)

↑が真なら、ロングポジションのみの重み  \textbf{w} \in W := \left\{ (w_1,...,w_k)' \in \mathbb{R}^k_{\geq 0} : \sum_{i=1}^k w_i = 1\right\} による、ポートフォリオ

  • Expected Return は  \textbf{w}'\textbf{m},
  • Variance は  \textbf{w}'\textbf{S}\textbf{w}.

Markowitz のMPTに立脚して、Maximum Sharpe-ratio なポートフォリオを求めるに際し、「ふつう」は、 (\textbf{m},\textbf{S}) のそれぞれを推定値で置き換える(i.e. plug-in)なりして、次の最適化問題を解く。

 \textbf{w}^* = \text{argmax}_{\textbf{w} \in W} ~ {\textbf{w}'\textbf{m} \over \textbf{w}'\textbf{S}\textbf{w}}. \sqrt{\textbf{w}'\textbf{S}\textbf{w}} だったっけ?)

最もシンプルなベイズモデル

ここでは、  (1) に次の仮定を加えた Generative Model を仮定して、最適ポートフォリオの事後分布を考えたい!

 \theta = (\textbf{m}', \text{vech}(\textbf{S})')' \sim p(\theta) =_d p(\textbf{m}) p(\textbf{S}). \qquad (2)
(太字の \thetaの出し方がわからない)

 p(\textbf{S}) は 半正定値行列上の事前分布。
観察されたリターン  \textbf{Y}:=(\textbf{y}_1,...,\textbf{y}_T)' で条件付けた  \theta の事後分布

 p(\theta \mid \textbf{Y}) \propto p(\textbf{Y} \mid \theta) p(\theta)

を考えることで、ポステリア上の  \textbf{w}^* による最適ポートフォリオを考えたい。

最適化問題は同様。

結果

Maximum Sharpe-ratio の意味での最適ポートフォリオの epistemic uncertainty を定量化できる。裏では重みも求まってる。


発展

 (1) をBEKKの多変量GARCHで入れ替えて、 \textbf{S} の deterministic な時間発展を記述する。Generative Model は

 \textbf{y}_t = \textbf{m} + \underbrace{\textbf{H}_t^{1/2} \textbf{z}_t}_{=\textbf{e}_t}; \quad \textbf{z}_t \stackrel{\text{iid}}{\sim} \mathcal{N}(\textbf{z} \mid \textbf{0}_k, \textbf{I}_k),

 \textbf{H}_t = \textbf{C} + \textbf{A}\textbf{e}_{t-1}\textbf{e}_{t-1}'\textbf{A}' + \textbf{B}\textbf{H}_{t-1}\textbf{B}'.

ここで、 \textbf{C} は正定値行列。適当な事前分布を設定して、事後分布を考える。

MCMCの出力  \left\{ \Theta^{(1)}, ..., \Theta^{(R)} \right\} から、各時点  t = 1,...,T における最適なロングポジション

 (\textbf{w}^*_t)^{(r)} = \text{argmax}_{\textbf{w} \in W} ~ {\textbf{w}' \textbf{m}^{(r)} \over \sqrt{\textbf{w}' \textbf{H}_t^{(r)} \textbf{w}}}; \qquad (r=1,...,R)

による最適ポートフォリオを構成すると:


もっと

 \textbf{m} は時間を通じて一定だから、基本的には横方向にしかプルプルしません。これだとさみしいから、Time-varying expected return を仮定してみる。Generative Model は

 \textbf{y}_t = \textbf{m}_t + \underbrace{\textbf{H}_t^{1/2} \textbf{z}_t}_{=\textbf{e}_t}; \quad \textbf{z}_t \stackrel{\text{iid}}{\sim} \mathcal{N}(\textbf{z} \mid \textbf{0}, \textbf{I}),

 \textbf{H}_t = \textbf{C} + \textbf{A}\textbf{e}_{t-1}\textbf{e}_{t-1}'\textbf{A}' + \textbf{B}\textbf{H}_{t-1}\textbf{B}',

 \textbf{m}_t = \textbf{m}_{t-1} + \textbf{w}_t; \quad \textbf{w}_t \stackrel{\text{iid}}{\sim} \mathcal{N}(\textbf{w} \mid \textbf{0}_k, \sigma^2_{m} \textbf{I}_k).

適当な事前分布を設定してから、同様に「時変的最適ポートフォリオをサンプリング」すると、冒頭の画像が出来上がる。最適化問題も同様に

 (\textbf{w}^*_t)^{(r)} = \text{argmax}_{\textbf{w} \in W} ~ {\textbf{w}' \textbf{m}_t^{(r)} \over \sqrt{\textbf{w}' \textbf{H}_t^{(r)} \textbf{w}}}. \qquad (r=1,...,R)

画面がやかましくなってしまうから、モンテカルロ標本からランダムに15標本選んで描画してます。

時系列的に可視化すると:

青い網掛は portfolio variance です。

これの方が、各時点におけるSharpe-ratio maximal portfolioの期待リターンとボラの時変性をモデル化できて、なおかつその不確実性も評価できるから、うれしい気がする。(しらんけど)

おわり

 \textbf{m}_t に予測に使えそうな構造を課して(例えばDeep Markov化)、ちゃんとモデル選択したら、更にうれしいかも

参考

Engle, R. F. and Kroner, K. F. (1995). "Multivariate Simultaneous Generalized Arch", Econometric Theory, 11(1), 122-150.

残る疑問

はてなぶろぐの (tex: ) で太字のギリシャ文字を書く方法がわからない

Hamiltonian Monte Carloを可視化する

Toy problem(2次元のガウシアンからサンプリング)でHMCの挙動を見てみる。

↓こういう風に、あるいは

↓こういう風にLeapfrog積分をして、

その行き先をM-Hに通す。これをR回。

なお、暗に各iterationのmomentumがガウシアンに従うと仮定してる。
Automatic differentiationやらを導入すればよりgeneralなケースで使える。

Juliaのコード + エエ感じのアニメーション

github.com

参考

Neal, R. M. (2011), “MCMC Using Hamiltonian Dynamics”. Handbook of Markov Chain Monte Carlo.

利回り曲線をベイズ推定(Diebold-Liのモデル + Stochastic Volatility)

これは何

↓こういうことをする。

具体的に、JGBのデータから

  • Time-varying Level (水準)
  • Time-varying Slope (傾き)
  • Time-varying Curve (曲率)
  • Exponential Decay (減衰)

などのイールドカーブの特徴を要約する量をベイズで推定をする。

もう少し構造を課すと予測にも使える。

Diebold-Li (2006) のモデル

満期が  \tau = (\tau_1,...,\tau_n)' の時刻  t=1,...,T におけるイールド  \textbf{y}_t(\tau) = (y_t(\tau_1), ..., y_t(\tau_n))' は、次の線形ガウス型状態空間モデルによってモデル化される。

 \textbf{y}_t(\tau) = \textbf{F}(\tau) \textbf{b}_t + \textbf{v}_t, \quad \textbf{v}_t \sim \mathcal{N}(\textbf{v}_t \!\mid \! \textbf{0}_n, \Sigma_v),

 \textbf{b}_t = \textbf{b}_{t-1} + \textbf{w}_t, \quad \textbf{w}_t \sim \mathcal{N}(\textbf{w}_t \!\mid \! \textbf{0}_3, \Sigma_w).

なお、

 \textbf{F}(\tau; \lambda) = \begin{bmatrix}1 & f(\tau_1; \lambda) & g(\tau_1; \lambda) \\ \vdots & \vdots & \vdots \\ 1 & f(\tau_n; \lambda) & g(\tau_n; \lambda)\end{bmatrix},

 f(\tau_i; \lambda) = {1 - e^{-\tau_i \lambda} \over \tau_i \lambda}, \quad g(\tau_i; \lambda) = {1 - e^{-\tau_i \lambda} \over \tau_i \lambda} - e^{-\tau_i \lambda}.

例えば、日本の場合、財務省のページから入手できるデータからは  \tau=(2,5,10,20,30,40) となる。

 \textbf{b}_t の各成分は、それぞれ
イールドカーブの全体的な水準 (level)
・傾き (slope)
・曲がり具合 (curvature)
を表現する。

拡張

拡張として、Stochastic volatilityを導入し、ガウス非線形状態空間モデルとしてモデル化する。

 \textbf{v}_t \sim N(\textbf{v}_t \!\mid \! \textbf{0}_n, \sigma^2_t \textbf{I}_n),

 \text{log}~ \sigma^2_t = \text{log}~ \sigma^2_{t-1} + u_t, \quad u_t \sim N(u_t \!\mid\! 0, \sigma^2_u).

イールドカーブが全体的に上下するダイナミクスを、説明可能なTime-varying levelという確率過程によって説明するか、説明不可能なlevelのlog-volatilityという確率過程によって説明するかの「選択」が可能となるように明示化する。

参考

Diebold, F. X. and Li, C (2006), "Forecasting the Term Structure of Government Bond Yields", Journal of Econometrics, 130(2), 337--364.

【PyMC3.11.5】pymc.GaussianRandomWalkで表現できない確率過程を記述する方法

PyMCっていろんな密度関数が実装されていて、疑似データを作るなどシミュレーションを行いたいときにとても便利です。

一方で、確率過程に関しては大まかに(Mv-)RW過程、AR(p)過程、GARCH(1,1)過程の3つしか実装されていない有様で、状態空間モデルを記述するときに不便だったりします。

例えば、次のようなデータ生成過程に基づいた疑似データの生成を行いたいたいとします。

(観測)  y_t = \alpha_t + \epsilon_t, ~~~ \epsilon_t \sim N(0,\sigma^2_{\epsilon});

(状態)  \alpha_{t} = \mu + \phi (\alpha_{t-1} - c) + u_{t}, ~~~ u_{t} \sim N(0,\sigma^2_{u});

 t = 1,\dots,T.

上の特殊例として、 c=0, \phi=1 のときは、 (\mu,\sigma^2_{u},\alpha_0) を適当に指定してからPyMCの既存の "GaussianRandomWalk" を使えば、状態方程式の記述に関しては完結します。

 (\mu,\sigma^2_{u},\alpha_0)=(1,3,0) と設定した上で、"sample_prior_predictive"関数を使って100回サンプリングした時系列を可視化してみると、

import numpy as np
import pandas as pd
import pymc3 as pm
T = 100
N_SIMS = 100

with pm.Model(coords={'time': range(T)}) as model:
  alpha = pm.GaussianRandomWalk('alpha', mu=1, sigma=3, dims=('time'), init=pm.Uniform.dist(0, 0))
  simulation = pm.sample_prior_predictive(samples=N_SIMS)

ax = pd.DataFrame(simulation['alpha'].transpose()).plot(figsize=(18,6), alpha=0.3, legend=False)


↑こうなる。ここまではよい、PyMC最高。

さて、例えば  |\phi|<1(に設定 | を推定)してーよなど、必ずしも GaussianRandomWalk とか GARCH(1,1) process で記述できないけれども、漸化式を記述したい場合もあるかと思います。
そのようなときには、Theanoのscanに再帰を入力してやればいいみたいです。

 (\mu,\sigma^2_{u},\alpha_0)=(5,0.3,0)、特に  (\phi,c)=(0.9, \mu) と設定した上で100回サンプリングした時系列を可視化してみます。前回は (\phi,c)=(1, 0) でした。

μ = 5
α_0 = np.float64(0)

φ = 0.9
c = μ
fn_t = lambda u_t, α_t: μ + φ * (α_t - c) + u_t
with pm.Model(coords={'time': range(T)}) as model:
  u = pm.Normal('u', mu=0, sigma=0.3, shape=T)
  α, _ = theano.scan(fn=fn_t, sequences=[u], outputs_info=[α_0], n_steps=T)
  α = pm.Deterministic('α', pm.math.concatenate([pm.Uniform('α_0', lower=α_0, upper=α_0, shape=1), α]))
  
  simulation = pm.sample_prior_predictive(samples=N_SIMS)

ax = pd.DataFrame(simulation['alpha'].transpose()).plot(figsize=(18,6), alpha=0.3, legend=False)

関数 fn_t として漸化式を、e として状態方程式の白色雑音の確率変数列を定義して、theano.scanに突っ込んどります。
結果、 \alpha_{1:T} のシミュレーションが得られました。 \alpha_{0:T} を可視化しています。

おわり

S&P500の6銘柄を対象にMonte Carlo SimulationでMarkowitz Efficient Frontier

端的に

S&P500構成銘柄のうち、時価総額で上位6位の銘柄の2019年の株価を使用して、Monte Carlo SimulationによるMarkowitz Efficient Frontier集合を描いてみたい。それから、Sharpe Ratioを基準にOptimal Portfolioを探してみたい。

やってみよう

ライブラリなど
import numpy as np
import pandas as pd

import matplotlib.pyplot as plt
import seaborn as sns

#!pip install yfinance --upgrade --no-cache-dir
import yfinance as yf

sns.set(rc={'figure.figsize':(12,8)})
ASSETS = ['AAPL', 'FB', 'GOOG', 'GOOGL', 'AMZN', 'MSFT']
ASSETS.sort()

START = '2019-01-01'
UNTIL = '2019-12-31'

SEED = 619
np.random.seed(SEED)
df = yf.download(ASSETS, start=START, end=UNTIL)
共分散行列、期待収益率、重み
n_assets = len(ASSETS)
n_portfolios = n_assets ** 8

returns = df['Adj Close'].pct_change().dropna()
avg = returns.mean() * len(returns)
cov = returns.cov() * len(returns)

weights = np.random.random(size=(n_portfolios, n_assets))
weights /= np.sum(weights, axis=1).reshape(-1,1)
Metricsを用意

 \omegaを重み行列とすると、Portfolioの

  • 期待収益率については重み付けをするだけ。
  • ボラティリティは、 \SigmaをHistoricalな共分散行列とすると、 \omega^{T}\Sigma\omega
portfolio_returns = np.dot(weights, avg)

def portfolioMetrics(_weights, _cov_matrix, _i):
    return np.sqrt(np.dot(_weights[_i].T, np.dot(_cov_matrix, _weights[_i])))

portfolio_volatility = [portfolioMetrics(weights, cov, i) for i in range(len(weights))]
portfolio_volatility = np.array(portfolio_volatility)

df = pd.DataFrame({'returns': portfolio_returns, 'volatility': portfolio_volatility})
df['sharpe_ratio'] = df.returns / df.volatility
Efficient Frontierの関数
def returnFrontier(_df, _portfolio_volatility, _n_points):
    frontier_returns = np.linspace(_df.returns.min(), _df.returns.max(), _n_points)
    frontier_volatility = []
    idx = []
    for i in range(_n_points):
        _mask = np.isclose(portfolio_returns, frontier_returns[i], atol=0.01)
        if _mask.sum() != 0:
            frontier_volatility.append(np.min(_portfolio_volatility[np.where(_mask)[0]]))
            idx.append(i)
    frontier_returns = frontier_returns[idx]
    return frontier_returns, frontier_volatility
プロットするだけの関数
def _plot():
    _max = df.loc[df.sharpe_ratio.idxmax()]
    _min = df.loc[df.volatility.idxmin()]

    ax = sns.scatterplot(x=df.volatility, y=df.returns, hue=df.sharpe_ratio)
    ax.scatter(x=_max.volatility, y=_max.returns, s=300, color='yellow', marker='*')
    ax.scatter(x=_min.volatility, y=_min.returns, s=100, color='yellow', marker='h')
    ax.plot(frontier_volatility, frontier_returns, color='yellow')
    ax.set(title=f'Efficient Frontier of: {ASSETS}', xlabel='Volatility', ylabel='Expected Return')

    for i in range(n_assets):
        ax.scatter(x=np.sqrt(cov.values.diagonal()[i]), y=avg[i], s=100, color='black', marker='o')

結果

Sharpe Ratioが最大(272.37%)となるPortfolioの重みが求まった。

  • Return:54.18%
  • Volatility:19.89%
frontier_returns, frontier_volatility = returnFrontier(df, portfolio_volatility, 500)
_plot()

f:id:geonhee619:20211110201341p:plain

感想/展望

  • 思ったより綺麗にでてきた。

"2020年"の日経225をGeometric Brownian MotionでMonte Carlo Simulationする

一言で

2019年のデータを用いて2020年の日経平均株価のシミュレーションをしたい。

概要

確率過程 Sが次のStochastic Differential Equation (SDE)を満たすとする。

 dS(t) = \mu S(t) dt + \sigma S(t)dW(t).

 W(t): Brownian Motion,  \mu : Drift coef.,  \sigma : Diffusion coef..

このSDEには次のsolutionがある。

 S(t) = S\left(0\right)e^{\left(\mu-\frac{\sigma^2}{2}\right)t+\sigma W(t)}.

シミュレーションの際には、資産価格 SGBMに従うと仮定した上で、次の離散化した形を用いる。

 S\left(t_{i+1}\right) = S\left(t_{i}\right)e^{\left(\mu-\frac{\sigma^2}{2}\right)\left(t_{i+1}-t_i \right)+\sigma Z\sqrt{t_{i+1}-t_i}}.
ただし、 Z_i \sim N\left(0,1\right);i=0,1,...,T-1.

やってみよう

ライブラリなど
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
ASSET = '^N225'
START = '2019-01-17'
UNTIL = '2020-12-30'
df = yf.download(ASSET, start=START, end=UNTIL)
returns = np.log(df.Close / df.Close.shift(1)).dropna()
準備
TRAIN_START = '2019-01-17'
TRAIN_UNTIL = '2019-12-30'
TEST_START = '2020-01-06'
TEST_UNTIL = '2020-12-30'

train = returns[TRAIN_START:TRAIN_UNTIL]
test = returns[TEST_START:TEST_UNTIL]

INDEX =[_iter.date() for _iter in df['Adj Close'][TRAIN_UNTIL:TEST_UNTIL].index]

N = len(test)
T = len(test)
S_0 = df['Adj Close'][TRAIN_UNTIL]
mu = train.mean()
sigma = train.std()
GBMの関数を用意

Brownian Increment  dWの( シミュレーション回数 \times N)の行列を用意する。行列は各行が一つのPathに該当するので、Brownian Path  W を累積和で計算。

def geometricBrownianMotionSimulation(_n_simulations):

    dt = T / N
    dW = np.random.normal(scale=np.sqrt(dt),
                          size=(_n_simulations, T + 1))
    def _expCumSum(x):
        return np.exp(np.cumsum((mu - 0.5 * sigma ** 2) * dt + sigma * x, axis=1))

    S_t = S_0 * _expCumSum(dW)
    S_t[:, 0] = S_0
    S_t = np.transpose(S_t)

    return S_t
プロットする関数を用意
def _plot(_simulated_df):
    ax = _simulated_df.plot(alpha=0.1, legend=False, figsize=(20,8))
    line_1, = ax.plot(INDEX, _simulated_df.mean(axis=1), color='red')
    line_2, = ax.plot(INDEX, df['Adj Close'][TRAIN_UNTIL:TEST_UNTIL], color='blue')
    ax.set_title(f'Simulation of Nikkei 225 w/ GBM; # of Simulations : {n_simulations}')
    ax.legend((line_1, line_2), ('"Counterfactual"', 'Factual'))

結果

n_simulations = 10
simulated_df = pd.DataFrame(geometricBrownianMotionSimulation(n_simulations),
                            index=INDEX)
_plot(simulated_df)


n_simulations = 100
simulated_df = pd.DataFrame(geometricBrownianMotionSimulation(n_simulations),
                            index=INDEX)
_plot(simulated_df)


n_simulations = 1000
simulated_df = pd.DataFrame(geometricBrownianMotionSimulation(n_simulations),
                            index=INDEX)
_plot(simulated_df)


n_simulations = 10000
simulated_df = pd.DataFrame(geometricBrownianMotionSimulation(n_simulations),
                            index=INDEX)
_plot(simulated_df)


感想/展望

  • 3月以降が面白い
  • 髪の毛みたい。