spb/wp7_uqo Public
UQO Working Paper No. 7 — Options-implied information for cross-asset return and volatility prediction: evidence from 3.8B option contracts.
Python 66.5%
TeX 32.7%
Makefile 0.8%
1#!/usr/bin/env python32# =============================================================================3# Author: Simon-Pierre Boucher4# Contact: contact@spboucher.ai5# =============================================================================6"""Step 12 — Supplementary figures (not referenced by the paper).78The working paper is tables-only by design; these figures are reproducible9visual companions built from the result CSVs, useful for talks and quick10inspection. Colors follow a CVD-validated categorical palette; every series11is direct-labeled so identity never relies on color alone.1213Inputs : results/rolling_r2.csv, results/irf_results.csv,14 results/portfolio_sort_results.csv, results/subperiod_results.csv15Outputs: figures/fig_rolling_r2.(png|pdf), figures/fig_irf.(png|pdf),16 figures/fig_portfolio_sorts.(png|pdf), figures/fig_subperiod.(png|pdf)17"""1819import warnings2021import matplotlib22matplotlib.use("Agg")23import matplotlib.pyplot as plt24import pandas as pd2526import _bootstrap # noqa: F40127from wp7 import config2829warnings.filterwarnings('ignore')3031# CVD-validated categorical palette (see AUDIT trail: adjacent-pair ΔE ≥ 8)32BLUE, ORANGE, AQUA, YELLOW = "#2a78d6", "#eb6834", "#1baf7a", "#eda100"33INK, INK_2 = "#0b0b0b", "#52514e"3435plt.rcParams.update({36 "figure.dpi": 150,37 "font.size": 9,38 "axes.edgecolor": INK_2,39 "axes.labelcolor": INK,40 "axes.titlecolor": INK,41 "axes.spines.top": False,42 "axes.spines.right": False,43 "axes.grid": True,44 "grid.color": "#e5e4e0",45 "grid.linewidth": 0.6,46 "xtick.color": INK_2,47 "ytick.color": INK_2,48 "savefig.facecolor": "white",49 "axes.facecolor": "white",50})515253def _save(fig, name: str) -> None:54 for ext in ("png", "pdf"):55 fig.savefig(config.FIGURES_DIR / f"{name}.{ext}", bbox_inches="tight")56 plt.close(fig)57 print(f" saved figures/{name}.png|.pdf")585960def fig_rolling_r2() -> None:61 """252-day rolling explanatory power of option-implied information."""62 df = pd.read_csv(config.RESULTS_DIR / "rolling_r2.csv", parse_dates=["date"])63 fig, ax = plt.subplots(figsize=(7.2, 3.4))64 series = [("1D_RV", "1-day RV forecasting (HAR+IV)", BLUE),65 ("5D_Return", "5-day return predictability", ORANGE)]66 for key, label, color in series:67 sub = df[df["target"] == key].sort_values("date")68 ax.plot(sub["date"], sub["r2"], color=color, lw=2)69 ax.annotate(label, xy=(sub["date"].iloc[-1], sub["r2"].iloc[-1]),70 xytext=(6, 0), textcoords="offset points",71 va="center", color=INK, fontsize=8.5)72 ax.set_ylabel("Rolling $R^2$ (252-day window)")73 ax.set_title("Option-implied information content through time", loc="left")74 ax.margins(x=0.02)75 fig.subplots_adjust(right=0.72)76 _save(fig, "fig_rolling_r2")777879def _dodge(values: list, min_gap: float) -> list:80 """Push label anchor positions apart until every pair clears min_gap."""81 order = sorted(range(len(values)), key=lambda i: values[i])82 adjusted = list(values)83 for prev, cur in zip(order, order[1:]):84 if adjusted[cur] - adjusted[prev] < min_gap:85 adjusted[cur] = adjusted[prev] + min_gap86 return adjusted878889def fig_irf() -> None:90 """Average VAR(5) impulse responses (20 tickers, standardized units)."""91 df = pd.read_csv(config.RESULTS_DIR / "irf_results.csv")92 avg = df.groupby("horizon")[["iv_to_iv", "rv_to_iv", "iv_to_rv", "rv_to_rv"]].mean()93 fig, ax = plt.subplots(figsize=(6.4, 3.6))94 series = [("iv_to_rv", "RV response to IV shock", BLUE),95 ("rv_to_rv", "RV response to RV shock", ORANGE),96 ("iv_to_iv", "IV response to IV shock", AQUA),97 ("rv_to_iv", "IV response to RV shock", YELLOW)]98 ends = _dodge([avg[key].iloc[-1] for key, _, _ in series], min_gap=0.07)99 for (key, label, color), label_y in zip(series, ends):100 ax.plot(avg.index, avg[key], color=color, lw=2)101 ax.annotate(label, xy=(avg.index[-1], label_y),102 xytext=(6, 0), textcoords="offset points",103 va="center", color=INK, fontsize=8.5)104 ax.axhline(0, color=INK_2, lw=0.8)105 ax.set_xlabel("Horizon (days)")106 ax.set_ylabel("Response (SD units)")107 ax.set_title("Average impulse responses, bivariate VAR(5): ATM IV ↔ RV",108 loc="left")109 ax.margins(x=0.02)110 fig.subplots_adjust(right=0.70)111 _save(fig, "fig_irf")112113114def fig_portfolio_sorts() -> None:115 """Annualized long-short (Q5−Q1) returns of the 5-day sorts."""116 df = pd.read_csv(config.RESULTS_DIR / "portfolio_sort_results.csv")117 ls = df[(df["quintile"] == "L/S(5-1)") & (df["return_horizon"] == "5-Day")]118 ls = ls.sort_values("annualized_return_pct")119 colors = [BLUE if v >= 0 else ORANGE for v in ls["annualized_return_pct"]]120121 fig, ax = plt.subplots(figsize=(6.4, 3.2))122 bars = ax.barh(ls["sort_variable"], ls["annualized_return_pct"],123 color=colors, height=0.62)124 for bar, val, sharpe in zip(bars, ls["annualized_return_pct"], ls["sharpe_ratio"]):125 ax.annotate(f"{val:+.1f}% (SR {sharpe:.2f})",126 xy=(val, bar.get_y() + bar.get_height() / 2),127 xytext=(5 if val >= 0 else -5, 0), textcoords="offset points",128 va="center", ha="left" if val >= 0 else "right",129 color=INK, fontsize=8)130 ax.axvline(0, color=INK_2, lw=0.8)131 ax.set_xlabel("Annualized L/S return (%)")132 ax.set_title("Long-short quintile portfolios, 5-day returns (Q5 − Q1)",133 loc="left")134 ax.grid(axis="y", visible=False)135 xmin, xmax = ax.get_xlim()136 ax.set_xlim(xmin - 0.42 * (xmax - xmin), xmax + 0.18 * (xmax - xmin))137 _save(fig, "fig_portfolio_sorts")138139140def fig_subperiod() -> None:141 """HAR-RV vs HAR+IV in-sample R² across the nine subperiods."""142 df = pd.read_csv(config.RESULTS_DIR / "subperiod_results.csv")143 rv = df[df["model"].isin(["HAR-RV", "HAR+IV"])]144 pivot = rv.pivot_table(index="subperiod", columns="model", values="r2")145 pivot = pivot.reindex(list(config.SUBPERIODS))146147 fig, ax = plt.subplots(figsize=(7.2, 3.6))148 x = range(len(pivot))149 w = 0.38150 ax.bar([i - w / 2 for i in x], pivot["HAR-RV"], width=w - 0.04,151 color=ORANGE, label="HAR-RV")152 ax.bar([i + w / 2 for i in x], pivot["HAR+IV"], width=w - 0.04,153 color=BLUE, label="HAR-RV + IV surface")154 ax.set_xticks(list(x))155 ax.set_xticklabels([s.split(" (")[0] for s in pivot.index],156 rotation=30, ha="right", fontsize=8)157 ax.set_ylabel("In-sample $R^2$ (1-day RV)")158 ax.set_title("Stability of the IV-surface improvement across subperiods",159 loc="left")160 ax.grid(axis="x", visible=False)161 ax.legend(frameon=False, fontsize=8.5, loc="upper left")162 _save(fig, "fig_subperiod")163164165def main():166 print("=" * 70)167 print("SUPPLEMENTARY FIGURES")168 print("=" * 70)169 config.ensure_output_dirs()170 fig_rolling_r2()171 fig_irf()172 fig_portfolio_sorts()173 fig_subperiod()174 print("\nFIGURES COMPLETE.")175176177if __name__ == "__main__":178 main()179