"""SLEEVE del portafoglio + REGISTRY degli sleeve attivi.

Per AGGIUNGERE una strategia al portafoglio:
  1. Validala col gauntlet onesto (scripts/analysis/research_lab.py + hold-out + cross-asset).
  2. Scrivi una funzione `_<nome>_returns() -> pd.Series` che ritorna i suoi rendimenti netti
     per-barra (datetime-indexed, CAUSALE, netto fee). Deve passare il guard di causalità.
  3. Avvolgila in uno Sleeve(nome, peso, fn[, pos_fn]) e aggiungila a active_sleeves().
Niente sleeve non validati: il portafoglio è solo per edge che reggono il gauntlet.
"""
from __future__ import annotations
import numpy as np
import pandas as pd

from src.data.downloader import load_data
from src.strategies.trend_portfolio import TrendPortfolio, CANONICAL, resample_1d, simple_returns
from src.portfolio.portfolio import Sleeve

ASSETS = ("BTC", "ETH")


# ----------------------------- TP01 (PORT LF1d) -----------------------------
def _tp01_returns() -> pd.Series:
    """TP01: TSMOM vol-target long-flat, 50/50 BTC+ETH, a 1d (>=12h: vedi nota look-ahead nel modulo).
    Rendimenti netti per-barra del portafoglio (causale: posizione decisa a close[i-1], tenuta in i)."""
    tp = TrendPortfolio(**CANONICAL)
    series = {}
    for a in ASSETS:
        df = resample_1d(load_data(a, "1h"))
        r = simple_returns(df["close"].values.astype(float))
        tgt = tp.target_series(df)
        held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
        net = held * r - tp.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0
        series[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"]))
    J = pd.concat(series, axis=1, join="inner").fillna(0.0)
    return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)


def _tp01_positions() -> dict:
    tp = TrendPortfolio(**CANONICAL)
    return {a: round(tp.current_target(resample_1d(load_data(a, "1h"))), 4) for a in ASSETS}


def tp01_sleeve(weight: float = 1.0) -> Sleeve:
    return Sleeve("TP01_trend_1d", weight, _tp01_returns, pos_fn=_tp01_positions)


# ----------------------------- XS01: Cross-Sectional Momentum (Hyperliquid) -----------------------------
# Universo certificato Hyperliquid (19 alt, 1d, dal 2024) in data/raw/hl_*_1d.parquet
# (fetch+certify: scripts/analysis/fetch_hyperliquid.py). Market-neutral, scorrelato a TP01 (~-0.06).
# CAVEAT ONESTI: storia corta (~2.5 anni, 2024-2026); STAT-MODE (book a 19 gambe market-neutral
# non eseguibile a 2k, serve ~20k); l'edge e' nella DISPERSIONE cross-section (complementare al
# trend di TP01: lavora quando TP01 e' in cash). Validato: scripts/portfolio/xsec_research.py.
import glob as _glob
from pathlib import Path as _Path
# BLEND di lookback (2026-06-19): fonde 30g+90g del momentum cross-sectional (z-score per
# lookback, mediato) come TP01 fonde gli orizzonti -> piu' robusto del singolo L=30: FULL Sh
# 0.80->1.10, DD 21%->14%, corr a TP01 -0.06->-0.12, 100% anni+. Diario 2026-06-19-xsec-blend.md.
# + GATE DI DISPERSIONE (2026-06-19): entra solo se la dispersione cross-section del momentum
# supera il percentile ESPANDENTE causale disp_pct (altrimenti flat: in regime compatto XS e'
# rumore). Plateau robusto p15-p35; a p30: portafoglio FULL 1.50->1.74, HOLD 1.06->1.56.
# Diario 2026-06-19-xsec-dispgate.md.
XS_CFG = dict(lookbacks=(30, 90), H=10, k=5, mode="mom", target_vol=0.20, disp_pct=30, disp_minhist=20)
_HL_DIR = _Path(__file__).resolve().parents[2] / "data" / "raw"
# UNIVERSO ESPLICITO = 19 ALT LIQUIDI MAJOR. NB (2026-06-19): allargare a 52 asset (incluso
# small-cap WIF/JUP/ORDI/PYTH/TAO...) DILUISCE l'edge -> momentum cross-section NEGATIVO sui 52.
# I major sono il sweet spot. NON usare glob-all (i parquet extra certificati servono ad altra
# ricerca, non a XS01). Vedi diario 2026-06-19-xsec-universe-expansion.md.
XS_UNIVERSE = ["BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "AVAX", "LINK", "LTC", "ADA",
               "ARB", "OP", "SUI", "APT", "INJ", "TIA", "SEI", "NEAR", "AAVE"]


