In [156]:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import math
import scipy.stats as st

import seaborn as sns
sns.set(font=['IPAmincho'])

The Vasicek model


In [5]:
#Simulate interest rate oath by the Vasicek model
def vasicek(r0, K, theta, sigma, T=1., N=10, seed=777):
    np.random.seed(seed)
    dt = T/float(N)
    rates = [r0]
    
    for i in range(N):
        dr = K*(theta-rates[-1]*dt) + sigma*np.random.normal()
        rates.append(rates[-1] + dr)
    
    return range(N+1), rates

In [6]:
#Simulate interest rate oath by the Vasicek model
def CIR(r0, K, theta, sigma, T=1., N=10, seed=777):
    np.random.seed(seed)
    dt = T/float(N)
    rates = [r0]
    
    for i in range(N):
        dr = K*(theta-rates[-1]*dt) + sigma*math.sqrt(rates[-1])*np.random.normal()
        rates.append(rates[-1] + dr)
    
    return range(N+1), rates

In [7]:
x, y = vasicek(0.01875, 0.20, 0.0001, 0.012, 10., 200)
plt.figure(figsize=(10,5))
plt.plot(x, y)


Out[7]:
[<matplotlib.lines.Line2D at 0x116aa0320>]

In [8]:
x, y = CIR(0.01875, 0., 0.0001, 0.012, 10., 200)
plt.figure(figsize=(10,5))
plt.plot(x, y)


Out[8]:
[<matplotlib.lines.Line2D at 0x1170fd1d0>]

In [9]:
x, y = vasicek(0.01875, 0.20, 0.01, 0.012, 10., 200)
plt.figure(figsize=(10,5))
plt.plot(x, y)


Out[9]:
[<matplotlib.lines.Line2D at 0x11721feb8>]

In [10]:
x, y = vasicek(0.01875, 0.20, 0.01, 0.012, 10., 200, seed=666)
plt.figure(figsize=(10,5))
plt.plot(x, y)


Out[10]:
[<matplotlib.lines.Line2D at 0x117343240>]

In [11]:
x, y = vasicek(0.01875, 0.20, 0.01, 0.012, 10., 200, seed=888)
plt.figure(figsize=(10,5))
plt.plot(x, y)


Out[11]:
[<matplotlib.lines.Line2D at 0x1173a2630>]

Pricing a zero-coupon bond by the Vasicek model


In [12]:
# Get zero coupon bond price by Vasicek model
def exact_zcb(theta, kappa, sigma, tau, r0=0.):
    B = (1 - np.exp(-kappa*tau))/kappa
    A = np.exp((theta - (sigma**2)/(2*(kappa**2)))*(B - tau) - (sigma**2)/(4*kappa)*(B**2))
    return A*np.exp(-r0*B)

In [13]:
Ts = np.r_[0.0:25.5:0.5]
zcbs = [exact_zcb(0.5, 0.02, 0.03, t, 0.015) for t in Ts]

plt.figure(figsize=(10,5))
plt.title("Zero Coupon Bond (ZCB) Value by Time")
plt.plot(Ts, zcbs, label='ZCB')
plt.ylabel("Value ($)")
plt.xlabel("Time in years")
plt.legend()
plt.grid(True)
plt.show()


Value of early-exercise


In [14]:
def exercise_value(K, R, t):
    return K*math.exp(-R*t)

In [15]:
Ts = np.r_[0.0:25.5:0.5]
Ks = [exercise_value(0.95, 0.015, t) for t in Ts]
zcbs = [exact_zcb(0.5, 0.02, 0.03, t, 0.015) for t in Ts]

plt.figure(figsize=(10,5))
plt.title("Zero Coupon Bond (ZCB) Value by Time "
          "and Strike(K) Values by Time")
plt.plot(Ts, zcbs, label='ZCB')
plt.plot(Ts, Ks, label='K', linestyle="--", marker=".")
plt.ylabel("Value ($)")
plt.xlabel("Time in years")
plt.legend()
plt.grid(True)
plt.show()


