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.client import HttpClient

deribit_url = "https://test.deribit.com/api/v2/public/get_book_summary_by_currency"
async with HttpClient() as cli:
    futures = await cli.get(deribit_url, params=dict(currency="BTC", kind="future"))
    options = await cli.get(deribit_url, params=dict(currency="BTC", kind="option"))
from decimal import Decimal
from quantflow.options.surface import VolSurfaceLoader, VolSecurityType
from datetime import timezone
from dateutil.parser import parse

def parse_maturity(v: str):
    return parse(v).replace(tzinfo=timezone.utc, hour=8)
    
loader = VolSurfaceLoader()

for future in futures["result"]:
    if (bid := future["bid_price"]) and (ask := future["ask_price"]):
        maturity = future["instrument_name"].split("-")[-1]
        if maturity == "PERPETUAL":
            loader.add_spot(VolSecurityType.spot, bid=Decimal(bid), ask=Decimal(ask))
        else:
            loader.add_forward(
                VolSecurityType.forward,
                maturity=parse_maturity(maturity),
                bid=Decimal(str(bid)),
                ask=Decimal(str(ask))
            )

for option in options["result"]:
    if (bid := option["bid_price"]) and (ask := option["ask_price"]):
        _, maturity, strike, ot = option["instrument_name"].split("-")
        loader.add_option(
            VolSecurityType.option,
            strike=Decimal(strike),
            maturity=parse_maturity(maturity),
            call=ot == "C",
            bid=Decimal(str(bid)),
            ask=Decimal(str(ask))
        )
    

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
0 2024-04-05 08:00:00+00:00 0.031564 66111.25 382.50 19.25376
1 2024-04-26 08:00:00+00:00 0.089098 66957.5 1228.75 21.12627
2 2024-06-28 08:00:00+00:00 0.261701 59600.0 -6128.75 -37.60685
3 2024-09-27 08:00:00+00:00 0.511016 62738.75 -2990.00 -9.13621
4 2024-12-27 08:00:00+00:00 0.760331 59121.25 -6607.50 -13.96039

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

(68)#\[\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 50000.0 66111.25 -0.279316 -1.572176 0.031564 0.585179 0.0001 1.0 6.611125 False bid
1 56000.0 66111.25 -0.165987 -0.934286 0.031564 0.396275 0.0002 2.0 13.222250 False bid
2 58000.0 66111.25 -0.130896 -0.736769 0.031564 0.353862 0.0004 4.0 26.444500 False bid
3 58000.0 66111.25 -0.130896 -0.736769 0.031564 0.768443 0.0115 115.0 760.279375 False ask
4 59000.0 66111.25 -0.113801 -0.640550 0.031564 0.654031 0.0095 95.0 628.056875 False bid
... ... ... ... ... ... ... ... ... ... ... ...
183 62000.0 62738.75 -0.011845 -0.016570 0.511016 0.535214 0.1450 1450.0 9097.118750 False bid
184 62000.0 62738.75 -0.011845 -0.016570 0.511016 0.554995 0.1505 1505.0 9442.181875 False ask
185 85000.0 62738.75 0.303672 0.424803 0.511016 0.199854 0.0010 10.0 62.738750 True bid
186 90000.0 62738.75 0.360830 0.504761 0.511016 0.230823 0.0010 10.0 62.738750 True bid
187 40000.0 59121.25 -0.390711 -0.448079 0.760331 0.311763 0.0075 75.0 443.409375 False bid

188 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

(69)#\[\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
109
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()
/home/runner/work/quantflow/quantflow/quantflow/sp/heston.py:57: RuntimeWarning:

invalid value encountered in scalar divide

/home/runner/work/quantflow/quantflow/quantflow/sp/heston.py:58: RuntimeWarning:

invalid value encountered in scalar divide

/home/runner/work/quantflow/quantflow/quantflow/sp/heston.py:57: RuntimeWarning:

invalid value encountered in divide

/home/runner/work/quantflow/quantflow/quantflow/sp/heston.py:58: RuntimeWarning:

invalid value encountered in divide

/home/runner/work/quantflow/quantflow/quantflow/utils/marginal.py:269: RuntimeWarning:

invalid value encountered in divide

/home/runner/work/quantflow/quantflow/quantflow/utils/transforms.py:204: RuntimeWarning:

invalid value encountered in divide
  message: ABNORMAL_TERMINATION_IN_LNSRCH
  success: False
   status: 2
      fun: nan
        x: [ 6.055e-01  2.180e-01  5.249e-04  1.327e+00  0.000e+00]
      nit: 33
      jac: [-4.874e-05  1.208e-06  1.927e-03 -5.804e-04 -1.448e-02]
     nfev: 372
     njev: 62
 hess_inv: <5x5 LbfgsInvHessProduct with dtype=float64>
pricer.model
Heston(variance_process=CIR(rate=0.6055216151845482, kappa=0.0, sigma=1.3279208512904284, theta=0.2180423558594295, sample_algo=<SamplingAlgorithm.implicit: 'implicit'>), rho=-1e-08)
cal.plot(index=4, 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)