"""XV03 — Betting Against Beta (BAB) — Low-beta anomaly MECHANISM: Score = -roll_beta(ret, W) (long low-beta alts / short high-beta alts). The BAB anomaly (Frazzini & Pedersen 2014): within an asset cross-section, lower-beta assets deliver higher risk-adjusted returns — because levered/constrained investors bid up high-beta assets above fair value. Score = NEGATIVE rolling beta to equal-weight market, so top-ranked = lowest beta. Grid: beta window W in {30, 60}; universe in {all, majors}; long-short. Also test: blend BAB + dispersion condition (only enter if cross-sectional vol is high). """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec") import xslib as xs import numpy as np print("XV03 — Betting Against Beta (BAB)") print("=" * 60) # -------------------------------------------------------------------------- # Score functions # -------------------------------------------------------------------------- def score_bab(ret, win): """BAB score: negative rolling beta to equal-weight market (causal).""" beta = xs.roll_beta(ret, win) # (n,A): higher = more market exposure return -beta # higher score = lower beta = long candidate def score_bab_beta_adj(ret, win): """BAB score adjusted: z-score the negative beta cross-sectionally.""" return xs.xs_zscore(-xs.roll_beta(ret, win)) # -------------------------------------------------------------------------- # Run 5 targeted backtests # -------------------------------------------------------------------------- # 1) All alts, W=30 (shorter window — more reactive), LS rep1 = xs.study_xs( "XV03_ALL_W30_LS", lambda P: score_bab(P.ret, 30), universe="all", H=10, k=5, long_short=True ) print(xs.fmt(rep1)) print("JSON:", xs.as_json(rep1)) print() # 2) All alts, W=60 (longer window — more stable beta estimates), LS rep2 = xs.study_xs( "XV03_ALL_W60_LS", lambda P: score_bab(P.ret, 60), universe="all", H=10, k=5, long_short=True ) print(xs.fmt(rep2)) print("JSON:", xs.as_json(rep2)) print() # 3) Majors only (19 XS01 assets), W=60, LS # Cleaner universe: major liquid alts, should reduce noise rep3 = xs.study_xs( "XV03_MAJORS_W60_LS", lambda P: score_bab(P.ret, 60), universe="majors", H=10, k=5, long_short=True ) print(xs.fmt(rep3)) print("JSON:", xs.as_json(rep3)) print() # 4) All alts, W=60, XS-zscored BAB, shorter rebalance H=5 # XS z-score normalizes the beta signal each day cross-sectionally rep4 = xs.study_xs( "XV03_ALL_W60_ZS_H5", lambda P: score_bab_beta_adj(P.ret, 60), universe="all", H=5, k=5, long_short=True ) print(xs.fmt(rep4)) print("JSON:", xs.as_json(rep4)) print() # 5) BAB blend: combine W=30 and W=60 betas (multi-horizon, inspired by XS01 blend) # Average the two z-scored BAB signals rep5 = xs.study_xs( "XV03_ALL_BLEND3060_LS", lambda P: xs.xs_zscore(score_bab(P.ret, 30)) + xs.xs_zscore(score_bab(P.ret, 60)), universe="all", H=10, k=5, long_short=True ) print(xs.fmt(rep5)) print("JSON:", xs.as_json(rep5)) print() # -------------------------------------------------------------------------- # Pick best: earns_slot > hold-out sharpe > distinctness from XS01 # -------------------------------------------------------------------------- all_reps = [rep1, rep2, rep3, rep4, rep5] def score_rep(r): earns = int(r["earns_slot"]) hold_sh = r["holdout"].get("sharpe", -9) full_sh = r["full"]["sharpe"] corr_xs01 = r["corr_xs01"] if r["corr_xs01"] is not None else 1.0 distinctness = 1 - abs(corr_xs01) return (earns, hold_sh, full_sh, distinctness) best = max(all_reps, key=score_rep) print("=" * 60) print(f"BEST CONFIG: {best['name']}") print(xs.fmt(best)) print("JSON:", xs.as_json(best))