2
4

More than 1 year has passed since last update.

Python3ではじめるシステムトレード:ネルソンーシーゲルモデルー金利予測の原点

Last updated at Posted at 2023-07-05

金利曲線をモデリングするために、1987年にCharles R. NelsonとAndrew F. SiegelはNelson-Siegelモデルを提唱しました。特に、このモデルはゼロクーポンボンドのイールドカーブを表現するのに適しています。様々な計算手法が提案されています。ここでは単純な方法で計算していますが、状態空間方程式を用いたものも多数報告されています。またストラング先生の5版のIntroduction to Linear Algebraで紹介されている主成分分析を用いたものもあります。ここでの説明は初歩的なものであり、コメントをいただけると助かります。

コードは
https://github.com/innovation1005/qiita_innovation1005
にアップしてあります。

Nelson-Siegelモデルは以下の式で表されます。

$$Y(t) = b_0 + b_1 * [(1 - \exp(-t/\tau)) / (t/\tau)] + b_2 * {[(1 - \exp(-t/\tau)) / (t/\tau)] - \exp(-t/\tau)}$$

ここで、

$Y(t)$ は満期までの時間 t のゼロクーポンボンドのイールドを表します。
$b_0$, $b_1$, $b_2$ はモデルのパラメータで、イールドカーブの水準(level)、傾き(slope)、および曲率(curvature)を表します。
$\tau$ はスケーリングパラメータで、カーブの形状を調整します。
モデルの特徴は、これら3つの要素を使用して金利曲線を捉えることです。

水準・レベル($b_0$)は、長期的なイールド(時間が無限大に近づくときのイールド)を反映します。
傾き($b_1$)は短期と長期のイールドとの間のスプレッド(金利差)を表します。
曲率($b_2$)は中期のイールドの動きを表します。
Nelson-Siegelモデルは金利モデルの基礎となっています。

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import statsmodels.api as sm
from statsmodels.tsa.arima.model import ARIMA
from statsmodels.api import OLS
from pandas.tseries.offsets import DateOffset

import pandas_datareader.data as web
start="1949/5/16"

金利のデータをFREDから入手します。

10-Year Treasury Constant Maturity Rate (DGS10) daily
5-Year Treasury Constant Maturity Rate (DGS5) daily
1-Year Treasury Constant Maturity Rate (DGS1) daily

モデルを簡単にするために、3つの満期の米国財務省債券のConstant Maturity Rate(CMR)を使います。ゼロカーブは使いません。また長期の金利なので対数を取ってみました。

ty10 = web.DataReader("DGS10", 'fred',start)#Population, Total for World
ty5 = web.DataReader("DGS5", 'fred',start)#Population, Total for World
ty1 = web.DataReader("DGS1", 'fred',start)#Population, Total for World
ty=pd.concat([ty1,ty5,ty10],axis=1).dropna()

ty.plot(figsize=(4,3))
ty.columns=['1y','5y','10y']
plt.show()
lnty=np.log(ty)
lnty.plot(figsize=(4,3))
plt.show()

image.png

水準、傾き、曲率の3パラメータの算出

Nelson-Siegelモデルの米国債のイールドカーブを3つの満期のCMRから構築します。日足のデータが手に入るので、3つのCRMを水準、傾き、曲率の3つの変数で説明するモデルを最小二乗法を用いて求めます。つまり水準、傾き、曲率の係数は日々計算します。

# Nelson-Siegel model shape functions
def ns_shape_0(t, lambda_):
    return np.ones_like(t)

def ns_shape_1(t, lambda_):
    return (1 - np.exp(-lambda_*t)) / (lambda_*t)

def ns_shape_2(t, lambda_):
    return ((1 - np.exp(-lambda_*t)) / (lambda_*t)) - np.exp(-lambda_*t)
t_values = np.array([1, 5, 10])

# Choose a value for the shape parameter lambda
lambda_ = 0.1
endog=lnty
# Perform the linear regression to estimate b0, b1, b2 for each time point
X=[]
for t in endog.index:
    # Calculate the values of the shape functions for each maturity
    shape_0_values = ns_shape_0(t_values, lambda_)
    shape_1_values = ns_shape_1(t_values, lambda_)
    shape_2_values = ns_shape_2(t_values, lambda_)

    # Create a DataFrame with the shape function values
    shape_df = pd.DataFrame({
        'shape_0': shape_0_values,
        'shape_1': shape_1_values,
        'shape_2': shape_2_values,
    }, index=['1y', '5y', '10y'])

    y = endog.loc[t, :]
    model = OLS(y, shape_df)
    results = model.fit()
    #estimated_params.loc[t,['b0', 'b1', 'b2']] = results.params.values
    X.append(results.params.values)

