"""XM03 — Vol-Scaled (Risk-Adjusted) Momentum
MECHANISM: Score = past_return(close, L) / roll_std(ret, L)
This is a Sharpe-like signal: normalises raw momentum by the volatility of that asset
over the same window. Should favour assets that moved up *smoothly* (high Sharpe trend)
over those that had large one-off jumps (noisy high return).
Grid: L in {30, 60, 90}; universe in {all, majors}; long_short True/False.
Goal: test if risk-adjusted scoring is DISTINCT from plain XS01 momentum and ADDS to the
live TP01+XS01+VRP01 portfolio.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs
import numpy as np


def vol_adj_momentum(P, L: int) -> np.ndarray:
    """Causal Sharpe-like score: past_return / roll_std(ret, L).
    Higher = long. Returns (n_days x n_assets).
    Avoid divide-by-zero by replacing 0-vol rows with NaN -> harness treats NaN as neutral.
    """
    pr = xs.past_return(P.close, L)          # causal past return over L days
    rv = xs.roll_std(P.ret, L)               # causal rolling std of daily returns
    # Replace zeros/near-zeros with NaN to avoid Inf
    rv_safe = np.where(rv < 1e-8, np.nan, rv)
    score = pr / rv_safe
    return score


print("XM03 — Vol-Scaled (Risk-Adjusted) Momentum")
print("=" * 60)

# 1) All universe, L=30 (short horizon vol-adj)
rep1 = xs.study_xs(
    "XM03_ALL_L30",
    lambda P: vol_adj_momentum(P, 30),
    universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep1))
print("JSON:", xs.as_json(rep1))
print()

# 2) All universe, L=60 (medium horizon)
rep2 = xs.study_xs(
    "XM03_ALL_L60",
    lambda P: vol_adj_momentum(P, 60),
    universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep2))
print("JSON:", xs.as_json(rep2))
print()

# 3) All universe, L=90 (long horizon)
rep3 = xs.study_xs(
    "XM03_ALL_L90",
    lambda P: vol_adj_momentum(P, 90),
    universe="all", H=10, k=5, long_short=True
)
print(xs.fmt(rep3))
print("JSON:", xs.as_json(rep3))
print()

# 4) Majors only (same universe as XS01), L=60 — can vol-adj beat plain MOM on XS01 turf?
rep4 = xs.study_xs(
    "XM03_MAJORS_L60",
    lambda P: vol_adj_momentum(P, 60),
    universe="majors", H=10, k=5, long_short=True
)
print(xs.fmt(rep4))
print("JSON:", xs.as_json(rep4))
print()

# 5) All universe, L=60, long-only — does vol-adj work as selection filter?
rep5 = xs.study_xs(
    "XM03_ALL_L60_LO",
    lambda P: vol_adj_momentum(P, 60),
    universe="all", H=10, k=5, long_short=False
)
print(xs.fmt(rep5))
print("JSON:", xs.as_json(rep5))
print()


def score_rep(r):
    earns = int(r["earns_slot"])
    hold_sh = r["holdout"].get("sharpe", -9)
    full_sh = r["full"]["sharpe"]
    corr_xs01 = r.get("corr_xs01") or 1.0
    distinctness = 1 - abs(corr_xs01)
    return (earns, hold_sh, full_sh, distinctness)


all_reps = [rep1, rep2, rep3, rep4, rep5]
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))
