式を逆向きに使う
ブラック=ショールズの式はボラティリティを価格に変えます。市場ではその逆で、価格が提示され、ボラティリティは未知です。インプライド・ボラティリティとは、式が市場価格を再現する sigma のことです。代数的な逆関数はないので探索します。試しの sigma で価格を計算し、価格誤差(モデル引く市場)を見て、誤差がゼロになるまで sigma を動かします。最も単純な探索は、値の格子を試して最良のものを残す方法です。
import math
def norm_cdf(x):
return 0.5 * (1 + math.erf(x / math.sqrt(2)))
def bs_call(spot, strike, rate, sigma, t):
d1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
return spot * norm_cdf(d1) - strike * math.exp(-rate * t) * norm_cdf(d1 - sigma * math.sqrt(t))
market = 8.0 # the quoted price of the 100-strike, 6-month call on a stock at 100
for sigma in (0.10, 0.15, 0.20, 0.25, 0.30):
model = bs_call(100, 100, 0.05, sigma, 0.5)
print(sigma, round(model, 4), "error", round(model - market, 4))なぜ答えはひとつだけなのか
コールの価格は sigma とともに単調に上がります。ベガは正なので、誤差がゼロを横切るのは一度だけです。だから答えは一意で、動かす向きも分かります。モデルが高すぎれば sigma を下げます。解が存在するのは、市場価格が*本質的価値の下限* max(S - K * exp(-rT), 0)(ボラティリティ0)と現物価格 S(ボラティリティ無限大)の間に厳密に収まるときだけです。範囲外の気配は無裁定に反し、正しい答えは数ではなくエラーです。
# The bounds: no volatility can explain a price outside them
import math
def norm_cdf(x):
return 0.5 * (1 + math.erf(x / math.sqrt(2)))
def bs_call(spot, strike, rate, sigma, t):
d1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
return spot * norm_cdf(d1) - strike * math.exp(-rate * t) * norm_cdf(d1 - sigma * math.sqrt(t))
spot, strike, rate, t = 100, 90, 0.05, 1
floor = max(spot - strike * math.exp(-rate * t), 0)
print(round(floor, 4), spot) # 14.3894 100
print(round(bs_call(spot, strike, rate, 0.0001, t), 4)) # 14.3894 tiny volatility: the floor
print(round(bs_call(spot, strike, rate, 5.0, t), 4)) # 98.8513 huge volatility: close to the spot区間を半分にしていく
格子は粗く、答えは格子の細かさ止まりです。誤差の符号を使うほうが優れています。sigma を非常に低い値と非常に高い値で挟み、中点を取って誤差を見ます。正なら中点は高すぎるので下半分を残し、負なら上半分を残します。1回ごとに区間は半分になるので、約30回で8桁が得られます。次のトピックでこれに名前を付けて一般化します。ここでは市場を読む道具です。
# Implied volatility by halving the interval, then reprice to check it
import math
def norm_cdf(x):
return 0.5 * (1 + math.erf(x / math.sqrt(2)))
def bs_call(spot, strike, rate, sigma, t):
d1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
return spot * norm_cdf(d1) - strike * math.exp(-rate * t) * norm_cdf(d1 - sigma * math.sqrt(t))
def implied_vol(price, spot, strike, rate, t, tol=1e-8):
low, high = 0.0001, 5.0
while high - low > tol:
mid = (low + high) / 2
if bs_call(spot, strike, rate, mid, t) > price:
high = mid
else:
low = mid
return (low + high) / 2
iv = implied_vol(8.0, 100, 100, 0.05, 0.5)
print(round(iv, 6)) # 0.240534
print(round(bs_call(100, 100, 0.05, iv, 0.5), 6)) # 8.0スマイル
ブラック=ショールズが完全に正しければ、同じ株・同じ日のオプションはすべて同じボラティリティを示すはずです。実際はそうならず、低い行使価格は高い行使価格より高いボラティリティを示すことが多く(*スキュー*)、あるいは中央より両端が高くなります(*スマイル*)。その形はモデルが捉え損ねているもの(ジャンプ、ファットテール、保険需要)を表します。そのためオプションはインプライド・ボラティリティで提示されます。行使価格や満期をまたいで比べられる1つの数で、そこから価格が決まります。
# A smile: market prices at five strikes, converted to implied volatilities
import math
def norm_cdf(x):
return 0.5 * (1 + math.erf(x / math.sqrt(2)))
def bs_call(spot, strike, rate, sigma, t):
d1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
return spot * norm_cdf(d1) - strike * math.exp(-rate * t) * norm_cdf(d1 - sigma * math.sqrt(t))
def implied_vol(price, spot, strike, rate, t, tol=1e-8):
low, high = 0.0001, 5.0
while high - low > tol:
mid = (low + high) / 2
low, high = (low, mid) if bs_call(spot, strike, rate, mid, t) > price else (mid, high)
return (low + high) / 2
strikes = [90, 95, 100, 105, 110]
quotes = [bs_call(100, k, 0.05, v, 0.5) for k, v in zip(strikes, [0.28, 0.24, 0.21, 0.22, 0.25])] # prices from a made-up smile
for strike, quote in zip(strikes, quotes):
print(strike, round(quote, 4), round(implied_vol(quote, 100, strike, 0.05, 0.5), 4))- インプライド・ボラティリティは
bs_callを市場価格に一致させるsigma。逆関数がないので探索で求める - 価格は
sigmaとともに上がるので答えは1つで、誤差の符号が動かす向きを教える。範囲外には答えがない - 区間の二分は速く収束する(8桁で約30回)。スマイルは行使価格に対するインプライド・ボラティリティ
次は二分法とニュートン法による求根です。この探索の背後にある一般的な方法と、はるかに速い方法を学びます。
ラボ:採点される内容
call_price_errorでモデルと市場を比べる25点implied_vol_scanでボラティリティの格子を探す25点implied_volで式を逆算する25点implied_vol_smileでスマイル全体を読む25点