Volatility Surface

Volatility Surface#

In this notebook we illustrate the use of the Volatility Surface tool in the library. We use deribit options on BTCUSD as example.

First thing, fetch the data

from quantflow.data.deribit import Deribit

async with Deribit() as cli:
    loader = await cli.volatility_surface_loader("eth")

Once we have loaded the data, we create the surface and display the term-structure of forwards

vs = loader.surface()
vs.maturities = vs.maturities[1:]
vs.term_structure()
maturity ttm forward basis rate_percent open_interest volume
0 2025-01-10 08:00:00+00:00 0.028770 3409.625 11.000 11.79452 8557971 9974074
1 2025-01-31 08:00:00+00:00 0.086304 3430.00 31.375 10.81969 39346283 11685675
2 2025-02-28 08:00:00+00:00 0.163016 3460.00 61.375 11.07228 7327748 5916886
3 2025-03-28 08:00:00+00:00 0.239729 3485.75 87.125 10.61952 132334513 7865794
4 2025-06-27 08:00:00+00:00 0.489044 3585.50 186.875 10.97604 55403049 5259700
5 2025-09-26 08:00:00+00:00 0.738359 3677.25 278.625 10.69141 28519844 3107817
6 2025-12-26 08:00:00+00:00 0.987674 3766.875 368.250 10.43036 3132621 2113949
vs.spot
SpotPrice(security=<VolSecurityType.spot: 'spot'>, bid=Decimal('3398.60'), ask=Decimal('3398.65'), open_interest=385233113, volume=231072964)

bs method#

This method calculate the implied Black volatility from option prices. By default it uses the best option in the surface for the calculation.

The options_df method allows to inspect bid/ask for call options at a given cross section. Prices of options are normalized by the Forward price, in other words they are given as base currency price, in this case BTC.

Moneyness is defined as

(65)#\[\begin{equation} k = \log{\frac{K}{F}} \end{equation}\]
vs.bs()
df = vs.disable_outliers(0.95).options_df()
df
/home/runner/work/quantflow/quantflow/quantflow/options/bs.py:32: RuntimeWarning: overflow encountered in multiply
  sig2 = sigma * sigma * ttm
/home/runner/work/quantflow/quantflow/quantflow/options/bs.py:34: RuntimeWarning: invalid value encountered in divide
  d1 = (-k + 0.5 * sig2) / sig
/home/runner/work/quantflow/quantflow/quantflow/options/bs.py:45: RuntimeWarning: overflow encountered in multiply
  sig2 = sigma * sigma * ttm
/home/runner/work/quantflow/quantflow/quantflow/options/bs.py:47: RuntimeWarning: invalid value encountered in divide
  d1 = (-k + 0.5 * sig2) / sig
/home/runner/work/quantflow/quantflow/quantflow/options/bs.py:65: RuntimeWarning: some derivatives were zero
  return newton(
strike forward moneyness moneyness_ttm ttm implied_vol price price_bp forward_price call side
0 2700.0 3409.625 -0.233351 -1.375752 0.028770 0.840704 0.0027 27.0 9.205988 False bid
1 2800.0 3409.625 -0.196983 -1.161342 0.028770 0.794658 0.0039 39.0 13.297538 False bid
2 2800.0 3409.625 -0.196983 -1.161342 0.028770 0.817727 0.0044 44.0 15.002350 False ask
3 2900.0 3409.625 -0.161892 -0.954456 0.028770 0.762385 0.0060 60.0 20.457750 False bid
4 2900.0 3409.625 -0.161892 -0.954456 0.028770 0.779661 0.0065 65.0 22.162562 False ask
... ... ... ... ... ... ... ... ... ... ... ...
335 9000.0 3766.875 0.870979 0.876397 0.987674 0.816529 0.0860 860.0 323.951250 True ask
336 10000.0 3766.875 0.976339 0.982413 0.987674 0.812254 0.0685 685.0 258.030937 True bid
337 10000.0 3766.875 0.976339 0.982413 0.987674 0.820919 0.0710 710.0 267.448125 True ask
338 11000.0 3766.875 1.071650 1.078316 0.987674 0.825773 0.0595 595.0 224.129063 True bid
339 11000.0 3766.875 1.071650 1.078316 0.987674 0.831393 0.0610 610.0 229.779375 True ask

340 rows × 11 columns

The plot function is enabled only if plotly is installed

vs.plot().update_layout(height=500, title="BTC Volatility Surface")

The moneyness_ttm is defined as

(66)#\[\begin{equation} \frac{1}{\sqrt{T}} \ln{\frac{K}{F}} \end{equation}\]

where \(T\) is the time-to-maturity.

vs.plot3d().update_layout(height=800, title="BTC Volatility Surface", scene_camera=dict(eye=dict(x=1, y=-2, z=1)))

Model Calibration#

We can now use the Vol Surface to calibrate the Heston stochastic volatility model.

from quantflow.options.calibration import HestonCalibration, OptionPricer
from quantflow.sp.heston import Heston

pricer = OptionPricer(Heston.create(vol=0.5))
cal = HestonCalibration(pricer=pricer, vol_surface=vs, moneyness_weight=-0)
len(cal.options)
/home/runner/work/quantflow/quantflow/quantflow/options/bs.py:65: RuntimeWarning:

some derivatives were zero
189
cal.model
Heston(variance_process=CIR(rate=0.25, kappa=1.0, sigma=0.8, theta=0.25, sample_algo=<SamplingAlgorithm.implicit: 'implicit'>), rho=0.0)
cal.fit()
  message: CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH
  success: True
   status: 0
      fun: 0.0006518013907978364
        x: [ 5.740e-01  5.361e-01  4.633e+00  2.222e+00  0.000e+00]
      nit: 44
      jac: [ 1.110e-05 -1.039e-05  6.378e-06  6.241e-06 -9.146e-04]
     nfev: 486
     njev: 81
 hess_inv: <5x5 LbfgsInvHessProduct with dtype=float64>
pricer.model
Heston(variance_process=CIR(rate=np.float64(0.5739854856456917), kappa=np.float64(4.633482027251869), sigma=np.float64(2.2219023294883646), theta=np.float64(0.5360688369440264), sample_algo=<SamplingAlgorithm.implicit: 'implicit'>), rho=np.float64(-1e-08))
cal.plot(index=6, max_moneyness_ttm=1)

#

Serialization

It is possible to save the vol surface into a json file so it can be recreated for testing or for serialization/deserialization.

with open("../tests/volsurface.json", "w") as fp:
    fp.write(vs.inputs().model_dump_json())
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[12], line 1
----> 1 with open("../tests/volsurface.json", "w") as fp:
      2     fp.write(vs.inputs().model_dump_json())

File ~/.cache/pypoetry/virtualenvs/quantflow-lUXkzsy2-py3.12/lib/python3.12/site-packages/IPython/core/interactiveshell.py:324, in _modified_open(file, *args, **kwargs)
    317 if file in {0, 1, 2}:
    318     raise ValueError(
    319         f"IPython won't let you open fd={file} by default "
    320         "as it is likely to crash IPython. If you know what you are doing, "
    321         "you can use builtins' open."
    322     )
--> 324 return io_open(file, *args, **kwargs)

FileNotFoundError: [Errno 2] No such file or directory: '../tests/volsurface.json'
from quantflow.options.surface import VolSurfaceInputs, surface_from_inputs
import json

with  open("../tests/volsurface.json", "r") as fp:
    inputs = VolSurfaceInputs(**json.load(fp))

vs2 = surface_from_inputs(inputs)