X=pd.DataFrame(X,index=endog.index,columns=['b0','b1','b2'])
plt.plot(X)
plt.show()

3つのパラメータを時系列データとしてプロットします。
image.png
対数を取っているので、金利の動きとは異なり、金利は1980年の前半でピークをつくりその後下落します。対数のグラフも同様ですが、その上下動の大きさに違いがあります。3つのパラメータを時系列データとして表現したとき、2000年を境にその上下動が大きくなります。

3パラメータの予測

パラメートの予測モデルを作ります。3つのパラメータの推移を時系列データとして扱い、1期前の金利の係数分と乱数がつぎの期の金利に反映される1次の自己回帰モデルを用いて、これら3つのパラメータを表現します。

def forecastARIMA(nforecasts,X=X['b0'],order=(1,0,0)):
    endog=X.copy()
    da=[]
    fv=[]
    f = []
    f250=[]

    if not isinstance(endog.index, pd.PeriodIndex):
        endog.index = endog.index.to_period('D')  # ここでは日次データを仮定しています
    # Get the number of initial training observations
    nobs = len(endog)
    n_init_training = int(nobs * 0.1)
    nobs2=n_init_training
    # Create model for initial training sample, fit parameters
    training_endog = endog.iloc[:n_init_training]
    mod = ARIMA(training_endog, order=order)
    res = mod.fit(method='yule_walker')
    # Save initial forecast
    da.append(training_endog.index[-1])
    fv=res.fittedvalues.values.tolist()
    f.append(res.forecast(steps=nforecasts).values[-1])
    f250.append(res.forecast(steps=250).values[-1])
    #print(len(fv),fv)
    print('init',n_init_training)
    print('random walk rmse', training_endog.diff().var()**0.5)
    print('ar1 rmse        ',(np.mean(res.resid**2))**0.5,len(res.resid))

    # Step through the rest of the sample
    for t in range(n_init_training, nobs):
        # Update the results by appending the next observation
        updated_endog = endog.iloc[t-nobs2:t+1]
        mod = ARIMA(updated_endog,order=order)
        res = mod.fit(method='yule_walker')
        # Save the new set of forecasts
        da.append(updated_endog.index[-1])
        fv.append(res.fittedvalues.values[-1])
        f.append(res.forecast(steps=nforecasts).values[-1])
        f250.append(res.forecast(steps=250).values[-1])

    print('repeat',nobs)
    print('random walk rmse         ', endog.diff().var()**0.5)
    fv=pd.DataFrame(fv,index=X.index,columns=["fv"])
    print('ar1 rmse  in sample      ',(np.mean((fv.values.T-endog.values.T)**2)**0.5,len(res.resid))[0])

    fc=pd.DataFrame(f,index=X.index[-len(f):],columns=['fc'])
    
    tmp=pd.concat([fc,endog],axis=1).dropna()
    tmp.columns=['fc','ny']
    # ここでは日次データを仮定しています,
    print("out of sample forecast", np.mean((fc.iloc[:-1].T.values-endog.iloc[-len(fc)+1:].T.values)**2)**0.5)

    print('random walk rmse 250         ', endog.diff(250).var()**0.5)
    fc250=pd.DataFrame(f250,index=X.index[-len(f250):],columns=["fc250"])
    print("out of sample forecast",np.mean((fc250.iloc[:-1].T.values-endog.iloc[-len(fc250)+1:].T.values)**2)**0.5)
    return fv,fc, fc250

水準の予測

b0,b0f,b0f_250=forecastARIMA(nforecasts = 1,X=X['b0'],order=(1,0,0))

image.png
ランダムウォークの基準に比べ長期の予測にかなりの改善がみられることが分かります。

傾きの予測

b1,b1f,b1f_250=forecastARIMA(nforecasts = 1,X=X['b1'],order=(1,0,0))

image.png

ランダムウォークの基準に比べ長期の予測にかなりの改善がみられることが分かります。