def _xsec_returns() -> pd.Series:
    cols = {}
    for sym in XS_UNIVERSE:
        p = _HL_DIR / f"hl_{sym.lower()}_1d.parquet"
        if not p.exists():
            continue
        d = pd.read_parquet(p)
        cols[sym] = pd.Series(d["close"].values.astype(float),
                              index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
    if len(cols) < 10:
        raise FileNotFoundError("universo Hyperliquid XS01 incompleto: gira scripts/analysis/fetch_hyperliquid.py")
    C = pd.concat(cols, axis=1, join="inner").sort_index().dropna()
    px = C.values; n, A = px.shape
    lookbacks, H, k, mode, tv = XS_CFG["lookbacks"], XS_CFG["H"], XS_CFG["k"], XS_CFG["mode"], XS_CFG["target_vol"]
    disp_pct = XS_CFG.get("disp_pct", 0); minhist = XS_CFG.get("disp_minhist", 20)
    mlb = max(lookbacks)
    dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
    W = np.zeros((n, A)); w = np.zeros(A); disp_hist = []
    for i in range(n):
        if i >= mlb and i % H == 0:
            rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks]
            disp_i = float(np.mean([r.std() for r in rLs]))          # dispersione cross-section del momentum
            thr = np.percentile(disp_hist, disp_pct) if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf
            if disp_i >= thr:                                        # gate: entra solo in regime disperso
                score = np.zeros(A); cnt = 0                          # blend: media z-score cross-sectional
                for rL in rLs:
                    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); lo, hi = order[:k], order[-k:]
                    if mode == "mom": w[hi] = 0.5 / k; w[lo] = -0.5 / k
                    else: w[lo] = 0.5 / k; w[hi] = -0.5 / k
            else:
                w = np.zeros(A)                                       # regime compatto -> flat
            disp_hist.append(disp_i)
        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 * (0.001 / 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 pd.Series(s.values * scale, index=C.index)


def xsec_sleeve(weight: float = 0.3) -> Sleeve:
    return Sleeve("XS01_xsec_hl", weight, _xsec_returns)


# ----------------------------- VRP01: Options Short-Vol (put credit spread + gate IV-rank) -----------------------------
# Income short-vol settimanale. Idee da FinanceOld/OptionsAgent (Bear Call Spread + gate d'ingresso)
# portate sul VRP: (a) PUT CREDIT SPREAD rischio-definito (vendi put -0.28, compra put -0.10) che
# CAPPA la coda (worst-week -16.6%->-7.4%, DD 33%->14%); (b) GATE IV-RANK>0.30 causale = vendi vol solo
# quando ricca -> ribalta l'HOLD-OUT da -0.25 a +0.28 (e' l'alpha); (c) crash-skip IV-rank>0.90.
# Premio BS su DVOL reale (data/raw/dvol_*.parquet via scripts/research/fetch_dvol.py), payoff sul path
# certificato, fee opzioni Deribit (cap 12.5% del premio). CAVEAT ONESTI: premio MODELLATO su IV-ATM
# (skew non esplicito), book a 1d, f di stress reale non catturato -> e' un LEAD robusto, non deploy
# pieno. Scorrelato a TP01 (~+0.07). Ricerca: scripts/research/options_vrp_v2.py.
# Diario 2026-06-20-financeold-analysis-vrp-v2.md.
from scipy.stats import norm as _norm

VRP_CFG = dict(short_delta=-0.28, long_delta=-0.10, f=1.0, tenor_d=7,
               gate_ivr=0.30, crash_skip=0.90, gate_vrp=True, fee_frac=0.125)


def _bs_put(S, K, T, sig):
    if T <= 0 or sig <= 0:
        return max(K - S, 0.0)
    d1 = (np.log(S / K) + 0.5 * sig ** 2 * T) / (sig * np.sqrt(T))
    return K * _norm.cdf(-(d1 - sig * np.sqrt(T))) - S * _norm.cdf(-d1)        # r=0


def _strike_from_delta(S, T, sig, target_delta):
    return S * np.exp(0.5 * sig ** 2 * T - (-_norm.ppf(-target_delta)) * sig * np.sqrt(T))


def _vrp_weekly_asset(asset: str) -> pd.Series:
    """Put credit spread settimanale con gate causali. Ritorna rendimenti SETTIMANALI (su collaterale
    = strike corto, cash-secured) indicizzati alla data di scadenza. Causale: strike/premio/gate da
    dati <= sell-date; payoff a scadenza sui prezzi certificati."""
    df = resample_1d(load_data(asset, "1h"))
    s = pd.Series(df["close"].values.astype(float), index=pd.to_datetime(df["datetime"]))
    if s.index.tz is None:
        s.index = s.index.tz_localize("UTC")
    dv = pd.read_parquet(_HL_DIR / f"dvol_{asset.lower()}.parquet")
    d = pd.Series(dv["close"].values.astype(float), index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True))
    J = pd.concat({"px": s, "dvol": d}, axis=1, join="inner").sort_index().dropna()
    px = J["px"].values; dvf = J["dvol"].values / 100.0; idx = J.index
    n = len(px); cfg = VRP_CFG; tn = cfg["tenor_d"]; T = tn / 365.25
    rets = {}
    i = 60
    while i + tn < n:
        S0 = px[i]; sig = dvf[i]
        skip = False
        if cfg["gate_vrp"] and i >= 31:                                   # VRP>0: DVOL > RV30 causale
            rv = np.std(np.diff(np.log(px[i - 30:i + 1]))) * np.sqrt(365.25)
            if (sig - rv) <= 0:
                skip = True
        if not skip and (cfg["gate_ivr"] > 0 or cfg["crash_skip"] < 1.0) and i >= 60:
            ivr = float((dvf[:i] < dvf[i]).mean())                        # IV-rank espandente causale
            if cfg["gate_ivr"] > 0 and ivr < cfg["gate_ivr"]:
                skip = True
            if cfg["crash_skip"] < 1.0 and ivr > cfg["crash_skip"]:
                skip = True
        if skip:
            rets[idx[i + tn]] = 0.0; i += tn; continue
        Ks = _strike_from_delta(S0, T, sig, cfg["short_delta"])
        Kl = _strike_from_delta(S0, T, sig, cfg["long_delta"])
        net_prem = (_bs_put(S0, Ks, T, sig) - _bs_put(S0, Kl, T, sig)) * cfg["f"]
        S1 = px[i + tn]
        payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1)
        pnl = net_prem - payoff - cfg["fee_frac"] * abs(net_prem)
        rets[idx[i + tn]] = pnl / Ks
        i += tn
    return pd.Series(rets)


