"""EQ-MOM01 — Momentum cross-sectional settoriale (SPDR), backtest onesto.

Prima ricerca del fronte equity (branch research/equities-ib). L'edge "noioso e robusto" piu'
plausibile in un mercato efficiente: ruotare nei settori a momentum forte. Domanda chiave (come nel
crypto col soffitto TP01): NON "fa soldi?" (un long-only equity cavalca il toro) ma **batte/ADDS a
SPY buy&hold?** — il baseline vero in equity. Anche vs equal-weight 9 settori (isola il timing del
momentum dal tilt equal-weight).

DATI: cache su disco eq_*.parquet (ADJUSTED div+split), via eqlib (nessun IB). 9 settori CLASSICI
dal 1998 (27.5y) per il backtest lungo; 11 settori (2018+) come robustezza.

COSTRUZIONE (causale): ogni REB giorni, momentum = blend di lookback [63,126,252]g con SKIP recente
(12-1 classico), z-score cross-sectional mediato. long-only: full-invested nei top-k (confronto
like-for-like con SPY). long-short: dollar-neutral top-k vs bottom-k. Posizione decisa a <= i-1,
tenuta da i (W[:-1]*dret[1:]). Netto fee sul turnover. Opz. vol-target.

GIUDIZIO: standalone (FULL / pre-2015 / hold-out 2015+ / per-anno, CAGR, Sharpe, maxDD) vs SPY e
EW-settori; marginale vs SPY (corr, uplift blend full+hold, edge in-sample, persistenza multi-cut);
plateau su lookback/k/reb/skip; sweep fee.
"""
import sys
from pathlib import Path
import numpy as np, pandas as pd

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "scripts" / "research"))
import eqlib
from eqlib import panel, load_eq, SECTORS_CLASSIC, SECTORS

ANN = np.sqrt(252.0)
EQ_HOLDOUT = pd.Timestamp("2015-01-01", tz="UTC")   # OOS lungo: ultimi ~11 anni (post-GFC, dove il momentum e' decaduto)


def _sh(r):
    r = np.asarray(pd.Series(r).dropna(), float)
    return float(np.mean(r) / np.std(r) * ANN) if len(r) > 2 and np.std(r) > 0 else 0.0


def _cagr(r, idx):
    r = np.asarray(r, float); yrs = (idx[-1] - idx[0]).days / 365.25
    return float(np.prod(1 + r) ** (1 / yrs) - 1) if yrs > 0 else 0.0


def _dd(r):
    eq = np.cumprod(1 + np.asarray(r, float)); pk = np.maximum.accumulate(eq)
    return float(np.max((pk - eq) / pk)) if len(eq) else 0.0


