import warnings
from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from cycler import cycler
from IPython.display import display
from quantfinlab.dataio.equity_ohlcv import load_ohlcv
from quantfinlab.dataio.option_chain import load_option_chain
from quantfinlab.dataio.rates import load_par_yield_curve
from quantfinlab.options.quote_cleaning import (
attach_spot_from_series,
convert_quotes_to_usd_equivalent,
clean_option_quotes,
surface_ready_quotes,
)
from quantfinlab.options.rates_dividends import (
attach_rates,
add_discount_factors,
infer_carry_from_forward,
infer_dividend_yield_from_forward,
)
from quantfinlab.options.parity import infer_forwards_from_parity
from quantfinlab.options.iv import implied_vol_table
from quantfinlab.options.greeks import compute_greeks_numpy
from quantfinlab.options.model_risk import (
choose_model_engine,
add_calibration_weights,
calibration_quotes,
choose_surface_date,
balanced_model_quotes,
common_model_quotes,
compare_model_fits,
model_fair_values,
residual_scores,
signal_dates,
next_day_residual_check,
residual_entry_schedule,
market_summary,
)
from quantfinlab.options.svi import fit_svi_surface, fit_svi_holdout
from quantfinlab.options.ssvi import fit_ssvi_surface
from quantfinlab.options.sabr import fit_sabr_surface, fit_sabr_holdout
from quantfinlab.options.merton import fit_merton_jump_diffusion
from quantfinlab.options.heston import fit_heston_mc
from quantfinlab.options.bates import fit_bates_mc
from quantfinlab.backtest.options import (
matched_option_schedule,
hedge_book_from_schedules,
run_scheduled_option_hedging_backtest,
hedging_diagnostics,
scheduled_hedge_comparison,
)
from quantfinlab.plotting.options import (
calibration_quote_map,
smile_term_structure,
model_quote_overlay,
svi_smiles,
svi_ssvi_errors,
ssvi_residuals,
sabr_smiles,
sabr_terms,
merton_tail_fit,
heston_mc_check,
heston_bates_fit,
model_speed_accuracy,
benchmark_errors,
model_disagreement,
residual_deciles,
scheduled_hedge_equity,
)
warnings.filterwarnings("ignore")
pd.set_option("display.float_format", lambda x: f"{x:,.6f}")
pd.set_option("display.max_columns", 120)
colors = ["#069AF3", "#FE420F", "#00008B", "#008080", "#CC79A7", "#DC143C", "#9614fa", "#0072B2", "#7BC8F6", "#04D8B2", "#800080", "#FF8072"]
plt.rcParams["axes.prop_cycle"] = cycler(color=colors)
plt.rcParams.update({"figure.figsize": (7, 3.5), "figure.dpi": 300, "savefig.dpi": 250, "axes.grid": True, "grid.alpha": 0.20, "axes.spines.top": False, "axes.spines.right": False, "axes.titlesize": 11, "axes.labelsize": 10, "xtick.labelsize": 9, "ytick.labelsize": 9, "legend.fontsize": 8})
engine = "auto"
ann_days = 365.25
random_state = 7
root = Path.cwd()
if root.name.lower() == "notebooks":
root = root.parent
data_dir = root / "data"
backend_report = choose_model_engine(engine=engine)
display(backend_report)
btc_spot = load_ohlcv(
data_dir / "btc_usd_ohlcv.csv",
source="yfinance_csv",
fields=("close",),
)
btc_spot = btc_spot["close"].rename("BTC")
btc_spot = btc_spot[btc_spot > 0]
btc_options = load_option_chain(
data_dir / "btc_options_chain.parquet",
source="btc_deribit",
annualization_days=365.0,
)
us_rates = load_par_yield_curve(
data_dir / "us_treasury_yields.csv",
source="us_treasury",
)
btc_quotes = attach_spot_from_series(
btc_options,
btc_spot,
date_col="date",
spot_col="spot",
method="previous",
overwrite=False,
)
btc_quotes = convert_quotes_to_usd_equivalent(
btc_quotes,
spot_col="spot",
price_cols=("bid", "ask", "mid", "last", "mark"),
unit="auto",
contract_size=1.0,
)
clean_quotes, clean_report = clean_option_quotes(
btc_quotes,
min_dte=3,
max_dte=180,
moneyness_range=(0.45, 1.75),
max_relative_spread=0.60,
closest_atm_pairs=None,
min_pairs_per_expiry=0,
annualization_days=365.0,
)
rate_start = clean_quotes["date"].min() - pd.Timedelta(days=30)
rate_end = clean_quotes["date"].max()
us_rates = us_rates.loc[(us_rates.index >= rate_start) & (us_rates.index <= rate_end)].copy()
clean_quotes = attach_rates(
clean_quotes,
us_rates,
date_col="date",
tau_col="tau",
out_col="rate",
)
clean_quotes = add_discount_factors(
clean_quotes,
rate_col="rate",
tau_col="tau",
out_col="discount_factor",
)
clean_quotes = infer_forwards_from_parity(
clean_quotes,
date_col="date",
expiry_col="expiry",
strike_col="strike",
option_type_col="option_type",
mid_col="mid",
discount_col="discount_factor",
spot_col="spot",
out_col="forward",
)
forward_bad = ~np.isfinite(pd.to_numeric(clean_quotes["forward"], errors="coerce")) | (pd.to_numeric(clean_quotes["forward"], errors="coerce") <= 0)
forward_fallback = clean_quotes["spot"] * np.exp(clean_quotes["rate"].fillna(0.0) * clean_quotes["tau"].fillna(0.0))
clean_quotes.loc[forward_bad, "forward"] = forward_fallback.loc[forward_bad]
forward_report = pd.DataFrame([{"forward_fallback_share": float(forward_bad.mean()), "forward_fallback_rows": int(forward_bad.sum())}])
clean_quotes = infer_carry_from_forward(
clean_quotes,
spot_col="spot",
forward_col="forward",
tau_col="tau",
out_col="implied_carry",
)
clean_quotes = infer_dividend_yield_from_forward(
clean_quotes,
rate_col="rate",
carry_col="implied_carry",
out_col="implied_dividend_yield",
)
iv_quotes = implied_vol_table(
clean_quotes,
price_cols=("bid", "mid", "ask"),
option_type_col="option_type",
spot_col="spot",
forward_col="forward",
strike_col="strike",
tau_col="tau",
rate_col="rate",
discount_col="discount_factor",
model="black76",
engine=engine,
)
iv_quotes["iv_ok"] = iv_quotes["iv_mid_success"].fillna(False) & np.isfinite(iv_quotes["iv_mid"])
iv_quotes = compute_greeks_numpy(iv_quotes, iv_col="iv_mid")
surface_quotes = surface_ready_quotes(
iv_quotes,
min_iv=0.05,
max_iv=3.50,
min_tau=3 / 365.0,
max_tau=180 / 365.0,
min_weight=0.05,
max_weight=20.0,
)
surface_quotes = add_calibration_weights(
surface_quotes,
price_uncertainty="half_spread",
iv_uncertainty="spread_over_vega",
expiry_balance=True,
)
btc_calibration = calibration_quotes(
surface_quotes,
min_dte=3.0,
max_dte=120.0,
min_vega=0.0,
max_relative_spread=0.85,
otm_only=True,
)
main_date = choose_surface_date(
btc_calibration,
min_expiries=4,
min_quotes=80,
prefer_tail_coverage=True,
)
btc_day = btc_calibration.loc[btc_calibration["date"].eq(main_date)].copy()
btc_model_quotes = balanced_model_quotes(
btc_day,
target_dtes=(7, 14, 21, 30, 45, 60),
target_ks=(-0.35, -0.25, -0.17, -0.10, -0.04, 0.00, 0.04, 0.10, 0.17, 0.25),
min_quotes_per_expiry=6,
prefer_tail_coverage=True,
)
btc_svi = fit_svi_surface(
btc_day,
weight_col="obs_weight",
engine=engine,
)
btc_ssvi = fit_ssvi_surface(
btc_day,
weight_col="obs_weight",
engine=engine,
)
btc_sabr = fit_sabr_surface(
btc_day,
betas=(1.0, 0.7, 0.5),
primary_beta=1.0,
weight_col="obs_weight",
engine=engine,
)
btc_merton = fit_merton_jump_diffusion(
btc_model_quotes,
weight_col="obs_weight",
engine=engine,
max_nfev=45,
fit_by_expiry=True,
)
btc_heston = fit_heston_mc(
btc_model_quotes,
paths_opt=2048,
paths_final=8192,
steps_per_year=52,
random_method="antithetic",
common_random_numbers=True,
engine=engine,
random_state=random_state,
max_nfev=70,
)
btc_bates = fit_bates_mc(
btc_model_quotes,
heston_start=btc_heston,
jump_start=btc_merton,
paths_opt=2048,
paths_final=8192,
steps_per_year=52,
random_method="antithetic",
common_random_numbers=True,
engine=engine,
random_state=random_state,
max_nfev=60,
)
btc_common_quotes = common_model_quotes(
btc_day,
model_quotes=btc_model_quotes,
min_tail_count=6,
)
model_comparison = compare_model_fits(
btc_common_quotes,
fits={
"svi": btc_svi,
"ssvi": btc_ssvi,
"sabr": btc_sabr,
"merton": btc_merton,
"heston": btc_heston,
"bates": btc_bates,
},
engine=engine,
)
dates_for_signal = signal_dates(
btc_calibration,
min_quotes=60,
min_expiries=3,
min_near_atm_quotes=10,
)
dates_for_signal = dates_for_signal[-120:]
btc_svi_daily = fit_svi_holdout(
btc_calibration,
dates=dates_for_signal,
train_mode="anchor_strikes",
weight_col="obs_weight",
warm_start=True,
engine=engine,
)
btc_sabr_daily = fit_sabr_holdout(
btc_calibration,
dates=dates_for_signal,
beta=1.0,
train_mode="anchor_strikes",
weight_col="obs_weight",
warm_start=True,
engine=engine,
)
btc_fair_values = model_fair_values(
btc_calibration,
fits={"svi": btc_svi_daily, "sabr": btc_sabr_daily},
ensemble_method="capped_weighted",
max_model_weight=0.70,
min_model_weight=0.30,
engine=engine,
)
btc_scores = residual_scores(
btc_fair_values,
residual_col="ensemble_price_residual",
quote_cost_col="half_spread",
model_uncertainty_col="model_disagreement",
score_method="cost_adjusted_z",
)
btc_validation = next_day_residual_check(
btc_scores,
option_quotes=btc_calibration,
hedge_delta_col="delta",
cost_model="scheduled",
calendar="crypto_24_7",
engine=engine,
)
btc_residual_schedule = residual_entry_schedule(
btc_validation,
selector_name="btc_residual_fixed_3d",
hold_days=3,
max_entries=60,
entry_spacing_days=3,
require_signal_direction=True,
chronological=True,
)
btc_matched_schedule = matched_option_schedule(
btc_residual_schedule,
option_quotes=surface_quotes,
same_date=True,
same_option_type=True,
same_quantity_sign=True,
target_abs_delta=0.50,
dte_tolerance_days=7,
min_future_marks=3,
selector_name="btc_matched_atm_fixed_3d",
)
schedule_report = pd.DataFrame([
{"schedule": "residual", "entries": len(btc_residual_schedule)},
{"schedule": "matched_atm", "entries": len(btc_matched_schedule)},
])
btc_hedge_book = hedge_book_from_schedules(
surface_quotes,
schedules=[btc_residual_schedule, btc_matched_schedule],
lookahead_days=12,
)
empty_result = {
"nav": pd.DataFrame(),
"returns": pd.DataFrame(),
"pnl": pd.DataFrame(),
"components": pd.DataFrame(),
"exposures": pd.DataFrame(),
"trades": pd.DataFrame(),
"summary": pd.DataFrame(),
"diagnostics": {},
}
if btc_residual_schedule.empty or btc_matched_schedule.empty or btc_hedge_book.empty:
btc_residual_results = empty_result
btc_matched_results = empty_result
else:
btc_residual_results = run_scheduled_option_hedging_backtest(
option_path=btc_hedge_book,
entry_schedule=btc_residual_schedule,
spot_series=btc_spot,
greeks=btc_hedge_book,
hedge_price_series=btc_spot,
hedge_dividend_series=None,
hedge_beta_series=None,
strategies=("unhedged", "delta"),
option_multiplier=1.0,
trading_cost_bps=2.0,
use_bid_ask_costs=True,
delta_band=0.10,
delta_inner_band=0.03,
delta_share_lot=0.001,
delta_cooldown_days=0,
max_hold_days=3,
quote_price_unit="usd",
valuation_currency="USD",
pnl_mode="usd_equivalent",
calendar="crypto_24_7",
missing_option_mark="skip_day",
)
btc_matched_results = run_scheduled_option_hedging_backtest(
option_path=btc_hedge_book,
entry_schedule=btc_matched_schedule,
spot_series=btc_spot,
greeks=btc_hedge_book,
hedge_price_series=btc_spot,
hedge_dividend_series=None,
hedge_beta_series=None,
strategies=("unhedged", "delta"),
option_multiplier=1.0,
trading_cost_bps=2.0,
use_bid_ask_costs=True,
delta_band=0.10,
delta_inner_band=0.03,
delta_share_lot=0.001,
delta_cooldown_days=0,
max_hold_days=3,
quote_price_unit="usd",
valuation_currency="USD",
pnl_mode="usd_equivalent",
calendar="crypto_24_7",
missing_option_mark="skip_day",
)
btc_hedge_comparison = scheduled_hedge_comparison(
{
"matched_atm_fixed_3d": btc_matched_results,
"residual_selector_fixed_3d": btc_residual_results,
},
normalize_by=("premium", "traded_notional", "initial_vega"),
)
btc_residual_diag = hedging_diagnostics(btc_residual_results)
btc_matched_diag = hedging_diagnostics(btc_matched_results)
btc_summary = market_summary(
asset="btc",
quotes=btc_day,
model_comparison=model_comparison,
validation=btc_validation,
hedge_comparison=btc_hedge_comparison,
engine=engine,
)
display(clean_report)
display(forward_report)
display(model_comparison)
display(btc_validation.attrs.get("summary", pd.DataFrame()))
display(schedule_report)
display(btc_hedge_comparison)
display(btc_residual_diag)
display(btc_matched_diag)
display(btc_summary)
fig = plt.figure(figsize=(15, 11), constrained_layout=True, dpi=300)
gs = fig.add_gridspec(4, 4)
ax_quote_map = fig.add_subplot(gs[0, 0])
ax_smile_terms = fig.add_subplot(gs[0, 1])
ax_model_overlay = fig.add_subplot(gs[0, 2])
ax_svi = fig.add_subplot(gs[0, 3])
ax_svi_ssvi = fig.add_subplot(gs[1, 0])
ax_ssvi_resid = fig.add_subplot(gs[1, 1])
ax_sabr_smiles = fig.add_subplot(gs[1, 2])
ax_sabr_params = fig.add_subplot(gs[1, 3])
ax_merton = fig.add_subplot(gs[2, 0])
ax_heston_mc = fig.add_subplot(gs[2, 1])
ax_heston_bates = fig.add_subplot(gs[2, 2])
ax_error_runtime = fig.add_subplot(gs[2, 3])
ax_benchmark = fig.add_subplot(gs[3, 0])
ax_disagreement = fig.add_subplot(gs[3, 1])
ax_deciles = fig.add_subplot(gs[3, 2])
ax_hedge = fig.add_subplot(gs[3, 3])
calibration_quote_map(ax_quote_map, btc_day, "k", "tau", "BTC calibration quotes by maturity and moneyness")
smile_term_structure(ax_smile_terms, btc_day, "k", "tau", "iv_mid", "BTC IV smile term structure")
model_quote_overlay(ax_model_overlay, btc_day, btc_model_quotes, "k", "tau", "Balanced model panel by expiry")
svi_smiles(ax_svi, btc_day, btc_svi, "SVI fitted smiles")
svi_ssvi_errors(ax_svi_ssvi, btc_svi, btc_ssvi, "SVI vs SSVI error by expiry")
ssvi_residuals(ax_ssvi_resid, btc_day, btc_ssvi, "SSVI residual heatmap")
sabr_smiles(ax_sabr_smiles, btc_day, btc_sabr, "SABR fitted smiles")
sabr_terms(ax_sabr_params, btc_sabr, "SABR parameter term structure")
merton_tail_fit(ax_merton, btc_model_quotes, btc_merton, "Merton jump-tail fit")
heston_mc_check(ax_heston_mc, btc_heston, "Heston MC convergence")
heston_bates_fit(ax_heston_bates, btc_model_quotes, btc_heston, btc_bates, "Heston vs Bates fit")
model_speed_accuracy(ax_error_runtime, model_comparison, "Model error vs runtime")
benchmark_errors(ax_benchmark, model_comparison, "Common benchmark errors")
model_disagreement(ax_disagreement, btc_fair_values, "Daily model disagreement")
residual_deciles(ax_deciles, btc_validation, "Residual deciles vs next hedged P&L")
scheduled_hedge_equity(ax_hedge, {"matched_atm_fixed_3d": btc_matched_results, "residual_selector_fixed_3d": btc_residual_results}, btc_hedge_comparison, "Scheduled BTC hedge comparison")
for ax in fig.axes:
ax.title.set_fontsize(10)
ax.tick_params(axis="both", labelsize=8)
fig.suptitle(
f"BTC model calibration, residual selection, and scheduled hedging | {pd.Timestamp(main_date).date()}",
fontsize=10,
y=1.01,
)
plt.show()