def _vrp_combo_returns() -> pd.Series:
    """Sleeve VRP01: book 50/50 BTC+ETH del put credit spread gated, su griglia GIORNALIERA.
    Il rendimento settimanale e' piazzato sul giorno di scadenza, 0.0 sugli altri giorni (preserva
    lo Sharpe annualizzato senza smoothing): cosi' lo sleeve e' presente ogni giorno (peso costante)."""
    rB = _vrp_weekly_asset("BTC"); rE = _vrp_weekly_asset("ETH")
    wk = pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1).sort_index()
    if wk.empty:
        return wk
    days = pd.date_range(wk.index.min().normalize(), wk.index.max().normalize(), freq="1D", tz="UTC")
    daily = pd.Series(0.0, index=days)
    daily.loc[wk.index.normalize()] = wk.values                           # lump settimanale sul giorno scadenza
    return daily


def vrp_sleeve(weight: float = 0.20) -> Sleeve:
    return Sleeve("VRP01_shortvol", weight, _vrp_combo_returns)


# ----------------------------- SKH01-V2-DD: Skyhook dual-TF regime+breakout (BTC/ETH) -----------------------------
# Sistema dual-timeframe (segnale 690m, exec 230m): entra solo quando coincidono REGIME
# (BuzVola/BuzVolume tipo-Chande) E PATTERN (Donchian breakout). NON e' un trend-follower.
# Vincitrice dell'onda DD-reduction (famiglia ASYM_LS): exit a percentuale fissa ASIMMETRICA
# (long sl4%/tp10%, short sl2%/tp8% piu' stretto) -> taglia il DD standalone (BTC 21% / ETH 27%)
# alzando hold-out (minHold +1.26) e valore di portafoglio. Quasi-ortogonale a TP01 (corr ~0.09):
# blend 0.75*TP01+0.25*SKH -> hold-out Sharpe 0.31->1.17 (+0.87), DD full 14%->9%. Marginal ADDS,
# has_insample_edge, robust_oos (multicut 7/7 anni), is_hedge=False. Verificato leak-free (causalita'
# 0/400) + 2 scettici avversariali. Diario 2026-06-23-skyhook.md.
# CAVEAT ONESTI: equity marcata a fine-trade (daily lumpy); ETH DD 27% ha margine sottile vs 30%;
# il book opera a 230m -> ribilanciamento piu' frequente del resto (verificare costi reali a deploy).
from src.strategies.skyhook import SKH01_V2_DD, build_frames, skyhook_entries
from src.backtest.harness import backtest_signals


