"""XS cross-sectional con UNIVERSO TOP-LIQUIDITÀ DINAMICO (Hyperliquid 52 certificati).

Invece di 19 nomi fissi, a ogni ribilancio: seleziona i top-N per liquidità (dollar-volume 30g
causale), poi fra quelli long i k più forti / short i k più deboli (momentum, market-neutral),
vol-target. Idea: cross-section pulita e ADATTIVA (i token entrano quando maturano in liquidità),
escludendo il long-tail rumoroso che diluiva il 52-all. Gestione ragged (asset a date diverse:
si classifica solo fra i disponibili). Causale. Confronto vs fisso-19 + 52-all + contributo TP01.

    uv run python scripts/portfolio/xsec_dynuniverse.py
"""
from __future__ import annotations
import sys, glob
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd
from src.portfolio.portfolio import to_daily, metrics, HOLDOUT
from src.portfolio.sleeves import tp01_sleeve, XS_UNIVERSE

RAW = PROJECT_ROOT / "data" / "raw"
FEE = 0.001


def load_close_vol():
    close, vol = {}, {}
    for p in sorted(glob.glob(str(RAW / "hl_*_1d.parquet"))):
        sym = Path(p).stem.replace("hl_", "").replace("_1d", "").upper()
        d = pd.read_parquet(p)
        ix = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
        close[sym] = pd.Series(d["close"].values.astype(float), index=ix)
        vol[sym] = pd.Series(d["volume"].values.astype(float), index=ix)
    C = pd.concat(close, axis=1, join="outer").sort_index()
    V = pd.concat(vol, axis=1, join="outer").sort_index().reindex(C.index)
    return C, V


def xs_dynamic(C, V, N=20, lb=60, hold=10, k=5, mode="mom", tv=0.20, fixed=None):
    """fixed=lista simboli -> universo statico (ignora liquidità). Altrimenti top-N per liquidità."""
    cols = list(C.columns); A = len(cols)
    px = C.values; n = len(px)
    dret = np.full((n, A), 0.0); dret[1:] = np.where(np.isfinite(px[1:]) & np.isfinite(px[:-1]), px[1:] / px[:-1] - 1.0, 0.0)
    dvol = V.values * px
    liq = pd.DataFrame(dvol, index=C.index, columns=cols).rolling(30, min_periods=15).mean().shift(1).values
    fixed_mask = np.array([c in fixed for c in cols]) if fixed else None
    W = np.zeros((n, A)); w = np.zeros(A)
    for i in range(n):
        if i >= lb and i % hold == 0:
            retlb = np.where(np.isfinite(px[i]) & np.isfinite(px[i - lb]), px[i] / px[i - lb] - 1.0, np.nan)
            avail = np.isfinite(retlb) & np.isfinite(px[i])
            if fixed is not None:
                avail &= fixed_mask
                cand = np.where(avail)[0]
            else:
                avail &= np.isfinite(liq[i])
                idx = np.where(avail)[0]
                if len(idx) > N:
                    cand = idx[np.argsort(liq[i][idx])[-N:]]   # top-N per liquidità
                else:
                    cand = idx
            w = np.zeros(A)
            ke = min(k, len(cand) // 2)
            if ke >= 1:
                order = cand[np.argsort(retlb[cand])]
                lo, hi = order[:ke], order[-ke:]
                if mode == "mom": w[hi] = 0.5 / ke; w[lo] = -0.5 / ke
                else: w[lo] = 0.5 / ke; w[hi] = -0.5 / ke
        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 / 2.0)
    s = pd.Series(net, index=C.index)
    rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
    scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
    return to_daily(pd.Series(s.values * scale, index=C.index))


def ev(d):
    f = metrics(d); o = metrics(d[d.index >= HOLDOUT])
    yr = [float((1 + g).prod() - 1) for _, g in d.groupby(d.index.year)]
    pct = sum(v > 0 for v in yr) / len(yr) if yr else 0
    return f, o, pct


