[5]:
from hestonpy.models.heston import Heston
from hestonpy.models.calibration.volatilitySmile import VolatilitySmile
from hestonpy.option.data import get_options_data, filter_data_for_maturity

import matplotlib.pyplot as plt
import matplotlib.font_manager as font_manager

fontdict_title = {
    'fontsize': 20,
    'fontweight': 'bold'
    }
from datetime import datetime
import numpy as np

Calibration of Heston models on market data#

We will calibrate our models on S&P smiles. You can also try with Apple, but there is less liquidity on the market, so less available maturities. Some parameters,

[6]:
symbol = '^SPX'
all_market_data, spot, maturities = get_options_data(symbol)
if symbol == '^SPX':
    considered_maturities = [maturities[7], maturities[14], maturities[28], maturities[38]]
else:
    considered_maturities = [maturities[1], maturities[3], maturities[8], maturities[14]]

r = 0.00
params = {
    "vol_initial": 0.06,
    "kappa": 1.25,
    "theta": 0.06,
    "drift_emm": 0.00,
    "sigma": 0.6,
    "rho": -0.8,
}

For each maturity/smile we filter the data (based on bid-ask spread, the moneyness etc.), then we calibrate with a local optimiser to initialise our global optimiser.

[8]:
fig, axs = plt.subplots(2, 2, layout="constrained")
fig.suptitle(f'Market smiles: {symbol}', **fontdict_title)

