モデルを市場に合わせる
価格モデルにはパラメータがあり、正しい値は教科書ではなく今日の市場にあります。較正は、モデルの価格が観測された価格(通常はその日のオプション気配)にできるだけ近くなるようにパラメータを選びます。手順は3つの部分からなります。乖離を測る目的関数(ほとんどの場合は差の二乗和で、流動性の高い気配をより重くするよう重み付けすることもあります)。それを減らすようパラメータを動かす探索。そして結果が妥当かの確認です。日ごとに安定しているか、もっともらしい範囲に収まっているか、フィットに使わなかった気配でも良く合うか、を見ます。
# The objective: how far the model's prices are from the market's, as a (weighted) sum of squares
def sum_squared_error(model, market, weights=None):
if weights is None:
weights = [1.0] * len(model)
return sum(w * (m - y) ** 2 for w, m, y in zip(weights, model, market))
market = [4.20, 8.60, 14.10]
model = [4.00, 8.80, 14.20]
print(round(sum_squared_error(model, market), 4)) # 0.09 0.2^2 + 0.2^2 + 0.1^2
print(round(sum_squared_error(model, market, [1, 4, 1]), 4)) # 0.21 the middle quote counts four times as much2つの探索道具:根と最小値
よく出てくる課題が2つあります。1つ目はインプライド・ボラティリティです。市場価格が与えられたとき、モデルがその価格を出すボラティリティ、つまり model(sigma) - price = 0 の根を見つけます。価格はボラティリティとともに単調に上がるので、二分法が使えます。差の符号が変わる区間から始め、半分に分け、符号がまだ変わる側を残し、これを繰り返します。1ステップごとに精度が倍になり、失敗しません。2つ目は最小値です。谷が1つの関数には、黄金分割探索が使えます。ステップごとに区間を 0.618 倍に縮め、2つの関数値のうち1つを再利用するので、1ステップに評価は1回で、微分は不要です。ライブラリ(scipy.optimize)はより速く多数のパラメータも扱えますが、この2つで考え方が分かります。
# Implied volatility by bisection: the root of (model price - market price) in sigma
import math
def norm_cdf(x):
return 0.5 * (1 + math.erf(x / math.sqrt(2)))
def bs_call(s, k, t, r, sigma):
d1 = (math.log(s / k) + (r + sigma ** 2 / 2) * t) / (sigma * math.sqrt(t))
return s * norm_cdf(d1) - k * math.exp(-r * t) * norm_cdf(d1 - sigma * math.sqrt(t))
def bisect_root(f, lo, hi, tol=1e-10):
f_lo = f(lo)
while hi - lo > tol:
mid = (lo + hi) / 2
f_mid = f(mid)
if f_lo * f_mid <= 0:
hi = mid
else:
lo, f_lo = mid, f_mid
return (lo + hi) / 2
market_price = bs_call(100.0, 105.0, 1.0, 0.02, 0.27) # a quote generated with a volatility of 27%
implied = bisect_root(lambda sigma: bs_call(100.0, 105.0, 1.0, 0.02, sigma) - market_price, 0.01, 2.0)
print(round(implied, 6)) # 0.27 recovered# Golden-section search: one function evaluation per step, the interval shrinks by 0.618 each time
import math
def golden_section_minimize(f, lo, hi, tol=1e-8):
ratio = (math.sqrt(5) - 1) / 2
a, b = lo, hi
c, d = b - ratio * (b - a), a + ratio * (b - a)
fc, fd = f(c), f(d)
while b - a > tol:
if fc < fd:
b, d, fd = d, c, fc
c = b - ratio * (b - a)
fc = f(c)
else:
a, c, fc = c, d, fd
d = a + ratio * (b - a)
fd = f(d)
return (a + b) / 2
evaluations = []
def valley(x):
evaluations.append(x)
return (x - 1.3) ** 2 + 0.5
print(round(golden_section_minimize(valley, 0.0, 5.0), 5), len(evaluations) < 60) # 1.3 True about 40 evaluations for 1e-8ボラティリティを較正する、責任をもって
部品を組み合わせると、複数のオプション価格に1つのボラティリティを較正するとは、price_fn(strike, sigma) と市場価格の二乗誤差の和を sigma について最小化することです。price_fn を引数で渡すので、同じコードがどのモデルにも使えます。パラメータが複数あるモデル(Heston は5つ、SABR は beta を固定すれば3つ)では探索は多次元になり、局所最小に陥ることがあります。複数の初期値から始め、パラメータに範囲を設け、生の価格誤差よりも相対誤差やインプライド・ボラティリティの誤差を使って、安いアウト・オブ・ザ・マネーのオプションが無視されないようにします。そのうえで、較正したパラメータが日ごとに安定していること、フィットに使っていないオプションもモデルが良く評価できることを確認します。完璧でも不安定なフィットは過剰適合です。
# Calibrate one volatility to three option prices: minimise the sum of squared errors over sigma
import math
def norm_cdf(x):
return 0.5 * (1 + math.erf(x / math.sqrt(2)))
def bs_call(s, k, t, r, sigma):
d1 = (math.log(s / k) + (r + sigma ** 2 / 2) * t) / (sigma * math.sqrt(t))
return s * norm_cdf(d1) - k * math.exp(-r * t) * norm_cdf(d1 - sigma * math.sqrt(t))
def golden_section_minimize(f, lo, hi, tol=1e-8):
ratio = (math.sqrt(5) - 1) / 2
a, b = lo, hi
c, d = b - ratio * (b - a), a + ratio * (b - a)
fc, fd = f(c), f(d)
while b - a > tol:
if fc < fd:
b, d, fd = d, c, fc
c = b - ratio * (b - a)
fc = f(c)
else:
a, c, fc = c, d, fd
d = a + ratio * (b - a)
fd = f(d)
return (a + b) / 2
def calibrate_sigma(strikes, market_prices, price_fn, lo, hi):
def error(sigma):
return sum((price_fn(k, sigma) - p) ** 2 for k, p in zip(strikes, market_prices))
return golden_section_minimize(error, lo, hi)
strikes = [90.0, 100.0, 110.0]
quotes = [bs_call(100.0, k, 1.0, 0.02, 0.23) for k in strikes] # market quotes made with sigma = 23%
sigma = calibrate_sigma(strikes, quotes, lambda k, s: bs_call(100.0, k, 1.0, 0.02, s), 0.01, 1.5)
print(round(sigma, 4)) # 0.23- 較正は価格誤差の(重み付き)二乗和を最小にするモデルパラメータを選ぶ。日ごとの安定性と、フィットに使わなかった気配での精度を確認する
- 二分法は符号が変わる区間を半分にして根(インプライド・ボラティリティ)を見つけ、黄金分割探索は谷が1つの関数の最小値を1ステップ1評価で見つける
- 価格関数は引数で渡し、パラメータに範囲を設け、複数パラメータのモデルでは複数の初期値から始め、ありえないフィットは却下する
次は予測のためのニューラルネットワークです。式を書く代わりに、データから予測を学習します。
ラボ:採点される内容
sum_squared_errorで乖離を測る25点bisect_rootで f(x) = 0 を解く25点golden_section_minimizeで最小点を見つける25点calibrate_sigmaで市場に合わせる25点