債券の発行者はコールを行使する権利を保有しているので、コール付きゼロクーポン債券の価格は、 $$callable~zero~coupon~bond~price~=~min(ZCB, K(割引))$$ となる.
この債券価格は現在の金利の水準が与えられた時の近似である.
行使価格は未来に行使すればするほど価値が下がる.
上の図だと3年ぐらいで変わる.
債権の価値は3年より前でZCBだが、3年よりあとで行使価格になる. 次のステップとして、policy iterationの形で、早期行使を考慮することが考えられる.

Policy iteration by finite differences


In [16]:
x = [1, 1, 0]
all((j==1 for j in x))


Out[16]:
False

In [154]:
class VasicekCZCB:
    def __init__(self):
        self.norminv = st.distributions.norm.ppf
        #使ってない.
        self.nor = st.distributions.norm.cdf
        
    
    #メインの関数。これを呼び出すことで、金利とそれに対応する価値を計算する.
    #r0: t=0における短期金利.
    #R: 「行使価格」に対する金利. 割り引くため.
    #ratio: 行使価格(割合として与えられる).
    #sigma: 短期金利のボラティリティ.
    #theta: 短期金利のmean.
    #M: the number of steps. 時間の分割.
    #prob: 上限と下限を決める確率.
    #grid_struct_const: 状態の変化幅...ではない. 
    def vasicek_czcb_values(self, r0, R, ratio, T, sigma, kappa, theta, M, prob=1e-6, max_policy_iter=10,
                            grid_struct_const=0.25, rs=None):
        #vasicek modelのパラメーターを生成する.
        #N: 状態の分割数.
        #N*Mのグリッドになる.
        r_min, dr, N, dtau = self.vasicek_params(r0, M, sigma, kappa, theta, T, prob, grid_struct_const, rs)
        #状態の初期設定. 
        #計算には使わない.
        r = np.r_[0:N]*dr + r_min
        
        #Nの数は異なってもrの最小値と最大値は一致.
        #r_0は初期値. 気にするな.
        
        #値を入れる配列をあらかじめ作っておく. これにより計算時間を減らせる.
        v_mplus1 = np.ones(N)
        
        #時間を進めていく.
        #iterateでは、ある時間が与えられた時に、翌期の
        for i in range(1, M+1):
            #割り引かれた行使価格. i*dtauは行使までの時間.
            K = self.exercise_call_price(R, ratio, i*dtau)
            #今行使したら.
            #状態個分の行使価格(割引).
            eex = np.ones(N)*K
            #陰的差分法の各項を決める.
            #ここに書く必要はない. 書かない方がいい. forループの上に出すべき?
            #時間を固定してすべての状態について導出. 
            subdiagonal, diagonal, superdiagonal = self.vasicek_diagonals(sigma, kappa, theta, r_min, dr, N, dtau)
            v_mplus1, iterations = self.iterate(subdiagonal, diagonal, superdiagonal, v_mplus1, eex, max_policy_iter)
            
        return r, v_mplus1

    #vasicek modelのパラメーターを生成する.
    def vasicek_params(self, r0, M, sigma, kappa, theta, T, prob, grid_struct_const=0.25, rs=None):
        #金利の上限と下限. rsがNonneなら計算する. 所与としてできるということ. デフォルトは計算.
        (r_min, r_max) = (rs[0], rs[-1]) if not rs is None else self.vasicek_limits(r0, sigma, kappa, theta, T, prob)
        dt = T/float(M)
        #グリッッドの時間幅はわかるが、価値の変化幅は分からないので.
        N = self.calculate_N(grid_struct_const, dt, sigma, r_max, r_min)
        dr = (r_max-r_min)/(N-1)
        #上限は返さなくていい. コールに限れば.
        return r_min, dr, N, dt

    def calculate_N(self, max_structure_const, dt, sigma, r_max, r_min):
        N = 0
        while True:
            N += 1
            #grid_structure_interval = sigma^2*dt/dr^2
            grid_structure_interval = dt*(sigma**2)/(((r_max-r_min)/float(N))**2)
            #N=0だと一個の目が非常に大きい.
            #少しずつ絞る.
            #grid_structure_intervalは徐々に大きくなる.
            if grid_structure_interval > max_structure_const:
                break
        return N
    
    def vasicek_limits(self, r0, sigma, kappa, theta, T, prob=1e-6):
        er = theta+(r0-theta)*math.exp(-kappa*T)
        variance = (sigma**2)*T if kappa==0 else (sigma**2)/(2*kappa)*(1-math.exp(-2*kappa*T))
        stdev = math.sqrt(variance)
        #meanとstdを与えて正規分布を得る。probを与えれば、そのprobabilityに対応する値が返ってくる.
        #今回は金利が返ってくる.
        r_min = self.norminv(prob, er, stdev)
        r_max = self.norminv(1-prob, er, stdev)
        return r_min, r_max

    #パラメーターはまとめて生成する.
    def vasicek_diagonals(self, sigma, kappa, theta, r_min, dr, N, dtau):
        #ようするにr. 
        #vaisicekで金利がどこに変化するかは分からないが、とりあえず自分の位置の金利はわかる.
        rn = np.r_[0:N]*dr + r_min
        subdiagonals = kappa*(theta-rn)*dtau/(2*dr) - 0.5*(sigma**2)*dtau/(dr**2)
        diagonals = 1 + rn*dtau + sigma**2*dtau/(dr**2)
        superdiagonals = -kappa*(theta-rn)*dtau/(2*dr) - 0.5*(sigma**2)*dtau/(dr**2)

        # Implement boundary conditions.
        if N > 0:
            v_subd0 = subdiagonals[0]
            superdiagonals[0] = superdiagonals[0] - subdiagonals[0]
            diagonals[0] += 2*v_subd0
            subdiagonals[0] = 0

        if N > 1:
            v_superd_last = superdiagonals[-1]
            superdiagonals[-1] = superdiagonals[-1] - subdiagonals[-1]
            diagonals[-1] += 2*v_superd_last
            superdiagonals[-1] = 0

        return subdiagonals, diagonals, superdiagonals
    
    #行使するかしないか.
    def check_exercise(self, V, eex):
        return V > eex

    def exercise_call_price(self, R, ratio, tau):
        K = ratio*np.exp(-R*tau)
        return K

    def vasicek_policy_diagonals(self, subdiagonal, diagonal, superdiagonal, v_old, v_new, eex):
        #行使されるかどうかTrue or False.
        #すべての状態について.
        has_early_exercise = self.check_exercise(v_new, eex)
        #行使する状態について各iに関するdiagonalに0を入れる.
        #これはf_i+1 = f_iとするためである.
        subdiagonal[has_early_exercise] = 0
        superdiagonal[has_early_exercise] = 0
        policy = v_old/eex
        policy_values = policy[has_early_exercise]
        #行使された場合、価値はeex(K)となる。
        #よって、bK = v_oldであり、その価値が最後まで引き継がれる.
        #diagonal[has_early_exercise] = v_old/eex[has_early_exercise]
        diagonal[has_early_exercise] = policy_values
        #diagonal*eex = v_old
        return subdiagonal, diagonal, superdiagonal

    def iterate(self, subdiagonal, diagonal, superdiagonal, v_old, eex, max_policy_iter=10):
        #このv_mplus1は不要.
        #書いておくとわかりやすいためか.
        v_mplus1 = v_old
        v_m = v_old
        
        #変化(行使)したか変化してないか. 変化したら1、してなければ0.
        change = np.zeros(len(v_old))
        prev_changes = np.zeros(len(v_old))

        iterations = 0
        while iterations <= max_policy_iter:
            iterations += 1
            
            #解く.
            #v_oldは一期先の価格.
            #これは繰り返すごとに変わる. diagonalsの変化によって.
            v_mplus1 = self.tridiagonal_solve(subdiagonal, diagonal, superdiagonal, v_old)
            
            #diagonalsを新しいv(v_mplus1)に応じて変える.
            subdiagonal, diagonal, superdiagonal = self.vasicek_policy_diagonals(subdiagonal, diagonal, superdiagonal,
                                              v_old, v_mplus1, eex)
            
            #実際に計算上iterateしているのは上の二つ.
            #ひたすらdiagonalsを更新している.
            #v_oldは固定.
            #diagonalsが更新されるのでv_mplus1は変わる.
            
            #行使するかせざるか. True or False. 
            is_eex = self.check_exercise(v_mplus1, eex)
            #行使するなら1にする.
            change[is_eex] = 1
            
            #2回目以上のiteration、すなわち1回目のiterationで行使が確認されたあと.
            #変わっているところがないかチェック.
            if iterations > 1:
                change[v_mplus1 != v_m] = 1

            is_no_more_eex = False if True in is_eex else True
            #早期行使がされていないならやめる.
            if is_no_more_eex:
                break
                
            #早期行使がなされた場合、された後の価値の変化を追う必要がある.
            #価値に行使価格を代入する.
            #ただし、これは計算には直接使わずcheckのために使う.
            #最後の方に書いてもいい. ここに書くのはややややこしい.
            v_mplus1[is_eex] = eex[is_eex]
            
            #どの要素が変化したのかしてないのか.
            #変化がなくなればすべて1になる(収束)
            changes = (change == prev_changes)
            
            #すべての値が変化していたらiterationをやめる.
            is_no_further_changes = all((x == 1) for x in changes)
            if is_no_further_changes:
                break
            
            prev_changes = change
            v_m = v_mplus1
            
        return v_mplus1, (iterations-1)

    #Thomas algorism
    def tridiagonal_solve(self, a, b, c, d):
        nf = len(a)  # Number of equations        
        #forループをしなくても各要素を演算.
        #もともとがndarrayなので変化なし.
        ac, bc, cc, dc = map(np.array, (a, b, c, d))  # Copy the array
        #ac, bc, cc, dc = (a, b, c, d)  # Copyでは無い場合.
        for it in range(1, nf):
            mc = ac[it]/bc[it-1]
            bc[it] = bc[it] - mc*cc[it-1] 
            dc[it] = dc[it] - mc*dc[it-1]

        xc = ac
        xc[-1] = dc[-1]/bc[-1]
        
        #状態を逆戻りして解く.
        #最後にはdc[-1]/bc[-1]が入っている.
        #それ以外に(dc[il]-cc[il]*xc[il+1])/bc[il]を入れる.
        #xcは最初はaで、最後にrを返す.
        for il in range(nf-2, -1, -1):
            xc[il] = (dc[il]-cc[il]*xc[il+1])/bc[il]
                        
        #ここはあっても無くてもいい.
        del bc, cc, dc  # Delete variables from memory

        return xc

