"""LEVERAGE SWEEP — cosa succede a PORT06 con leva 1..10 nei vari scenari (2026-06-18).

Domanda utente: "se porto a leva 10 cosa succede?". Estende lev_frontier() di
accel50_research.py da 1-6 a 1-10 e su PIU' scenari, e soprattutto aggiunge le
NON-LINEARITA' che il modello daily-lineare NASCONDE e che mordono a leva alta:

  1) DRAWDOWN: nel modello daily scala ~lineare con la leva (DD_L ~ DD_1 * L) fino
     alla rovina. Ma e' un DD su daily AGGREGATI: nasconde le escursioni intraday.
  2) VOLATILITY DRAG: la crescita GEOMETRICA (CAGR) e' concava nella leva: cresce,
     ha un massimo (~leva di Kelly), poi CROLLA. Oltre Kelly piu' leva = MENO ritorno
     E piu' rischio. Con questa serie ad altissimo Sharpe il picco e' lontano -> il
     modello fa sembrare la leva "gratis": e' l'illusione da smontare.
  3) ROVINA / LIQUIDAZIONE: equity non puo' andare sotto zero. A leva L un ritorno di
     periodo r da' (1 + L*r); se L*r <= -1 -> conto azzerato. Il modello daily lo
     vede solo sul worst-day aggregato (docile); la realta' e' INTRADAY (gap, flash
     crash, maintenance-margin Deribit) -> la leva di rovina REALE e' molto piu' bassa.

Modello base: scala lineare dei daily return canonici (== live, parita' validata),
`base` corrisponde a leva 2 (come in accel50). Per leva L: r_L = base * (L/2).

    uv run python scripts/analysis/leverage_sweep.py
"""
from __future__ import annotations

import sys
from pathlib import Path

import numpy as np
import pandas as pd

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


def _load_base() -> tuple[pd.Series, int]:
    """Daily return PORT06 canonici (== live a leva 2) + indice di split OOS."""
    from scripts.portfolios._defs import PORTFOLIOS
    from scripts.analysis.combine_portfolio import port_returns, SPLIT
    from src.portfolio.sleeves import all_sleeve_equities, sleeve_returns_df

    p = PORTFOLIOS["PORT06"]
    eq = all_sleeve_equities()
    members = {sid: eq[sid] for sid in p.sleeve_ids}
    w = p.weight_vector(sleeve_returns_df(p.sleeve_ids))
    return port_returns(members, w), SPLIT


def _metrics(r: pd.Series) -> dict:
    """CAGR geometrica, maxDD, Sharpe, worst-day, equity finale e flag rovina.
    Clip a -100%/giorno: oltre, il conto e' azzerato (rovina) e resta a zero."""
    ruined = bool((r <= -1.0).any())
    rc = r.clip(lower=-1.0)               # un -100% azzera; non si recupera da zero
    curve = (1 + rc).cumprod()
    if ruined:                            # congela a zero dal primo azzeramento
        first = int(np.argmax(rc.values <= -1.0))
        curve.iloc[first:] = 0.0
    years = len(r) / 365.0
    final = float(curve.iloc[-1])
    cagr = (final ** (1 / years) - 1) * 100 if final > 0 else -100.0
    peak = curve.cummax()
    dd = float(((curve - peak) / peak).min() * 100)
    sharpe = float(r.mean() / r.std() * np.sqrt(365)) if r.std() > 0 else 0.0
    simple = (final - 1) * 100 if final > 0 else -100.0   # ritorno totale intra-periodo
    return {"cagr": cagr, "cagr_simple": simple, "dd": dd, "sharpe": sharpe,
            "worst": float(r.min() * 100), "final": final, "ruined": ruined}