def momentum(universe=tuple(SECTORS_CLASSIC), lookbacks=(63, 126, 252), k=3, reb=21,
             skip=21, mode="long", target_vol=None, fee_side=0.0002):
    """Serie netta daily del book momentum settoriale. mode='long' (top-k full-invested) o
    'ls' (dollar-neutral top-k vs bottom-k)."""
    P = panel(universe, how="inner")
    idx = P.index; px = P.values; n, A = px.shape
    dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
    mlb = max(lookbacks) + skip
    W = np.zeros((n, A)); w = np.zeros(A)
    for i in range(n):
        if i >= mlb and i % reb == 0:
            score = np.zeros(A); cnt = 0
            for Lb in lookbacks:
                a, b = i - skip - Lb, i - skip
                rL = px[b] / px[a] - 1.0
                sd = rL.std()
                if sd > 0:
                    score += (rL - rL.mean()) / sd; cnt += 1
            if cnt:
                score /= cnt
                order = np.argsort(score)
                w = np.zeros(A)
                if mode == "long":
                    w[order[-k:]] = 1.0 / k                       # full-invested nei top-k
                else:
                    w[order[-k:]] = 0.5 / k; w[order[:k]] = -0.5 / k
        W[i] = w
    gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
    turn = np.zeros(n); turn[0] = np.abs(W[0]).sum(); turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
    net = gross - turn * fee_side
    s = pd.Series(net, index=idx)
    if target_vol:
        rv = s.rolling(63, min_periods=20).std().shift(1) * ANN
        scale = np.clip(np.nan_to_num(target_vol / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
        s = pd.Series(s.values * scale, index=idx)
    return s


def spy_bh():
    d = load_eq("SPY")["close"].astype(float)
    return pd.Series(d.values[1:] / d.values[:-1] - 1.0, index=d.index[1:])


def ew_sectors_bh(universe=tuple(SECTORS_CLASSIC)):
    P = panel(universe, how="inner"); dret = P.pct_change().dropna()
    return dret.mean(axis=1)


def _line(name, r, idx=None, bench=None):
    idx = idx if idx is not None else r.index
    r = r.reindex(idx).fillna(0.0) if hasattr(r, "reindex") else pd.Series(r, index=idx)
    h = r[r.index >= EQ_HOLDOUT]; isamp = r[r.index < EQ_HOLDOUT]
    extra = ""
    if bench is not None:
        J = pd.concat({"r": r, "b": bench}, axis=1, join="inner").dropna()
        extra = f"  corr_SPY {J['r'].corr(J['b']):+.2f}"
    print(f"  {name:26} CAGR {_cagr(r.values, r.index)*100:>5.1f}%  Sh {_sh(r):>5.2f}  "
          f"(pre15 {_sh(isamp):>5.2f} | OOS15+ {_sh(h):>5.2f})  maxDD {_dd(r.values)*100:>4.0f}%{extra}")


def main():
    print("=" * 100)
    print("  EQ-MOM01 — Momentum cross-sectional settoriale (9 SPDR classici, 1998+)")
    print("=" * 100)
    spy = spy_bh(); ew = ew_sectors_bh()
    base = momentum()                                  # long, lb[63,126,252], k=3, reb21, skip21
    common = base.index
    print(f"  periodo {common[0].date()}..{common[-1].date()}  ({len(common)}g)  hold-out OOS = {EQ_HOLDOUT.date()}+\n")

    print("  --- BASELINE da battere ---")
    _line("SPY buy&hold", spy, common)
    _line("EW 9 settori buy&hold", ew, common, bench=spy)
    print("\n  --- EQ-MOM01 ---")
    _line("MOM long top-3", base, common, bench=spy)
    _line("MOM long top-3 vt15%", momentum(target_vol=0.15), common, bench=spy)
    _line("MOM long-short top-3", momentum(mode="ls"), common, bench=spy)

    # MARGINALE vs SPY (il test che conta in equity)
    print("\n  --- MARGINALE vs SPY buy&hold (aggiunge al baseline?) ---")
    J = pd.concat({"spy": spy, "c": base}, axis=1, join="inner").dropna()
    JH = J[J.index >= EQ_HOLDOUT]
    print(f"    corr(MOM,SPY) full {J['spy'].corr(J['c']):+.3f}  | OOS {JH['spy'].corr(JH['c']):+.3f}")
    for wt in (0.25, 0.5):
        bf = _sh((1-wt)*J["spy"]+wt*J["c"]) - _sh(J["spy"])
        bh = _sh((1-wt)*JH["spy"]+wt*JH["c"]) - _sh(JH["spy"])
        print(f"    blend {int((1-wt)*100)}/{int(wt*100)} SPY/MOM: uplift Sharpe FULL {bf:+.3f}  OOS {bh:+.3f}")
    # per-decade (multi-cut onesto)
    print("    Sharpe MOM per blocco: ", {f"{y}s": round(_sh(base[(base.index.year>=y)&(base.index.year<y+5)]),2)
          for y in (1999,2004,2009,2014,2019,2024)})

    # PLATEAU
    print("\n  --- PLATEAU (Sharpe FULL / pre15 / OOS15+) long-only ---")
    print(f"    {'cfg':24} {'FULL':>6} {'pre15':>6} {'OOS':>6} {'CAGR%':>6} {'DD%':>5}")
    for lbs in [(126,), (63,126,252), (252,)]:
        for k in (2, 3, 4):
            for reb in (21,):
                s = momentum(lookbacks=lbs, k=k, reb=reb)
                tag = f"lb{'-'.join(map(str,lbs))} k{k}"
                h=s[s.index>=EQ_HOLDOUT]; ii=s[s.index<EQ_HOLDOUT]
                print(f"    {tag:24} {_sh(s):>6.2f} {_sh(ii):>6.2f} {_sh(h):>6.2f} {_cagr(s.values,s.index)*100:>6.1f} {_dd(s.values)*100:>5.0f}")

    # sweep fee + robustezza 11 settori (2018+)
    print("\n  --- ROBUSTEZZA ---")
    for fee in (0.0, 0.0002, 0.0005, 0.001):
        s = momentum(fee_side=fee); print(f"    fee {fee*100:.2f}%/lato: Sh FULL {_sh(s):.2f}  OOS {_sh(s[s.index>=EQ_HOLDOUT]):.2f}")
    s11 = momentum(universe=tuple(SECTORS)); spy11 = spy.reindex(s11.index)
    print(f"    11 settori (2018+): MOM Sh {_sh(s11):.2f} vs SPY {_sh(spy11):.2f}  (periodo {s11.index[0].date()}+)")


if __name__ == "__main__":
    main()