############################################################
##### Absolute
############################################################
for maturity, ax in zip(considered_maturities, axs.flatten()):

    print("="*80)
    print(maturity)
    print("="*80,"\n")

    ####################################
    ### Getting and filtering data
    ####################################

    full_market_data = filter_data_for_maturity(all_market_data, maturity)
    time_to_maturity = full_market_data['Time to Maturity'].iloc[0]
    strikes = full_market_data['Strike'].values
    bid_prices = full_market_data["Bid"].values
    ask_prices = full_market_data['Ask'].values
    market_ivs = full_market_data['Implied Volatility'].values
    market_prices = full_market_data['Call Price'].values

    marketVolatilitySmile = VolatilitySmile(
        strikes=strikes,
        time_to_maturity=time_to_maturity,
        atm=spot,
        market_ivs=market_ivs,
        r=r
    )
    market_data = marketVolatilitySmile.filters(full_market_data, select_mid_ivs=True)

    ####################################
    ### Calibration
    ####################################
    heston = Heston(spot=spot, r=r, **params)
    initial_params = marketVolatilitySmile.calibration(
        price_function=heston.call_price,
        guess_correlation_sign='unknown',
        initial_guess=[params['kappa'], params['theta'], params['sigma'], params['rho']],
        speed='local',
    )

    # Absolute calibration
    calibrated_params = marketVolatilitySmile.calibration(
        relative_errors=False,
        price_function=heston.call_price,
        guess_correlation_sign='unknown',
        initial_guess=[initial_params['kappa'], initial_params['theta'], initial_params['sigma'], initial_params['rho']],
        speed='global',
        power='mse'
    )
    calibrated_prices = heston.call_price(
        strike=marketVolatilitySmile.strikes, time_to_maturity=time_to_maturity, **calibrated_params
    )
    print(marketVolatilitySmile.evaluate_calibration(calibrated_prices, 'price'))
    calibrated_ivs = marketVolatilitySmile.compute_smile(prices=calibrated_prices)
    print(marketVolatilitySmile.evaluate_calibration(calibrated_ivs, 'iv'))

    # Relative calibration
    calibrated_params_relative = marketVolatilitySmile.calibration(
        relative_errors=True,
        price_function=heston.call_price,
        guess_correlation_sign='negative',
        initial_guess=[initial_params['kappa'], initial_params['theta'], initial_params['sigma'], initial_params['rho']],
        speed='global',
        power='mse'
    )
    calibrated_prices_relative = heston.call_price(
        strike=marketVolatilitySmile.strikes, time_to_maturity=time_to_maturity, **calibrated_params_relative
    )
    print(marketVolatilitySmile.evaluate_calibration(calibrated_prices_relative, 'price'))
    calibrated_ivs_relative = marketVolatilitySmile.compute_smile(prices=calibrated_prices_relative)
    print(marketVolatilitySmile.evaluate_calibration(calibrated_ivs_relative, 'iv'))

    # Some plots
    ask_ivs = market_data['Ask ivs'].values
    bid_ivs = market_data['Bid ivs'].values
    forward = marketVolatilitySmile.atm * np.exp(marketVolatilitySmile.r * marketVolatilitySmile.time_to_maturity)

    if ax == axs.flatten()[-2]:
        ax.axvline(1, linestyle="--", color="gray", label="ATM")
        ax.plot(marketVolatilitySmile.strikes / forward, calibrated_ivs, label="absolute calibration", marker='+', color='blue', linestyle="dotted", markersize=4)
        ax.plot(marketVolatilitySmile.strikes / forward, calibrated_ivs_relative, label="relative calibration", marker='+', color='green', linestyle="dotted", markersize=4)
        ax.scatter(marketVolatilitySmile.strikes / forward, marketVolatilitySmile.market_ivs, label="mid", marker='o', color='red', s=20)
        ax.scatter(marketVolatilitySmile.strikes / forward, bid_ivs, label="bid", marker=6, color='black', s=20)
        ax.scatter(marketVolatilitySmile.strikes / forward, ask_ivs, label="ask", marker=7, color='gray', s=20)
        ax.legend(loc='upper right', ncol=2, alignment='left')
    else:
        ax.axvline(1, linestyle="--", color="gray")
        ax.plot(marketVolatilitySmile.strikes / forward, calibrated_ivs, marker='+', color='blue', linestyle="dotted", markersize=4)
        ax.plot(marketVolatilitySmile.strikes / forward, calibrated_ivs_relative, marker='+', color='green', linestyle="dotted", markersize=4)
        ax.scatter(marketVolatilitySmile.strikes / forward, marketVolatilitySmile.market_ivs, marker='o', color='red', s=20)
        ax.scatter(marketVolatilitySmile.strikes / forward, bid_ivs, marker=6, color='black', s=20)
        ax.scatter(marketVolatilitySmile.strikes / forward, ask_ivs, marker=7, color='gray', s=20)

    ax.set_xlabel("Moneyness [%]")
    ax.set_ylabel("Implied Volatility [%]")

    date = datetime.strptime(maturity, '%Y-%m-%d').date().strftime("%d-%B-%y")
    title = f"{date}: {marketVolatilitySmile.time_to_maturity * 252 / 5:.1f} semaines"
    ax.set_title(title)
    ax.grid(visible=True, which="major", linestyle="--", dashes=(5, 10), color="gray", linewidth=0.5, alpha=0.8)

plt.show()
================================================================================
2025-04-15
================================================================================

Calibrated parameters: v0=0.031 | kappa=1.335 | theta=0.088 | sigma=0.395 | rho=1.000

at minimum 0.183945 accepted 1
Parameters: kappa=1.335 | theta=0.088 | sigma=0.395 | rho=1.000

at minimum 0.183928 accepted 1
Parameters: kappa=1.165 | theta=0.096 | sigma=0.394 | rho=1.000

at minimum 0.183817 accepted 1
Parameters: kappa=0.054 | theta=1.405 | sigma=0.391 | rho=1.000

at minimum 0.183817 accepted 1
Parameters: kappa=0.057 | theta=1.337 | sigma=0.391 | rho=1.000

at minimum 0.183816 accepted 1
Parameters: kappa=0.041 | theta=1.825 | sigma=0.390 | rho=1.000

at minimum 0.183816 accepted 1
Parameters: kappa=0.046 | theta=1.651 | sigma=0.390 | rho=1.000

at minimum 0.183817 accepted 1
Parameters: kappa=0.050 | theta=1.530 | sigma=0.390 | rho=1.000

at minimum 0.187207 accepted 1
Parameters: kappa=0.038 | theta=2.146 | sigma=0.429 | rho=0.888

