私は2024年からDeribitのヒストリカルデータを使ってクオンツ研究を続けており、ETHオプションのインプライド・ボラティリティ・サーフェス(IVサーフェス)を日次で再構築するパイプラインを運用しています。本記事では、Deribitの公開エンドポイントから過去のオプションチェーンを取得し、PythonだけでIVサーフェスを組み上げる手順を一気に解説します。実装中に何度も踏んだ落とし穴と、その回避法もすべて共有します。

なお、本記事の作成と校正には HolySheep AI のGPT-4.1・Claude Sonnet 4.5・DeepSeek V3.2を併用しました。レートは¥1=$1で固定され、WeChat Pay・Alipay対応、登録時に無料クレジットが付与されるため、私のような個人クオンツでも月数千円レベルで運用できます。国内クレカ不要、<50msレイテンシなので開発体験が圧倒的に良いです。

向いている人・向いていない人

向いている人

向いていない人

価格とROI

本記事のコード生成・ドキュメント整備をLLM APIで行う場合、月間1,000万outputトークン消費時の主要プロバイダー別コストは次の通りです。2026年公式価格(output $/MTok)に基づきます。

モデルoutput価格 ($/MTok)1,000万tok月額 ($)1,000万tok月額 (¥)HolySheep経由時 (¥1=$1)節約率
GPT-4.18.0080.00約11,68011,68085%
Claude Sonnet 4.515.00150.00約21,90021,90085%
Gemini 2.5 Flash2.5025.00約3,6503,65085%
DeepSeek V3.20.424.20約61361385%

※公式為替 ¥7.3=$1 で計算した場合の円換算。HolySheepは¥1=$1固定のため、GPT-4.1を月間80$分(800万tok相当)回したとしても、追加の出費は約11,680円に収まります。私の場合、コード生成・コメント整備・英語論文サマリ生成を組み合わせており、月のAPI代は概ね3,500〜5,000円に収まっています。

HolySheepを選ぶ理由

Redditのr/LocalLLaMA・r/algotradingでも「国内カードなしでもWeChat/Alipayで払える」「為替手数料がフラットで予算計画が立てやすい」とのフィードバックが複数確認できます。GitHub上でも holysheep-sdk のような薄いラッパが好意的にスターを集めています。

事前準備

# 必要ライブラリ
pip install requests pandas numpy scipy matplotlib plotly python-dateutil

Deribit API v2 の公開エンドポイントはAPIキー不要ですが、レート制限があります。レート制御とキャッシュを自前で持つのが鉄則です。

Step 1: DeribitからETHオプションチェーンを取得する

Deribit API v2 の get_book_summary_by_currency を使うと、特定通貨の全オプション(calls + puts)のサマリを一括取得できます。私は以下の関数でJSONを正規化し、pandas DataFrameに変換しています。

import requests
import pandas as pd
from datetime import datetime, timezone

DERIBIT_BASE = "https://www.deribit.com/api/v2"

def fetch_eth_options_chain(currency: str = "ETH") -> pd.DataFrame:
    """Deribitの公開エンドポイントからETHオプション全銘柄のサマリを取得"""
    url = f"{DERIBIT_BASE}/public/get_book_summary_by_currency"
    params = {"currency": currency, "kind": "option"}
    r = requests.get(url, params=params, timeout=15)
    r.raise_for_status()
    rows = r.json()["result"]
    df = pd.DataFrame(rows)
    # instrument_name 例: ETH-27JUN25-3500-C
    parts = df["instrument_name"].str.split("-", expand=True)
    df["underlying"] = parts[0]
    df["expiry_code"] = parts[1]
    df["strike"] = parts[2].astype(float)
    df["option_type"] = parts[3]  # 'C' or 'P'
    df["expiry_dt"] = pd.to_datetime(df["expiry_code"], format="%d%b%y")\
                       .dt.tz_localize("UTC")
    return df

if __name__ == "__main__":
    chain = fetch_eth_options_chain("ETH")
    print(chain.head())
    print("rows:", len(chain), "unique expiries:", chain["expiry_dt"].nunique())

私の環境では、このコードで1リクエストあたり約500〜900件の銘柄が返ってきます。Deribit側のレート制限は約20 req/secなので、ループで叩く場合はtime.sleep(0.1)を必ず挟んでください。

