モンテカルロ法で条件付期待値計算をする際の試行錯誤−1
問題設定
ここではある時点における、適当な確率変数の条件付期待値を計算する事を考えたい。例えば適当な確率空間を設定し、以下のような条件付き期待値の計算を行うとする*1。
・・・(A)
ここでは確率測度の下で以下の確率微分方程式に従うものとしている。
ただし、,はそれぞれ一定値とし、は標準ブラウン運動(の差分)とする。この確率微分方程式をモンテカルロシミュレーションさせる際には、数値差分法に起因する余分な計算誤差を減らすために一度確率微分方程式解くことで
という形にしてから実行することにした。
解析解(ブラックショールズ方程式の解)
この問題設定は俗にいうブラックショールズ方程式の枠組みが当てはまるものとなっているので、その解は
:標準正規分布の累積分布関数
と表現する事ができる。これを基準にモンテカルロ法での評価値の良しあしを考える。
素直なモンテカルロ法での実装
(A)を素直にモンテカルロ法で評価してやる場合には、モンテカルロパス数をとして
・・・(B)
というある種の標本平均値をモンテカルロシミュレーションを通して計算したから計算してやればよい。
これはその辺の本に載ってる本当に素直なモンテカルロのやり方。
塔定理を使った変形と実装方法
一方、条件付期待値に関する俗に言う塔定理(tower property)を使うと
(ただし)
と変形することができるので、これを使って解析解とモンテカルロシミュレーションの組み合わせで条件付期待値を評価してやりたい、つまり
・・・(C)
という量を計算することで評価値を算出する。ここで(C)のやり方の場合、ブラックショールズ方程式の解をモンテカルロパス数分計算しなければならない点に注意。
teramonagi氏が本当にやりたかったこと
ここで私がとりあえずやりたい事は
「(B)と(C)の計算方法においてどちらがより大きくor小さくモンテカルロ誤差(標準誤差)が出てくるのか?」
ということなんで、とりあえずやってみた。特に(C)の場合、塔定理を適用する際の時点に関して任意性()があるので、そこはパラメーターとして複数のsに対して計算を実施。結果は以下のようになった。
(横軸が、縦軸が標準誤差、緑線が(B)、青線が(C)に対応。パス数は双方10,000パスで固定)
・・・つまり条件付のタイミングが早い()の場合はブラックショールズ方程式の解析解をモンテカルロ数分だけ計算することになるので、ほぼモンテカルロ誤差がなくなる一方、条件付のタイミングが遅い(満期に近い、)の場合は満期時点でのペイオフ関数()をモンテカルロパスごとに計算することになるので、結果としてはほぼ素直なモンテカルロ法の結果と変わらないということか。非常に素直な結論。逆にそんなのあるのかしらないけど、条件付期待値をある時点でだけ解析的に評価できるのならばモンテカルロ誤差は減らせるということでもある。
モンテカルロ誤差自体は
のように見積もれるので、やの分布から考察するってのもありか。
以下、計算に使ったコード。
# -*- coding: utf-8 -*- import numpy as np import matplotlib.pyplot as plt from scipy.stats import norm #generate underlying(stock) path by geometric brownian motion def generate_path(s0, T, r, vol, N, M): dt = T/M w = np.cumsum(np.reshape(np.random.standard_normal(N*M), (N,M)), 1) * (dt**0.5) t = np.cumsum(np.ones((N,M)), 1)*dt return s0 * np.exp((r-0.5*vol**2)*t + vol*w) #call option price by black-scholes model def call_price_bs(s0, k, T, r, vol): d1 = (np.log(s0/k)+(r+0.5*(vol**2.0))*T) / (vol * np.sqrt(T)) d2 = d1-vol*np.sqrt(T) return (s0*norm.cdf(d1) - k*np.exp(-r*T)*norm.cdf(d2)) #call option price by montecarlo def call_price_monte(s0, k, T, r, vol, N, M): s = generate_path(s0, T, r, vol, N, M) discount = np.exp(-r*T) callprice = map(lambda x: discount*max(x, 0), s[:, -1] - k) return (np.average(callprice), np.std(callprice)/(N**0.5)) #call option price by conditional montecarlo def call_price_monte_conditional(s0, k, t, T, r, vol, N, M): s_t = generate_path(s0, t, r, vol, N, M)[:, -1] discount = np.exp(-r*t) callprice = [discount * call_price_bs(s, k, T-t, r, vol) for s in s_t] return (np.average(callprice), np.std(callprice)/(N**0.5)) if __name__ == "__main__": #initial stock price S0 = 100.0 #strike K = 100.0 #maturity T = 3.0 #interest rate R = 0.01 #volatility VOL = 0.2 #number of montecarlo path N = 10000 #number of time-grid M = 200 print "Exact solution" print call_price_bs(S0, K, T, R, VOL) print "Simple montecarlo " price_monte = call_price_monte(S0, K, T, R, VOL, N, M) print "montecarlo with tower property" ts = np.linspace(0, T, 10, endpoint=True) price_monte_conditional = [call_price_monte_conditional(S0, K, t, T, R, VOL, N, M) for t in ts] #plot montecarlo error plt.plot(ts, map(lambda x:x[1], price_monte_conditional), ts, [price_monte[1] for i in ts]) plt.show()