def sweep(base: pd.Series, split: int) -> None:
    full, oos = base, base.iloc[split:]
    print(f"PORT06 daily — FULL {base.index[0].date()}→{base.index[-1].date()} "
          f"({len(full)}g) | OOS {base.index[split].date()}→ ({len(oos)}g, regime CALMO)")
    print(f"  worst-day base(leva2) = {base.min()*100:+.2f}%  | Sharpe leva-invariante "
          f"(modello lineare): FULL {_metrics(full).get('sharpe'):.2f} / OOS {_metrics(oos).get('sharpe'):.2f}")
    print()
    hdr = ("leva  CAGR_full%   DD_full%   x_full   CAGR_oos%   DD_oos%   "
           "worstday%   anni→€50/g(2k)   rovina?")
    print(hdr); print("-" * len(hdr))
    for L in range(1, 11):
        f = L / 2.0
        mf, mo = _metrics(full * f), _metrics(oos * f)
        # anni a €50/g da €2000, su CAGR OOS geometrica (come accel50)
        co = mo["cagr"]
        if co > 0 and mo["final"] > 0:
            k = 2020 * (50 / (2020 * ((1 + co/100) ** (1/365) - 1)))  # capitale per €50/g
            anni = np.log(k / 2020) / np.log(1 + co / 100)
            anni_s = f"{max(anni,0):.1f}"
        else:
            anni_s = "∞"
        flag = "💀 RUIN" if (mf["ruined"] or mo["ruined"]) else ("⚠ alto" if mf["dd"] < -25 else "ok")
        print(f"{L:>3}  {mf['cagr']:>9.0f}  {mf['dd']:>8.1f}  {mf['final']:>7.1f}x  "
              f"{mo['cagr']:>9.0f}  {mo['dd']:>8.1f}  {mf['worst']:>8.2f}   "
              f"{anni_s:>12}      {flag}")


def kelly_and_ruin(base: pd.Series, split: int) -> None:
    """Leva di Kelly (picco crescita geometrica) e leve di rovina sotto shock realistici."""
    print("\n=== NON-LINEARITA' che il modello daily nasconde ===\n")
    # Kelly: f* (in unita' di leva) che massimizza E[log(1+ (L/2)*base)]
    grid = np.linspace(0.1, 60, 600)
    def glog(L, r):
        rr = (r * (L / 2.0)).clip(lower=-0.999)
        return np.mean(np.log1p(rr))
    for label, r in [("FULL", base), ("OOS-calmo", base.iloc[split:])]:
        gl = [glog(L, r) for L in grid]
        kstar = grid[int(np.argmax(gl))]
        print(f"  Kelly {label:9}: leva ottimale ~{kstar:.0f}x  "
              f"(oltre, piu' leva = MENO crescita geometrica). "
              f"Half-Kelly prudente ~{kstar/2:.0f}x")
    print("\n  -> Sharpe altissimo + regime OOS calmo spingono Kelly a leve assurde:")
    print("     e' l'ARTEFATTO del backtest, NON un via libera. Il drag morde tardi.")

    print("\n=== ROVINA / LIQUIDAZIONE (il rischio VERO, intraday) ===")
    mday = base.min() / 2.0           # worst-day a leva 1
    print(f"  worst-DAY storico (leva1)   = {mday*100:+.2f}%  -> rovina daily a leva "
          f"{-1/mday:.0f}x (irrealistico: aggregato/diversificato)")
    # shock INTRADAY realistici sull'esposizione NETTA del book reale
    # 7 sleeve equal-weight, position_size 0.5 -> esposizione lorda per leva = L*0.5
    # (frazione del capitale a mercato); un crash correlato colpisce quella frazione.
    ps = 0.5
    print(f"  Config live: 7 sleeve EW, position_size={ps} -> esposizione ~ leva×{ps} "
          f"del capitale (a leva 3 = 1.5x; a leva 10 = 5x).")
    for shock in (0.10, 0.20, 0.40):
        Lruin = 1.0 / (shock * ps)
        print(f"  crash correlato intraday {shock*100:>4.0f}% sull'esposizione: "
              f"azzera il conto a leva ~{Lruin:.0f}x  "
              f"(disaster-SL on-book a -30%/pos mitiga ma NON in un gap-through)")
    print("\n  NB: il backtest e' su daily AGGREGATI di un paniere con stop in regime "
          "calmo. Sottostima la coda intraday, i gap, lo slippage a size grande e la "
          "maintenance-margin Deribit (che liquida PRIMA del -100% del modello).")


