"""XU02 — Rebalance/Holding Sweep (Low-Vol + Momentum) MECHANISM: Study how holding period H and portfolio size k interact with signal quality. Two signals: (1) pure momentum blend (z30+z90 same as XS01), (2) low-vol rank (short volatile, long stable). Goal: find whether a DIFFERENT H/k pair or signal gives something DISTINCT from XS01. Hypothesis: - XS01 uses H=10, k=5 (momentum). A longer H reduces turnover, captures slower signal decay. - Low-vol selection (long stable alts, short volatile ones) is conceptually orthogonal to momentum. - Sweep: H in {5,10,20,30}, k in {3,5,8}, signal in {momentum, low_vol}. - Keep <=5 backtests: focus on the most contrasting configs. Grid (5 backtests): 1. MOM H=5 k=5 — fast rebalance, same as XS01 direction, more turnover 2. MOM H=30 k=5 — slow rebalance, lower turnover, tests signal persistence 3. LVOL H=10 k=5 — low-vol signal at standard H/k (conceptually distinct from momentum) 4. LVOL H=20 k=5 — low-vol with slower rebalance 5. LVOL H=10 k=3 — low-vol tight portfolio, more concentrated """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec") import xslib as xs import numpy as np print("XU02 — Rebalance/Holding Sweep (Low-Vol + Momentum)") print("=" * 60) def mom_blend(P): """Z-blend of 30d and 90d momentum (same signal as XS01).""" z30 = xs.xs_zscore(xs.past_return(P.close, 30)) z90 = xs.xs_zscore(xs.past_return(P.close, 90)) return np.nanmean(np.stack([z30, z90], axis=0), axis=0) def low_vol(P): """Low-vol signal: score = -rolling_std(ret, 30). Higher score = lower vol = long. Cross-sectionaly ranks alts: stable (low realized vol) go long, volatile go short.""" rv = xs.roll_std(P.ret, 30) return -rv # negate: higher = lower vol = prefer long # 1) Momentum H=5 k=5 — fast rebalance (more turnover, tests short-term signal) rep1 = xs.study_xs( "XU02_MOM_H5k5", mom_blend, universe="majors", H=5, k=5, long_short=True ) print(xs.fmt(rep1)) print("JSON:", xs.as_json(rep1)) print() # 2) Momentum H=30 k=5 — slow rebalance, lower turnover, tests signal persistence rep2 = xs.study_xs( "XU02_MOM_H30k5", mom_blend, universe="majors", H=30, k=5, long_short=True ) print(xs.fmt(rep2)) print("JSON:", xs.as_json(rep2)) print() # 3) Low-vol H=10 k=5 — standard H/k but conceptually distinct signal rep3 = xs.study_xs( "XU02_LVOL_H10k5", low_vol, universe="majors", H=10, k=5, long_short=True ) print(xs.fmt(rep3)) print("JSON:", xs.as_json(rep3)) print() # 4) Low-vol H=20 k=5 — slower rebalance, low-vol is a structural trait (changes slowly) rep4 = xs.study_xs( "XU02_LVOL_H20k5", low_vol, universe="majors", H=20, k=5, long_short=True ) print(xs.fmt(rep4)) print("JSON:", xs.as_json(rep4)) print() # 5) Low-vol H=10 k=3 — tighter portfolio (top/bottom 3 most extreme) rep5 = xs.study_xs( "XU02_LVOL_H10k3", low_vol, universe="majors", H=10, k=3, long_short=True ) print(xs.fmt(rep5)) print("JSON:", xs.as_json(rep5)) print() # Summary print("=" * 60) print("SUMMARY: All 5 configs ranked by hold-out Sharpe") all_reps = [rep1, rep2, rep3, rep4, rep5] ranked = sorted(all_reps, key=lambda r: r["holdout"].get("sharpe", -999), reverse=True) for r in ranked: h_sh = r["holdout"].get("sharpe", 0) f_sh = r["full"]["sharpe"] c_xs01 = r["corr_xs01"] verdict = r["marginal"].get("verdict", "N/A") earns = r["earns_slot"] print(f" {r['name']:25s} FULL={f_sh:+.2f} HOLD={h_sh:+.2f} corr_xs01={c_xs01} " f"verdict={verdict} earns_slot={earns}") # Pick best by marginal robustness -> earns_slot -> hold-out -> distinctness best = None for r in ranked: if r["earns_slot"]: best = r break if best is None: # fallback: best hold-out with corr_xs01 < 0.6 candidates = [r for r in ranked if (r.get("corr_xs01") or 1.0) < 0.6 and r["holdout"].get("sharpe", 0) > 0] best = candidates[0] if candidates else ranked[0] print() print("BEST CONFIG:") print(xs.fmt(best)) print("JSON:", xs.as_json(best))