#!/usr/bin/env python3 # ============================================================================= # Author: Simon-Pierre Boucher # Contact: contact@spboucher.ai # ============================================================================= """Step 09 — Portfolio sorts and economic significance. Daily equal-weighted quintile sorts on each option-implied variable (long-short Q5−Q1 performance), a 3×3 double sort on ATM IV × implied skewness, and transaction-cost sensitivity of the implied-skewness strategy. Inputs : data/processed/merged_options_rv.parquet Outputs: results/portfolio_sort_results.csv, results/double_sort_iv_skew.csv, results/transaction_cost_analysis.csv """ import warnings import numpy as np import pandas as pd import _bootstrap # noqa: F401 from wp7 import config from wp7.data_io import load_merged from wp7.portfolio import double_sort, portfolio_sort warnings.filterwarnings('ignore') def main(): print("=" * 70) print("PORTFOLIO SORTS & ECONOMIC SIGNIFICANCE") print("=" * 70) config.ensure_output_dirs() df = load_merged() stocks = df[~df['ticker'].isin(config.NON_STOCK_TICKERS)].copy() # ── Single sorts ── all_sort_results = [] for sort_var, sort_label in config.SORT_VARIABLES.items(): for ret_var, ret_label in [('ret_1d', '1-Day'), ('ret_5d', '5-Day')]: res = portfolio_sort(stocks, sort_var, ret_var, n_quantiles=5) if res is None: continue print(f"\n {sort_label} → {ret_label} Returns:") print(f" {'Q':>6} {'Mean(bps)':>10} {'Ann.Ret%':>10} {'Ann.Vol%':>10} " f"{'Sharpe':>8} {'t-stat':>8} {'N':>6}") for q in [1, 2, 3, 4, 5, 'LS_5_1']: if q not in res: continue r = res[q] q_label = f"Q{q}" if isinstance(q, int) else "L/S(5-1)" print(f" {q_label:>6} {r['mean_daily']*10000:>10.2f} " f"{r['annualized_return']*100:>10.2f} " f"{r['annualized_vol']*100:>10.2f} {r['sharpe']:>8.3f} " f"{r['t_stat']:>8.3f} {r['n_days']:>6}") all_sort_results.append({ 'sort_variable': sort_label, 'return_horizon': ret_label, 'quintile': q_label, 'mean_daily_bps': r['mean_daily'] * 10000, 'annualized_return_pct': r['annualized_return'] * 100, 'annualized_vol_pct': r['annualized_vol'] * 100, 'sharpe_ratio': r['sharpe'], 't_statistic': r['t_stat'], 'n_days': r['n_days'], 'pct_positive': r['pct_positive'], 'max_drawdown_pct': r['max_drawdown'] * 100, }) sort_df = pd.DataFrame(all_sort_results) sort_df.to_csv(config.RESULTS_DIR / "portfolio_sort_results.csv", index=False) print("\n" + "=" * 70) print("LONG-SHORT PORTFOLIO SUMMARY (Q5 - Q1)") print("=" * 70) ls = sort_df[sort_df['quintile'] == 'L/S(5-1)'].copy() print(ls[['sort_variable', 'return_horizon', 'mean_daily_bps', 'annualized_return_pct', 'sharpe_ratio', 't_statistic']] .round(3).to_string(index=False)) # ── Double sort: ATM IV × implied skewness → 5-day returns ── print("\n" + "=" * 70) print("DOUBLE SORT: IV_ATM x IMPLIED_SKEWNESS → 5-Day Returns") print("=" * 70) ds = stocks[['iv_atm_30d', 'implied_skewness', 'ret_5d', 'ticker', 'trade_date']].dropna() ds_results = double_sort(ds, 'iv_atm_30d', 'implied_skewness', 'ret_5d') ds_pivot = ds_results.pivot(index='q1', columns='q2', values='mean_bps') ds_pivot.index = ['Low IV', 'Med IV', 'High IV'] ds_pivot.columns = ['Low Skew', 'Med Skew', 'High Skew'] print(ds_pivot.round(2).to_string()) ds_t = ds_results.pivot(index='q1', columns='q2', values='t_stat') ds_t.index = ['Low IV', 'Med IV', 'High IV'] ds_t.columns = ['Low Skew', 'Med Skew', 'High Skew'] print("\nt-statistics:") print(ds_t.round(3).to_string()) ds_results.to_csv(config.RESULTS_DIR / "double_sort_iv_skew.csv", index=False) # ── Transaction-cost sensitivity (implied-skewness L/S, weekly) ── print("\n" + "=" * 70) print("TRANSACTION COST SENSITIVITY (Long-Short on Implied Skewness, 5D)") print("=" * 70) res_skew = portfolio_sort(stocks, 'implied_skewness', 'ret_5d', n_quantiles=5) if res_skew and 'LS_5_1' in res_skew: ls_gross = res_skew['LS_5_1'] print(f" {'TC (bps)':>10} {'Net Ret(bps)':>12} {'Ann.Ret%':>10} {'Sharpe':>8}") tc_results = [] for tc_bps in [0, 5, 10, 15, 20, 30, 50]: # Weekly rebalance: full two-sided turnover spread over 5 days turnover_per_day = 2.0 / 5 daily_tc = tc_bps / 10000 * turnover_per_day net_daily = ls_gross['mean_daily'] - daily_tc net_ann = net_daily * 52 net_sharpe = (net_daily / ls_gross['std_daily'] * np.sqrt(52)) \ if ls_gross['std_daily'] > 0 else 0 print(f" {tc_bps:>10} {net_daily*10000:>12.2f} " f"{net_ann*100:>10.2f} {net_sharpe:>8.3f}") tc_results.append({'tc_bps': tc_bps, 'net_daily_bps': net_daily * 10000, 'net_ann_ret_pct': net_ann * 100, 'net_sharpe': net_sharpe}) pd.DataFrame(tc_results).to_csv( config.RESULTS_DIR / "transaction_cost_analysis.csv", index=False) print("\nPORTFOLIO SORTS COMPLETE.") if __name__ == "__main__": main()