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 09 — Portfolio sorts and economic significance.78Daily equal-weighted quintile sorts on each option-implied variable9(long-short Q5−Q1 performance), a 3×3 double sort on ATM IV × implied10skewness, and transaction-cost sensitivity of the implied-skewness strategy.1112Inputs : data/processed/merged_options_rv.parquet13Outputs: results/portfolio_sort_results.csv, results/double_sort_iv_skew.csv,14 results/transaction_cost_analysis.csv15"""1617import warnings1819import numpy as np20import pandas as pd2122import _bootstrap # noqa: F40123from wp7 import config24from wp7.data_io import load_merged25from wp7.portfolio import double_sort, portfolio_sort2627warnings.filterwarnings('ignore')282930def main():31 print("=" * 70)32 print("PORTFOLIO SORTS & ECONOMIC SIGNIFICANCE")33 print("=" * 70)34 config.ensure_output_dirs()3536 df = load_merged()37 stocks = df[~df['ticker'].isin(config.NON_STOCK_TICKERS)].copy()3839 # ── Single sorts ──40 all_sort_results = []41 for sort_var, sort_label in config.SORT_VARIABLES.items():42 for ret_var, ret_label in [('ret_1d', '1-Day'), ('ret_5d', '5-Day')]:43 res = portfolio_sort(stocks, sort_var, ret_var, n_quantiles=5)44 if res is None:45 continue4647 print(f"\n {sort_label} → {ret_label} Returns:")48 print(f" {'Q':>6} {'Mean(bps)':>10} {'Ann.Ret%':>10} {'Ann.Vol%':>10} "49 f"{'Sharpe':>8} {'t-stat':>8} {'N':>6}")5051 for q in [1, 2, 3, 4, 5, 'LS_5_1']:52 if q not in res:53 continue54 r = res[q]55 q_label = f"Q{q}" if isinstance(q, int) else "L/S(5-1)"56 print(f" {q_label:>6} {r['mean_daily']*10000:>10.2f} "57 f"{r['annualized_return']*100:>10.2f} "58 f"{r['annualized_vol']*100:>10.2f} {r['sharpe']:>8.3f} "59 f"{r['t_stat']:>8.3f} {r['n_days']:>6}")6061 all_sort_results.append({62 'sort_variable': sort_label,63 'return_horizon': ret_label,64 'quintile': q_label,65 'mean_daily_bps': r['mean_daily'] * 10000,66 'annualized_return_pct': r['annualized_return'] * 100,67 'annualized_vol_pct': r['annualized_vol'] * 100,68 'sharpe_ratio': r['sharpe'],69 't_statistic': r['t_stat'],70 'n_days': r['n_days'],71 'pct_positive': r['pct_positive'],72 'max_drawdown_pct': r['max_drawdown'] * 100,73 })7475 sort_df = pd.DataFrame(all_sort_results)76 sort_df.to_csv(config.RESULTS_DIR / "portfolio_sort_results.csv", index=False)7778 print("\n" + "=" * 70)79 print("LONG-SHORT PORTFOLIO SUMMARY (Q5 - Q1)")80 print("=" * 70)81 ls = sort_df[sort_df['quintile'] == 'L/S(5-1)'].copy()82 print(ls[['sort_variable', 'return_horizon', 'mean_daily_bps',83 'annualized_return_pct', 'sharpe_ratio', 't_statistic']]84 .round(3).to_string(index=False))8586 # ── Double sort: ATM IV × implied skewness → 5-day returns ──87 print("\n" + "=" * 70)88 print("DOUBLE SORT: IV_ATM x IMPLIED_SKEWNESS → 5-Day Returns")89 print("=" * 70)90 ds = stocks[['iv_atm_30d', 'implied_skewness', 'ret_5d', 'ticker', 'trade_date']].dropna()91 ds_results = double_sort(ds, 'iv_atm_30d', 'implied_skewness', 'ret_5d')9293 ds_pivot = ds_results.pivot(index='q1', columns='q2', values='mean_bps')94 ds_pivot.index = ['Low IV', 'Med IV', 'High IV']95 ds_pivot.columns = ['Low Skew', 'Med Skew', 'High Skew']96 print(ds_pivot.round(2).to_string())9798 ds_t = ds_results.pivot(index='q1', columns='q2', values='t_stat')99 ds_t.index = ['Low IV', 'Med IV', 'High IV']100 ds_t.columns = ['Low Skew', 'Med Skew', 'High Skew']101 print("\nt-statistics:")102 print(ds_t.round(3).to_string())103104 ds_results.to_csv(config.RESULTS_DIR / "double_sort_iv_skew.csv", index=False)105106 # ── Transaction-cost sensitivity (implied-skewness L/S, weekly) ──107 print("\n" + "=" * 70)108 print("TRANSACTION COST SENSITIVITY (Long-Short on Implied Skewness, 5D)")109 print("=" * 70)110 res_skew = portfolio_sort(stocks, 'implied_skewness', 'ret_5d', n_quantiles=5)111 if res_skew and 'LS_5_1' in res_skew:112 ls_gross = res_skew['LS_5_1']113 print(f" {'TC (bps)':>10} {'Net Ret(bps)':>12} {'Ann.Ret%':>10} {'Sharpe':>8}")114 tc_results = []115 for tc_bps in [0, 5, 10, 15, 20, 30, 50]:116 # Weekly rebalance: full two-sided turnover spread over 5 days117 turnover_per_day = 2.0 / 5118 daily_tc = tc_bps / 10000 * turnover_per_day119 net_daily = ls_gross['mean_daily'] - daily_tc120 net_ann = net_daily * 52121 net_sharpe = (net_daily / ls_gross['std_daily'] * np.sqrt(52)) \122 if ls_gross['std_daily'] > 0 else 0123 print(f" {tc_bps:>10} {net_daily*10000:>12.2f} "124 f"{net_ann*100:>10.2f} {net_sharpe:>8.3f}")125 tc_results.append({'tc_bps': tc_bps, 'net_daily_bps': net_daily * 10000,126 'net_ann_ret_pct': net_ann * 100, 'net_sharpe': net_sharpe})127 pd.DataFrame(tc_results).to_csv(128 config.RESULTS_DIR / "transaction_cost_analysis.csv", index=False)129130 print("\nPORTFOLIO SORTS COMPLETE.")131132133if __name__ == "__main__":134 main()135