"""XS02b — Long-mom + short-rev multi-horizon Score = xs_zscore(past_return(close, 90)) + xs_zscore(-past_return(close, 5)) Long-term winners (90d) that have recently dipped (5d reversal). This is structurally distinct from plain XS01 momentum because it FADES the very-recent move while keeping the intermediate-term trend, blending momentum with mean-reversion. Grid: universe x H x k (<=5 study_xs calls). """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec") import xslib as xs import numpy as np def score_xs02b(P): """Score = xs_zscore(90d mom) + xs_zscore(-5d return). Higher = long: intermediate-term winner AND short-term dipper. Fully causal: past_return(close, L) at row i uses close[i-L..i]. """ mom_long = xs.xs_zscore(xs.past_return(P.close, 90)) # 90d momentum rev_short = xs.xs_zscore(-xs.past_return(P.close, 5)) # 5d reversal (negate: dip = good) return mom_long + rev_short if __name__ == "__main__": results = [] # Run 1: majors, H=10, k=5, L/S — canonical XS01-like setup but new signal r1 = xs.study_xs("XS02b_maj_H10_k5_LS", score_xs02b, universe="majors", H=10, k=5, long_short=True, target_vol=0.20) print(xs.fmt(r1)) print("JSON:", xs.as_json(r1)) results.append(r1) # Run 2: all (49 alts), H=10, k=5, L/S — broader universe r2 = xs.study_xs("XS02b_all_H10_k5_LS", score_xs02b, universe="all", H=10, k=5, long_short=True, target_vol=0.20) print(xs.fmt(r2)) print("JSON:", xs.as_json(r2)) results.append(r2) # Run 3: majors, H=5, k=5, L/S — faster rebalance r3 = xs.study_xs("XS02b_maj_H5_k5_LS", score_xs02b, universe="majors", H=5, k=5, long_short=True, target_vol=0.20) print(xs.fmt(r3)) print("JSON:", xs.as_json(r3)) results.append(r3) # Run 4: all, H=5, k=7, L/S — broader universe, faster, wider basket r4 = xs.study_xs("XS02b_all_H5_k7_LS", score_xs02b, universe="all", H=5, k=7, long_short=True, target_vol=0.20) print(xs.fmt(r4)) print("JSON:", xs.as_json(r4)) results.append(r4) # Run 5: majors, H=10, k=5, long-only — for comparison r5 = xs.study_xs("XS02b_maj_H10_k5_LO", score_xs02b, universe="majors", H=10, k=5, long_short=False, target_vol=0.20) print(xs.fmt(r5)) print("JSON:", xs.as_json(r5)) results.append(r5) # ---- Summary ---- print("\n\n=== XS02b GRID SUMMARY ===") for r in results: f = r["full"] h = r["holdout"] m = r.get("marginal", {}) print(f" {r['name']:35s} FULL Sh={f['sharpe']:+.2f} DD={f['maxdd']:.1%}" f" HOLD Sh={h['sharpe']:+.2f}" f" corr_xs01={r.get('corr_xs01',float('nan')):+.2f}" f" verdict={m.get('verdict','?')}" f" earns_slot={r.get('earns_slot','?')}") # Pick best by: earns_slot > hold-out > corr distinctness def sort_key(r): es = 1 if r.get("earns_slot") else 0 mv = 1 if r.get("marginal", {}).get("verdict") == "ADDS" else 0 ho = r["holdout"]["sharpe"] cxs = abs(r.get("corr_xs01", 1.0)) return (es, mv, ho, -cxs) best = max(results, key=sort_key) print(f"\nBEST CONFIG: {best['name']}") print(xs.fmt(best)) print("JSON:", xs.as_json(best))