In [155]:
r0 = 0.05
R = 0.05
ratio = 0.95
sigma = 0.03
kappa = 0.15
theta = 0.05
prob = 1e-6
M = 250
max_policy_iter=10
grid_struct_interval = 0.25
rs = np.r_[0.0:2.0:0.1]

Vasicek = VasicekCZCB()
r, vals = Vasicek.vasicek_czcb_values(r0, R, ratio, 1., sigma, kappa, theta, M, prob,
                                      max_policy_iter, grid_struct_interval, rs)

plt.figure(figsize=(10,5))
plt.title("Callable Zero Coupon Bond Values by r")
plt.plot(r, vals, label='1 yr')

for T in [5., 7., 10., 20.]:
    r, vals = Vasicek.vasicek_czcb_values(r0, R, ratio, T, sigma, kappa, theta, M, prob, 
                                          max_policy_iter, grid_struct_interval, rs)
    plt.plot(r, vals, label=str(T)+' yr', linestyle="--", marker=".")

plt.ylabel("Value ($)")
plt.xlabel("r")
plt.legend()
plt.grid(True)
plt.show()


上図はコール付きゼロクーポン債の、異なる満期と異なる金利に対する価値である。
1年満期のコール付きゼロクーポン債を考える。
この場合、行使価格はK(1年で割引)で固定されている.
あとはそれぞれの金利が与えられたときに、Kを上回るか上回らないかを考えればいい.
金利はvasicek modelで決まり、債権の価値は金利で決まる.
格子は縦軸が金利に、横軸が時間となる.
アメリカンなので早期行使を考えなくてはならない.
行使されたら価値はKになる.


In [ ]: