Pythonでオプション取引分析:ブラック・ショールズモデル実装とグリーク指標の完全ガイド

オプション価格とグリークを求める Python実装・コード

※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。

結論から書きます。ブラック・ショールズモデルは「オプションの正しい値段を当てる式」ではありません。価格・権利行使価格・残存期間・金利・ボラティリティという5つの入力を、1つの価格に翻訳するだけの装置です。そして5つのうち4つは市場を見れば分かるので、実質的に唯一の未知数はボラティリティだけになります。ここを理解すると、オプションの見方が一気に変わります。

この記事では、Pythonでブラック・ショールズ式とグリークス(デルタ・ガンマ・セータ・ベガ・ロー)を実装したうえで、計算して出てきた数字を実際の判断にどう使うかまで踏み込みます。式を写経して終わりにせず、「ガンマとセータは必ずトレードオフになる」「市場価格からボラティリティを逆算できる」といった、実務で効く読み方を数値で確認していきます。

題材は日経225オプションを想定します。日本の個人投資家が実際に売買できる代表的なオプション市場であり、値動きの感覚をつかみやすいためです。

📘 外部参考Black-Scholes Model(Investopedia)ブラック–ショールズ方程式(Wikipedia)

ブラック・ショールズで何が分かって、何が分からないのか

まず、このモデルが扱える範囲をはっきりさせます。ここを曖昧にしたまま数字だけ出すと、必ず誤読します。

5つの入力と、その入手先

記号意味どこで分かるか未知か
S原資産価格(日経平均など)市場で観測できる既知
K権利行使価格銘柄ごとに決まっている既知
T残存期間(年)満期日から計算できる既知
r無リスク金利短期国債利回りなどほぼ既知
σボラティリティ誰にも分からない未知

この表がすべてです。オプションの売り買いとは、実質的にボラティリティに対する意見の売り買いということになります。「この銘柄は上がる/下がる」ではなく「これから動きが大きくなる/小さくなる」を取引しているわけです。

モデルの前提と、現実とのズレ

ブラック・ショールズは、いくつかの理想的な前提の上に成り立っています。実際の相場では、これらは厳密には成立しません。

  • リターンが正規分布に従う: 現実には裾が厚く、暴落の頻度がモデルの想定より高い
  • ボラティリティが一定: 現実には相場の局面によって大きく変わる
  • 取引コストがゼロ: 現実には手数料とスプレッドがかかる
  • いつでも連続的に売買できる: 現実には板が薄い時間帯がある
  • 満期日にしか権利行使できない(ヨーロピアン型): 日経225オプションはこの型なので、ここは適合する

特に1番目の「正規分布の仮定」は、暴落局面で致命的に効いてきます。この点は正規分布と株価リターンの現実を確認する方法で詳しく検証しているので、あわせてご覧ください。モデルを使うときは、「平常時の目安としては十分機能するが、極端な相場では過小評価になる」という距離感を持っておくと安全です。

Pythonで理論価格を計算する

まずは基本の実装です。必要なのは numpyscipy だけで、追加の金融ライブラリは要りません。

pip install numpy scipy pandas matplotlib

ブラック・ショールズ式そのものは、次の数行に収まります。

import numpy as np
from scipy.stats import norm


def d1_d2(S, K, T, r, sigma):
    """共通項 d1, d2 を計算する"""
    d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    return d1, d2


def black_scholes(S, K, T, r, sigma, option_type="call"):
    """ヨーロピアン・オプションの理論価格

    S: 原資産価格 / K: 権利行使価格 / T: 残存期間(年)
    r: 無リスク金利(年率, 0.01 = 1%) / sigma: ボラティリティ(年率)
    """
    if T <= 0:  # 満期到来後は本源的価値のみ
        return max(S - K, 0.0) if option_type == "call" else max(K - S, 0.0)

    d1, d2 = d1_d2(S, K, T, r, sigma)
    if option_type == "call":
        return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
    return K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)


