"""XV02 — Low Idiosyncratic Volatility Anomaly. Score = -roll_std(residual_return(ret, beta_win=60), 30) (negative idiosyncratic volatility: low idio-vol = long, high idio-vol = short). Distinct from total-vol because we strip the market factor first (beta*mkt), keeping only the firm-specific noise. In equities this is the "low idio-vol" anomaly (Ang et al. 2006): low idiosyncratic volatility stocks outperform. Testing if the same holds cross-sectionally on the HL alt panel. Grid (<=5 calls total): 1. majors H=10 k=5 LS (baseline) 2. all H=10 k=5 LS (broader universe) 3. majors H=5 k=5 LS (faster rebalance) 4. majors H=10 k=4 LS (narrower book) 5. majors H=10 k=5 LS, shorter beta window (30d) [to test sensitivity] """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec") import xslib as xs import numpy as np # --------------------------------------------------------------------------- # SCORE: negative idiosyncratic vol over last 30 days # (idio ret = ret - rolling_beta_60d * market_ret, then 30d rolling std) # --------------------------------------------------------------------------- def score_idiovol(P, beta_win=60, vol_win=30): """Low idiosyncratic volatility score (higher = lower idio-vol = long).""" idio = xs.residual_return(P.ret, beta_win) # n_days x n_assets idio_vol = xs.roll_std(idio, vol_win) # rolling std of idio ret # negate: lower vol → higher score → long return -idio_vol results = [] # ---- 1. Baseline: majors, H=10, k=5, LS (beta_win=60, vol_win=30) ---- rep1 = xs.study_xs( "XV02_majors_H10k5", lambda P: score_idiovol(P, beta_win=60, vol_win=30), universe="majors", H=10, k=5, long_short=True, ) print(xs.fmt(rep1)) print("JSON:", xs.as_json(rep1)) results.append(rep1) # ---- 2. Broader universe: all, H=10, k=5, LS ---- rep2 = xs.study_xs( "XV02_all_H10k5", lambda P: score_idiovol(P, beta_win=60, vol_win=30), universe="all", H=10, k=5, long_short=True, ) print(xs.fmt(rep2)) print("JSON:", xs.as_json(rep2)) results.append(rep2) # ---- 3. Faster rebalance: majors, H=5, k=5, LS ---- rep3 = xs.study_xs( "XV02_majors_H5k5", lambda P: score_idiovol(P, beta_win=60, vol_win=30), universe="majors", H=5, k=5, long_short=True, ) print(xs.fmt(rep3)) print("JSON:", xs.as_json(rep3)) results.append(rep3) # ---- 4. Narrower book: majors, H=10, k=4, LS ---- rep4 = xs.study_xs( "XV02_majors_H10k4", lambda P: score_idiovol(P, beta_win=60, vol_win=30), universe="majors", H=10, k=4, long_short=True, ) print(xs.fmt(rep4)) print("JSON:", xs.as_json(rep4)) results.append(rep4) # ---- 5. Shorter beta window: majors, H=10, k=5, LS, beta_win=30 ---- rep5 = xs.study_xs( "XV02_majors_H10k5_bw30", lambda P: score_idiovol(P, beta_win=30, vol_win=30), universe="majors", H=10, k=5, long_short=True, ) print(xs.fmt(rep5)) print("JSON:", xs.as_json(rep5)) results.append(rep5) # ---- Summary ---- print("\n=== XV02 GRID SUMMARY ===") for r in results: earns = r["earns_slot"] print( f" {r['name']:30s} FULL {r['full']['sharpe']:+.2f} " f"HOLD {r['holdout'].get('sharpe', 0):+.2f} " f"corr_xs01 {r['corr_xs01']} " f"marginal={r['marginal']['verdict']} " f"earns_slot={earns}" ) # ---- Best config: pick by earns_slot first, then hold-out ---- earners = [r for r in results if r["earns_slot"]] if earners: best = max(earners, key=lambda r: r["holdout"].get("sharpe", -999)) print(f"\nBEST (earns_slot): {best['name']}") else: # fallback: best hold-out Sharpe with distinct XS01 corr distinct = [r for r in results if (r["corr_xs01"] or 1.0) < 0.6] if distinct: best = max(distinct, key=lambda r: r["holdout"].get("sharpe", -999)) else: best = max(results, key=lambda r: r["holdout"].get("sharpe", -999)) print(f"\nBEST (hold-out, no earns_slot): {best['name']}") print("\n--- BEST CONFIG ---") print(xs.fmt(best)) print("JSON:", xs.as_json(best))