Skip to content

Volatility Surface

This tutorial covers the full workflow for building an implied volatility surface: fetching option quotes from Deribit, extracting implied forwards and discount factors from option prices, and inspecting the surface inputs.

Fetching Data from Deribit

The Deribit client exposes a high-level volatility_surface_loader method that fetches all option quotes for a given asset and assembles them into a VolSurfaceLoader:

import asyncio
from quantflow.data.deribit import Deribit

async def load():
    async with Deribit() as cli:
        loader = await cli.volatility_surface_loader("btc")
    return loader

loader = asyncio.run(load())

Key parameters of volatility_surface_loader:

Parameter Default Description
asset required Underlying asset, e.g. "btc", "eth", "sol"
inverse True Inverse options (settled in the underlying)
use_perp False Derive spot from the perpetual contract
exclude_open_interest 0 Drop strikes with open interest below this threshold

Building the Surface

Before creating the surface, calibrate the forwards and discount curves with calibrate_curves:

loader.calibrate_curves()

This step infers the forward price of each maturity from put-call parity and fits the asset discount curve to the resulting discount factors. The surface prices options off these calibrated forwards: skipping the call leaves the surface with forwards taken from the raw futures quotes, which are often illiquid or stale.

See Extracting Forwards and Discount Factors below for how the calibration works.

The loader holds the raw market data. Call surface() to construct a VolSurface:

surface = loader.surface()

Then run bs() to populate implied volatilities via Black-Scholes inversion:

surface.bs()

bs() solves for the implied volatility that matches each bid and ask price and marks each option as converged or not.

Removing Outliers

Raw option quotes often contain illiquid or stale prices that produce unrealistic implied volatilities. disable_outliers() removes them in two passes per maturity.

surface.disable_outliers()

Inspecting Surface Inputs

The examples below use a saved snapshot of a real ETH surface. The workflow is identical for a live surface fetched from Deribit.

import json

import pandas as pd

from docs.examples._utils import FIXTURES
from quantflow.options.inputs import OptionInput
from quantflow.options.surface import VolSurface, VolSurfaceInputs, surface_from_inputs

# Load a saved volatility surface snapshot from JSON
with open(FIXTURES / "volsurface_btc.json") as fp:
    surface_inputs = VolSurfaceInputs(**json.load(fp))

# Build the VolSurface from the inputs and calculate implied volatilities
surface: VolSurface = surface_from_inputs(surface_inputs)
surface.bs()
surface.disable_outliers()

# Print the term structure (forward prices and implied rates per maturity)
print(surface.term_structure().to_string(index=False))

# Display the surface inputs for converged options only
inputs = surface.inputs(converged=True)
option_inputs = [i for i in inputs.inputs if isinstance(i, OptionInput)]
df = pd.DataFrame([i.model_dump() for i in option_inputs])
print("\n\n10 Converged option inputs")
print(
    df[["maturity", "strike", "option_type", "bid", "ask", "iv_bid", "iv_ask"]]
    .head(10)
    .to_string(index=False)
)

term_structure() shows forward prices and the interest rate implied by the forward-spot basis for each maturity. The option inputs table lists the bid/ask prices together with the corresponding implied volatilities for each strike:

                 maturity      ttm  forward implied_forward forward_basis       rate bid_ask_spread   basis open_interest   volume
2026-05-22 08:00:00+00:00 0.002503 77568.75        77568.75          0.00 -0.1441613            2.5  -28.00       6770230  2446790
2026-05-23 08:00:00+00:00 0.005243 77441.25        77441.25          0.00 -0.3825828          372.5 -155.50         54290     6260
2026-05-24 08:00:00+00:00 0.007983  77552.5         77552.5           0.0 -0.0714548            565  -44.25         10410    10150
2026-05-25 08:00:00+00:00 0.010723  77557.5         77557.5           0.0 -0.0471850            565  -39.25             0        0
2026-05-29 08:00:00+00:00 0.021682 77616.25        77616.25          0.00  0.0115890            2.5   19.50      96001750  6722440
2026-06-05 08:00:00+00:00 0.040860 77623.75        77623.75          0.00  0.0085143            2.5   27.00       8424810  2094940
2026-06-12 08:00:00+00:00 0.060038 77621.25        77621.25          0.00  0.0052581           97.5   24.50          3160     3170
2026-06-26 08:00:00+00:00 0.098394 77698.75        77698.75          0.00  0.0133507            2.5  102.00     587314120 20900300
2026-07-31 08:00:00+00:00 0.194284 77838.75        77838.75          0.00  0.0160272           17.5  242.00       5813390   845200
2026-09-25 08:00:00+00:00 0.347709 78168.75        78168.75          0.00  0.0211223            2.5  572.00     289191460  3088870
2026-12-25 08:00:00+00:00 0.597024  78890.0         78890.0           0.0  0.0276855            5.0 1293.25     125882130  1281370
2027-03-26 08:00:00+00:00 0.846339 79551.25        79551.25          0.00  0.0293924           17.5 1954.50      16356610  1525090


