"""TP01 × DVOL — la vol IMPLICITA (forward-looking) migliora il risk-sizing di TP01? (ESEGUIBILE)

A differenza degli sleeve diversificatori (XS01/VRP01/carry = STAT-MODE, non eseguibili a $600),
questo TOCCA il book live: TP01 è BTC/ETH perp su Deribit, già armato. Oggi vol-targeta sulla vol
REALIZZATA 30g (backward-looking). Ipotesi: il DVOL (vol implicita 30g Deribit, forward-looking,
che spesso ANTICIPA i salti di vol realizzata) come denominatore del vol-target → de-risking più
tempestivo prima dei crash → hold-out migliore e/o DD più basso, SENZA peggiorare il FULL.

Onestà: DVOL parte 2021-03 → confronto TUTTE le varianti sulla FINESTRA COMUNE 2021-2026 (perdo
2018-2021, incluso il toro 2021 pre-DVOL). Baseline ricalcolato sulla stessa finestra. Hold-out 2025+.
Tutto causale (vol/segnale ≤ close[i]), fee 0.10% RT, long-flat, leva cap 2x — config CANONICA TP01.

VARIANTI (denominatore del vol-target):
  REALIZED  -> 30g realizzata (baseline canonica)
  DVOL      -> DVOL/100 (implicita)
  BLEND     -> 0.5·realizzata + 0.5·DVOL
  MAX       -> max(realizzata, DVOL)  (sizing più difensivo: la più alta delle due)
  DERISK    -> realizzata, ma posizione ×0.5 quando DVOL > pctl espandente causale (gate crash)

    uv run python scripts/research/tp01_dvol_overlay.py
"""
from __future__ import annotations
import sys
from pathlib import Path

import numpy as np
import pandas as pd

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))

from src.data.downloader import load_data
from src.strategies.trend_portfolio import (
    resample_1d, simple_returns, realized_vol, tsmom_blend, CANONICAL,
)

RAW = ROOT / "data" / "raw"
SQ = np.sqrt(365.25)
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
TGT_VOL = CANONICAL["target_vol"]; LEV = CANONICAL["leverage"]; FEE = CANONICAL["fee_side"]
HZ = CANONICAL["horizons_days"]; VW = CANONICAL["vol_win_days"]


def _components(asset: str):
    df = resample_1d(load_data(asset, "1h"))
    c = df["close"].values.astype(float)
    idx = pd.to_datetime(df["datetime"])
    if idx.dt.tz is None:
        idx = idx.dt.tz_localize("UTC")
    idx = pd.DatetimeIndex(idx)                            # tz-aware (UTC)
    r = simple_returns(c)
    rv = realized_vol(r, VW, 365.25)                       # 30g realizzata annualizzata (bpd=1)
    direction = np.clip(tsmom_blend(c, HZ), 0, None)       # long-flat
    dv = pd.read_parquet(RAW / f"dvol_{asset.lower()}.parquet")
    dser = pd.Series(dv["close"].values.astype(float) / 100.0,
                     index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True)).sort_index()
    dvol = dser.reindex(idx, method="ffill").values
    return c, r, idx, rv, direction, dvol


def _net_returns(asset: str, mode: str, tvol: float = TGT_VOL) -> pd.Series:
    c, r, idx, rv, direction, dvol = _components(asset)
    derisk = np.ones(len(c))
    if mode == "realized":
        vol = rv
    elif mode == "dvol":
        vol = dvol
    elif mode == "blend":
        vol = 0.5 * rv + 0.5 * dvol
    elif mode == "max":
        vol = np.fmax(rv, dvol)
    elif mode == "derisk":
        vol = rv
        # gate crash causale: DVOL sopra il suo percentile espandente (90%) -> dimezza l'esposizione
        dd = pd.Series(dvol, index=idx)
        rank = dd.expanding(min_periods=60).apply(lambda x: (x[:-1] < x[-1]).mean() if len(x) > 1 else 0.5, raw=True)
        derisk = np.where(rank.values > 0.90, 0.5, 1.0)
    else:
        raise ValueError(mode)
    with np.errstate(divide="ignore", invalid="ignore"):
        scal = np.where((vol > 0) & np.isfinite(vol), tvol / vol, 0.0)
    tgt = np.clip(direction * scal * derisk, -LEV, LEV)
    tgt[~np.isfinite(tgt)] = 0.0
    pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1]           # decisa a close[t-1], tenuta in t
    gross = pos * r
    turn = np.abs(np.diff(pos, prepend=0.0))
    net = np.clip(gross - FEE * turn, -0.99, None); net[0] = 0.0
    return pd.Series(net, index=idx)


def portfolio(mode: str, tvol: float = TGT_VOL) -> pd.Series:
    b = _net_returns("BTC", mode, tvol); e = _net_returns("ETH", mode, tvol)
    J = pd.concat({"B": b, "E": e}, axis=1, join="inner").fillna(0.0)
    return 0.5 * J["B"] + 0.5 * J["E"]


