"""XV06 — Low Vol-of-Vol (stability of volatility) Score = -roll_std(roll_std(ret, inner_win), outer_win) Idea: assets whose volatility is most STABLE (predictable) are preferred long; assets with high vol-of-vol (erratic/spiky volatility) are shorted. Lower vol-of-vol = higher score = long bias. Canonical: inner_win=10, outer_win=30. """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec") import xslib as xs import numpy as np def score_xv06(P, inner=10, outer=30): """Score = -roll_std(roll_std(ret, inner), outer). Causal: each row uses only past data (rolling windows, no future leakage). Higher score = lower vol-of-vol = more stable volatility = preferred long. """ # inner rolling std: daily vol estimate inner_vol = xs.roll_std(P.ret, inner) # outer rolling std of that vol: vol-of-vol vov = xs.roll_std(inner_vol, outer) # negate: lower vov = higher score = long return -vov # --- Grid: 5 studies max --- # 1) Canonical: inner=10, outer=30, all universe, H=10, k=5, L/S rep1 = xs.study_xs( "XV06-i10-o30-H10-k5-LS", lambda P: score_xv06(P, inner=10, outer=30), universe="all", H=10, k=5, long_short=True ) print(xs.fmt(rep1)) print("JSON:", xs.as_json(rep1)) # 2) Majors only (19 assets, better liquidity) rep2 = xs.study_xs( "XV06-i10-o30-H10-k5-LS-majors", lambda P: score_xv06(P, inner=10, outer=30), universe="majors", H=10, k=5, long_short=True ) print(xs.fmt(rep2)) print("JSON:", xs.as_json(rep2)) # 3) Wider outer window: inner=10, outer=60 rep3 = xs.study_xs( "XV06-i10-o60-H10-k5-LS", lambda P: score_xv06(P, inner=10, outer=60), universe="all", H=10, k=5, long_short=True ) print(xs.fmt(rep3)) print("JSON:", xs.as_json(rep3)) # 4) Faster rebalance H=5 rep4 = xs.study_xs( "XV06-i10-o30-H5-k5-LS", lambda P: score_xv06(P, inner=10, outer=30), universe="all", H=5, k=5, long_short=True ) print(xs.fmt(rep4)) print("JSON:", xs.as_json(rep4)) # 5) More concentrated k=3 rep5 = xs.study_xs( "XV06-i10-o30-H10-k3-LS", lambda P: score_xv06(P, inner=10, outer=30), universe="all", H=10, k=3, long_short=True ) print(xs.fmt(rep5)) print("JSON:", xs.as_json(rep5)) # Pick best by: earns_slot first, then holdout Sharpe, then distinctness reps = [rep1, rep2, rep3, rep4, rep5] earners = [r for r in reps if r["earns_slot"]] if earners: best = max(earners, key=lambda r: r["holdout"].get("sharpe", -999)) else: # fallback: positive full + hold-out + corr_xs01 < 0.6 candidates = [r for r in reps if r["full"]["sharpe"] > 0 and r["holdout"].get("sharpe", 0) > 0 and (r.get("corr_xs01") or 1.0) < 0.6] if candidates: best = max(candidates, key=lambda r: r["holdout"].get("sharpe", -999)) else: best = max(reps, key=lambda r: r["full"]["sharpe"]) print("\n=== BEST CONFIG ===") print(xs.fmt(best)) print("JSON:", xs.as_json(best))