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%
7.9 KB · 191 lines python
Raw Blame History
1#!/usr/bin/env python32# =============================================================================3# Author: Simon-Pierre Boucher4# Contact: contact@spboucher.ai5# =============================================================================6"""Step 07 — Extended descriptive statistics and data-quality analysis.78Panels A–E and G run from the processed panel alone. Panel F (VIX regimes)9and Panel H (raw options data quality) additionally need the raw stores and10are skipped gracefully when those are absent.1112Inputs : data/processed/merged_options_rv.parquet13         (+ index_5min.duckdb and options.duckdb for panels F/H)14Outputs: results/descriptive_*.csv (8 tables)15"""1617import warnings1819import pandas as pd2021import _bootstrap  # noqa: F40122from wp7 import config23from wp7.data_io import RawDataUnavailableError, load_merged, load_vix_daily, open_raw_db2425warnings.filterwarnings('ignore')2627SUMMARY_VARS = [28    'iv_atm_30d', 'iv_atm_90d', 'iv_term_slope', 'iv_skew_25d',29    'implied_skewness', 'implied_kurtosis_proxy',30    'pc_volume_ratio', 'pc_oi_ratio', 'net_gamma_exposure',31    'avg_vega_30d', 'avg_theta_30d', 'total_option_volume', 'total_oi',32    'rv_daily', 'rvol_daily', 'rv_weekly', 'daily_return',33    'realized_skew', 'realized_kurt',34    'ret_1d', 'ret_5d', 'rv_fwd_1d', 'rv_fwd_5d',35]3637CORR_VARS = ['iv_atm_30d', 'iv_term_slope', 'iv_skew_25d', 'implied_skewness',38             'implied_kurtosis_proxy', 'pc_volume_ratio', 'pc_oi_ratio',39             'rv_daily', 'rv_weekly', 'ret_1d', 'ret_5d']4041AUTOCORR_VARS = ['iv_atm_30d', 'iv_skew_25d', 'rv_daily', 'daily_return', 'pc_volume_ratio']424344def group_stats(data: pd.DataFrame, label: str) -> dict:45    """One summary row per asset group."""46    return {47        'Group': label,48        'N_obs': len(data),49        'N_tickers': data['ticker'].nunique(),50        'Date_min': str(data['trade_date'].min().date()),51        'Date_max': str(data['trade_date'].max().date()),52        'Mean_IV_ATM': data['iv_atm_30d'].mean(),53        'Std_IV_ATM': data['iv_atm_30d'].std(),54        'Mean_RV': data['rv_daily'].mean(),55        'Mean_Skew': data['iv_skew_25d'].mean(),56        'Mean_Ret_1d': data['ret_1d'].mean(),57        'Std_Ret_1d': data['ret_1d'].std(),58        'Mean_PC_ratio': data['pc_volume_ratio'].mean(),59    }606162def main():63    print("=" * 70)64    print("EXTENDED DESCRIPTIVE STATISTICS")65    print("=" * 70)66    config.ensure_output_dirs()67    res_dir = config.RESULTS_DIR6869    df = load_merged()70    df['year'] = df['trade_date'].dt.year7172    # ── Panel A: summary statistics ──73    print("\n--- PANEL A: SUMMARY STATISTICS ---")74    summary = df[SUMMARY_VARS].describe(75        percentiles=[0.01, 0.05, 0.25, 0.5, 0.75, 0.95, 0.99]).T76    summary['skewness'] = df[SUMMARY_VARS].skew()77    summary['kurtosis'] = df[SUMMARY_VARS].kurtosis()78    summary['pct_missing'] = df[SUMMARY_VARS].isnull().mean() * 10079    summary.to_csv(res_dir / "descriptive_summary_stats.csv")80    print(summary[['count', 'mean', 'std', '1%', '50%', '99%',81                   'skewness', 'kurtosis', 'pct_missing']].round(4).to_string())8283    # ── Panel B: coverage by year ──84    print("\n--- PANEL B: COVERAGE BY YEAR ---")85    coverage = df.groupby('year').agg(86        n_obs=('ticker', 'count'),87        n_tickers=('ticker', 'nunique'),88        avg_iv_atm=('iv_atm_30d', 'mean'),89        avg_rv=('rv_daily', 'mean'),90        avg_skew=('iv_skew_25d', 'mean'),91        avg_ret=('daily_return', 'mean'),92        std_ret=('daily_return', 'std'),93    ).round(6)94    coverage.to_csv(res_dir / "descriptive_coverage_by_year.csv")95    print(coverage.to_string())9697    # ── Panel C: coverage by asset group ──98    print("\n--- PANEL C: BY ASSET GROUP ---")99    stocks_list = [t for t in df['ticker'].unique() if t not in config.NON_STOCK_TICKERS]100    groups = pd.DataFrame([101        group_stats(df[df['ticker'].isin(stocks_list)], 'Stocks'),102        group_stats(df[df['ticker'].isin(config.ETF_TICKERS)], 'ETFs'),103        group_stats(df[df['ticker'].isin(config.INDEX_OPTION_TICKERS)], 'Indices'),104        group_stats(df, 'All'),105    ])106    groups.to_csv(res_dir / "descriptive_by_group.csv", index=False)107    print(groups.to_string(index=False))108109    # ── Panel D: correlation matrix ──110    print("\n--- PANEL D: CORRELATION MATRIX ---")111    corr_matrix = df[CORR_VARS].corr().round(3)112    corr_matrix.to_csv(res_dir / "descriptive_correlation_matrix.csv")113    print(corr_matrix.to_string())114115    # ── Panel E: autocorrelation structure ──116    print("\n--- PANEL E: AUTOCORRELATION STRUCTURE ---")117    autocorr_results = []118    for var in AUTOCORR_VARS:119        for lag in [1, 5, 10, 22]:120            ac = df.groupby('ticker')[var].apply(lambda x: x.autocorr(lag=lag)).mean()121            autocorr_results.append({'variable': var, 'lag': lag, 'avg_autocorr': ac})122    autocorr_df = pd.DataFrame(autocorr_results)123    autocorr_df.to_csv(res_dir / "descriptive_autocorrelations.csv", index=False)124    print(autocorr_df.pivot(index='variable', columns='lag', values='avg_autocorr')125          .round(4).to_string())126127    # ── Panel G: cross-sectional dispersion by year ──128    print("\n--- PANEL G: CROSS-SECTIONAL DISPERSION ---")129    cs_disp = df.groupby('year').agg(130        iv_atm_cs_std=('iv_atm_30d', 'std'),131        skew_cs_std=('iv_skew_25d', 'std'),132        rv_cs_std=('rv_daily', 'std'),133        ret_cs_std=('daily_return', 'std'),134        n_tickers=('ticker', 'nunique'),135    ).round(6)136    cs_disp.to_csv(res_dir / "descriptive_cross_sectional_dispersion.csv")137    print(cs_disp.to_string())138139    # ── Panel F: statistics by VIX regime (needs raw VIX series) ──140    print("\n--- PANEL F: STATISTICS BY VIX REGIME ---")141    try:142        vix = load_vix_daily()143        dfv = df.merge(vix, on='trade_date', how='left')144        dfv['vix_regime'] = pd.cut(dfv['vix_close'], bins=config.VIX_REGIME_BINS,145                                   labels=config.VIX_REGIME_LABELS_VERBOSE)146        regime_stats = dfv.groupby('vix_regime', observed=True).agg(147            n_obs=('ticker', 'count'),148            pct_obs=('ticker', lambda x: len(x) / len(dfv) * 100),149            mean_iv_atm=('iv_atm_30d', 'mean'),150            mean_rv=('rv_daily', 'mean'),151            mean_skew=('iv_skew_25d', 'mean'),152            mean_ret_1d=('ret_1d', 'mean'),153            std_ret_1d=('ret_1d', 'std'),154            mean_pc_ratio=('pc_volume_ratio', 'mean'),155            mean_impl_skew=('implied_skewness', 'mean'),156        ).round(6)157        regime_stats.to_csv(res_dir / "descriptive_vix_regimes.csv")158        print(regime_stats.to_string())159    except RawDataUnavailableError as exc:160        print(f"  [Panel F skipped — raw stores unavailable]\n  {exc}")161162    # ── Panel H: raw options data quality (needs options.duckdb) ──163    print("\n--- PANEL H: OPTIONS DATA QUALITY ---")164    try:165        opt_con = open_raw_db("options")166        quality = opt_con.execute("""167            SELECT168                EXTRACT(YEAR FROM trade_date) AS year,169                COUNT(*) AS n_records,170                COUNT(DISTINCT ticker) AS n_tickers,171                AVG(CASE WHEN bid_iv > 0 AND ask_iv > 0 THEN 1.0 ELSE 0.0 END) AS pct_valid_iv,172                AVG(CASE WHEN volume > 0 THEN 1.0 ELSE 0.0 END) AS pct_with_volume,173                AVG(CASE WHEN open_interest > 0 THEN 1.0 ELSE 0.0 END) AS pct_with_oi,174                AVG(CASE WHEN delta IS NOT NULL AND delta != 0 THEN 1.0 ELSE 0.0 END) AS pct_valid_greeks,175                AVG(ask_price - bid_price) AS avg_spread176            FROM option_chain177            GROUP BY year178            ORDER BY year179        """).fetchdf()180        opt_con.close()181        quality.to_csv(res_dir / "descriptive_options_quality.csv", index=False)182        print(quality.round(4).to_string(index=False))183    except RawDataUnavailableError as exc:184        print(f"  [Panel H skipped — raw stores unavailable]\n  {exc}")185186    print("\nDESCRIPTIVE STATISTICS COMPLETE.")187188189if __name__ == "__main__":190    main()191