10 Converged option inputs
                 maturity strike option_type    bid    ask    iv_bid    iv_ask
2026-05-22 08:00:00+00:00  74000         put 0.0001 0.0002 0.4266102   0.47244
2026-05-22 08:00:00+00:00  75000         put 0.0003 0.0004 0.3831767 0.4050766
2026-05-22 08:00:00+00:00  75500         put 0.0004 0.0006 0.3399481 0.3720871
2026-05-22 08:00:00+00:00  76000         put 0.0008 0.0011 0.3246269 0.3558524
2026-05-22 08:00:00+00:00  76500         put 0.0015 0.0019 0.3052505 0.3346447
2026-05-22 08:00:00+00:00  77000         put 0.0029 0.0033 0.2941343 0.3167239
2026-05-22 08:00:00+00:00  77500         put  0.005 0.0055  0.272236 0.2973453
2026-05-22 08:00:00+00:00  78000        call 0.0032 0.0036  0.276864 0.2983897
2026-05-22 08:00:00+00:00  78500        call 0.0015 0.0019 0.2767819 0.3046884
2026-05-22 08:00:00+00:00  79000        call 0.0008 0.0009 0.2980222 0.3082881

Serialising and Restoring

inputs() serialises the surface to a VolSurfaceInputs object — a list of SpotInput, ForwardInput, and OptionInput records — that can be stored or transmitted as JSON and later reconstructed via surface_from_inputs:

from quantflow.options.surface import surface_from_inputs

inputs = surface.inputs(converged=True)   # VolSurface -> VolSurfaceInputs
surface2 = surface_from_inputs(inputs)    # VolSurfaceInputs -> VolSurface

Extracting Forwards and Discount Factors

Pricing an option requires two market inputs beyond the option price itself: the forward price \(F\) of the underlying at expiry, and the quote discount factor \(D_q\) for that maturity, where the quote currency is usually USD.

In liquid markets these quantities are directly observable. Futures and forward contracts give \(F\) outright, and interest rate swaps or government bond strips give \(D_q\). In many option markets, however, neither is quoted directly.

Deribit Forward

Crypto options on Deribit are a clear example. There is no liquid term structure of interest rates, and while Deribit quotes futures for each expiry, they are often illiquid, with wide bid-ask spreads and stale or outright wrong prices.

The forward for each expiry must therefore be inferred from the options themselves.

Even when forwards are available, the discount factor used to value options may differ from the rate implied by the forward-spot basis. For equity options the carry includes dividends and repo costs that are not captured by a simple interest rate curve. For crypto inverse options the discount factor reflects funding in the underlying asset rather than in dollars.

For these reasons, quantflow extracts \(D_q\) and \(D_a\) directly from the market prices of options using put-call parity.

Put-call parity and the implied forward

For each maturity, the parity relationship is fitted in the normalized form:

\[\begin{equation} \frac{C - P}{S} = D_a - D_q \frac{K}{S} \end{equation}\]

where \(S\) is the spot price and \(D_a\) the asset discount factor. The same equation holds for inverse options with the left hand side replaced by \(c - p\), the price difference in units of the underlying.

The price difference is linear in the strike, so a regression across strikes identifies \(D_a\) and \(D_q\), and the line crosses zero exactly at the forward:

\[\begin{equation} F = S \frac{D_a}{D_q} \end{equation}\]

put_call_parities collects the most liquid pairs at each maturity, ranked by the bid-ask spread of the parity price, and calibrate_forward fits the regression and returns the implied forward.

Discount curve calibration

The calibrate_curves method builds the discount curves on top of the calibrated forwards. With the forward of each maturity held fixed, put-call parity identifies the quote discount factor \(D_q\) as its only remaining parameter, and the asset discount factor follows from the forward formula \(D_a = D_q F / S\).

  • Quote curve: pass a YieldCurve type for quote_curve to fit it to the per maturity quote discount factors. Leave it as None to keep the current quote curve and treat it as known: this is the setup for exchanges that settle without discounting, such as Deribit, where the quote curve is a NoDiscountCurve with \(D_q = 1\) at every maturity.
  • Asset curve: always fitted to the discount factors \(D_a = D_q F / S\). It cannot be a no discount curve, since the parity forwards and the quote curve define it.

See the curve calibration tutorial for how the forwards and the discount factor split are estimated.