"""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))
