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:
Build synthetic option-chain observations
Fit BSM, SABR, and Heston
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']
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.