def metrics(daily: pd.Series, lo=None) -> dict:
    if lo is not None:
        daily = daily[daily.index >= lo]
    r = daily.values
    sh = float(np.mean(r) / np.std(r) * SQ) if np.std(r) > 0 else 0.0
    eq = np.cumprod(1.0 + r); pk = np.maximum.accumulate(eq)
    dd = float(np.max((pk - eq) / pk)) if len(eq) else 0.0
    yrs = (daily.index[-1] - daily.index[0]).days / 365.25 if len(daily) > 1 else 1.0
    cagr = eq[-1] ** (1 / yrs) - 1 if yrs > 0 and len(eq) and eq[-1] > 0 else -1.0
    s = pd.Series(eq, index=daily.index); yearly = {}
    for y, g in s.groupby(s.index.year):
        if len(g) > 1:
            yearly[int(y)] = float(g.iloc[-1] / g.iloc[0] - 1)
    return dict(sharpe=sh, dd=dd, cagr=cagr, tot=float(eq[-1] - 1) if len(eq) else 0.0, yearly=yearly)


def main():
    modes = ["realized", "dvol", "blend", "max", "derisk"]
    series = {m: portfolio(m) for m in modes}
    # vero inizio DVOL (dove TUTTE le varianti hanno dati validi) — non il primo indice del prezzo
    dstart = max(pd.read_parquet(RAW / f"dvol_{a.lower()}.parquet")["timestamp"].min() for a in ("BTC", "ETH"))
    dstart = pd.Timestamp(dstart, unit="ms", tz="UTC") + pd.Timedelta(days=VW)   # +warmup vol-win
    series = {m: s[s.index >= dstart] for m, s in series.items()}
    common = series["realized"].index
    base = series["realized"]

    print("=" * 96)
    print(f"  TP01 × DVOL — vol-target con denominatore di vol diverso. Finestra COMUNE "
          f"{common[0].date()} -> {common[-1].date()} ({len(base)}g). Hold-out 2025+.")
    print("=" * 96)
    print(f"  {'variante':<10} {'FULL Sh':>8} {'FULL DD':>8} {'CAGR':>7} | "
          f"{'HOLD Sh':>8} {'HOLD ret':>9} {'HOLD DD':>8} | per-anno PnL")
    for m in modes:
        s = series[m]; f = metrics(s); h = metrics(s, lo=HOLDOUT)
        ys = " ".join(f"{y}:{p*100:+.0f}" for y, p in sorted(f['yearly'].items()))
        tag = "  (baseline)" if m == "realized" else ""
        print(f"  {m:<10} {f['sharpe']:>+8.2f} {f['dd']*100:>7.1f}% {f['cagr']*100:>+6.0f}% | "
              f"{h['sharpe']:>+8.2f} {h['tot']*100:>+8.1f}% {h['dd']*100:>7.1f}% | {ys}{tag}")

    print("\n  DELTA vs baseline (realized) sulla stessa finestra:")
    bf = metrics(base); bh = metrics(base, lo=HOLDOUT)
    for m in modes:
        if m == "realized":
            continue
        f = metrics(series[m]); h = metrics(series[m], lo=HOLDOUT)
        print(f"    {m:<8}: ΔFULL Sh {f['sharpe']-bf['sharpe']:+.2f}  ΔFULL DD {(f['dd']-bf['dd'])*100:+.1f}pp  "
              f"ΔHOLD Sh {h['sharpe']-bh['sharpe']:+.2f}  ΔHOLD ret {(h['tot']-bh['tot'])*100:+.1f}pp")

    print("\n  CONTROLLO DECISIVO — il taglio di DD del DVOL è 'posizioni più piccole' o vero timing?")
    print("  Confronto le varianti DVOL con il realized a target_vol RIDOTTO (stesso de-levering, ma")
    print("  senza DVOL). Se realized-ridotto eguaglia/batte il DVOL a parità di DD → DVOL non aggiunge.")
    for tv in (0.15, 0.13):
        s = portfolio("realized", tvol=tv); s = s[s.index >= dstart]
        f = metrics(s); h = metrics(s, lo=HOLDOUT)
        print(f"    realized @ vol-tgt {tv*100:.0f}%: FULL Sh {f['sharpe']:+.2f}  DD {f['dd']*100:.1f}%  "
              f"CAGR {f['cagr']*100:+.0f}%  |  HOLD Sh {h['sharpe']:+.2f}")
    mx = metrics(series["max"]); mxh = metrics(series["max"], lo=HOLDOUT)
    print(f"    (vs max-DVOL:        FULL Sh {mx['sharpe']:+.2f}  DD {mx['dd']*100:.1f}%  "
          f"CAGR {mx['cagr']*100:+.0f}%  |  HOLD Sh {mxh['sharpe']:+.2f})")


if __name__ == "__main__":
    main()
