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-17 08:00:00+00:00 0.029319 3707.875 11.100 10.94313 2996207 2034590
1 2025-01-31 08:00:00+00:00 0.067675 3725.75 28.975 11.87369 41237556 2050088
2 2025-02-28 08:00:00+00:00 0.144387 3760.25 63.475 11.94998 6021507 1229501
3 2025-03-28 08:00:00+00:00 0.221100 3789.375 92.600 11.28775 134754445 13851107
4 2025-06-27 08:00:00+00:00 0.470415 3899.625 202.850 11.40242 56171043 1705216
5 2025-09-26 08:00:00+00:00 0.719730 3999.50 302.725 10.96512 28441339 921999
6 2025-12-26 08:00:00+00:00 0.969045 4097.625 400.850 10.64463 3653430 198951
vs.spot
SpotPrice(security=<VolSecurityType.spot: 'spot'>, bid=Decimal('3696.75'), ask=Decimal('3696.80'), open_interest=368777456, volume=169863242)

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
strike forward moneyness moneyness_ttm ttm implied_vol price price_bp forward_price type side
0 2800.0 3707.875 -0.280840 -1.640158 0.029319 0.802613 0.0009 9.0 3.337087 put bid
1 2900.0 3707.875 -0.245748 -1.435218 0.029319 0.778100 0.0015 15.0 5.561813 put bid
2 2900.0 3707.875 -0.245748 -1.435218 0.029319 0.803914 0.0018 18.0 6.674175 put ask
3 3000.0 3707.875 -0.211847 -1.237226 0.029319 0.731426 0.0021 21.0 7.786537 put bid
4 3000.0 3707.875 -0.211847 -1.237226 0.029319 0.757386 0.0025 25.0 9.269687 put ask
... ... ... ... ... ... ... ... ... ... ... ...
324 9000.0 4097.625 0.786817 0.799285 0.969045 0.782265 0.0870 870.0 356.493375 call bid
325 9000.0 4097.625 0.786817 0.799285 0.969045 0.797723 0.0920 920.0 376.981500 call ask
326 10000.0 4097.625 0.892178 0.906315 0.969045 0.792830 0.0730 730.0 299.126625 call bid
327 10000.0 4097.625 0.892178 0.906315 0.969045 0.809534 0.0780 780.0 319.614750 call ask
328 11000.0 4097.625 0.987488 1.003136 0.969045 0.807663 0.0635 635.0 260.199187 call bid

329 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)
183
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.0008777594326965321
        x: [ 4.998e-01  5.725e-01  9.010e+00  3.093e+00 -1.361e-02]
      nit: 46
      jac: [ 5.577e-06 -3.834e-05 -1.021e-05  4.595e-05  1.268e-04]
     nfev: 468
     njev: 78
 hess_inv: <5x5 LbfgsInvHessProduct with dtype=float64>
pricer.model
Heston(variance_process=CIR(rate=np.float64(0.4997796694901599), kappa=np.float64(9.009565000894508), sigma=np.float64(3.093098620022631), theta=np.float64(0.5724934475451571), sample_algo=<SamplingAlgorithm.implicit: 'implicit'>), rho=np.float64(-0.013613159222352116))
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)