at minimum 0.183816 accepted 1
Parameters: kappa=0.043 | theta=1.773 | sigma=0.390 | rho=1.000

at minimum 0.183815 accepted 1
Parameters: kappa=0.037 | theta=2.064 | sigma=0.390 | rho=1.000

at minimum 0.183815 accepted 1
Parameters: kappa=0.033 | theta=2.282 | sigma=0.390 | rho=1.000

['requested number of basinhopping iterations completed successfully'] True
Calibrated parameters: v0=0.031 | kappa=0.033 | theta=2.282 | sigma=0.390 | rho=1.000

{'MSE': np.float64(0.184), 'RMSE': np.float64(0.429), 'MAE': np.float64(0.311), 'MSE_%': np.float64(4.567), 'RMSE_%': np.float64(2.137), 'MAE_%': np.float64(13.576)}
{'MSE': np.float64(0.498), 'RMSE': np.float64(0.706), 'MAE': np.float64(0.471), 'MSE_%': np.float64(0.089), 'RMSE_%': np.float64(0.299), 'MAE_%': np.float64(2.142)}
at minimum 0.110745 accepted 1
Parameters: kappa=1.406 | theta=0.691 | sigma=0.001 | rho=-0.623

at minimum 0.110778 accepted 1
Parameters: kappa=0.996 | theta=0.947 | sigma=0.001 | rho=-0.425

at minimum 0.014858 accepted 1
Parameters: kappa=10.000 | theta=0.386 | sigma=5.081 | rho=-0.673

at minimum 0.014883 accepted 1
Parameters: kappa=9.524 | theta=0.405 | sigma=5.085 | rho=-0.675

at minimum 0.014868 accepted 1
Parameters: kappa=9.996 | theta=0.370 | sigma=4.912 | rho=-0.662

at minimum 0.994388 accepted 1
Parameters: kappa=8.634 | theta=0.004 | sigma=5.284 | rho=-0.999

at minimum 0.014904 accepted 1
Parameters: kappa=9.247 | theta=0.429 | sigma=5.204 | rho=-0.684

at minimum 0.014966 accepted 1
Parameters: kappa=8.493 | theta=0.479 | sigma=5.323 | rho=-0.694

['success condition satisfied'] True
Calibrated parameters: v0=0.031 | kappa=10.000 | theta=0.386 | sigma=5.081 | rho=-0.673

{'MSE': np.float64(5.378), 'RMSE': np.float64(2.319), 'MAE': np.float64(0.761), 'MSE_%': np.float64(1.486), 'RMSE_%': np.float64(1.219), 'MAE_%': np.float64(9.319)}
{'MSE': np.float64(0.561), 'RMSE': np.float64(0.749), 'MAE': np.float64(0.437), 'MSE_%': np.float64(0.165), 'RMSE_%': np.float64(0.406), 'MAE_%': np.float64(2.196)}
================================================================================
2025-04-25
================================================================================

Calibrated parameters: v0=0.030 | kappa=0.926 | theta=0.001 | sigma=0.152 | rho=1.000

at minimum 3.690574 accepted 1
Parameters: kappa=0.926 | theta=0.001 | sigma=0.152 | rho=1.000

at minimum 3.690574 accepted 1
Parameters: kappa=0.926 | theta=0.001 | sigma=0.152 | rho=1.000

at minimum 3.690574 accepted 1
Parameters: kappa=0.926 | theta=0.001 | sigma=0.152 | rho=1.000

at minimum 3.690574 accepted 1
Parameters: kappa=0.926 | theta=0.001 | sigma=0.152 | rho=1.000

at minimum 3.690747 accepted 1
Parameters: kappa=1.015 | theta=0.003 | sigma=0.152 | rho=1.000

at minimum 3.690574 accepted 1
Parameters: kappa=0.926 | theta=0.001 | sigma=0.152 | rho=1.000

['success condition satisfied'] True
Calibrated parameters: v0=0.030 | kappa=0.926 | theta=0.001 | sigma=0.152 | rho=1.000