def _skyhook_returns() -> pd.Series:
    """SKH01-V2-DD: book 50/50 BTC+ETH del sistema regime+breakout dual-TF, riportato su griglia
    GIORNALIERA. Causale (decide a close[i], exit intrabar TP/SL/max_bars, non-overlap), netto 0.10% RT."""
    series = {}
    for a in ASSETS:
        ltf, htf = build_frames(load_data(a, "5m"))
        ent = skyhook_entries(ltf, htf, SKH01_V2_DD)
        m = backtest_signals(ltf, ent, fee_rt=0.001, leverage=1.0, asset=a, tf="230m")
        s = pd.Series(m.equity, index=pd.DatetimeIndex(pd.to_datetime(m.eq_index, utc=True)))
        series[a] = s.resample("1D").last().ffill().pct_change().dropna()
    J = pd.concat(series, axis=1, join="inner").fillna(0.0)
    return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)


def _skyhook_positions(load5m=None) -> dict:
    """Stato corrente del book Skyhook per asset (introspezione live): se c'e' un trade APERTO ORA
    -> dir/entry/sl/tp/barre-trascorse; altrimenti 'flat'. Replica la logica non-overlap di
    entry+exit (TP/SL/max_bars) fino all'ultima barra 230m chiusa. Causale: usa solo barre chiuse.

    load5m: callable(asset)->df5 opzionale (per il live: feed certificato + coda fresca effimera,
    vedi src/live/livefeed.fresh_5m). Default = feed certificato su disco (load_data)."""
    _load = load5m if load5m is not None else (lambda a: load_data(a, "5m"))
    out = {}
    for a in ASSETS:
        ltf, htf = build_frames(_load(a))
        ent = skyhook_entries(ltf, htf, SKH01_V2_DD)
        H = ltf["high"].values; L = ltf["low"].values; Cc = ltf["close"].values
        n = len(ltf); i = 0; open_pos = "flat"
        while i < n:
            e = ent[i]
            if e is None:
                i += 1; continue
            d, sl, tp, mb = e["dir"], e["sl"], e["tp"], e["max_bars"]
            exit_idx = None
            for s in range(1, mb + 1):
                j = i + s
                if j >= n:                                   # non ancora uscito: posizione APERTA ora
                    break
                hit = (L[j] <= sl or H[j] >= tp) if d == 1 else (H[j] >= sl or L[j] <= tp)
                if hit or s == mb:
                    exit_idx = j; break
            if exit_idx is None:
                open_pos = dict(dir="LONG" if d == 1 else "SHORT", entry=round(float(Cc[i]), 2),
                                sl=round(float(sl), 2), tp=round(float(tp), 2),
                                bars_in=int((n - 1) - i), max_bars=int(mb))
                break
            i = exit_idx + 1
        out[a] = open_pos
    return out


def skyhook_sleeve(weight: float = 0.25) -> Sleeve:
    return Sleeve("SKH01_skyhook", weight, _skyhook_returns, pos_fn=_skyhook_positions)


_TRADE_FREQ_CACHE = None