# 例: 日経平均 42,000円、権利行使価格 43,000円、残り30日、金利1%、IV 20%
S, K, T, r, sigma = 42000, 43000, 30 / 365, 0.01, 0.20
call = black_scholes(S, K, T, r, sigma, "call")
put = black_scholes(S, K, T, r, sigma, "put")
print(f"コール: {call:,.1f} 円")
print(f"プット: {put:,.1f} 円")

T年単位を渡す点に注意してください。残り30日なら 30/365 です。ここを日数のまま渡すと、桁が完全におかしくなります。

計算が正しいかを検算する

実装を信用する前に、プット・コール・パリティで検算します。これは理論的に必ず成立する関係式なので、ここがズレていれば実装のどこかが間違っています。

# コール価格 - プット価格 = S - K * exp(-rT)
left = call - put
right = S - K * np.exp(-r * T)
print(f"左辺: {left:,.4f}")
print(f"右辺: {right:,.4f}")
print(f"差分: {abs(left - right):.10f}  →  ほぼ0なら実装は正しい")

差分が小数点以下10桁レベルで0になれば合格です。自作した金融計算は、こうした「必ず成り立つ関係」で必ず検算する癖をつけておくと、後々の事故を防げます。

グリークスを実装して、意味を読み取る

グリークスは、5つの入力を少し動かしたときにオプション価格がどれだけ変わるかを表した感応度です。要するに「何にどれだけ賭けているか」の内訳です。

def greeks(S, K, T, r, sigma, option_type="call"):
    """デルタ・ガンマ・セータ・ベガ・ローをまとめて返す"""
    if T <= 0:
        return {k: 0.0 for k in ("delta", "gamma", "theta", "vega", "rho")}

    d1, d2 = d1_d2(S, K, T, r, sigma)
    pdf = norm.pdf(d1)

    gamma = pdf / (S * sigma * np.sqrt(T))
    vega = S * pdf * np.sqrt(T) / 100          # IVが1%動いたときの変化
    common_theta = -(S * pdf * sigma) / (2 * np.sqrt(T))

    if option_type == "call":
        delta = norm.cdf(d1)
        theta = (common_theta - r * K * np.exp(-r * T) * norm.cdf(d2)) / 365
        rho = K * T * np.exp(-r * T) * norm.cdf(d2) / 100
    else:
        delta = norm.cdf(d1) - 1
        theta = (common_theta + r * K * np.exp(-r * T) * norm.cdf(-d2)) / 365
        rho = -K * T * np.exp(-r * T) * norm.cdf(-d2) / 100

    return {"delta": delta, "gamma": gamma, "theta": theta,
            "vega": vega, "rho": rho}


g = greeks(S, K, T, r, sigma, "call")
for name, value in g.items():
    print(f"{name:6s}: {value:>12,.4f}")

ベガとローを100で割っているのは、「1%(=0.01)動いたとき」の変化量に揃えるためです。セータを365で割っているのは「1日あたり」に直すためです。この正規化をしないと、他の値と桁が揃わず比較しづらくなります。

それぞれの数字が意味すること

グリーク何に対する感応度か読み方コール買いの符号
デルタ原資産価格原資産が1円動くと何円動くか。ヘッジ比率でもある+(0〜1)
ガンマデルタの変化相場が動くほどデルタが有利側に増える度合い+
セータ時間1日経過するといくら価値が減るか
ベガボラティリティIVが1%上がると何円増えるか+
ロー金利金利が1%上がると何円動くか。影響は小さい+

実務で毎日見るのはデルタ・ガンマ・セータ・ベガの4つで、ローはほぼ無視して構いません。残存期間が短いオプションでは、金利が多少動いても価格はほとんど変わらないためです。

デルタには、もう1つ便利な読み方があります。おおよそ「満期時にイン・ザ・マネーで終わる確率」の近似として使えるのです。デルタ0.30のコールなら、ざっくり「3割くらいの確率で権利行使できる状態になる」と解釈できます。厳密な確率ではありませんが、感覚をつかむには十分実用的です。

