SPB Git

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%
6.0 KB · 158 lines python
Raw Blame History
1#!/usr/bin/env python32# =============================================================================3# Author: Simon-Pierre Boucher4# Contact: contact@spboucher.ai5# =============================================================================6"""Step 03 — RQ2: IV term structure + smile vs GARCH/HAR-RV for RV forecasting.78In-sample panel R² for five nested models, per-ticker rolling out-of-sample9evaluation, and Diebold-Mariano tests of HAR-RV vs HAR-RV + IV surface.1011Inputs : data/processed/merged_options_rv.parquet12Outputs: results/rq2_insample.csv, results/rq2_oos_results.csv,13         results/rq2_diebold_mariano.csv14"""1516import warnings1718import numpy as np19import pandas as pd2021import _bootstrap  # noqa: F40122from wp7 import config23from wp7.data_io import load_merged24from wp7.econometrics import add_constant, ols, standardize, winsorize25from wp7.forecasting import diebold_mariano, ols_forecast, rolling_evaluation2627warnings.filterwarnings('ignore')2829TARGETS = {'1-Day RV': 'rv_fwd_1d', '5-Day RV': 'rv_fwd_5d'}3031WINSORIZE_COLS = ['rv_fwd_1d', 'rv_fwd_5d', 'rv_lag1', 'rv_w', 'rv_m', 'sq_return',32                  'iv_atm_30d', 'iv_atm_90d', 'iv_term_slope', 'iv_skew_25d',33                  'implied_skewness', 'implied_kurtosis_proxy']343536def main():37    print("=" * 70)38    print("RQ2: IV SURFACE vs GARCH/HAR-RV FOR RV FORECASTING")39    print("=" * 70)40    config.ensure_output_dirs()4142    df = load_merged()43    df = df.sort_values(['ticker', 'trade_date']).reset_index(drop=True)44    print(f"Loaded {len(df):,} rows")4546    # GARCH(1,1) proxy regressor: yesterday's squared daily return47    df['sq_return'] = df['daily_return'] ** 24849    # Per-ticker winsorization of every model input and target50    for col in WINSORIZE_COLS:51        if col in df.columns:52            df[col] = df.groupby('ticker')[col].transform(lambda x: winsorize(x))5354    models = config.RQ2_MODELS5556    # ── In-sample panel R² ──57    print("\n--- IN-SAMPLE PANEL REGRESSIONS ---")58    insample_results = []59    for target_label, target_col in TARGETS.items():60        for model_name, features in models.items():61            sub = df[features + [target_col, 'ticker']].dropna()62            if len(sub) < 100:63                continue6465            X = add_constant(standardize(sub[features].values))66            y = sub[target_col].values67            coefs = ols(X, y)68            y_pred = X @ coefs69            ss_res = np.sum((y - y_pred) ** 2)70            ss_tot = np.sum((y - y.mean()) ** 2)71            r2 = 1 - ss_res / ss_tot72            adj_r2 = 1 - (1 - r2) * (len(y) - 1) / (len(y) - len(features) - 1)7374            insample_results.append({75                'target': target_label,76                'model': model_name,77                'r2': r2,78                'adj_r2': adj_r2,79                'n_obs': len(sub),80                'n_features': len(features),81            })82            print(f"  {target_label} | {model_name}: R²={r2:.6f}, "83                  f"Adj-R²={adj_r2:.6f}, N={len(sub):,}")8485    insample_df = pd.DataFrame(insample_results)86    insample_df.to_csv(config.RESULTS_DIR / "rq2_insample.csv", index=False)8788    # ── Out-of-sample rolling evaluation (30 tickers with most data) ──89    print("\n--- OUT-OF-SAMPLE ROLLING EVALUATION ---")90    top_tickers = df.groupby('ticker').size().nlargest(30).index.tolist()91    df_top = df[df['ticker'].isin(top_tickers)]9293    oos_results_all = []94    for target_label, target_col in TARGETS.items():95        print(f"\n  {target_label}:")96        oos = rolling_evaluation(df_top, models, target_col, target_label,97                                 window=500, step=250)98        oos_results_all.append(oos)99        if len(oos) > 0:100            summary = oos.groupby('model').agg({101                'avg_mse': 'mean', 'avg_mae': 'mean',102                'avg_r2_oos': 'mean', 'avg_qlike': 'mean',103                'n_windows': 'sum',104            }).round(6)105            print(summary.to_string())106107    oos_df = pd.concat(oos_results_all, ignore_index=True)108    oos_df.to_csv(config.RESULTS_DIR / "rq2_oos_results.csv", index=False)109110    # ── Diebold-Mariano: HAR-RV vs HAR-RV + IV surface (10 largest tickers) ──111    print("\n--- MODEL COMPARISON: DIEBOLD-MARIANO TESTS ---")112    f_har = ['rv_lag1', 'rv_w', 'rv_m']113    f_full = f_har + ['iv_atm_30d', 'iv_atm_90d', 'iv_term_slope',114                      'iv_skew_25d', 'implied_skewness', 'implied_kurtosis_proxy']115    dm_results = []116    for target_label, target_col in TARGETS.items():117        for ticker in top_tickers[:10]:118            td = df[df['ticker'] == ticker][f_full + [target_col]].dropna()119            if len(td) < 600:120                continue121122            train, test = td.iloc[:500], td.iloc[500:]123            r1 = ols_forecast(train, test, f_har, target_col)124            r2 = ols_forecast(train, test, f_full, target_col)125126            # Forecast errors on the test window (raw, before truncation at 0)127            X1 = np.column_stack([np.ones(len(test)), test[f_har].values])128            e1 = test[target_col].values - X1 @ r1['coefs']129            X2 = np.column_stack([np.ones(len(test)), test[f_full].values])130            e2 = test[target_col].values - X2 @ r2['coefs']131132            dm_stat = diebold_mariano(e1, e2)133            dm_results.append({134                'ticker': ticker,135                'target': target_label,136                'mse_har': r1['mse'],137                'mse_har_iv': r2['mse'],138                'mse_improvement_pct': (r1['mse'] - r2['mse']) / r1['mse'] * 100,139                'dm_statistic': dm_stat,140                'dm_significant_5pct': abs(dm_stat) > 1.96,141            })142143    dm_df = pd.DataFrame(dm_results)144    dm_df.to_csv(config.RESULTS_DIR / "rq2_diebold_mariano.csv", index=False)145    print(dm_df.to_string(index=False))146147    print("\n" + "=" * 70)148    print("RQ2 SUMMARY")149    print("=" * 70)150    print("\nIn-sample R² comparison:")151    print(insample_df.pivot(index='model', columns='target', values='r2')152          .round(6).to_string())153    print("\nRQ2 COMPLETE.")154155156if __name__ == "__main__":157    main()158