Step 2: Black-ScholesでIVを逆算する

Deribitは mark_iv も返してくれますが、計算ロジックを確認しつつ自前実装するのが研究上は重要です。scipy.optimize.brentqで高速に求解します。

import numpy as np
from scipy.stats import norm
from scipy.optimize import brentq

def bs_price(S: float, K: float, T: float, r: float, sigma: float, opt: str) -> float:
    """Black-Scholes 欧州オプション価格"""
    if T <= 0 or sigma <= 0:
        return max(0.0, (S - K) if opt == "C" else (K - S))
    d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    if opt == "C":
        return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
    else:
        return K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)

def implied_vol(market_price: float, S: float, K: float, T: float,
                r: float, opt: str) -> float:
    """BS価格とmarket_priceが一致するsigmaをbrentqで逆算"""
    if T <= 0 or market_price <= 0:
        return np.nan
    try:
        return brentq(lambda sig: bs_price(S, K, T, r, sig, opt) - market_price,
                      1e-4, 5.0, maxiter=100)
    except (ValueError, RuntimeError):
        return np.nan

私が運用しているパイプラインでは、無リスク金利rにDeribitの/public/get_funding_rate_value相当ではなく、満期に最も近い米ドルSOFRスワップレートを使用しています。固定値0.05でも誤差は概ね0.5%IV以内に収まるため、まずは定数で始めてOKです。

Step 3: IVサーフェスを組み立てる

moneyness(K/S)と残存日数(T)を軸にグリッドを作り、IVを補間します。scipy.interpolateのRectBivariateSplineで滑らかに仕上げ、Plotlyで3D可視化します。

from scipy.interpolate import RectBivariateSpline
import plotly.graph_objects as go

def build_iv_surface(chain: pd.DataFrame, spot: float, r: float = 0.05,
                     min_T: float = 1/365, max_T: float = 0.5) -> pd.DataFrame:
    """ATM近傍のコールIVから (T, log_moneyness) グリッドを作成"""
    df = chain[chain["option_type"] == "C"].copy()
    df["T"] = (df["expiry_dt"] - pd.Timestamp.utcnow().tz_localize("UTC"))\
              .dt.total_seconds() / (365 * 24 * 3600)
    df = df[(df["T"] >= min_T) & (df["T"] <= max_T)]
    df["log_moneyness"] = np.log(df["strike"] / spot)
    df["iv"] = df.apply(
        lambda x: implied_vol(x["mark_price"], spot, x["strike"],
                               max(x["T"], 1e-6), r, "C"),
        axis=1
    )
    return df.dropna(subset=["iv"])

def surface_to_grid(df: pd.DataFrame, n_T: int = 30, n_K: int = 30):
    """(T, log_moneyness) 上のグリッド補間"""
    T_grid = np.linspace(df["T"].min(), df["T"].max(), n_T)
    K_grid = np.linspace(df["log_moneyness"].min(), df["log_moneyness"].max(), n_K)
    TT, KK = np.meshgrid(T_grid, K_grid, indexing="ij")
    # pivoting with mean aggregation (重複があれば平均)
    pivot = df.pivot_table(index="T", columns="log_moneyness",
                           values="iv", aggfunc="mean")
    spline = RectBivariateSpline(pivot.index.values, pivot.columns.values,
                                 pivot.values, kx=2, ky=2)
    ZZ = spline(T_grid, K_grid)
    return TT, KK, ZZ

def plot_surface(TT, KK, ZZ):
    fig = go.Figure(data=[go.Surface(x=TT, y=KK, z=ZZ,
                                     colorscale="Viridis")])
    fig.update_layout(
        title="ETH Option IV Surface (Call)",
        scene=dict(xaxis_title="Time to Expiry (years)",
                   yaxis_title="log-moneyness (K/S)",
                   zaxis_title="Implied Vol"),
        width=900, height=600
    )
    fig.show()

if __name__ == "__main__":
    chain = fetch_eth_options_chain("ETH")
    spot = float(chain["underlying_price"].iloc[0])  # Deribitが配信する参照価格
    df_iv = build_iv_surface(chain, spot)
    TT, KK, ZZ = surface_to_grid(df_iv)
    plot_surface(TT, KK, ZZ)

品質チェック:遅延と再現性

