こんにちは。システムトレーダーの卵ことKenKenです。前回は「確率的トレンド」や「ランダムウォーク」についての理論を簡単にまとめました。本日は、日経平均株価のデータについてランダムウォークかについての検証結果をまとめます。理論面については前回の記事をご参照ください。今回も「Python3ではじめるシステムトレード ──環境構築と売買戦略」を参考にしております。
statsmodelsによるADF検定
これまでの記事同様に、今回も内閣府が公表している景気の分類を以下のように分けてADF検定を行う。尚、バブル経済期については、バブルのピークまでとそれ以降に分けた場合も検証した。
循環期 | 景気 | 期間-始点 | 終点 |
1, 2 | 戦後復興期(recover) | 1949/5/16 | 1954/11/30 |
3, 4, 5, 6 | 高度経済成長気(growth) | 1954/12/1 | 1971/12/31 |
7, 8, 8, 10 | 安定期(stable) | 1972/1/1 | 1986/11/30 |
11 | バブル経済期(bubble) | 1986/12/1 | 1993/10/31 |
12, 13, 14, 15,16 | 経済変革期(reform) | 1993/11/1 | 2019/11/30 |
独自設定 | 新型コロナウィルス期(covid-19) | 2019/12/1 | 現在(2020/11/25) |
statsmodelでは以下について検定が行える。今回は、1~3を対象とする。
- \(\Delta W_t = \gamma W_{t-1} + \sum^{t}_{i=1}\sigma_i W_{t-1} + w_t\)
- \(\Delta W_t = \alpha + \gamma W_{t-1} + \sum^{t}_{i=1}\sigma_i W_{t-1} + w_t\)
- \(\Delta W_t = \alpha + \beta \cdot t + \gamma W_{t-1} + \sum^{t}_{i=1}\sigma_i W_{t-1} + w_t\)
- \(\Delta W_t = \alpha + \beta \cdot t + \eta \cdot t^2+ \gamma W_{t-1} + \sum^{t}_{i=1}\sigma_i W_{t-1} + w_t\)
1. ドリフト無しモデル
ドリフト無しのモデル(1)についてADF検定を実施し、その後、各期間における回帰係数\(\gamma\)がマイナスであるか確かめる。回帰係数がプラスであると、\(\kappa\)が1より大きくなりモデルは発散してしまう。これらについてまとめた結果は以下の通り。
import numpy as np import pandas_datareader as pdr import matplotlib.pyplot as plt %matplotlib inline import statsmodels.api as sm import pandas as pd # 日経平均株価のデータを取得 N225 = pdr.DataReader('NIKKEI225', 'fred', '1949/5/16').dropna() ln_N225 = np.log(N225) # 対数価格に変換 # 期間の設定 starts = ['1949/5/16', '1949/5/16', '1954/12/1', '1972/1/1', '1986/12/1', '1986/12/1', '1990/1/1','1993/11/1', '2019/12/1'] ends = ['2020/11/25', '1954/11/30', '1971/12/31', '1986/11/30', '1993/10/31', '1989/12/31', '1992/8/31', '2019/11/30', '2020/11/25'] # 各期間においてADF検定を実施し、p値を格納。 p_values = [] for start, end in zip(starts, ends): p_value = sm.tsa.adfuller(ln_N225[start:end], regression='nc')[1] p_values.append(p_value) gamma_list = [] # 各期間ドリフト無しランダムウォークの回帰係数を算出し、格納。 for start, end in zip(starts, ends): y = ln_N225[start:end].diff().dropna() x = ln_N225[start:end].shift(1).dropna() model = sm.OLS(y, x) results = model.fit() gamma = results.params[0] gamma_list.append(gamma) # dfにまとめる names = ['全期間', '戦後復興期(recover)', '高度経済成長期(growth)', '安定期(stable)', 'バブル経済期(bubble)', 'バブルのピークまで', 'バブルのピークから谷', '経済変革期(reform)', '新型コロナウイルス期(covid-19)'] pd.options.display.precision = 4 # 小数点以下4桁に設定 df = pd.DataFrame([starts, ends, p_values, gamma_list], index=['期間始点', '終点', 'ADF p-値', 'γ'], columns=names).T # 判定結果を加える df['判定'] = ['X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X']

