"""XR04 — Volume-shock reversal. IDEA: Long recent losers that ALSO had a volume spike. score = -past_return(L) * (volume_z > 1) The intuition: large volume + price drop signals capitulation/panic selling. The oversold name with elevated volume is more likely to bounce vs a name that drifted down quietly. L=3 is the suggested lookback. Grid (<=5 calls): 1. majors H=5 k=3 LS=True L=3 (baseline config) 2. majors H=5 k=5 LS=True L=3 (more positions) 3. all H=5 k=5 LS=True L=3 (wider universe) 4. majors H=3 k=3 LS=True L=5 (slightly longer reversal) 5. majors H=5 k=3 LS=True L=3 with volume_z threshold = 0.5 (lower bar) """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec") import xslib as xs import numpy as np def vol_shock_reversal_score(P, L=3, vz_thresh=1.0): """score = -past_return(L) when volume_z > vz_thresh, else 0. Higher score = more reversal candidate with volume spike. Causally computed: all data at row i uses data <=i.""" ret_L = xs.past_return(P.close, L) # (n, A) vz = xs.volume_z(P.vol, 20) # rolling 20d volume z-score # Only count the reversal signal when volume is elevated mask = vz > vz_thresh # bool (n, A) score = np.where(mask, -ret_L, 0.0) # Where no data (NaN), set to NaN so harness skips score = np.where(np.isfinite(ret_L) & np.isfinite(vz), score, np.nan) return score results = [] # 1. Baseline: majors, H=5, k=3, L=3, vz_thresh=1.0 r1 = xs.study_xs( "XR04-majors-H5k3-L3", lambda P: vol_shock_reversal_score(P, L=3, vz_thresh=1.0), universe="majors", H=5, k=3, long_short=True ) print(xs.fmt(r1)) print("JSON:", xs.as_json(r1)) results.append(r1) # 2. More positions: majors, H=5, k=5, L=3 r2 = xs.study_xs( "XR04-majors-H5k5-L3", lambda P: vol_shock_reversal_score(P, L=3, vz_thresh=1.0), universe="majors", H=5, k=5, long_short=True ) print(xs.fmt(r2)) print("JSON:", xs.as_json(r2)) results.append(r2) # 3. Wider universe: all, H=5, k=5, L=3 r3 = xs.study_xs( "XR04-all-H5k5-L3", lambda P: vol_shock_reversal_score(P, L=3, vz_thresh=1.0), universe="all", H=5, k=5, long_short=True ) print(xs.fmt(r3)) print("JSON:", xs.as_json(r3)) results.append(r3) # 4. Slightly longer reversal window: majors, H=3, k=3, L=5 r4 = xs.study_xs( "XR04-majors-H3k3-L5", lambda P: vol_shock_reversal_score(P, L=5, vz_thresh=1.0), universe="majors", H=3, k=3, long_short=True ) print(xs.fmt(r4)) print("JSON:", xs.as_json(r4)) results.append(r4) # 5. Lower volume threshold: majors, H=5, k=3, L=3, vz_thresh=0.5 r5 = xs.study_xs( "XR04-majors-H5k3-L3-vz05", lambda P: vol_shock_reversal_score(P, L=3, vz_thresh=0.5), universe="majors", H=5, k=3, long_short=True ) print(xs.fmt(r5)) print("JSON:", xs.as_json(r5)) results.append(r5) # Pick best by: earns_slot first, then hold-out sharpe, then distinctness def rank_key(r): earns = int(r.get("earns_slot", False)) h_sh = r["holdout"].get("sharpe", -99) f_sh = r["full"].get("sharpe", -99) corr_xs01 = r.get("corr_xs01") or 1.0 distinct = 1 if corr_xs01 < 0.6 else 0 return (earns, h_sh, f_sh, distinct) best = max(results, key=rank_key) print("\n=== BEST CONFIG ===") print(xs.fmt(best)) print("JSON:", xs.as_json(best))