"""XV04 — Low Downside-Vol / Semivariance Score = -roll_std(min(ret, 0), W) Only downside dispersion is penalized; upside is irrelevant. Buy lowest semivariance (most defensive), short highest. W=30 canonical; small grid over universe/H/k. """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec") import xslib as xs import numpy as np def score_xv04(P, W=30): """Score = -roll_std(min(ret, 0), W). Causal: each row uses only past W returns. Higher score = lower downside vol = more preferred (long). """ # clip positive returns to 0 so only downside contributes down = np.minimum(P.ret, 0.0) # rolling std of downside returns (semideviation) semi = xs.roll_std(down, W) # negate: lower semideviation = higher score = long bias return -semi # --- Grid: 5 studies max --- # 1) Canonical: all universe, W=30, H=10, k=5, L/S rep1 = xs.study_xs( "XV04-W30-H10-k5-LS", lambda P: score_xv04(P, W=30), universe="all", H=10, k=5, long_short=True ) print(xs.fmt(rep1)) print("JSON:", xs.as_json(rep1)) # 2) Majors universe (higher liquidity, 19 assets) rep2 = xs.study_xs( "XV04-W30-H10-k5-LS-majors", lambda P: score_xv04(P, W=30), universe="majors", H=10, k=5, long_short=True ) print(xs.fmt(rep2)) print("JSON:", xs.as_json(rep2)) # 3) Longer window W=60, all universe rep3 = xs.study_xs( "XV04-W60-H10-k5-LS", lambda P: score_xv04(P, W=60), universe="all", H=10, k=5, long_short=True ) print(xs.fmt(rep3)) print("JSON:", xs.as_json(rep3)) # 4) W=30, faster rebalance H=5 rep4 = xs.study_xs( "XV04-W30-H5-k5-LS", lambda P: score_xv04(P, W=30), universe="all", H=5, k=5, long_short=True ) print(xs.fmt(rep4)) print("JSON:", xs.as_json(rep4)) # 5) W=30, k=3 (more concentrated) rep5 = xs.study_xs( "XV04-W30-H10-k3-LS", lambda P: score_xv04(P, W=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["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))