def book_trade_frequency() -> dict:
    """Frequenza operativa del book eseguibile (TP01+SKH01), conteggiata sui dati certificati.
    Strategia + storico fissi -> cache a livello di MODULO: calcolata una sola volta per processo
    (le nuove barre aggiungono pochissimo). Niente magic-number: e' contata, non stimata.
      - SKH01: round-trip DISCRETI (apri+chiudi) no-overlap dell'harness, per asset e per anno.
      - TP01: posizione CONTINUA vol-target -> 'turnover' annuo (somma |Δpeso|), non trade discreti."""
    global _TRADE_FREQ_CACHE
    if _TRADE_FREQ_CACHE is not None:
        return _TRADE_FREQ_CACHE
    skh = {}
    for a in ASSETS:
        ltf, htf = build_frames(load_data(a, "5m"))
        ent = skyhook_entries(ltf, htf, SKH01_V2_DD)
        yr = pd.to_datetime(pd.Series(ltf["timestamp"]).astype("int64"), unit="ms", utc=True).dt.year.values
        yc: dict[int, int] = {}
        for i, e in enumerate(ent):
            if e is not None:
                y = int(yr[i]); yc[y] = yc.get(y, 0) + 1
        skh[a] = dict(by_year={int(k): int(v) for k, v in sorted(yc.items())},
                      total=int(sum(yc.values())), first=int(min(yc)), last=int(max(yc)))
    sf = min(v["first"] for v in skh.values()); sl = max(v["last"] for v in skh.values())
    skh_combo = sum(v["total"] for v in skh.values()) / (sl - sf + 1)
    tp = TrendPortfolio(**CANONICAL); turns = []
    for a in ASSETS:
        df = resample_1d(load_data(a, "1h"))
        tgt = np.asarray(tp.target_series(df), dtype=float)
        years = pd.DatetimeIndex(pd.to_datetime(df["datetime"])).year   # ndarray-aligned key
        d = np.abs(np.diff(np.nan_to_num(tgt, nan=0.0), prepend=0.0))    # turnover = somma |Δpeso|
        ty = pd.Series(d).groupby(years).sum()                          # groupby posizionale (RangeIndex)
        turns.append(float(ty.mean()))
    _TRADE_FREQ_CACHE = dict(
        skh=skh,
        skh_per_year={a: skh[a]["total"] / (skh[a]["last"] - skh[a]["first"] + 1) for a in ASSETS},
        skh_combo_per_year=skh_combo,
        tp01_turnover_per_year=sum(turns) / len(turns))
    return _TRADE_FREQ_CACHE


# ----------------------------- REGISTRY -----------------------------
def active_sleeves() -> list[Sleeve]:
    """Sleeve ATTIVI nel portafoglio (pesi rinormalizzati; sleeve a date diverse si attivano
    quando parte la loro storia). Aggiungere qui SOLO strategie validate col gauntlet.

    SKH01 cablato @0.25 effettivo: i tre sleeve preesistenti scalati nel restante 0.75 mantenendo
    il loro rapporto 55:25:20 (-> 41.25/18.75/15), cosi' Skyhook pesa esattamente 25% del book."""
    return [
        tp01_sleeve(weight=0.4125),    # trend difensivo, BTC/ETH, dal 2019 (l'unico deployable pieno)
        xsec_sleeve(weight=0.1875),    # cross-sectional momentum Hyperliquid, dal 2024 (scorrelato, stat-mode)
        vrp_sleeve(weight=0.15),       # options short-vol (put credit spread + gate IV-rank), dal 2021 (lead modellato, scorrelato)
        skyhook_sleeve(weight=0.25),   # dual-TF regime+breakout BTC/ETH, dal 2019 (quasi-ortogonale, exit %-asimmetrici, research)
    ]


def deribit_book_sleeves() -> list[Sleeve]:
    """BOOK DERIBIT-ONLY realmente eseguibile (TP01 + SKH01, 75/25): le DUE gambe direzionali
    BTC/ETH sullo stesso venue (Deribit), entrambe dal 2019. Esclude XS01 (Hyperliquid, stat-mode)
    e VRP01 (opzioni modellate). FULL Sharpe ~1.78 / HOLD ~1.17 / DD ~9% (research; SKH01 daily-step).
    Pensato per il deploy reale a basso capitale: stesso conto, stesso feed, ribilanciamento
    periodico (vedi src.portfolio.portfolio.rebalance_sim + scripts/portfolio/run_deribit_book.py).
    TP01 e' gia' armato live; SKH01 e' il candidato 2a gamba (da validare codice d'esecuzione)."""
    return [
        tp01_sleeve(weight=0.75),
        skyhook_sleeve(weight=0.25),
    ]
