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

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 77598.35911382852537831902685  29.60911382852537831902685 0.0082831            2.5    1.60911382852537831902685       6770230  2446790
2026-05-23 08:00:00+00:00 0.005243 77441.25 77600.17644995557007574824483 158.92644995557007574824483 0.0084216          372.5    3.42644995557007574824483         54290     6260
2026-05-24 08:00:00+00:00 0.007983  77552.5 77602.05240313828584006335962  49.55240313828584006335962 0.0085596            565    5.30240313828584006335962         10410    10150
2026-05-25 08:00:00+00:00 0.010723  77557.5 77603.98666746586987612894103  46.48666746586987612894103 0.0086971            565    7.23666746586987612894103             0        0
2026-05-29 08:00:00+00:00 0.021682 77616.25 77612.30073863617567043569158  -3.94926136382432956430842 0.0092422            2.5   15.55073863617567043569158      96001750  6722440
2026-06-05 08:00:00+00:00 0.040860 77623.75 77629.02520609156141245040027   5.27520609156141245040027 0.0101775            2.5   32.27520609156141245040027       8424810  2094940
2026-06-12 08:00:00+00:00 0.060038 77621.25 77648.42937291661505238917760  27.17937291661505238917760 0.0110893           97.5   51.67937291661505238917760          3160     3170
2026-06-26 08:00:00+00:00 0.098394 77698.75 77694.87088770245469964361377  -3.87911229754530035638623 0.0128433            2.5   98.12088770245469964361377     587314120 20900300
2026-07-31 08:00:00+00:00 0.194284 77838.75 77850.89691987975645944409471  12.14691987975645944409471 0.0168304           17.5  254.14691987975645944409471       5813390   845200
2026-09-25 08:00:00+00:00 0.347709 78168.75 78194.68078513916634502740600  25.93078513916634502740600 0.0220762            2.5  597.93078513916634502740600     289191460  3088870
2026-12-25 08:00:00+00:00 0.597024  78890.0 78897.02079649570464230284028   7.02079649570464230284028 0.0278346            5.0 1300.27079649570464230284028     125882130  1281370
2027-03-26 08:00:00+00:00 0.846339 79551.25 79624.86364768373006001578267  73.61364768373006001578267 0.0304852           17.5 2028.11364768373006001578267      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.4296027 0.4756759
2026-05-22 08:00:00+00:00  75000         put 0.0003 0.0004 0.3867201 0.4087615
2026-05-22 08:00:00+00:00  75500         put 0.0004 0.0006 0.3437437 0.3761185
2026-05-22 08:00:00+00:00  76000         put 0.0008 0.0011 0.3290328 0.3605039
2026-05-22 08:00:00+00:00  76500         put 0.0015 0.0019 0.3105115 0.3401456
2026-05-22 08:00:00+00:00  77000         put 0.0029 0.0033 0.3008198 0.3235688
2026-05-22 08:00:00+00:00  77500         put  0.005 0.0055 0.2812789 0.3064379
2026-05-22 08:00:00+00:00  78000        call 0.0032 0.0036  0.269719 0.2911088
2026-05-22 08:00:00+00:00  78500        call 0.0015 0.0019 0.2713976 0.2990624
2026-05-22 08:00:00+00:00  79000        call 0.0008 0.0009 0.2935673 0.3037445

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 discount factor \(D\) for that maturity.

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\). In many option markets, however, neither is quoted directly. Crypto options on Deribit are a clear example: there is no liquid term structure of interest rates and the forward for each expiry must 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 can extract \(D_q\) and \(D_a\) directly from the market prices of options using put-call parity. The calibrate_curves method supports three modes:

  • Both curves: pass a YieldCurve type for both quote_curve and asset_curve. A single OLS regression per maturity identifies \(D_q\) and \(D_a\) simultaneously from the slope and intercept.
  • Asset curve only: pass a type for asset_curve and leave quote_curve as None. The existing quote_curve on the loader is treated as known and \(D_a\) is computed analytically from each put-call pair using the known \(D_q\).
  • Quote curve only: pass a type for quote_curve and leave asset_curve as None. The same simultaneous OLS is run but only the quote discount factors are used to fit the curve.