def per_year(base: pd.Series) -> None:
    """Sweep leva 1→10 ANNO PER ANNO: la media FULL nasconde l'anno peggiore.
    Per ogni anno civile: ritorno geometrico e maxDD INTRA-anno ad ogni leva."""
    years = sorted({d.year for d in base.index})
    levs = list(range(1, 11))
    print("\n=== ANNO PER ANNO (ritorno % geometrico intra-anno) ===")
    print("anno   g   wDay@L1  " + "".join(f"L{L:>2}      " for L in levs))
    print("-" * (24 + 9 * len(levs)))
    worst_dd = {}
    for y in years:
        r = base[base.index.year == y]
        cells = []
        for L in levs:
            m = _metrics(r * (L / 2.0))
            cells.append("RUIN" if m["ruined"] else f"{m['cagr_simple']:+.0f}")
            worst_dd.setdefault(L, []).append((y, _metrics(r * (L / 2.0))["dd"]))
        wd = r.min() / 2.0 * 100
        tag = "  <calmo" if y >= 2025 else ""
        print(f"{y}  {len(r):>3}  {wd:>6.2f}%  " + "".join(f"{c:>7} " for c in cells) + tag)

    print("\n=== ANNO PER ANNO (maxDD % intra-anno) ===")
    print("anno   g   wDay@L1  " + "".join(f"L{L:>2}      " for L in levs))
    print("-" * (24 + 9 * len(levs)))
    for y in years:
        r = base[base.index.year == y]
        cells = []
        for L in levs:
            m = _metrics(r * (L / 2.0))
            cells.append("RUIN" if m["ruined"] else f"{m['dd']:.1f}")
        wd = r.min() / 2.0 * 100
        tag = "  <calmo" if y >= 2025 else ""
        print(f"{y}  {len(r):>3}  {wd:>6.2f}%  " + "".join(f"{c:>7} " for c in cells) + tag)

    # anno peggiore per DD ad ogni leva
    print("\n=== ANNO PEGGIORE per drawdown, ad ogni leva ===")
    for L in levs:
        yw, ddw = min(worst_dd[L], key=lambda t: t[1])
        print(f"  leva {L:>2}: worst-anno {yw}  maxDD {ddw:.1f}%"
              + ("   💀 RUIN in qualche anno" if any(_metrics(base[base.index.year == yy] * (L/2.0))["ruined"] for yy in years) else ""))