def main():
    C, V = load_close_vol()
    print("=" * 96)
    print(f"  XS UNIVERSO TOP-LIQUIDITÀ DINAMICO — {len(C.columns)} asset certificati [{C.index[0].date()} -> {C.index[-1].date()}]")
    print("=" * 96)
    tp = tp01_sleeve().daily()

    print("\n  (1) SWEEP N (top-liquidità) x config (mom) — FULL Sh / OOS25 Sh / anni+ / corrTP")
    print(f"    {'config':<28}{'FULL':>7}{'OOS25':>7}{'anni+':>7}{'corrTP':>8}")
    best = None
    for N in (12, 15, 20, 25):
        for lb, hold, k in [(30, 10, 5), (60, 10, 5), (90, 10, 5)]:
            d = xs_dynamic(C, V, N=N, lb=lb, hold=hold, k=k)
            f, o, pct = ev(d)
            corr = float(pd.concat({"a": tp, "b": d}, axis=1, join="inner").dropna().corr().iloc[0, 1])
            tag = f"top{N} L{lb}H{hold}k{k}"
            print(f"    {tag:<28}{f['sharpe']:>7.2f}{o['sharpe']:>7.2f}{pct*100:>6.0f}%{corr:>+8.2f}")
            if (best is None or f['sharpe'] > best[1]['sharpe']) and corr < 0.4 and o['sharpe'] > 0:
                best = (tag, f, o, corr, d, (N, lb, hold, k))

    print("\n  (2) BASELINE di confronto (stessa finestra):")
    for name, kw in [("fisso-19 major (L30H10k5)", dict(lb=30, hold=10, k=5, fixed=set(XS_UNIVERSE))),
                     ("fisso-19 major (L90H10k5)", dict(lb=90, hold=10, k=5, fixed=set(XS_UNIVERSE))),
                     ("52-all (L60H10k5)", dict(lb=60, hold=10, k=5))]:
        d = xs_dynamic(C, V, **kw); f, o, pct = ev(d)
        print(f"    {name:<28} FULL {f['sharpe']:.2f}  OOS25 {o['sharpe']:.2f}  anni+ {pct*100:.0f}%")

    if best is None:
        print("\n  Nessuna config dinamica scorrelata+positiva. Il top-liquidità non aiuta.")
        return
    tag, f, o, corr, d, cfg = best
    print(f"\n  === MIGLIOR DINAMICO: {tag} | FULL {f['sharpe']:.2f} ret {f['ret']*100:+.0f}% DD {f['maxdd']*100:.0f}% | OOS25 {o['sharpe']:.2f} | corrTP {corr:+.2f} ===")
    per = [(int(y), round(float((1 + g).prod() - 1), 3)) for y, g in d.groupby(d.index.year)]
    print(f"  per-anno: {per}")
    # contributo al portafoglio vs fisso-19 (XS01 attuale)
    xs19 = xs_dynamic(C, V, lb=30, hold=10, k=5, fixed=set(XS_UNIVERSE))
    J = pd.concat({"tp": tp, "dyn": d, "x19": xs19}, axis=1, join="inner").dropna()
    print(f"\n  CONTRIBUTO (finestra comune {J.index[0].date()}->{J.index[-1].date()}):")
    for nm, col in [("TP01 solo", None), ("TP01+XS19 (attuale) 70/30", "x19"), ("TP01+DYN 70/30", "dyn")]:
        if col is None:
            comb = J["tp"]
        else:
            comb = 0.7 * J["tp"] + 0.3 * J[col]
        mf = metrics(comb); mh = metrics(comb[comb.index >= HOLDOUT])
        print(f"    {nm:<28} FULL Sh {mf['sharpe']:.2f} DD {mf['maxdd']*100:.0f}% | HOLD Sh {mh['sharpe']:.2f}")
    print("\n  -> DINAMICO meglio del fisso-19? guarda FULL/OOS + contributo. Sennò: fisso-19 resta.")


if __name__ == "__main__":
    main()