{'MSE': np.float64(3.691), 'RMSE': np.float64(1.921), 'MAE': np.float64(1.276), 'MSE_%': np.float64(3.77), 'RMSE_%': np.float64(1.942), 'MAE_%': np.float64(14.201)}
{'MSE': np.float64(0.521), 'RMSE': np.float64(0.721), 'MAE': np.float64(0.582), 'MSE_%': np.float64(0.196), 'RMSE_%': np.float64(0.443), 'MAE_%': np.float64(3.37)}
at minimum 0.010113 accepted 1
Parameters: kappa=0.934 | theta=0.108 | sigma=0.001 | rho=-0.954

at minimum 0.010105 accepted 1
Parameters: kappa=1.942 | theta=0.069 | sigma=0.001 | rho=-0.992

at minimum 0.010096 accepted 1
Parameters: kappa=2.146 | theta=0.066 | sigma=0.001 | rho=-0.886

at minimum 0.010104 accepted 1
Parameters: kappa=2.466 | theta=0.061 | sigma=0.001 | rho=-1.000

at minimum 0.003794 accepted 1
Parameters: kappa=10.000 | theta=0.057 | sigma=1.016 | rho=-0.351

at minimum 0.003951 accepted 1
Parameters: kappa=1.194 | theta=0.218 | sigma=0.822 | rho=-0.352

at minimum 0.003967 accepted 1
Parameters: kappa=0.400 | theta=0.580 | sigma=0.804 | rho=-0.351

at minimum 0.003969 accepted 1
Parameters: kappa=0.283 | theta=0.803 | sigma=0.801 | rho=-0.351

at minimum 0.003794 accepted 1
Parameters: kappa=10.000 | theta=0.057 | sigma=1.016 | rho=-0.351

at minimum 0.003795 accepted 1
Parameters: kappa=9.980 | theta=0.057 | sigma=1.016 | rho=-0.351

['requested number of basinhopping iterations completed successfully'] True
Calibrated parameters: v0=0.030 | kappa=10.000 | theta=0.057 | sigma=1.016 | rho=-0.351

{'MSE': np.float64(17.683), 'RMSE': np.float64(4.205), 'MAE': np.float64(1.635), 'MSE_%': np.float64(0.379), 'RMSE_%': np.float64(0.616), 'MAE_%': np.float64(4.876)}
{'MSE': np.float64(0.951), 'RMSE': np.float64(0.975), 'MAE': np.float64(0.426), 'MSE_%': np.float64(0.455), 'RMSE_%': np.float64(0.674), 'MAE_%': np.float64(2.625)}
================================================================================
2025-07-18
================================================================================

Calibrated parameters: v0=0.037 | kappa=0.004 | theta=3.000 | sigma=0.318 | rho=-0.730

at minimum 0.975403 accepted 1
Parameters: kappa=0.004 | theta=3.000 | sigma=0.318 | rho=-0.730

at minimum 0.975403 accepted 1
Parameters: kappa=0.004 | theta=2.998 | sigma=0.318 | rho=-0.730

at minimum 0.975413 accepted 1
Parameters: kappa=0.004 | theta=2.927 | sigma=0.318 | rho=-0.730

at minimum 0.975429 accepted 1
Parameters: kappa=0.005 | theta=2.816 | sigma=0.318 | rho=-0.730

at minimum 0.975403 accepted 1
Parameters: kappa=0.004 | theta=2.999 | sigma=0.318 | rho=-0.730

at minimum 0.975403 accepted 1
Parameters: kappa=0.004 | theta=3.000 | sigma=0.318 | rho=-0.730

['success condition satisfied'] True
Calibrated parameters: v0=0.037 | kappa=0.004 | theta=3.000 | sigma=0.318 | rho=-0.730