p値についてみてみると、「バブルのピークから谷」以外では、帰無仮説を棄却できない。したがって、単純ランダムウォークという結果になった。しかし、最小二乗法でγを推定すると、「バブルのピークから谷」以外ではすべてプラスであることから、単純ランダムウォークである可能性である期間は見つからなかった。
2.ドリフト付きモデル
上記と同様の手順でドリフト付きモデルでADF検定を実施した。sm.tsa.adfuller()の引数regression=’c’にするだけでドリフト付きモデルによるADF検定となる。また、今回の最小二乗法では、切片を追加している点が先ほどとは異なる。
# ドリフト付きモデル # 各期間においてADF検定を実施し、p値を格納。 p_values = [] for start, end in zip(starts, ends): p_value = sm.tsa.adfuller(ln_N225[start:end], regression='c')[1] # c:ドリフト付き p_values.append(p_value) const_list = [] gamma_list = [] # 各期間ドリフト付きモデルの回帰係数を算出し、格納。 for start, end in zip(starts, ends): y = ln_N225[start:end].diff().dropna() x = ln_N225[start:end].shift(1).dropna() x = sm.add_constant(x) # 切片(ドリフト)を追加 model = sm.OLS(y, x) results = model.fit() const = results.params[0] gamma = results.params[1] # 回帰係数を保存 const_list.append(const) gamma_list.append(gamma) df = pd.DataFrame([starts, ends, p_values, gamma_list, const_list], index=['期間始点', '終点', 'ADF p-値', 'γ', '切片'], columns=names).T # 判定結果を加える df['判定'] = ['△', 'OK', 'OK', 'OK', 'OK', 'OK', 'OK', 'OK', 'OK']

「全期間」を除いて、ドリフト付きモデルでランダムウォークの可能性がある。「全期間」のp値は13.5%と他の期間に比べ低かった為、判定結果を”△”としている。
ドリフト+時間付きモデル
最後にドリフト付き+時間付きモデルでADF検定を実施した。sm.tsa.adfuller()の引数regression=’ct’にするだけでよい。また、今回の最小二乗法では、時間項を追加している点が先ほどとは異なる。
# ドリフト付きモデル # 各期間においてADF検定を実施し、p値を格納。 p_values = [] for start, end in zip(starts, ends): p_value = sm.tsa.adfuller(ln_N225[start:end], regression='ct')[1] # ct:ドリフト+時間付き p_values.append(p_value) const_list = [] gamma_list = [] beta_list = [] # 各期間においてドリフト+時間付きモデルの回帰係数を算出し、格納。 for start, end in zip(starts, ends): y = ln_N225[start:end].diff().dropna() x = ln_N225[start:end].shift(1).dropna() x = sm.add_constant(x) # 切片(ドリフト)を追加 x['time'] = range(len(y)) # 時間項を追加 model = sm.OLS(y, x) results = model.fit() const = results.params[0] gamma = results.params[1] beta = results.params[2] # 回帰係数を保存 const_list.append(const) gamma_list.append(gamma) beta_list.append(beta) df = pd.DataFrame([starts, ends, p_values, gamma_list, const_list, beta_list], index=['期間始点', '終点', 'ADF p-値', 'γ', '切片', '時間'], columns=names).T # 判定結果を加える df['判定'] = ['OK', 'OK', 'OK', 'OK', 'OK', 'OK', 'OK', 'OK', 'OK']

結果として、すべての期間でドリフト+時間付きモデルでランダムウォークである可能性があることが示された。
まとめ
今回は、日経平均株価についてランダムウォークについて検証をしてみた。概ね、ランダムウォークである可能性が示された。ランダムウォークだと、株価の予測は困難である。ランダムウォークの世界で勝負しても確率50%の運ゲーになってしまうから、ランダムウォーク性が成り立たないようなフィールドを見つけなけれならないのかもしれない。例えば、今回の検証では、景気ごとに期間を分けているがさらに期間を短くしてみるとか、時間軸を変更するとか、対象を個別銘柄にしてみるとか。。。効率的市場仮設が成立しないようなフィールドの探索が必要になってくるのかもしれない。前回から今回にかけて、確率過程やらランダムウォークについてまとめてきたが、十分に理解できていないので引き続き勉強は続けていきたい。
以上