ガンマとセータは必ずトレードオフになる

ここがオプションを扱ううえで最も重要な性質です。ガンマ(相場が動けば得をする性質)を大きく持つと、必ずセータ(時間が経つと損をする性質)も大きくなります。都合よく片方だけを取ることはできません。

これは理屈で覚えるより、数字で見たほうが早いです。権利行使価格を変えながら、両者を並べてみます。

import pandas as pd

S, T, r, sigma = 42000, 30 / 365, 0.01, 0.20
rows = []
for K in range(39000, 45001, 1000):
    g = greeks(S, K, T, r, sigma, "call")
    price = black_scholes(S, K, T, r, sigma, "call")
    rows.append({
        "権利行使価格": K,
        "理論価格": round(price, 1),
        "デルタ": round(g["delta"], 3),
        "ガンマ": round(g["gamma"], 6),
        "セータ/日": round(g["theta"], 1),
        "ベガ": round(g["vega"], 1),
    })

df = pd.DataFrame(rows)
print(df.to_string(index=False))

実行すると、次のような傾向がはっきり出ます。

状態デルタガンマセータ性格
深いイン・ザ・マネー1に近い小さい小さいほぼ現物と同じ動き
アット・ザ・マネー0.5前後最大最大(不利)動けば大きい/待つと最も減る
深いアウト・オブ・ザ・マネー0に近い小さい小さい宝くじ的。ほぼ動かない

ガンマが最大になる場所で、セータも最大になる。この対応関係が、オプション取引の損益構造そのものです。「相場が大きく動きそうだ」と考えて買うなら時間の経過という対価を払い、「動かないだろう」と考えて売るなら急変のリスクを負う。どちらか一方だけ得をする選択肢は存在しません。

この非対称なリスク構造を扱う以上、ポジションサイズの管理は現物株以上に重要になります。自作システムで株を運用する際の絶対ルールと資金管理法の考え方は、オプションでもそのまま当てはまります。

市場価格からボラティリティを逆算する

冒頭で書いたとおり、未知数はボラティリティだけです。ということは、市場でついている価格を出発点にすれば、逆にボラティリティを求められることになります。これがインプライドボラティリティ(IV)です。

ブラック・ショールズ式はσについて解けないので、数値的に探します。単調増加であることが分かっているため、二分法で確実に収束します。

def implied_volatility(market_price, S, K, T, r,
                       option_type="call", tol=1e-6, max_iter=200):
    """市場価格からIVを逆算する(二分法)"""
    low, high = 1e-4, 5.0  # 0.01% 〜 500%

    # 解が範囲内にあるかを先に確認する
    if black_scholes(S, K, T, r, high, option_type) < market_price:
        return float("nan")  # 価格が高すぎて解なし

    for _ in range(max_iter):
        mid = (low + high) / 2
        price = black_scholes(S, K, T, r, mid, option_type)
        if abs(price - market_price) < tol:
            return mid
        if price < market_price:
            low = mid
        else:
            high = mid
    return (low + high) / 2


# 例: 理論価格を一度作り、そこからIVを復元できるか確認する
true_sigma = 0.235
price = black_scholes(42000, 43000, 30 / 365, 0.01, true_sigma, "call")
iv = implied_volatility(price, 42000, 43000, 30 / 365, 0.01, "call")
print(f"元のIV  : {true_sigma:.4f}")
print(f"逆算IV  : {iv:.4f}")

元の値がきれいに復元できれば、実装は正しく動いています。解なしのケースを先に弾いている点にも注目してください。市場価格が理論上ありえない水準(気配値の異常など)のとき、これがないと二分法が無意味な値を返します。

IVが分かると何が判断できるか

