from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display
from cycler import cycler
from quantfinlab.dataio import load_option_chain, load_par_yield_curve
from quantfinlab.fixed_income.bootstrap import build_zero_curve_panel_from_par_yields
from quantfinlab.calibration.american_numerics import prepare_american_quotes, select_teaching_contracts, full_chain_tree_scan, pde_regime_grid, american_scan_summary, overlay_candidates, method_disagreement_table
from quantfinlab.calibration.lsm import lsm_regime_grid, lsm_regime_map, lsm_crossfit
from quantfinlab.options.american import tree_batch, tree_boundary, pde_price, boundary_distance, assignment_risk
from quantfinlab.backtest.overlays import run_overlay_backtest, overlay_summary, overlay_mechanics_table, covered_call_schedule, protective_put_schedule, collar_schedule
from quantfinlab.plotting.options import tree_convergence, premium_term_curves, premium_concentration, pricing_error_spread, bid_ask_hit_rate, boundary_compare, pde_tree_gap_curves, lsm_policy_curve, assignment_event_study, overlay_equity_drawdown, strategy_mechanics_bars, overlay_return_drawdown
palette = ["#069AF3","#FE420F", "#00008B", "#008080" , "#CC79A7",
"#9614fa", "#DC143C", "#7BC8F6", "#0072B2","#04D8B2", "#800080", "#FF8072"]
plt.rcParams["axes.prop_cycle"] = cycler(color=palette)
plt.rcParams.update({
"figure.figsize": (6, 3),
"figure.dpi": 200,
"savefig.dpi": 300,
"axes.grid": True,
"grid.alpha": 0.20,
"axes.spines.top": False,
"axes.spines.right": False,
"axes.titlesize": 12,
"axes.labelsize": 12,
"xtick.labelsize": 9,
"ytick.labelsize": 9,
"legend.fontsize": 7,
})
project_root = Path.cwd()
data_dir = project_root / "../data"
rates_path = data_dir / "us_treasury_yields.csv"
option_columns = [
"quote_date", "quote_readtime", "expire_date", "underlying_last", "strike",
"c_bid", "c_ask", "p_bid", "p_ask", "c_iv", "p_iv",
"c_delta", "p_delta", "c_gamma", "p_gamma", "c_vega", "p_vega",
"c_theta", "p_theta", "c_volume", "p_volume",
]
qqq_cache = data_dir / "cache" / "project13" / "qqq"
qqq_cache.mkdir(parents=True, exist_ok=True)
qqq_raw = load_option_chain(data_dir / "qqq_options_chain.parquet", source="optionsdx_qqq", columns=option_columns)
qqq_underlying = pd.read_csv(data_dir / "qqq_ohlcv.csv")
qqq_par = load_par_yield_curve(rates_path, source="us_treasury")
qqq_zero = build_zero_curve_panel_from_par_yields(qqq_par, method="pchip")
clean_quotes, clean_steps, qqq_dividends = prepare_american_quotes(qqq_raw, qqq_underlying, qqq_zero, min_dte=7, max_dte=180, moneyness_range=(0.65, 1.45), max_rel_spread=0.35)
representatives = select_teaching_contracts(clean_quotes)
tree_full = full_chain_tree_scan(clean_quotes, cache_path=qqq_cache / "qqq_tree_full_chain.parquet", settings={"dataset": "QQQ OptionsDX 2022"}, steps=300, engine="cpp", chunk_size=25000)
pde_cells = pde_regime_grid(clean_quotes, cache_path=qqq_cache / "qqq_pde_regime_grid.parquet", settings={"dataset": "QQQ OptionsDX 2022"})
lsm_cells = lsm_regime_grid(clean_quotes, dte_bins=(7, 14, 30, 60, 90, 120, 180), moneyness_bins=(0.65, 0.80, 0.90, 0.97, 1.03, 1.10, 1.25, 1.45), sigma_bins=(0.03, 0.15, 0.22, 0.32, 0.50, 2.50))
lsm_path = qqq_cache / "qqq_lsm_regime.parquet"
if lsm_path.exists():
lsm_results = pd.read_parquet(lsm_path)
else:
lsm_results = lsm_regime_map(lsm_cells, paths=64000, steps=40, degree=3, engine="cpp", repeats=3)
lsm_results.to_parquet(lsm_path, index=False)
regime_keys = ["option_type", "dte_bucket", "moneyness_bucket", "sigma_bucket", "dividend_bucket", "ex_div_bucket"]
if "ex_div_bucket" not in lsm_results.columns or "lsm_price_se" not in lsm_results.columns:
lsm_results = lsm_regime_map(lsm_cells, paths=64000, steps=40, degree=3, engine="cpp", repeats=3)
lsm_results.to_parquet(lsm_path, index=False)
pde_price_path = qqq_cache / "qqq_pde_regime_prices.parquet"
if pde_price_path.exists():
pde_results = pd.read_parquet(pde_price_path)
else:
pde_rows = []
for _, row in pde_cells.iterrows():
pde = pde_price(row["spot"], row["strike"], row["rate"], row["dividend_yield"], row["sigma_used"], row["tau"], row["option_type"], s_steps=100, t_steps=80, engine="cpp")
pde_rows.append({"date": row["date"], "expiry": row["expiry"], "option_type": row["option_type"], "strike": row["strike"], "pde_price": pde["price"], "pde_boundary_now": np.asarray(pde["boundary"], dtype=float)[0], "pde_max_residual": np.nanmax(pde["residuals"]), "coverage_rows": row["coverage_rows"], "dte_days": row["dte_days"], "moneyness": row["moneyness"], "sigma_used": row["sigma_used"], "dte_bucket": str(row["dte_bucket"]), "moneyness_bucket": str(row["moneyness_bucket"]), "sigma_bucket": str(row["sigma_bucket"]), "dividend_bucket": row["dividend_bucket"], "ex_div_bucket": str(row["ex_div_bucket"])} )
pde_results = pd.DataFrame(pde_rows)
pde_results.to_parquet(pde_price_path, index=False)
tree_for_methods = pde_results.merge(tree_full[["date", "expiry", "option_type", "strike", "american_tree_price", "european_tree_price"]], on=["date", "expiry", "option_type", "strike"], how="left")
for name in regime_keys:
lsm_results[name] = lsm_results[name].astype(str)
lsm_for_methods = lsm_results[regime_keys + ["lsm_price", "lsm_price_se", "exercise_probability"]].drop_duplicates(subset=regime_keys)
tree_for_methods[regime_keys] = tree_for_methods[regime_keys].astype(str)
tree_for_methods = tree_for_methods.merge(lsm_for_methods, on=regime_keys, how="left")
method_comparison = method_disagreement_table(tree_for_methods.rename(columns={"american_tree_price": "tree_price"}))
method_comparison["pde_tree_disagreement"] = method_comparison["pde_price"] - method_comparison["tree_price"]
assignment_base = tree_full.copy()
assignment_base["dte_bucket"] = pd.cut(assignment_base["dte_days"], [7, 14, 30, 60, 90, 120, 180], include_lowest=True)
assignment_base["moneyness_bucket"] = pd.cut(assignment_base["moneyness"], [0.65, 0.80, 0.90, 0.97, 1.03, 1.10, 1.25, 1.45], include_lowest=True)
assignment_base["sigma_bucket"] = pd.cut(assignment_base["sigma_used"], [0.03, 0.15, 0.22, 0.32, 0.50, 2.50], include_lowest=True)
assignment_base["dividend_bucket"] = np.where(assignment_base["dividend_in_life"].gt(0), "dividend", "none")
assignment_base["ex_div_bucket"] = pd.cut(assignment_base["days_to_next_dividend"].fillna(10_000), [-1, 7, 21, 10_000], labels=["0_7", "8_21", "none"], include_lowest=True)
for name in regime_keys:
assignment_base[name] = assignment_base[name].astype(str)
regime_method = method_comparison[regime_keys + ["pde_boundary_now", "model_disagreement"]].drop_duplicates(subset=regime_keys)
assignment_base = assignment_base.merge(regime_method, on=regime_keys, how="left")
assignment_base["boundary_distance"] = boundary_distance(assignment_base["spot"], assignment_base["pde_boundary_now"], assignment_base["option_type"])
assignment_base["model_disagreement"] = assignment_base["model_disagreement"].fillna(assignment_base["model_disagreement"].median())
assignment_scores = assignment_risk(assignment_base)
assignment_scores.loc[~assignment_scores["option_type"].astype(str).eq("call"), "assignment_risk"] = 0.0
assignment_scores["contract_key"] = assignment_scores["option_type"].astype(str) + "_" + pd.to_datetime(assignment_scores["expiry"]).dt.strftime("%Y-%m-%d") + "_" + assignment_scores["strike"].round(6).astype(str)
under = qqq_underlying.copy()
under["date"] = pd.to_datetime(under["date"]).dt.normalize()
under = under.set_index("date").sort_index()
contracts = float(int(1000.0 // 100.0))
aware_quotes = overlay_candidates(assignment_scores)
naive_call_schedule = covered_call_schedule(assignment_scores, contracts=contracts, rebalance_every=21, target_delta=0.30)
boundary_call_schedule = covered_call_schedule(assignment_scores, contracts=contracts, rebalance_every=21, target_delta=0.25, risk_weight=2.0)
naive_put_schedule = protective_put_schedule(assignment_scores, contracts=contracts, rebalance_every=21, target_delta=0.25)
fair_put_schedule = protective_put_schedule(aware_quotes, contracts=contracts, rebalance_every=21, target_delta=0.25, value_weight=1.0)
naive_collar_schedule = collar_schedule(assignment_scores, contracts=contracts, rebalance_every=21, put_delta=0.25, call_delta=0.25)
boundary_collar_schedule = collar_schedule(aware_quotes, contracts=contracts, rebalance_every=21, put_delta=0.25, call_delta=0.20, call_risk_weight=2.0, put_value_weight=1.0)
schedules = {
"buy_hold_qqq": pd.DataFrame(columns=["entry_date", "contract_key", "quantity", "label"]),
"naive_covered_call": naive_call_schedule,
"boundary_covered_call": boundary_call_schedule,
"naive_protective_put": naive_put_schedule,
"fair_value_protective_put": fair_put_schedule,
"naive_collar": naive_collar_schedule,
"boundary_aware_collar": boundary_collar_schedule,
}
overlay_results = run_overlay_backtest(schedules, assignment_scores, under["close"], shares=1000.0, dividends=under["dividends"], contract_multiplier=100.0, assignment_risk_threshold=0.25, assignment_defense_strategies={"boundary_covered_call", "boundary_aware_collar"})
overlay_table = overlay_summary(overlay_results)
mechanics = overlay_mechanics_table(overlay_results, shares=1000.0, dividends=under["dividends"])
tree_table = american_scan_summary(tree_full)
short_call_final = assignment_scores[assignment_scores["option_type"].astype(str).eq("call") & assignment_scores["spot"].gt(assignment_scores["strike"])].copy()
validity = pd.DataFrame([{"contracts_per_rebalance": contracts, "max_active_calls": mechanics["max_active_calls"].max(), "max_active_puts": mechanics["max_active_puts"].max(), "max_total_return": overlay_table["total_return"].max(), "max_total_premium_received": mechanics["total_premium_received"].max()}])
display(clean_steps)
display(tree_table.round(6))
display(pd.DataFrame([{"pde_regime_cells": len(pde_results), "lsm_regime_cells": len(lsm_results), "qqq_valid_rows": len(clean_quotes), "qqq_tree_rows": len(tree_full)}]))
display(overlay_table.round(6))
display(mechanics.round(6))
display(validity.round(6))
row = representatives.iloc[0]
tb = tree_boundary(row["spot"], row["strike"], row["rate"], row["dividend_yield"], row["sigma_used"], row["tau"], row["option_type"], steps=240, engine="cpp")
pde_one = pde_price(row["spot"], row["strike"], row["rate"], row["dividend_yield"], row["sigma_used"], row["tau"], row["option_type"], s_steps=120, t_steps=90, engine="cpp")
lsm_one = lsm_crossfit(row["spot"], row["strike"], row["rate"], row["dividend_yield"], row["sigma_used"], row["tau"], row["option_type"], paths=32000, steps=40, engine="cpp")
conv = pd.DataFrame({"steps": [80, 160, 300, 600], "price": [tree_batch([row["spot"]], [row["strike"]], [row["rate"]], [row["dividend_yield"]], [row["sigma_used"]], [row["tau"]], [row["option_type"]], steps=n, engine="cpp")[0] for n in [80, 160, 300, 600]]})
conv["reference_error"] = (conv["price"] - conv["price"].iloc[-1]).abs()
fig, axes = plt.subplots(3, 4, figsize=(22, 16))
axes = axes.ravel()
tree_convergence(axes[0], conv, "Tree Convergence")
premium_term_curves(axes[1], tree_full, option_type="put", title="Put Premium Term Curves")
premium_term_curves(axes[2], tree_full, option_type="call", title="Call Premium Term Curves")
premium_concentration(axes[3], tree_full, "Premium Concentration")
pricing_error_spread(axes[4], tree_full, "Spread-Scaled Error")
bid_ask_hit_rate(axes[5], tree_full, "Bid/Ask Fit")
boundary_compare(axes[6], pde_one, tb, "Free Boundary")
pde_tree_gap_curves(axes[7], method_comparison, "PDE-Tree Gap")
lsm_policy_curve(axes[8], lsm_one["coefficients"], strike=row["strike"], option_type=row["option_type"], title="LSM Policy")
assignment_event_study(axes[9], short_call_final[short_call_final["days_to_next_dividend"].between(0, 21)], "Assignment Risk")
overlay_equity_drawdown(axes[10], overlay_results, "Overlay Equity")
strategy_mechanics_bars(axes[11], mechanics, "Strategy Mechanics")
fig.tight_layout(pad=1.25, h_pad=1.0, w_pad=1.0)
plt.show()