def shock_2022() -> None:
    """STRESS con gli shock REALI 2022 (LUNA/3AC/FTX) sull'esposizione netta del book.
    Smonta il fill-al-livello del backtest daily: le fade+DIP01 COMPRANO i dip -> in un
    crollo sono net-LONG (catturate dalla parte sbagliata). Perdita = netta×shock; rovina
    a L_ruin = 1/(beta·ps·shock). I dati sono BTC/ETH 1h storici nostri, anno 2022."""
    from src.data.downloader import load_data
    print("\n=== STRESS SHOCK REALE 2022 (BTC/ETH storici, fill al GAP non al livello) ===")
    res = {}
    for a in ("BTC", "ETH"):
        df = load_data(a, "1h").set_index("datetime")
        d = df[(df.index >= "2022-01-01") & (df.index < "2023-01-01")]
        g = d.resample("1D").agg(open=("open", "first"), high=("high", "max"),
                                 low=("low", "min"), close=("close", "last")).dropna()
        res[a] = {"c2c": g["close"].pct_change().min(),
                  "o2l": (g["low"] / g["open"] - 1).min(),
                  "h1": d["close"].pct_change().min()}
        print(f"  {a}: worst-day {res[a]['c2c']*100:+.1f}% | worst intraday "
              f"{res[a]['o2l']*100:+.1f}% | worst 1h-candle {res[a]['h1']*100:+.1f}%")
    print("  crolli multi-giorno (close→low): LUNA BTC-29%/ETH-36% · 3AC-giu BTC-44%/ETH-52%"
          " · FTX BTC-26%/ETH-32%")
    ps = 0.5
    scen = {"ETH intraday (-27%)": 0.267, "BTC stretch giu (-44%)": 0.44,
            "ETH stretch giu (-52%)": 0.52}
    print("\n  LEVA DI ROVINA per net-exposure beta (ps=0.5):")
    print(f"  {'scenario':24}" + "".join(f"  b={b}" for b in (0.3, 0.5, 0.7, 1.0)))
    for name, dlt in scen.items():
        row = f"  {name:24}"
        for b in (0.3, 0.5, 0.7, 1.0):
            L = 1.0 / (b * ps * dlt)
            row += f"  {L:>4.1f}" + ("!" if L < 10 else " ")
        print(row)
    print("  ('!' = conto azzerato a leva <10). Le fade fadano = net-long nel crollo "
          "(beta alto proprio quando è pericoloso); il disaster-SL -30% NON scatta sotto "
          "-30% di mossa singola -> 2022 (max 1h -22%) il book mangia tutto.")


def recommended_leverage() -> None:
    """Leva MAX raccomandata data-driven: quella che tiene il DD di un 2022-repeat sotto
    una soglia recuperabile, dato un net-long realistico. Book-shock pesato sulla
    composizione reale (4 sleeve BTC + 3 ETH). Vincolo = capitulation peggiore (giugno)."""
    from src.data.downloader import load_data
    wB, wE, ps = 4 / 7, 3 / 7, 0.5

    def stretch(a, s, e):
        df = load_data(a, "1h").set_index("datetime")
        g = df[(df.index >= s) & (df.index <= e)]
        return g["low"].min() / g["open"].iloc[0] - 1

    june = wB * stretch("BTC", "2022-06-10", "2022-06-19") + wE * stretch("ETH", "2022-06-10", "2022-06-19")
    print("\n=== LEVA MAX RACCOMANDATA (vincolo: sopravvivi a un 2022-repeat) ===")
    print(f"  book-shock pesato (BTC {wB:.0%}/ETH {wE:.0%}), capitulation giugno = {june*100:.0f}%")
    print(f"  {'tolleranza DD nel 2022-repeat':30}  beta=0.5  beta=0.6  beta=0.7")
    for label, dd in [("30% conservativo", .30), ("50% recuperabile", .50),
                      ("70% pre-liquidazione", .70)]:
        row = f"  {label:30}"
        for b in (0.5, 0.6, 0.7):
            row += f"   {dd/(b*ps*abs(june)):6.1f}x"
        print(row)
    rec = 0.50 / (0.6 * ps * abs(june))
    print(f"\n  >>> RACCOMANDATA ~{rec:.1f}x (beta=0.6, DD<=50% recuperabile) -> PRUDENTE {int(rec)}x.")
    print(f"      A leva 3 (attuale): DD 2022-repeat ~{0.6*ps*3*abs(june)*100:.0f}% (recuperabile). "
          f"Leva 5 ~{0.6*ps*5*abs(june)*100:.0f}% (pre-liquidazione). Leva 10 = rovina.")


if __name__ == "__main__":
    base, split = _load_base()
    print("=== SWEEP LEVA 1→10 su PORT06 (modello daily lineare, == live) ===\n")
    sweep(base, split)
    per_year(base)
    kelly_and_ruin(base, split)
    shock_2022()
    recommended_leverage()