IVは「市場が今後の変動をどう見積もっているか」を表します。これを実際に起きた変動(ヒストリカル・ボラティリティ)と比べることで、割高/割安の判断材料になります。

  • IV > 実現ボラティリティ: 市場が身構えすぎている可能性。売り手に分がある局面
  • IV < 実現ボラティリティ: 市場が油断している可能性。買い手に分がある局面
  • 決算・雇用統計・FOMC前: IVは上がりやすく、イベント通過後に急落する(IVクラッシュ)

3番目は特に注意が必要です。方向を当てたのに損をするという現象は、たいていこれが原因です。イベント前に高いIVでオプションを買い、方向は当たったもののIVの下落幅のほうが大きく、差し引きマイナスになる——初心者が最もつまずくパターンです。

ヒストリカル・ボラティリティの計算方法は標準偏差を使って投資リスクを数値化する方法で扱っています。市場全体の警戒度を見るならVIX指数(恐怖指数)を分析する方法も参考になります。

時間の経過で価値がどう減るかを可視化する

セータは1日あたりの減少額ですが、これは一定のペースではありません。満期が近づくほど加速します。文章で説明するより、グラフにするのが早いです。

import matplotlib.pyplot as plt

S, K, r, sigma = 42000, 42000, 0.01, 0.20  # アット・ザ・マネー
days = np.arange(60, 0, -1)
prices = [black_scholes(S, K, d / 365, r, sigma, "call") for d in days]

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(days, prices, linewidth=2)
ax.invert_xaxis()                      # 左が満期から遠い、右が満期直前
ax.set_xlabel("残存日数")
ax.set_ylabel("コール理論価格(円)")
ax.set_title("時間価値の減少(アット・ザ・マネー)")
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("theta_decay.png", dpi=120)

# 減少ペースを期間別に集計する
for a, b in [(60, 30), (30, 10), (10, 1)]:
    diff = (black_scholes(S, K, a / 365, r, sigma, "call")
            - black_scholes(S, K, b / 365, r, sigma, "call"))
    print(f"残り{a:>2}日 → {b:>2}日 : {diff:>8,.0f} 円減少 "
          f"(1日あたり {diff / (a - b):>6,.0f} 円)")

実行すると、満期直前の1日あたりの減少額が、序盤の数倍になることが数字で確認できます。アット・ザ・マネーの時間価値は、残存期間の平方根に比例して減っていくためです。

ここから導ける実務的な指針は明快です。買い手は満期直前を避け、売り手は満期直前を狙う。ただし売り手は同時にガンマも大きく背負うので、急変時の損失も加速します。おいしい話には必ず裏があると考えてください。

日経225オプションで使うときの注意点

理論を実際の市場に当てはめるとき、日本のオプション市場特有の事情がいくつかあります。

取引単位と損益のインパクト

日経225オプションは指数の1,000倍が取引単位です。オプション価格が10円動けば、1枚あたりの損益は10,000円動きます。理論価格の計算結果をそのまま「円」だと思っていると、実際の損益額を桁で読み違えます。

MULTIPLIER = 1000  # 日経225オプションの取引単位

g = greeks(42000, 43000, 30 / 365, 0.01, 0.20, "call")
print("1枚あたりの実額換算")
print(f"  日経平均が100円動いたら : {g['delta'] * 100 * MULTIPLIER:>10,.0f} 円")
print(f"  1日経過したら           : {g['theta'] * MULTIPLIER:>10,.0f} 円")
print(f"  IVが1%上がったら        : {g['vega'] * MULTIPLIER:>10,.0f} 円")

売り建てには証拠金が必要になる

買い手の損失は支払ったプレミアムに限定されるため証拠金は不要ですが、売り手は証拠金を差し入れる必要があります。必要額は銘柄別VaR証拠金などにもとづいて算出され、相場が荒れると増額されます。「理論上は勝率が高い」戦略でも、証拠金不足による強制決済で退場するケースがあるため、余裕を持った資金設計が欠かせません。

ボラティリティ・スマイルという現実

