Derivative Calibration

AbaQuant’s structured calibration layer fits model parameters to option observations. This notebook covers three common workflows: a flat Black–Scholes–Merton (BSM) volatility, a SABR smile, and a compact Heston stochastic-volatility fit — all against synthetic (offline) option-chain data, so the notebook is stable and reproducible.

Calibrated parameters are conditional estimates, not physical truths — inspect convergence status, residual scale, and bounds before trusting a fit. See docs/domains/assumptions.rst.

Sections:

  1. Build synthetic option-chain observations

  2. Fit BSM, SABR, and Heston

  3. Visual diagnostics

Setup

import abaquant
print(f"AbaQuant version: {abaquant.__version__}")
AbaQuant version: 1.0.0rc1
import pandas as pd

from abaquant.derivatives.calibration import (
    BSMFlatVolCalibration,
    HestonCalibration,
    SABRSmileCalibration,
)
from abaquant.derivatives.models import BlackScholesMertonModel, SABRVolatilityModel
from abaquant.visualization import VisualizationError

1. Build synthetic option-chain observations

A BSM-generated chain (one flat volatility across strikes) and a SABR-generated implied-volatility smile.

def build_bsm_chain() -> pd.DataFrame:
    spot_price, maturity_years, risk_free_rate = 100.0, 1.0, 0.03
    dividend_yield, volatility = 0.01, 0.24
    rows = []
    for strike in (85.0, 95.0, 100.0, 105.0, 115.0):
        model = BlackScholesMertonModel(
            spot_price, strike, maturity_years, risk_free_rate, volatility, dividend_yield
        )
        rows.append({
            "option_type": "call", "strike": strike, "market_price": model.call_price(),
            "implied_volatility": volatility, "spot_price": spot_price,
            "maturity_years": maturity_years, "open_interest": 100,
        })
    return pd.DataFrame(rows)


def build_sabr_smile() -> pd.DataFrame:
    forward_price, maturity_years = 100.0, 1.0
    rows = []
    for strike in (80.0, 90.0, 100.0, 110.0, 120.0):
        implied_volatility = SABRVolatilityModel(
            forward_price, strike, maturity_years, initial_volatility=0.32,
            elasticity_parameter=0.8, spot_forward_correlation=-0.2, volatility_of_volatility=0.55,
        ).implied_vol()
        rows.append({
            "option_type": "call", "strike": strike, "implied_volatility": implied_volatility,
            "spot_price": 100.0, "forward_price": forward_price,
            "maturity_years": maturity_years, "open_interest": 100,
        })
    return pd.DataFrame(rows)

bsm_chain = build_bsm_chain()
sabr_smile = build_sabr_smile()
bsm_chain
option_type strike market_price implied_volatility spot_price maturity_years open_interest
0 call 85.0 19.299087 0.24 100.0 1.0 100
1 call 95.0 12.935429 0.24 100.0 1.0 100
2 call 100.0 10.375567 0.24 100.0 1.0 100
3 call 105.0 8.217469 0.24 100.0 1.0 100
4 call 115.0 4.979341 0.24 100.0 1.0 100

2. Fit BSM, SABR, and Heston

Each calibration class exposes .fit(), returning a CalibrationResult with fitted .parameters, a fit .error, and diagnostic tables/figures.

bsm_result = BSMFlatVolCalibration(
    bsm_chain, spot_price=100.0, maturity_years=1.0, risk_free_rate=0.03,
    dividend_yield=0.01, objective="price",
).fit()

sabr_result = SABRSmileCalibration(
    sabr_smile, forward_price=100.0, maturity_years=1.0, beta=0.8,
    initial_parameters={"alpha": 0.25, "rho": -0.1, "nu": 0.4},
).fit()

heston_result = HestonCalibration(
    bsm_chain.iloc[[1, 2, 3]], spot_price=100.0, maturity_years=1.0, risk_free_rate=0.03,
    dividend_yield=0.01, objective="iv", max_contracts=3, max_iter=2,
).fit()

calibration_summary = {
    "bsm_flat_volatility": bsm_result.parameters["volatility"],
    "bsm_rmse": bsm_result.error,
    "sabr_alpha": sabr_result.parameters["alpha"],
    "sabr_rho": sabr_result.parameters["rho"],
    "sabr_nu": sabr_result.parameters["nu"],
    "heston_v0": heston_result.parameters["v0"],
    "heston_rmse": heston_result.error,
}
for key, value in calibration_summary.items():
    print(f"{key:22s}: {value}")
bsm_flat_volatility   : 0.2400000000092515
bsm_rmse              : 3.319670354898209e-10
sabr_alpha            : 0.3199137479032007
sabr_rho              : -0.19918677549345226
sabr_nu               : 0.5508451706498183
heston_v0             : 0.06199705789315314
heston_rmse           : 0.012227668712151134
bsm_result.summary()
{'model_name': 'bsm_flat_vol',
 'objective': 'price',
 'option_type': 'call',
 'success': True,
 'error': 3.319670354898209e-10,
 'mean_absolute_error': 3.292804251486814e-10,
 'max_absolute_error': 3.654037072919891e-10,
 'observations': 5,
 'parameter_volatility': 0.2400000000092515}
bsm_result.error_table()
strike moneyness option_type market_price market_implied_volatility model_price model_implied_volatility market_value model_value residual
0 85.0 1.176471 call 19.299087 0.24 19.299087 0.24 19.299087 19.299087 2.479936e-10
1 95.0 1.052632 call 12.935429 0.24 12.935429 0.24 12.935429 12.935429 3.349854e-10
2 100.0 1.000000 call 10.375567 0.24 10.375567 0.24 10.375567 10.375567 3.579288e-10
3 105.0 0.952381 call 8.217469 0.24 8.217469 0.24 8.217469 8.217469 3.654037e-10
4 115.0 0.869565 call 4.979341 0.24 4.979341 0.24 4.979341 4.979341 3.400906e-10

3. Visual diagnostics

Model-vs-market fit, residuals, and a SABR parameter chart.

try:
    figures = {
        "bsm_model_vs_market": bsm_result.visualize(chart="model_vs_market"),
        "bsm_residuals": bsm_result.visualize(chart="residuals"),
        "sabr_parameters": sabr_result.visualize(chart="parameters"),
    }
    print(f"Created {len(figures)} figures: {list(figures)}")
except VisualizationError as exc:
    print(f"Visualization skipped (optional dependency missing): {exc}")
Created 3 figures: ['bsm_model_vs_market', 'bsm_residuals', 'sabr_parameters']
../../_images/9580259aea5b10935f9298a5ca6fafa642a730aac09b315296a8278bae5c3ed1.png ../../_images/de541243c30c5417e0fce959c9db435155e69cbc4fe6dfbab1bdd6e2612bbd71.png ../../_images/55d6d49c5824a140e2b47f7e0fcff3ce8e282935475333766f91486321631f0c.png

Takeaway

OptionChainAnalytics also exposes calibrate_bsm_flat_vol, calibrate_sabr, and calibrate_heston convenience methods that reuse an existing chain-analytics object — handy when you’re already working with a live or offline chain from notebook 17 — Option-Chain Analytics.