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:
Then run bs() to populate implied volatilities via Black-Scholes inversion:
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.
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_curveandasset_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_curveand leavequote_curveasNone. The existingquote_curveon 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_curveand leaveasset_curveasNone. The same simultaneous OLS is run but only the quote discount factors are used to fit the curve.