{'MSE': np.float64(0.975), 'RMSE': np.float64(0.988), 'MAE': np.float64(0.82), 'MSE_%': np.float64(2.335), 'RMSE_%': np.float64(1.528), 'MAE_%': np.float64(7.485)}
{'MSE': np.float64(0.125), 'RMSE': np.float64(0.354), 'MAE': np.float64(0.223), 'MSE_%': np.float64(0.051), 'RMSE_%': np.float64(0.225), 'MAE_%': np.float64(1.388)}
at minimum 0.001370 accepted 1
Parameters: kappa=0.022 | theta=3.000 | sigma=0.582 | rho=-0.722

at minimum 0.001370 accepted 1
Parameters: kappa=0.024 | theta=2.808 | sigma=0.582 | rho=-0.722

at minimum 0.001370 accepted 1
Parameters: kappa=0.022 | theta=2.999 | sigma=0.582 | rho=-0.722

['success condition satisfied'] True
Calibrated parameters: v0=0.037 | kappa=0.024 | theta=2.808 | sigma=0.582 | rho=-0.722

{'MSE': np.float64(20.174), 'RMSE': np.float64(4.492), 'MAE': np.float64(2.16), 'MSE_%': np.float64(0.137), 'RMSE_%': np.float64(0.37), 'MAE_%': np.float64(2.729)}
{'MSE': np.float64(0.167), 'RMSE': np.float64(0.408), 'MAE': np.float64(0.227), 'MSE_%': np.float64(0.044), 'RMSE_%': np.float64(0.211), 'MAE_%': np.float64(1.245)}
================================================================================
2026-01-16
================================================================================

Calibrated parameters: v0=0.040 | kappa=0.340 | theta=0.318 | sigma=0.655 | rho=-1.000

at minimum 6049.358096 accepted 1
Parameters: kappa=0.340 | theta=0.318 | sigma=0.655 | rho=-1.000

at minimum 6049.331692 accepted 1
Parameters: kappa=0.344 | theta=0.314 | sigma=0.654 | rho=-1.000

['success condition satisfied'] True
Calibrated parameters: v0=0.040 | kappa=0.344 | theta=0.314 | sigma=0.654 | rho=-1.000

{'MSE': np.float64(6049.332), 'RMSE': np.float64(77.777), 'MAE': np.float64(31.917), 'MSE_%': np.float64(124.849), 'RMSE_%': np.float64(11.174), 'MAE_%': np.float64(53.89)}
{'MSE': np.float64(1143.565), 'RMSE': np.float64(33.817), 'MAE': np.float64(14.747), 'MSE_%': np.float64(594.588), 'RMSE_%': np.float64(24.384), 'MAE_%': np.float64(101.997)}
at minimum 0.008761 accepted 1
Parameters: kappa=0.138 | theta=0.396 | sigma=0.518 | rho=-0.846

at minimum 0.008756 accepted 1
Parameters: kappa=0.156 | theta=0.362 | sigma=0.526 | rho=-0.846

at minimum 0.008777 accepted 1
Parameters: kappa=0.081 | theta=0.630 | sigma=0.503 | rho=-0.847

at minimum 0.008782 accepted 1
Parameters: kappa=0.062 | theta=0.800 | sigma=0.497 | rho=-0.847

at minimum 0.008633 accepted 1
Parameters: kappa=1.737 | theta=0.100 | sigma=1.039 | rho=-0.829

at minimum 0.008635 accepted 1
Parameters: kappa=1.477 | theta=0.104 | sigma=0.948 | rho=-0.831

['requested number of basinhopping iterations completed successfully'] True
Calibrated parameters: v0=0.040 | kappa=1.737 | theta=0.100 | sigma=1.039 | rho=-0.829

{'MSE': np.float64(6572.069), 'RMSE': np.float64(81.068), 'MAE': np.float64(18.632), 'MSE_%': np.float64(0.863), 'RMSE_%': np.float64(0.929), 'MAE_%': np.float64(4.461)}
{'MSE': np.float64(18.403), 'RMSE': np.float64(4.29), 'MAE': np.float64(1.019), 'MSE_%': np.float64(0.903), 'RMSE_%': np.float64(0.95), 'MAE_%': np.float64(3.123)}
../../_images/examples_calibration_calibration_heston_5_1.png