# --- 0. BULLETPROOF THE MARKET STATE ---
# Re-extract real market variables in case previous simulation cells overwrote them
S0, T, r, q = state['S0'], state['T'], state['r'], state['q']
import numpy as np
import time
import warnings
import plotly.graph_objects as go
from scipy.optimize import differential_evolution, minimize
from src.models.option_pricing_math_engine import (
bs_call_price, merton_jump_call, heston_call_price,
bates_call_price, implied_volatility
)
# --- 1. DATA SCALING (NO FILTERING) ---
if np.max(target_ivs) < 2.0:
target_ivs_pct = target_ivs * 100
else:
target_ivs_pct = target_ivs
# We are using the FULL dataset, just downsampled to ~30 strikes so it doesn't take hours
sample_step = max(1, len(valid_strikes) // 30)
target_strikes_sampled = valid_strikes[::sample_step]
target_ivs_sampled = target_ivs_pct[::sample_step]
# --- 2. SMART OBJECTIVE FUNCTION ---
def bates_objective_autonomous(params):
v0, kappa, theta, xi, rho, lam, mu_j, delta = params
valid_errors = []
nan_count = 0
with warnings.catch_warnings():
warnings.simplefilter("ignore")
for i, K in enumerate(target_strikes_sampled):
m_price = bates_call_price(S0, K, T, r, q, v0, kappa, theta, xi, rho, lam, mu_j, delta)
m_iv = implied_volatility(m_price, S0, K, T, r, q)
if np.isnan(m_iv):
nan_count += 1
else:
valid_errors.append((m_iv * 100 - target_ivs_sampled[i])**2)
# If it fails to price EVERYTHING, give it a heavy penalty
if len(valid_errors) == 0:
return 1e6
# Calculate MSE on the valid points
mse = np.mean(valid_errors)
# Add a soft penalty (10 points) for every strike it couldn't price.
# This teaches the optimizer to prefer parameters that price the whole curve.
return mse + (nan_count * 10.0)
# --- 3. BOUNDS ---
bates_bounds = [
(0.005, 0.45), (0.5, 5.0), (0.005, 0.45), (0.1, 2.0),
(-0.95, -0.2), (0.0, 3.0), (-0.5, 0.0), (0.01, 0.3)
]
bs_sigma = 0.1245
merton_params = {'sigma': 0.0850, 'lam': 0.9923, 'mu_j': -0.0825, 'delta': 0.0779}
heston_params = {'v0': 0.0115, 'kappa': 3.0293, 'theta': 0.0684, 'xi': 1.1963, 'rho': -0.6572}
# --- 4. FULL AUTONOMOUS HYBRID CALIBRATION ---
# print(f"Starting Calibration on {len(target_strikes_sampled)} strikes...")
start_time = time.time()
# # ==============================================================================
# # OPTION A: LIVE CALIBRATION (Run this ONCE to get your parameters)
# # ==============================================================================
# print("Phase 1: Global Search (This will take ~2-3 minutes)...")
# res_global = differential_evolution(
# bates_objective_autonomous,
# bates_bounds,
# popsize=8,
# maxiter=15,
# seed=42
# )
# print("Phase 2: Local Sniper Refining...")
# res_bates = minimize(
# bates_objective_autonomous,
# res_global.x,
# method='L-BFGS-B',
# bounds=bates_bounds,
# options={'maxiter': 50}
# )
# opt_params = res_bates.x
# bates_params_final = {
# 'v0': opt_params[0], 'kappa': opt_params[1], 'theta': opt_params[2], 'xi': opt_params[3],
# 'rho': opt_params[4], 'lam': opt_params[5], 'mu_j': opt_params[6], 'delta': opt_params[7]
# }
# print(f"✅ Calibration complete in {round(time.time() - start_time, 2)}s")
# print(f"Optimal Score: {res_bates.fun:.6f}\n")
# # This will print the exact code you need to copy!
# print("👇 COPY THESE PARAMETERS FOR OPTION B 👇")
# print("bates_params_final = {")
# for k, v in bates_params_final.items():
# print(f" '{k}': {v:.6f},")
# print("}")
# print("👆 ==================================== 👆")
# ==============================================================================
# OPTION B: HARDCODED PARAMETERS (Instant Execution)
# ==============================================================================
bates_params_final = {
'v0': 0.005000,
'kappa': 0.843574,
'theta': 0.190399,
'xi': 0.563919,
'rho': -0.464328,
'lam': 0.063745,
'mu_j': -0.500000,
'delta': 0.063779,
}
# print("✅ Using pre-calibrated hardcoded parameters. (Execution time: 0.0s)")
# ==============================================================================
# --- 5. GENERATE DATA FOR MASTER PLOT ---
smooth_strikes = np.linspace(min(valid_strikes), max(valid_strikes), 80)
bs_ivs, merton_ivs, heston_ivs, bates_ivs = [], [], [], []
print("Rendering plot curves...")
with warnings.catch_warnings():
warnings.simplefilter("ignore")
for K in smooth_strikes:
p_b = bates_call_price(S0, K, T, r, q, **bates_params_final)
bates_ivs.append(implied_volatility(p_b, S0, K, T, r, q) * 100)
p_bs = bs_call_price(S0, K, T, r, q, bs_sigma)
bs_ivs.append(implied_volatility(p_bs, S0, K, T, r, q) * 100)
p_m = merton_jump_call(S0, K, T, r, q, **merton_params)
merton_ivs.append(implied_volatility(p_m, S0, K, T, r, q) * 100)
p_h = heston_call_price(S0, K, T, r, q, **heston_params)
heston_ivs.append(implied_volatility(p_h, S0, K, T, r, q) * 100)
# --- 6. INTERACTIVE MASTER PLOT ---
fig = go.Figure()
fig.add_trace(go.Scatter(x=valid_strikes, y=target_ivs_pct, mode='markers', name='Market IV (WRDS)',
marker=dict(color='black', symbol='x', opacity=0.5)))
fig.add_trace(go.Scatter(x=smooth_strikes, y=bs_ivs, mode='lines', name='Black-Scholes',
line=dict(color='gray', width=2, dash='dot')))
fig.add_trace(go.Scatter(x=smooth_strikes, y=merton_ivs, mode='lines', name='Merton (Jump Diffusion)',
line=dict(color='blue', width=2, dash='dash')))
fig.add_trace(go.Scatter(x=smooth_strikes, y=heston_ivs, mode='lines', name='Heston (Stochastic Vol)',
line=dict(color='red', width=2)))
fig.add_trace(go.Scatter(x=smooth_strikes, y=bates_ivs, mode='lines', name='Bates (SVJ) Fit',
line=dict(color='#00b300', width=4)))
fig.add_vline(x=S0, line_width=1, line_dash="dash", line_color="black", annotation_text="At-the-Money")
fig.update_layout(
title=f"<b>02 | In-Sample Calibration: Fitting the SPX Volatility Skew</b><br>Target Expiry: {TARGET_EXDATE}",
xaxis_title="Strike Price", yaxis_title="Implied Volatility (%)",
template="plotly_white", hovermode="x unified", width=None, height=600
)
fig.show()