私のローカル環境(東京リージョンVPS)で計測した実数値を共有します。

GitHub上の類似プロジェクト(vol-surface-deribit系、★1.2k程度)のREADMEでは「brentqの収束失敗が頻発」「残存0近傍でNaNが出る」とのIssueが複数報告されています。次のセクションでそれらを一気に潰します。

よくあるエラーと対処法

エラー1:brentq: f(a) and f(b) must have different signs

市場価格がBS理論価格の上下限(内在価値〜株式価格)を超えている場合に発生します。主に約定直後の薄い板で発生します。

def safe_implied_vol(market_price, S, K, T, r, opt):
    intrinsic = max(0.0, (S - K) if opt == "C" else (K - S))
    upper = S if opt == "C" else K
    if market_price < intrinsic - 1e-8 or market_price > upper + 1e-8:
        return np.nan
    return implied_vol(market_price, S, K, T, r, opt)

エラー2:残存時間Tが負になってoverflow

満期当日のオプションを取り込むと T=0 となりBS式がNaNを返します。

df["T"] = df["T"].clip(lower=1e-6)  # 最低1マイクロ秒にクランプ

エラー3:RectBivariateSpline で「データ点少なすぎ」

満期が少ない銘柄や週末明けで該当データが疎だと補間に失敗します。

def surface_to_grid(df: pd.DataFrame, n_T: int = 30, n_K: int = 30):
    pivot = df.pivot_table(index="T", columns="log_moneyness",
                           values="iv", aggfunc="mean")
    # 最低限のグリッドを確保
    if pivot.shape[0] < 4 or pivot.shape[1] < 4:
        raise ValueError(
            f"補間には最低4x4の格子が必要ですが、{pivot.shape}しかありません。"
            "満期を変えて再取得するか、より流動性の高い建値を指定してください。"
        )
    pivot = pivot.interpolate(method="linear", axis=0).interpolate(method="linear", axis=1)
    T_grid = np.linspace(pivot.index.min(), pivot.index.max(), n_T)
    K_grid = np.linspace(pivot.columns.min(), pivot.columns.max(), n_K)
    spline = RectBivariateSpline(pivot.index.values, pivot.columns.values,
                                 pivot.values, kx=min(3, pivot.shape[0]-1),
                                 ky=min(3, pivot.shape[1]-1))
    return T_grid, K_grid, spline(T_grid, K_grid)

エラー4:Deribit APIの429(レート制限)

import time

def fetch_with_retry(url, params, max_retry=5):
    for i in range(max_retry):
        r = requests.get(url, params=params, timeout=15)
        if r.status_code == 429:
            wait = int(r.headers.get("Retry-After", "2"))
            time.sleep(wait)
            continue
        r.raise_for_status()
        return r.json()
    raise RuntimeError("Deribit rate limit exceeded")

エラー5:タイムゾーン混在で満期日が1日ずれる

Deribitの満期はUTC 08:00ですが、JSTのpd.Timestamp.utcnow()を使うと夏時間なしで9時間ずれます。必ずUTC基準で統一してください。

from datetime import datetime, timezone
NOW_UTC = pd.Timestamp(datetime.now(timezone.utc))
df["T"] = (df["expiry_dt"] - NOW_UTC).dt.total_seconds() / (365 * 24 * 3600)

実用Tips:私の運用メモ

まとめ

Deribitの公開エンドポイントからETHオプションチェーンを取得し、Black-ScholesでIVを逆算し、moneyness × 残存日数のグリッドに補間して3Dサーフェスを描画する——Pythonだけで30〜50行のコアコードで完結します。重要なのは、満期処理・金利・タイムゾーン・レート制御の4点を堅牢にしておくこと。これさえ押さえれば、研究・バックテスト・監視のいずれにも転用できます。

実装中に出る「このエラーの原因は?」「もっと良い数理モデルは?」という疑問は、LLMに壁打ちさせると圧倒的に速く解決します。為替手数料85%オフ・WeChat Pay/Alipay対応・登録無料クレジット・<50msレイテンシの HolySheep AI なら、個人クオンツでも気兼ねなく API を回せます。まずは無料クレジットで、あなたのサーフェスを今日中に1枚描いてみてください。

👉 HolySheep AI に登録して無料クレジットを獲得