σμ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.14·~50 分中級

求根:二分法とニュートン法

ロードマップ上の位置モジュール 4/11 · トピック 14/16
4.14 求根:二分法とニュートン法
—ラボスコア— / 100合計XP—連続日数—

f(x) = 0 を解く

多くの定量的な問いは、関数がゼロを横切る x を見つけることに行き着きます。債券を値付けする利回り、オプションに合うボラティリティ、プロジェクトの価値をゼロにする金利などです。f の根とは f(x) = 0 となる x のことです。たいてい公式はないので、プログラムが少しずつ近づきます。ほぼすべてを担う方法は2つで、二分法は遅いが確実、ニュートン法は速いが注意が要ります。どちらも関数そのものを引数に取り、Python では関数が値なのでそれができます。

def bisection(f, a, b, tol=1e-10):
    if f(a) * f(b) > 0:
        raise ValueError("f(a) and f(b) must have opposite signs")
    while b - a > tol:
        mid = (a + b) / 2
        if f(a) * f(mid) <= 0:       # the root is in the left half
            b = mid
        else:
            a = mid
    return (a + b) / 2

root = bisection(lambda x: x * x - 2, 0, 2)
print(root)                          # 1.414213562355144
print(root * root)                   # 1.9999999999492266

二分法:遅いが確実

f が連続で f(a) と f(b) の符号が逆なら、その間に根があります。中点を評価すると、2つの半分のどちらかは端点の符号がまだ逆なので、根はそこに閉じ込められます。f がどんな形でも1回ごとに区間は半分になるので、n 回後の誤差は (b - a) / 2**n で、必要な回数は事前に ceil(log2((b - a) / tol)) と分かります。導関数が要らないこの保証が、二分法を安全な既定の選択にしています。

# How many halvings does a given tolerance cost? About 3.3 per decimal digit
import math

def bisection_steps(a, b, tol):
    if b - a <= tol:
        return 0
    return math.ceil(math.log2((b - a) / tol))

for tol in (1e-2, 1e-4, 1e-8, 1e-12):
    print(tol, bisection_steps(0, 2, tol))       # 1e-2: 8, 1e-4: 15, 1e-8: 28, 1e-12: 41

ニュートン法:接線をたどる

ニュートン法は、現在の推定値で f を接線に置き換え、その直線がゼロを横切る点へ飛びます:x_new = x - f(x) / df(x)。単根の近くでは誤差が1回ごとにほぼ*2乗*されるので、正しい桁数は1から2、4、8、16と4回で倍々に増えます。導関数 df(インプライド・ボラティリティならすでにあるベガ)と良い初期値が必要です。

# Newton for sqrt(2): print every iterate and the error that remains
import math

def newton_step(f, df, x):
    return x - f(x) / df(x)

f = lambda x: x * x - 2
df = lambda x: 2 * x
x = 1.0
for step in range(6):
    print(step, x, abs(x - math.sqrt(2)))
    x = newton_step(f, df, x)
# Newton on the implied volatility of a call, using vega as the derivative
import math

def norm_cdf(x):
    return 0.5 * (1 + math.erf(x / math.sqrt(2)))

def norm_pdf(x):
    return math.exp(-0.5 * x * x) / math.sqrt(2 * math.pi)

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 vega(spot, strike, rate, sigma, t):
    d1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
    return spot * norm_pdf(d1) * math.sqrt(t)

market = 8.0
sigma = 0.5                                              # a rough start
for step in range(1, 6):
    error = bs_call(100, 100, 0.05, sigma, 0.5) - market
    sigma -= error / vega(100, 100, 0.05, sigma, 0.5)
    print(step, round(sigma, 10))

ニュートン法が失敗するとき

接線は局所的な像です。導関数がゼロか非常に小さければ、ジャンプは巨大になるか定義できません。出発点が根から遠いと、反復は遠くへ飛んだり巡回したりします(atan(x) を x = 2 から始めると限りなく増えます)。そのため堅牢なソルバーは割り算を守り、反復回数に上限を設け、誤った数を返さずにエラーを出します。多くのライブラリは区間を挟んだ状態から始め、その内側でニュートン法を使い、はみ出したら二分法に切り替えます。

どちらを選ぶか二分法は区間が必要で、3回ごとに約1桁ずつ精度が上がりますが、符号が違う限り失敗しません。ニュートン法は導関数とまともな初期値が必要で、桁数が爆発的に増えますが、静かに失敗することがあります。価格計算のコードでは、無裁定の境界から区間を作り、ニュートン法で磨き、どちらも収束しなければ明確にエラーを出すのが定番です。
  • f の根は f(x) = 0 となる x。ソルバーは f(と df)を関数の引数として受け取る
  • 二分法:符号が変わる側の半分を残す。ceil(log2((b - a) / tol)) 回で保証され、導関数は不要
  • ニュートン法:x - f(x) / df(x)。単根の近くでは1回ごとに桁が倍になるが、df・良い初期値・ガード(導関数ゼロ、非収束)が必要

次は二項ツリーです。株価を上下に動かし、後ろ向きにたどってオプションを値付けします。

ラボ:採点される内容

  • bisection で区間を半分にして根を挟み込む25点
  • bisection_steps で半分にする回数を数える25点
  • newton_step で接線を1回たどる25点
  • newton で根に収束させる25点
エクスプローラー · lab-79-1
▾ lab-79-1
…
bash · ブラウザ内
learner@quantcraft:~/lab-79-1$
Pythonは未読み込みPythonLn 1, Col 1Spaces: 4UTF-8

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

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

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

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

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

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