σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft Academyロードマップ
Ctrl K
AO
+1.2k 人の学習者
ログイン参加する ↗
ロードマップ
AO
+1.2k 人の学習者
ログイン
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python ロードマップ›モジュール 4 · 債券とオプションの価格計算›トピック 4.13·~50 分中級

インプライド・ボラティリティ

ロードマップ上の位置モジュール 4/11 · トピック 13/16
4.13 インプライド・ボラティリティ
—ラボスコア— / 100合計XP—連続日数—

式を逆向きに使う

ブラック=ショールズの式はボラティリティを価格に変えます。市場ではその逆で、価格が提示され、ボラティリティは未知です。インプライド・ボラティリティとは、式が市場価格を再現する 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点
エクスプローラー · lab-78-1
▾ lab-78-1
…
bash · ブラウザ内
learner@quantcraft:~/lab-78-1$
Pythonは未読み込みPythonLn 1, Col 1Spaces: 4UTF-8

このトピックをリセットしますか?

lab-78-1 の内容はすべてスターターファイルに戻り、ターミナルも消去されます。現在の解答は失われ、元に戻せません。

リセットされるもの
  • main.py → スターターコード
  • ターミナル履歴とテスト出力
  • 未提出の変更
保持されるもの
  • ラボの最高スコア
  • XPと連続日数
  • 講義の進捗とメモ

コードを実行するにはログインしてください。

レクチャーはすべて無料で読めます。コードの実行、テスト、ラボの提出にはアカウントの作成かログインが必要です。

  • ブラウザ内IDEとbashターミナル。インストール不要
  • 即時採点、XP、ロードマップでの現在地
アカウントを作成ログイン
または
GitHubで続ける