曲率の予測

b2,b2f,b2f_250=forecastARIMA(nforecasts = 1,X=X['b2'],order=(1,0,0))

image.png

今まで結果をグラフにしてみます。

X = pd.DataFrame({'b0': b0.values.T[0], 'b1': b1.values.T[0], 'b2': b2.values.T[0]},index=X.index[-len(b0):])
plt.plot(X)
Xf = pd.DataFrame({'b0': b0f.values.T[0], 'b1': b1f.values.T[0], 'b2': b2f.values.T[0]},index=X.index[-len(b0f):])
plt.plot(Xf)
Xf250=pd.DataFrame({'b0': b0f_250.values.T[0], 'b1': b1f_250.values.T[0], 'b2': b2f_250.values.T[0]},index=X.index[-len(b0f_250):])
plt.plot(Xf250)
plt.show()

image.png

3パラメータからのイールドカーブの予測

3つのパラメータの推定値から元の金利の1期先、250期先を予測します。

def NS2(endog,exog,fcst,fcst250):
    # Get the number of initial training observations
    da=[]
    fv=[]
    f = []
    f250=[]
    nobs = len(endog)
    n_init_training = len(endog)-len(fcst)
    nobs2=n_init_training
    # Create model for initial training sample, fit parameters
    training_endog = endog.iloc[:n_init_training]
    training_exog = exog.iloc[:n_init_training]
    mod = OLS(training_endog.values, sm.add_constant(training_exog.values))
    res = mod.fit()
    # Save initial forecast
    da.append(training_endog.index.date[0])
    fv=res.fittedvalues.tolist()
    f.append(res.predict(sm.add_constant(training_exog).values)[0])
    f250.append(res.predict(sm.add_constant(fcst250.iloc[:n_init_training]).values)[0])

    print('init',n_init_training, 'nobs',nobs)
    print('random walk rmse', training_endog.diff().var()**0.5)
    print('ar1 rmse        ',(np.mean(res.resid**2))**0.5,len(res.resid))

    for t in range(n_init_training, nobs):
        # Update the results by appending the next observation
        updated_endog = endog.iloc[t-nobs2:t+1]
        updated_exog  =  exog.iloc[t-nobs2:t+1]
        mod = OLS(updated_endog.values, sm.add_constant(updated_exog.values))
        res = mod.fit()
        # Save the new set of forecasts
        da.append(updated_endog.index.date[-1])
        fv.append(res.fittedvalues[-1])
        fX=fcst.values[t-nobs2]
        fX= np.hstack((1, fX))
        f.append(res.predict(fX)[0])
        fX250=fcst250.values[t-nobs2]
        fX250= np.hstack((1, fX250))
        f250.append(res.predict(fX250)[0])     
    fv=pd.DataFrame(fv,index=X.index)
    fc=pd.DataFrame(f,index=da)
    fc250=pd.DataFrame(f250,index=da)
    plt.plot(endog)
    plt.plot(fc,label="forecast")
    plt.plot(fc250.shift(250))
    plt.legend()
    #print(len(endog),len(f),len(f250),f)
    print('random walk rmse      ', endog.diff().var()**0.5)
    print('in sample             ',(np.mean((fv.values.T-endog.values.T)**2)**0.5,len(res.resid))[0])    
    print('out of sample         ',np.mean((fc.iloc[:-1].T.values-endog.iloc[-len(fc)+1:].T.values)**2)**0.5)
    print('random walk rmse 250  ', endog.diff(250).var()**0.5)
    print('out of sample         ',np.mean((fc250.iloc[:-1].T.values-endog.iloc[-len(fc250)+1:].T.values)**2)**0.5)

print(len(ty),len(X),len(Xf),len(Xf250))
ty0=ty['1y'].iloc[-len(X):]
NS2(endog=ty['1y'].iloc[-len(X):],exog=X,fcst=Xf,fcst250=Xf250)

image.png

ここでもランダムウォークの二乗平均平方根誤差よりも予測はいい結果を示しています。グリーンの線は250日先の予測です。

image.png

このようなモデルは状態空間表現を用いて、動的なモデルで表現することもでき、それは最近のはやりです。

Python3ではじめるシステムトレード【第2版】環境構築と売買戦略

「画像をクリックしていただくとpanrollingのホームページから書籍を購入していただけます。

2
4
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
2
4