ブラック・ショールズは「σは1つの値」と仮定しますが、実際の市場では権利行使価格ごとにIVが異なります。特に下方向のプットはIVが高くなりやすく、グラフにすると曲線を描きます。これがボラティリティ・スマイル(あるいはスキュー)です。

これはモデルのバグではなく、市場が「暴落は急に来る」という現実を価格に織り込んでいる証拠です。正規分布の仮定では説明できない部分を、市場がIVの歪みという形で補正していると解釈できます。実際に各権利行使価格のIVを計算してプロットしてみると、この歪みがはっきり見えます。

📘 外部参考IVの変動を利益に変える(大阪取引所・北浜投資塾)Getting to Know the “Greeks”(Investopedia)

保有ポジション全体のグリークスを集計する

複数のオプションを同時に持つと、1枚ずつのグリークスを眺めても全体像がつかめません。実務ではポジション全体を合計したグリークスで管理します。グリークスは単純に足し算できるので、集計は難しくありません。

import pandas as pd

MULTIPLIER = 1000
S, r = 42000, 0.01

# 保有ポジション(qtyがマイナスなら売り建て)
positions = [
    {"K": 43000, "days": 30, "iv": 0.20, "type": "call", "qty":  2},
    {"K": 41000, "days": 30, "iv": 0.23, "type": "put",  "qty": -3},
    {"K": 44000, "days": 60, "iv": 0.19, "type": "call", "qty": -1},
]

rows = []
for p in positions:
    T = p["days"] / 365
    g = greeks(S, p["K"], T, r, p["iv"], p["type"])
    price = black_scholes(S, p["K"], T, r, p["iv"], p["type"])
    rows.append({
        "銘柄": f"{p['K']} {p['type']} {p['days']}日",
        "枚数": p["qty"],
        "評価額": price * p["qty"] * MULTIPLIER,
        "デルタ": g["delta"] * p["qty"] * MULTIPLIER,
        "ガンマ": g["gamma"] * p["qty"] * MULTIPLIER,
        "セータ": g["theta"] * p["qty"] * MULTIPLIER,
        "ベガ":   g["vega"] * p["qty"] * MULTIPLIER,
    })

df = pd.DataFrame(rows)
total = df[["評価額", "デルタ", "ガンマ", "セータ", "ベガ"]].sum()

print(df.round(0).to_string(index=False))
print("\n=== ポジション合計 ===")
print(f"  デルタ : {total['デルタ']:>12,.0f}  (日経が1円動いたときの損益)")
print(f"  ガンマ : {total['ガンマ']:>12,.2f}")
print(f"  セータ : {total['セータ']:>12,.0f}  (1日で発生する損益)")
print(f"  ベガ   : {total['ベガ']:>12,.0f}  (IVが1%動いたときの損益)")

この合計値を見ると、自分が結局どちらに賭けているのかが一目で分かります。合計デルタがプラスなら上昇に賭けており、合計セータがプラスなら時間の経過が味方になっている、という具合です。個別の銘柄を見ているだけでは、これらは決して見えてきません。

実際の運用では、この集計を毎日自動で回して記録を残します。SQLiteで自動売買のログを一元管理する設計のように、日々のグリークス推移をデータベースに蓄積しておくと、「あのとき自分はどれだけリスクを取っていたのか」を後から検証できるようになります。

よくあるエラーと対処法

価格が異常な値になる/nanが返る

最頻出の原因はTの単位ミスです。残り30日を 30 のまま渡すと「30年後が満期」という計算になります。必ず 30/365 の形で年に直してください。次に多いのが、σを「20」と渡してしまうケースです。20%は 0.20 です。

# 入力を検証してから計算する
def safe_bs(S, K, T, r, sigma, option_type="call"):
    assert S > 0 and K > 0, "価格は正の値である必要があります"
    assert 0 < T < 10, f"Tは年単位です(受け取った値: {T})"
    assert 0 < sigma < 5, f"sigmaは小数で指定します(受け取った値: {sigma})"
    return black_scholes(S, K, T, r, sigma, option_type)

IVの逆算が収束しない

市場価格が本源的価値を下回っている場合、理論上そのIVは存在しません。板が薄いときの気配値をそのまま使うと起こりがちです。直近の約定値を使う、もしくは買い気配と売り気配の中値を使うことで回避できます。

満期直前で計算が不安定になる

Tが0に近づくと、ガンマの分母 S * sigma * sqrt(T) が0に近づき、値が発散します。残存1日を切ったら計算対象から外すのが実務的な対処です。この領域はモデルの適用範囲外だと割り切ってください。

残存期間を「暦日」で数えるか「営業日」で数えるか

意外と悩ましいのがここです。市場が閉まっている土日には、実質的に価格は動きません。にもかかわらず暦日で数えると、週末をまたぐだけで2日分の時間価値が失われる計算になります。実務では、暦日ベースを基本としつつ、金曜引け後の評価では週末分を割り引いて考える、といった調整をします。

import numpy as np

def business_days_to_expiry(today, expiry):
    """満期までの営業日数を数える(祝日は別途考慮が必要)"""
    return int(np.busday_count(today, expiry))


# 暦日と営業日で理論価格を比べてみる
cal = black_scholes(42000, 42000, 30 / 365, 0.01, 0.20, "call")
biz = black_scholes(42000, 42000, 21 / 252, 0.01, 0.20, "call")
print(f"暦日ベース (30/365) : {cal:,.1f} 円")
print(f"営業日ベース(21/252): {biz:,.1f} 円")
print(f"差             : {abs(cal - biz):,.1f} 円")

どちらが正解ということはありません。大切なのは自分の中で基準を統一し、比較のときに混ぜないことです。暦日で計算したIVと営業日で計算したIVを並べて「割安だ」と判断してしまうと、単なる計算方法の違いを優位性だと勘違いすることになります。

配当を無視してよいのか

本記事の実装は配当を考慮していません。日経225オプションの場合、指数先物の価格に配当の影響がすでに織り込まれているため、原資産に指数そのものではなく先物価格を使うことで実務上は対応できます。個別株オプションを扱う場合は、権利落ち日をまたぐかどうかで理論価格が変わるため、配当込みのモデルが必要になります。

まとめ

ブラック・ショールズとグリークスをPythonで実装し、数字の読み方まで見てきました。要点を整理します。

  • 5つの入力のうち未知数はボラティリティだけ。オプション取引とは、実質的にボラティリティの売買である
  • 実装したらプット・コール・パリティで必ず検算する
  • デルタは近似的に「イン・ザ・マネーで終わる確率」として読める
  • ガンマとセータは必ずトレードオフ。アット・ザ・マネーで両方が最大になる
  • 市場価格からIVを逆算し、実現ボラティリティと比べることで割高/割安を判断できる
  • イベント通過後のIVクラッシュにより、方向を当てても損をすることがある
  • 日経225オプションは指数の1,000倍。理論価格をそのまま損益額と読み違えない

最後に、モデルとの付き合い方について一言だけ。ブラック・ショールズは1970年代に生まれた式で、前提が現実と合っていない部分は多々あります。それでも半世紀にわたって使われ続けているのは、「同じ物差しで測る」ための共通言語として優秀だからです。IVという指標が成立するのも、全員が同じ式を使っているからにほかなりません。正解を出す装置ではなく、会話を成立させる装置だと捉えると、扱いを誤りにくくなります。

次の一歩としておすすめなのは、実際の日経225オプションの価格を取得して、各権利行使価格のIVをプロットしてみることです。教科書どおりの平坦な直線にはならず、はっきりとした歪みが現れます。モデルと現実の差を自分の目で確認すると、数式の意味が腹落ちします。手元のコードは今日書いたもので十分足りるので、あとはデータを流し込むだけです。

タイトルとURLをコピーしました