"""Genera docs/report/strategie_attive.html — documento autocontenuto (PNG base64)
con tutte le strategie ATTIVE di PORT06: descrizione, config live e grafici
esplicativi costruiti su EPISODI REALI di segnale (dati parquet locali).

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

import base64
import io
import sys
from datetime import datetime, timezone
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import numpy as np
import pandas as pd

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

from src.data.downloader import load_data
from scripts.portfolios._defs import PORTFOLIOS
from src.portfolio import weighting as W

OUT = PROJECT_ROOT / "docs" / "report" / "strategie_attive.html"
plt.rcParams.update({"font.size": 9.5, "axes.grid": True, "grid.alpha": 0.25,
                     "figure.facecolor": "white", "axes.facecolor": "#fbfbfd"})

C_UP, C_DN = "#2e9e6b", "#d64545"
C_ENTRY, C_TP, C_SL = "#1f6fd6", "#2e9e6b", "#d64545"


# ----------------------------------------------------------------- helpers
def b64(fig) -> str:
    buf = io.BytesIO()
    fig.savefig(buf, format="png", dpi=115, bbox_inches="tight")
    plt.close(fig)
    return base64.b64encode(buf.getvalue()).decode()


def candles(ax, d):
    t = mdates.date2num(pd.to_datetime(d["timestamp"], unit="ms", utc=True))
    w = (t[1] - t[0]) * 0.65 if len(t) > 1 else 0.02
    for k in range(len(d)):
        o, h, l, c = (d[x].iloc[k] for x in ("open", "high", "low", "close"))
        col = C_UP if c >= o else C_DN
        ax.plot([t[k], t[k]], [l, h], color=col, lw=0.7, zorder=2)
        ax.add_patch(plt.Rectangle((t[k] - w / 2, min(o, c)), w, abs(c - o) or 1e-9,
                                   facecolor=col, edgecolor=col, zorder=3))
    ax.xaxis_date()
    ax.xaxis.set_major_formatter(mdates.DateFormatter("%d %b\n%H:%M"))
    return t


def atr(df, n=14):
    h, l, c = df["high"].values, df["low"].values, df["close"].values
    pc = np.roll(c, 1); pc[0] = c[0]
    tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
    return pd.Series(tr).rolling(n).mean().values


def find_winner(sigs, df, since_idx):
    """Primo segnale (recente) il cui TP viene toccato entro max_bars."""
    h, l = df["high"].values, df["low"].values
    for s in reversed(sigs):
        if s.idx < since_idx:
            break
        tp = s.metadata.get("tp"); mb = s.metadata.get("max_bars", 24)
        if not tp:
            continue
        for j in range(s.idx + 1, min(s.idx + mb + 1, len(df))):
            hit = h[j] >= tp if s.direction == 1 else l[j] <= tp
            if hit:
                return s, j
    return None, None


def load_strategy(module):
    import importlib
    m = importlib.import_module(module)
    return next(v() for k, v in vars(m).items()
                if isinstance(v, type) and hasattr(v, "generate_signals")
                and getattr(v, "__module__", "") == m.__name__)


def mark_trade(ax, t, d0, s, jx, tp, sl, win_lo):
    ei = s.idx - win_lo
    ax.axvline(t[ei], color=C_ENTRY, lw=1, ls=":")
    ax.annotate(f"ENTRY {'LONG' if s.direction==1 else 'SHORT'}\n@{s.entry_price:.5g}",
                (t[ei], s.entry_price), xytext=(-65, 25 if s.direction == 1 else -35),
                textcoords="offset points", color=C_ENTRY, fontweight="bold",
                arrowprops=dict(arrowstyle="->", color=C_ENTRY))
    ax.axhline(tp, color=C_TP, lw=1.2, ls="--")
    ax.annotate("TP", (t[-1], tp), color=C_TP, fontweight="bold",
                xytext=(4, 0), textcoords="offset points")
    if sl:
        ax.axhline(sl, color=C_SL, lw=1.2, ls="--")
        ax.annotate("SL", (t[-1], sl), color=C_SL, fontweight="bold",
                    xytext=(4, 0), textcoords="offset points")
    if jx is not None:
        xi = jx - win_lo
        ax.annotate("EXIT take-profit", (t[xi], tp), xytext=(10, -28),
                    textcoords="offset points", color=C_TP, fontweight="bold",
                    arrowprops=dict(arrowstyle="->", color=C_TP))


# ----------------------------------------------------------------- grafici fade
def chart_fade(module, asset, params, band_fn, title, panel_fn=None):
    df = load_data(asset, "1h")
    ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
    strat = load_strategy(module)
    sigs = strat.generate_signals(df, ts, **params)
    since = int(len(df) * 0.85)
    s, j = find_winner(sigs, df, since)
    if s is None:
        s, j = find_winner(sigs, df, int(len(df) * 0.5))
    lo, hi = s.idx - 36, min((j or s.idx + 24) + 10, len(df) - 1)
    d = df.iloc[lo:hi].reset_index(drop=True)

    if panel_fn:
        fig, (ax, ax2) = plt.subplots(2, 1, figsize=(8.6, 4.6), sharex=True,
                                      height_ratios=[2.2, 1])
    else:
        fig, ax = plt.subplots(figsize=(8.6, 3.6))
        ax2 = None
    t = candles(ax, d)
    band_fn(ax, df, lo, hi, t)
    mark_trade(ax, t, d, s, j, s.metadata["tp"], s.metadata.get("sl"), lo)
    ax.set_title(title, loc="left", fontweight="bold")
    if panel_fn:
        panel_fn(ax2, df, lo, hi, t, s)
    return b64(fig)


def mr01_bands(ax, df, lo, hi, t):
    c = pd.Series(df["close"].values)
    ma = c.rolling(50).mean().values[lo:hi]
    sd = c.rolling(50).std().values[lo:hi]
    ax.plot(t, ma, color="#444", lw=1, label="SMA50 (= TP)")
    ax.plot(t, ma + 2.5 * sd, color="#9467bd", lw=1, ls="-.", label="banda ±2.5σ")
    ax.plot(t, ma - 2.5 * sd, color="#9467bd", lw=1, ls="-.")
    ax.legend(loc="upper left", fontsize=8)


def mr02_bands(ax, df, lo, hi, t):
    hh = pd.Series(df["high"].values).rolling(20).max().shift(1).values[lo:hi]
    ll = pd.Series(df["low"].values).rolling(20).min().shift(1).values[lo:hi]
    ax.plot(t, hh, color="#9467bd", lw=1, ls="-.", label="canale Donchian 20 (H/L)")
    ax.plot(t, ll, color="#9467bd", lw=1, ls="-.")
    ax.plot(t, (hh + ll) / 2, color="#444", lw=1, label="centro canale (= TP)")
    ax.legend(loc="upper left", fontsize=8)


def mr07_panel(ax2, df, lo, hi, t, s):
    c = df["close"].values
    r = pd.Series(c).pct_change()
    z = (r - r.rolling(50).mean()) / r.rolling(50).std()
    ax2.plot(t, z.values[lo:hi], color="#1f6fd6", lw=1)
    ax2.axhline(3.5, color=C_SL, ls="--", lw=1)
    ax2.axhline(-3.5, color=C_SL, ls="--", lw=1)
    ax2.set_ylabel("z rendimento")
    ax2.annotate("|z| ≥ 3.5 → fade", (t[s.idx - lo], 3.5), xytext=(8, 8),
                 textcoords="offset points", color=C_SL, fontsize=8)


def chart_dip01():
    df = load_data("BTC", "1h")
    ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
    strat = load_strategy("scripts.strategies.DIP01_dip_buy")
    sigs = strat.generate_signals(df, ts)
    s, j = find_winner(sigs, df, int(len(df) * 0.85))
    lo, hi = s.idx - 36, min((j or s.idx + 24) + 10, len(df) - 1)
    d = df.iloc[lo:hi].reset_index(drop=True)
    fig, (ax, ax2) = plt.subplots(2, 1, figsize=(8.6, 4.6), sharex=True,
                                  height_ratios=[2.2, 1])
    t = candles(ax, d)
    c = pd.Series(df["close"].values)
    ma = c.rolling(50).mean().values[lo:hi]
    ax.plot(t, ma, color="#444", lw=1, label="SMA50 (= TP)")
    ax.legend(loc="upper left", fontsize=8)
    mark_trade(ax, t, d, s, j, s.metadata["tp"], s.metadata.get("sl"), lo)
    ax.set_title("DIP01 — dip-buy sullo z-score (episodio reale BTC)", loc="left",
                 fontweight="bold")
    sd = c.rolling(50).std()
    z = ((c - c.rolling(50).mean()) / sd).values[lo:hi]
    ax2.plot(t, z, color="#1f6fd6", lw=1)
    ax2.axhline(-2.5, color=C_SL, ls="--", lw=1)
    ax2.set_ylabel("z prezzo")
    ax2.annotate("z incrocia sotto −2.5 → BUY", (t[s.idx - lo], -2.5), xytext=(8, -14),
                 textcoords="offset points", color=C_SL, fontsize=8)
    return b64(fig)


def chart_exit16():
    """Episodio reale: il wick BUCA lo SL ma il close non conferma -> niente stop,
    il trade va a TP. Cerca nelle MR01 ETH (dove EXIT-16 e' nato)."""
    df = load_data("ETH", "1h")
    ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
    strat = load_strategy("scripts.strategies.MR01_bollinger_fade")
    sigs = strat.generate_signals(df, ts, trend_max=3.0, ema_long=200)
    h, l, c = df["high"].values, df["low"].values, df["close"].values
    a = atr(df, 14)
    ep = None
    for s in reversed(sigs):
        tp, sl, mb = s.metadata["tp"], s.metadata["sl"], s.metadata["max_bars"]
        if s.direction != 1:
            continue
        wick = None
        for j in range(s.idx + 1, min(s.idx + mb + 1, len(df))):
            if h[j] >= tp:                 # TP raggiunto
                if wick is not None:
                    ep = (s, wick, j)
                break
            if l[j] <= sl and c[j] >= sl - 0.5 * a[j]:
                wick = j                   # wick sotto SL ma close non conferma
            elif c[j] < sl - 0.5 * a[j]:
                break                      # stop vero
        if ep:
            break
    s, wick, j = ep
    lo, hi = s.idx - 20, min(j + 8, len(df) - 1)
    d = df.iloc[lo:hi].reset_index(drop=True)
    fig, ax = plt.subplots(figsize=(8.6, 3.8))
    t = candles(ax, d)
    mark_trade(ax, t, d, s, j, s.metadata["tp"], s.metadata["sl"], lo)
    buf = s.metadata["sl"] - 0.5 * a[wick]
    ax.axhline(buf, color="#e08c1a", lw=1.1, ls=":")
    ax.annotate("conferma: SL − 0.5·ATR", (t[-1], buf), color="#e08c1a",
                xytext=(4, 0), textcoords="offset points", fontsize=8)
    ax.annotate("il WICK buca lo SL\nma il CLOSE non conferma\n→ NIENTE stop (EXIT-16)",
                (t[wick - lo], l[wick]), xytext=(15, -52), textcoords="offset points",
                color=C_SL, fontweight="bold",
                arrowprops=dict(arrowstyle="->", color=C_SL))
    ax.set_title("EXIT-16 — lo stop scatta solo sul CLOSE confermato (episodio reale ETH)",
                 loc="left", fontweight="bold")
    return b64(fig)


# ------------------------------------------------------------ honest / pairs / tsm
def chart_tr01():
    df = load_data("BTC", "1h")
    d = df.set_index(pd.to_datetime(df["timestamp"], unit="ms", utc=True))
    d4 = d.resample("4h").agg({"open": "first", "high": "max", "low": "min",
                               "close": "last"}).dropna().iloc[-1100:]
    ef = d4["close"].ewm(span=20, adjust=False).mean()
    es = d4["close"].ewm(span=100, adjust=False).mean()
    long = ef > es
    fig, ax = plt.subplots(figsize=(8.6, 3.4))
    ax.plot(d4.index, d4["close"], color="#333", lw=0.9, label="BTC 4h")
    ax.plot(d4.index, ef, color=C_TP, lw=1.1, label="EMA20")
    ax.plot(d4.index, es, color="#9467bd", lw=1.1, label="EMA100")
    ax.fill_between(d4.index, d4["close"].min(), d4["close"].max(), where=long,
                    alpha=0.10, color=C_TP, label="LONG (EMA20>EMA100)")
    ax.legend(loc="upper left", fontsize=8, ncol=2)
    ax.set_title("TR01 — trend EMA20/100 4h, long/flat (qui BTC; live: paniere di 5 equal-weight)",
                 loc="left", fontweight="bold")
    return b64(fig)


def _daily_panel():
    out = {}
    for a in ["ADA", "BNB", "BTC", "DOGE", "ETH", "LTC", "SOL", "XRP"]:
        df = load_data(a, "1h")
        s = pd.Series(df["close"].values,
                      index=pd.to_datetime(df["timestamp"], unit="ms", utc=True))
        out[a] = s.resample("1D").last().dropna()
    return pd.DataFrame(out).dropna()


def chart_rot02(panel):
    btc = panel["BTC"]
    sma = btc.rolling(100).mean()
    mom = panel.pct_change(60).iloc[-1] * 100
    order = mom.sort_values(ascending=False)
    fig, (ax, ax2) = plt.subplots(1, 2, figsize=(8.6, 3.4), width_ratios=[1.7, 1])
    view = slice(-540, None)
    ax.plot(btc.index[view], btc.values[view], color="#333", lw=0.9, label="BTC 1d")
    ax.plot(sma.index[view], sma.values[view], color="#9467bd", lw=1.1, label="SMA100")
    on = (btc > sma).values[view]
    ax.fill_between(btc.index[view], btc.values[view].min(), btc.values[view].max(),
                    where=on, alpha=0.10, color=C_TP, label="risk-ON")
    ax.legend(loc="upper left", fontsize=8)
    ax.set_title("ROT02 — gate di regime (BTC>SMA100)", loc="left", fontweight="bold")
    cols = [C_TP if (k < 3 and v > 0) else "#bbb" for k, v in enumerate(order.values)]
    ax2.bar(order.index, order.values, color=cols)
    ax2.set_title("momentum 60g: top-3 in book", loc="left", fontweight="bold")
    ax2.tick_params(axis="x", rotation=60)
    ax2.set_ylabel("%")
    return b64(fig)


def chart_pr01():
    a = load_data("ETH", "1h"); b = load_data("BTC", "1h")
    m = a[["timestamp", "close"]].rename(columns={"close": "ca"}).merge(
        b[["timestamp", "close"]].rename(columns={"close": "cb"}), on="timestamp")
    m["dt"] = pd.to_datetime(m["timestamp"], unit="ms", utc=True)
    m = m.iloc[-24 * 200:]
    r = np.log(m["ca"] / m["cb"])
    z = (r - r.rolling(50).mean()) / r.rolling(50).std()
    fig, (ax, ax2) = plt.subplots(2, 1, figsize=(8.6, 4.6), sharex=True,
                                  height_ratios=[1, 1.4])
    na = m["ca"] / m["ca"].iloc[0]; nb = m["cb"] / m["cb"].iloc[0]
    ax.plot(m["dt"], na, lw=0.9, label="ETH (gamba A)", color="#1f6fd6")
    ax.plot(m["dt"], nb, lw=0.9, label="BTC (gamba B)", color="#e08c1a")
    ax.legend(loc="upper left", fontsize=8); ax.set_ylabel("prezzi normalizzati")
    ax.set_title("PR01 — spread reversion ETH/BTC (market-neutral, 2 gambe)",
                 loc="left", fontweight="bold")
    ax2.plot(m["dt"], z, color="#333", lw=0.8)
    for y, col, lab in ((2, C_SL, "entry |z|≥2"), (-2, C_SL, None),
                        (0.75, C_TP, "exit |z|≤0.75"), (-0.75, C_TP, None)):
        ax2.axhline(y, color=col, ls="--", lw=1)
        if lab:
            ax2.annotate(lab, (m["dt"].iloc[-1], y), xytext=(4, 2),
                         textcoords="offset points", color=col, fontsize=8)
    ent = (z.shift(1).abs() < 2) & (z.abs() >= 2)
    ax2.plot(m["dt"][ent], z[ent], "v", color=C_SL, ms=6)
    ax2.set_ylabel("z-score log-ratio")
    return b64(fig)


def chart_tsm01(panel):
    P = panel.iloc[-380:]
    signs = pd.DataFrame({h: np.sign(P.iloc[-1] / P.iloc[-1 - h] - 1)
                          for h in (63, 126, 252)}).T
    fig, (ax, ax2) = plt.subplots(1, 2, figsize=(8.6, 3.2), width_ratios=[1.6, 1])
    btc = panel["BTC"].iloc[-380:]
    sma = panel["BTC"].rolling(100).mean().iloc[-380:]
    ax.plot(btc.index, btc.values, color="#333", lw=0.9, label="BTC 1d")
    ax.plot(sma.index, sma.values, color="#9467bd", lw=1.1, label="SMA100")
    off = (btc <= sma).values
    ax.fill_between(btc.index, btc.values.min(), btc.values.max(), where=off,
                    alpha=0.12, color=C_SL, label="risk-OFF → cash")
    ax.legend(loc="upper left", fontsize=8)
    ax.set_title("TSM01 — gate risk-off", loc="left", fontweight="bold")
    im = ax2.imshow(signs.values, cmap="RdYlGn", vmin=-1, vmax=1, aspect="auto")
    ax2.set_xticks(range(len(signs.columns)), signs.columns, rotation=60, fontsize=8)
    ax2.set_yticks(range(3), ["3 mesi", "6 mesi", "12 mesi"], fontsize=8)
    ax2.set_title("consenso momentum (verde=+)", loc="left", fontweight="bold")
    ax2.grid(False)
    return b64(fig)


def chart_sh01():
    df = load_data("BTC", "1h")
    d = df.iloc[-24:].reset_index(drop=True)
    fig, (ax, ax2) = plt.subplots(1, 2, figsize=(8.6, 3.6), width_ratios=[1, 1.4])
    t = candles(ax, d)
    ax.xaxis.set_major_locator(mdates.HourLocator(interval=8))
    ax.set_title("la 'forma': 24 barre → 17 feature", loc="left",
                 fontweight="bold", fontsize=9)
    ax.annotate("body/shadow, rendimenti,\npendenza, curvatura,\npos. max/min, RSI, estensione",
                (0.04, 0.04), xycoords="axes fraction", fontsize=8.5,
                bbox=dict(boxstyle="round", fc="#fffbe8", ec="#e0c25a"))
    # schema walk-forward
    ax2.set_xlim(0, 10); ax2.set_ylim(0, 3.4); ax2.axis("off")
    ax2.add_patch(plt.Rectangle((0.2, 1.9), 7.0, 0.9, fc="#dbe9fb", ec="#1f6fd6"))
    ax2.text(3.7, 2.35, "TRAIN: tutta la storia con esito noto (expanding)",
             ha="center", fontsize=9, color="#1f4f96")
    ax2.add_patch(plt.Rectangle((7.4, 1.9), 2.2, 0.9, fc="#d9f2e4", ec=C_TP))
    ax2.text(8.5, 2.35, "PREDICT\nultimo blocco", ha="center", va="center",
             fontsize=8.5, color="#1c6b46")
    ax2.annotate("", xy=(8.3, 1.75), xytext=(4.0, 1.75),
                 arrowprops=dict(arrowstyle="->", color="#555"))
    ax2.text(6.1, 1.5, "LogisticRegression: P(rendimento a 12 barre > 0)",
             ha="center", fontsize=8.5, color="#555")
    ax2.text(0.2, 0.85, "se proba ≥ 0.58 → entra a close, esce dopo H=12 barre\n"
                        "(nessun TP/SL: 11 famiglie di stop testate, 0 sopravvissute →\n"
                        " la coda si gestisce col CAP di famiglia al 5.88%)",
             fontsize=8.5, va="top",
             bbox=dict(boxstyle="round", fc="#fff", ec="#ccc"))
    ax2.set_title("walk-forward causale", loc="left", fontweight="bold", fontsize=9)
    ax2.text(0.2, 3.25, "live: storia full dal parquet, fit solo dell'ultimo blocco",
             fontsize=8, color="#666", va="top")
    fig.tight_layout()
    return b64(fig)


def chart_xs01():
    """Book corrente (long perdenti/short vincenti) + dispersione cross-section col gate."""
    from scripts.strategies.XS01_cross_sectional import aligned_panel, UNIVERSE, LB
    M = aligned_panel()
    logC = np.log(M.values)
    ts = pd.to_datetime(M.index, unit="ms", utc=True)
    dm = logC[-1] - logC[-1 - LB]
    dm = dm - dm.mean()
    w = -dm / np.sum(np.abs(dm))
    fig, (ax, ax2) = plt.subplots(1, 2, figsize=(8.6, 3.4), width_ratios=[1, 1.5])
    ax.bar(UNIVERSE, w * 100, color=[C_TP if x > 0 else C_SL for x in w])
    ax.set_ylabel("peso book %")
    ax.tick_params(axis="x", rotation=60)
    ax.set_title("book: long i perdenti relativi,\nshort i vincenti (mom 48h demeaned)",
                 loc="left", fontweight="bold", fontsize=9)
    D = logC[LB:] - logC[:-LB]
    disp = pd.Series(D.std(axis=1), index=ts[LB:]).iloc[-24 * 180:]
    ax2.plot(disp.index, disp.values, color="#333", lw=0.7)
    ax2.axhline(0.0313, color=C_SL, ls="--", lw=1.2)
    on = disp.values >= 0.0313
    ax2.fill_between(disp.index, 0, disp.values.max(), where=on, alpha=0.10, color=C_TP)
    ax2.annotate("disp_min 0.0313 (p50 TRAIN)\nentry solo sopra soglia",
                 (disp.index[-1], 0.0313), xytext=(-150, 10),
                 textcoords="offset points", color=C_SL, fontsize=8)
    ax2.set_title("dispersion-gate: std cross-section del momentum",
                  loc="left", fontweight="bold", fontsize=9)
    fig.tight_layout()
    return b64(fig)


def chart_weights():
    p = PORTFOLIOS["PORT06"]
    ids = p.sleeve_ids
    w = W.weight_vector("cap", ids, None, caps=p.caps)
    fam = {i: W.family_of(i) for i in ids}
    colors = {"FADE": "#1f6fd6", "HONEST": "#e08c1a", "PAIRS": "#9467bd",
              "TSM": "#5ab4ac", "SHAPE": "#d64545", "XSEC": "#8c564b"}
    order = sorted(ids, key=lambda i: (fam[i], i))
    fig, ax = plt.subplots(figsize=(8.6, 2.9))
    ax.bar(order, [w[i] * 100 for i in order], color=[colors[fam[i]] for i in order])
    ax.set_ylabel("peso %")
    ax.tick_params(axis="x", rotation=60)
    handles = [plt.Rectangle((0, 0), 1, 1, fc=c) for c in colors.values()]
    ax.legend(handles, colors.keys(), fontsize=8, ncol=5, loc="upper right")
    ax.set_title("PORT06 — pesi cap-weighting (tetti: PAIRS 33%, SHAPE 5.88%), ribilancio 1D",
                 loc="left", fontweight="bold")
    return b64(fig)


# ------------------------------------------------------- statistiche per anno
POS_T, LEV_T, FEE_T = 0.15, 3.0, 0.001     # convenzione TEST (canonica)


def yearly_stats(trades, ts):
    """trades [(i, j, ret_netto_leveraged)] -> ({anno: n/pnl/dd}, dd_totale).
    n e PnL per anno di ENTRY (Σ rendimenti netti per trade ×100); DD per anno
    sul path di equity compounding (pos 0.15), peak resettato a inizio anno."""
    years: dict[int, dict] = {}
    cap, peak, tot_peak, tot_dd = 1000.0, 1000.0, 1000.0, 0.0
    cur = None
    for i, j, ret in sorted(trades, key=lambda t: t[1]):
        # clamp: l'engine live-path lascia j >= n per il trade aperto al bordo
        # della serie (con dati freschi a fine-serie c'e' quasi sempre)
        ey = ts.iloc[i].year
        xy = ts.iloc[min(j, len(ts) - 1)].year
        d = years.setdefault(ey, {"n": 0, "pnl": 0.0, "dd": 0.0})
        d["n"] += 1
        d["pnl"] += ret * 100
        cap = max(cap + cap * POS_T * ret, 10.0)
        if cur != xy:
            cur, peak = xy, cap
        peak = max(peak, cap)
        tot_peak = max(tot_peak, cap)
        tot_dd = max(tot_dd, (tot_peak - cap) / tot_peak * 100)
        dx = years.setdefault(xy, {"n": 0, "pnl": 0.0, "dd": 0.0})
        dx["dd"] = max(dx["dd"], (peak - cap) / peak * 100)
    return years, tot_dd


def equity_yearly(eq: pd.Series):
    """Equity giornaliera -> {anno: pnl(ret% anno)/dd}, dd_totale (per i multi-asset)."""
    out = {}
    for y, g in eq.groupby(eq.index.year):
        pk = g.cummax()
        out[int(y)] = {"n": None, "pnl": (g.iloc[-1] / g.iloc[0] - 1) * 100,
                       "dd": float(((pk - g) / pk).max() * 100)}
    pk = eq.cummax()
    return out, float(((pk - eq) / pk).max() * 100)


def yearly_table(per_market: dict, note="") -> str:
    """{mercato: (years_dict, tot_dd)} -> tabella HTML anno × (trd, PnL%, DD%)."""
    yrs = sorted({y for ym, _ in per_market.values() for y in ym})
    head = "".join(f'<th colspan="3">{m}</th>' for m in per_market)
    sub = "".join("<th>trd</th><th>PnL%</th><th>DD%</th>" for _ in per_market)
    rows = []
    for y in yrs:
        cells = ""
        for ym, _ in per_market.values():
            d = ym.get(y)
            if d:
                n = "—" if d["n"] is None else d["n"]
                cells += f"<td>{n}</td><td>{d['pnl']:+.0f}</td><td>{d['dd']:.0f}</td>"
            else:
                cells += "<td>—</td><td>—</td><td>—</td>"
        rows.append(f"<tr><td><b>{y}</b></td>{cells}</tr>")
    cells = ""
    for ym, tot_dd in per_market.values():
        ns = [d["n"] for d in ym.values() if d["n"] is not None]
        n = sum(ns) if ns else "—"
        pnl = sum(d["pnl"] for d in ym.values())
        cells += f"<td><b>{n}</b></td><td><b>{pnl:+.0f}</b></td><td><b>{tot_dd:.0f}</b></td>"
    rows.append(f'<tr style="background:#f0f2f5"><td><b>TOT</b></td>{cells}</tr>')
    base = ("PnL = Σ rendimenti netti per trade (%, leva 3x = config live dal 2026-06-12, "
            "fee 0.10% RT); DD = max drawdown nell'anno (equity compounding pos 0.15); "
            "riga TOT: DD dell'intero periodo. NB: la finestra canonica di valutazione del "
            "portafoglio parte dal <b>2021</b> — gli anni 2018-2020 (regime microstrutturale "
            "diverso, spesso negativi) sono mostrati per onestà storica.")
    return (f'<table><tr><th rowspan="2">anno</th>{head}</tr><tr>{sub}</tr>'
            + "".join(rows) + f'</table><p class="sub">{note or base}</p>')


def stats_fades():
    """Per-anno delle 3 fade × BTC/ETH col PATH LIVE (EXIT-16 + trend 3.0)."""
    from scripts.analysis.risk_management import strats_for
    from scripts.analysis.trendmax_port06_impact import build_trades_variant
    out = {}
    for asset in ("BTC", "ETH"):
        df = load_data(asset, "1h")
        ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
        for nm, (fn, params) in strats_for(asset).items():
            tr = build_trades_variant(fn(df, **params), df, mode="exit16", trend_max=3.0)
            out.setdefault(nm, {})[asset] = yearly_stats(tr, ts)
    return out


def stats_dip():
    from scripts.analysis.dip01_exit16_impact import dip_entries, dip_trades
    df = load_data("BTC", "1h")
    ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
    tr = dip_trades(dip_entries(df), df, "exit16")
    return {"BTC": yearly_stats(tr, ts)}


def stats_pairs():
    """Tabella compatta: per anno 'PnL% (n)' per coppia + righe TOT/DD."""
    from scripts.analysis.pairs_research import pairs_sim
    from scripts.strategies.PR01_pairs_reversion import PAIRS as PAIRS_CFG
    cols, data = [], {}
    from scripts.analysis.pairs_research import pairs_sim_flat
    # le 5 coppie 1h universali + il BLEND ETH/BTC 15m flat-skip (mezza size = sleeve live)
    runs = [(f"{a}/{b}", lambda a=a, b=b, p=p: pairs_sim(a, b, **p)) for a, b, p in PAIRS_CFG]
    runs.append(("ETH/BTC·15m", lambda: pairs_sim_flat(
        "ETH", "BTC", tf="15m", n=66, z_in=1.674, z_exit=1.0, max_bars=35,
        flat_skip=True, pos=0.075)))
    for tag, fn in runs:
        r = fn()
        cols.append(tag)
        s = pd.Series(r["eq_v"], index=pd.to_datetime(r["eq_ts"], utc=True))
        ydd = {int(y): float(((g.cummax() - g) / g.cummax()).max() * 100)
               for y, g in s.groupby(s.index.year)}
        data[tag] = dict(yearly=r["yearly"], n=r["yearly_n"], dd=r["dd"],
                         trades=r["trades"], ydd=ydd)
    yrs = sorted({y for d in data.values() for y in d["yearly"]})
    rows = []
    for y in yrs:
        cells = "".join(
            (f"<td>{data[c]['yearly'][y]:+.0f} <span class='sub'>({data[c]['n'].get(y,0)}"
             f", dd {data[c]['ydd'].get(y,0):.0f})</span></td>")
            if y in data[c]["yearly"] else "<td>—</td>" for c in cols)
        rows.append(f"<tr><td><b>{y}</b></td>{cells}</tr>")
    tot = "".join(f"<td><b>{sum(data[c]['yearly'].values()):+.0f}</b> "
                  f"<span class='sub'>({data[c]['trades']}, dd {data[c]['dd']:.0f})</span></td>"
                  for c in cols)
    rows.append(f'<tr style="background:#f0f2f5"><td><b>TOT</b></td>{tot}</tr>')
    head = "".join(f"<th>{c}</th>" for c in cols)
    return (f'<table><tr><th>anno</th>{head}</tr>' + "".join(rows) + "</table>"
            '<p class="sub">cella: PnL% anno (n trade, max DD% anno) — Σ rendimenti netti '
            "per trade, fee 0.20% RT/coppia (2 gambe), leva 3x convenzione test. NB: la "
            "finestra canonica del portafoglio parte dal <b>2021</b>; gli anni precedenti "
            "(spesso negativi) sono mostrati per onestà storica.</p>")


def stats_multi():
    """TR01/ROT02/TSM01: per-anno ret/DD dall'equity giornaliera canonica (2021+)."""
    from scripts.analysis.combine_portfolio import IDX
    from scripts.analysis.honest_improve2 import _tr_basket_daily, _rot_daily_equity
    from scripts.analysis.tsmom_research import tsmom_sim
    from scripts.analysis.report_families import daily_from
    tr = _tr_basket_daily(["BNB", "BTC", "DOGE", "SOL", "XRP"], IDX)
    rot = _rot_daily_equity(IDX)
    t = tsmom_sim()
    tsm = daily_from(t["eq_ts"], t["eq_v"])
    note = ("PnL = ritorno % dell'anno dall'equity giornaliera canonica (2021-2026, leva 3x "
            "convenzione test); DD = max drawdown nell'anno; trd non applicabile (ribilancio "
            "giornaliero del book).")
    return (yearly_table({"TR01 (paniere 5)": equity_yearly(tr)}, note),
            yearly_table({"ROT02 (universo 8)": equity_yearly(rot)}, note),
            yearly_table({"TSM01 (universo 8)": equity_yearly(tsm)}, note))


def stats_xs01():
    """XS01 per-anno dall'engine canonico (UNGATED, come il backtest di portafoglio)."""
    from scripts.strategies.XS01_cross_sectional import xsec_sim
    r = xsec_sim()
    s = pd.Series(r["eq_v"], index=pd.to_datetime(r["eq_ts"], utc=True))
    years = {}
    for y in sorted(r["yearly"]):
        g = s[s.index.year == y]
        dd = float(((g.cummax() - g) / g.cummax()).max() * 100) if len(g) else 0.0
        years[int(y)] = {"n": r["yearly_n"].get(y, 0), "pnl": r["yearly"][y], "dd": dd}
    pk = s.cummax()
    note = ("PnL = Σ rendimenti netti per trade del book (%, gross 1, fee 0.20% RT/book = "
            "turnover 2×0.10%); DD dall'equity compounding (pos 0.15, leva 3x convenzione "
            "test). Engine canonico SENZA dispersion-gate (il gate agisce solo sul path "
            "live, come trend/hurst sulle fade → il live farà meglio del backtest).")
    return yearly_table({"XS01 (universo 8)": (years, float(((pk - s) / pk).max() * 100))},
                        note)


def stats_sh01():
    """SH01 per-anno dal walk-forward EXPANDING (il regime validato e ora live)."""
    from scripts.analysis.shape_ml_research import ml_wf_entries
    out = {}
    for asset in ("BTC", "ETH"):
        df = load_data(asset, "1h")
        ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
        c = df["close"].values
        ents = ml_wf_entries(df, W=24, H=12, model="logit", thresh=0.58)
        tr, last = [], -1
        for e in ents:
            i, d, mb = e["i"], e["d"], e["max_bars"]
            j = min(i + mb, len(c) - 1)
            if i <= last or j <= i:
                continue
            ret = (c[j] - c[i]) / c[i] * d * LEV_T - FEE_T * LEV_T
            tr.append((i, j, ret))
            last = j
        out[asset] = yearly_stats(tr, ts)
    return out


# ----------------------------------------------------------------- HTML
CSS = """
body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;margin:0;background:#f4f5f7;color:#222}
.wrap{max-width:960px;margin:0 auto;padding:24px 16px 60px}
h1{font-size:26px;margin:8px 0 2px} h2{font-size:20px;border-bottom:2px solid #ddd;
padding-bottom:6px;margin-top:38px} .sub{color:#666;font-size:13px}
.card{background:#fff;border:1px solid #e3e5e8;border-radius:10px;padding:18px 20px;margin:14px 0;
box-shadow:0 1px 3px rgba(0,0,0,.05)}
.badge{display:inline-block;font-size:11px;font-weight:700;padding:2px 9px;border-radius:10px;
margin-right:6px;color:#fff}
.b-fade{background:#1f6fd6}.b-honest{background:#e08c1a}.b-pairs{background:#9467bd}
.b-tsm{background:#5ab4ac}.b-shape{background:#d64545}.b-xsec{background:#8c564b}
.b-real{background:#2e9e6b}.b-sim{background:#8a8f98}
img{max-width:100%;border:1px solid #eee;border-radius:6px;margin-top:10px}
table{border-collapse:collapse;width:100%;font-size:13px;margin-top:8px}
td,th{border:1px solid #e3e5e8;padding:5px 9px;text-align:left}
th{background:#f0f2f5} code{background:#eef1f4;padding:1px 5px;border-radius:4px;font-size:12px}
.note{background:#fffbe8;border:1px solid #e8d99a;border-radius:8px;padding:10px 14px;
font-size:13px;margin-top:10px}
"""


def card(title, badges, desc_html, img_b64=None, table_html=""):
    img = f'<img src="data:image/png;base64,{img_b64}">' if img_b64 else ""
    return (f'<div class="card"><h3>{title}</h3><p>{badges}</p>'
            f'{desc_html}{table_html}{img}</div>')


def main():
    OUT.parent.mkdir(parents=True, exist_ok=True)
    now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
    ver = (PROJECT_ROOT / "VERSION").read_text().strip()
    print("genero grafici (episodi reali)...")
    panel = _daily_panel()

    print("calcolo statistiche per anno (engine canonici/live-path)...")
    st_fade = stats_fades()
    st_dip = stats_dip()
    t_pairs = stats_pairs()
    t_tr, t_rot, t_tsm = stats_multi()
    t_xs = stats_xs01()
    print("  SH01 walk-forward expanding (il piu' lento)...")
    st_sh = stats_sh01()

    g_w = chart_weights()
    g_mr01 = chart_fade("scripts.strategies.MR01_bollinger_fade", "BTC",
                        dict(trend_max=3.0, ema_long=200), mr01_bands,
                        "MR01 — fade della banda di Bollinger (episodio reale BTC)")
    g_mr02 = chart_fade("scripts.strategies.MR02_donchian_fade", "ETH",
                        dict(trend_max=3.0, ema_long=200), mr02_bands,
                        "MR02 — fade della rottura del canale Donchian (episodio reale ETH)")
    g_mr07 = chart_fade("scripts.strategies.MR07_return_reversal", "BTC",
                        dict(trend_max=3.0, ema_long=200),
                        lambda ax, df, lo, hi, t: None,
                        "MR07 — fade del rendimento estremo (episodio reale BTC)",
                        panel_fn=mr07_panel)
    g_e16 = chart_exit16()
    g_dip = chart_dip01()
    g_tr = chart_tr01()
    g_rot = chart_rot02(panel)
    g_pr = chart_pr01()
    g_tsm = chart_tsm01(panel)
    g_sh = chart_sh01()
    g_xs = chart_xs01()

    # metriche canoniche del default per l'intestazione (sempre aggiornate)
    pr = PORTFOLIOS["PORT06"].backtest()
    n_def = len(PORTFOLIOS["PORT06"].sleeve_ids)
    hdr = (f"FULL Sharpe {pr.full['sharpe']:.2f} / DD {pr.full['dd']:.2f}% — "
           f"OOS Sharpe {pr.oos['sharpe']:.2f} / DD {pr.oos['dd']:.2f}%")

    B = lambda f, t: f'<span class="badge b-{f}">{t}</span>'
    real = B("real", "ESECUZIONE REALE (testnet)")
    sim = B("sim", "SIMULATO")

    c_mr01 = card("MR01 — Bollinger Fade (BTC, ETH)", B("fade", "FADE") + real, """
<p>Quando il <b>close chiude fuori dalla banda</b> ±2.5σ attorno alla SMA50, entra CONTRO il
movimento (short sopra, long sotto). TP alla media (il prezzo "torna a casa"), SL a 2·ATR,
time-limit 24 barre. Edge OOS validato: BTC +201% / ETH +1238% (fee incluse).</p>""", g_mr01,
        yearly_table({"BTC": st_fade["MR01"]["BTC"], "ETH": st_fade["MR01"]["ETH"]}))

    c_mr02 = card("MR02 — Donchian Fade (BTC, ETH)", B("fade", "FADE") + real, """
<p>Fada la <b>rottura degli estremi del canale</b> Donchian a 20 barre (max/min recenti):
short sulla rottura del massimo, long sulla rottura del minimo. TP al centro del canale.
Stessa tesi di MR01 con trigger diverso → si combinano bene.</p>""", g_mr02,
        yearly_table({"BTC": st_fade["MR02"]["BTC"], "ETH": st_fade["MR02"]["ETH"]}))

    c_mr07 = card("MR07 — Return Reversal (BTC, ETH)", B("fade", "FADE") + real, """
<p>Guarda il <b>rendimento della singola barra</b>: se lo z-score supera ±3.5 (movimento
estremo in un'ora), fada il movimento. Exit in multipli di ATR. È la fade più selettiva
(esposizione ~8% del tempo).</p>""", g_mr07,
        yearly_table({"BTC": st_fade["MR07"]["BTC"], "ETH": st_fade["MR07"]["ETH"]}))

    c_e16 = card("EXIT-16 — perché lo stop scatta solo sul CLOSE", B("fade", "MECCANISMO COMUNE"), """
<p>Scoperta chiave della ricerca exit (34 agenti, 23 famiglie): <b>gli stop intrabar da wick
sono falsi negativi</b> — l'overshoot che buca lo stop e rientra è esattamente il movimento che
la fade sta comprando. Con EXIT-16 lo SL intrabar è disattivato: si esce solo se il <b>close</b>
della barra completata sfonda il livello di 0.5·ATR. Il TP intrabar resta. Impatto sul
portafoglio: OOS Sharpe 8.82→10.06. Esteso anche a DIP01 (2026-06-07, grid 36/36).</p>""", g_e16)

    c_dip = card("DIP01 — Dip Buy (BTC)", B("honest", "HONEST") + real, """
<p>Compra il <b>dip</b>: quando lo z-score del prezzo incrocia sotto −2.5 (sell-off rapido),
entra long. TP alla SMA50, EXIT-16 sul SL, max 24 barre. È l'unico sleeve BTC con round-trip
reali su Deribit testnet (TP limit resting + disaster-stop a −30% sul book).</p>""", g_dip, yearly_table(st_dip))

    c_tr = card("TR01 — Basket Trend (4h)", B("honest", "HONEST") + sim, """
<p><b>Trend-following difensivo</b>: long quando EMA20>EMA100 sulle 4 ore, flat altrimenti,
su un paniere equal-weight di 5 asset (BNB, BTC, DOGE, SOL, XRP). Cattura i trend lunghi che
le fade per costruzione non prendono. Valuta solo barre 4h COMPLETE.</p>""", g_tr, t_tr)

    c_rot = card("ROT02 — Dual Momentum (1d)", B("honest", "HONEST") + sim, """
<p><b>Rotazione</b>: ogni giorno ordina 8 crypto per momentum a 60 giorni e tiene le top-3
(solo se positive), gross 0.45. Gate di regime: tutto cash se BTC&lt;SMA100. Diversificare su 3
asset invece di 2 ha quasi dimezzato il DD (40%→26%) alzando il ritorno.</p>""", g_rot, t_rot)

    c_pr = card("PR01 — Pairs Reversion (ETH/BTC, LTC/ETH, ADA/ETH, BTC/LTC, ETH/SOL + ETH/BTC 15m)",
                B("pairs", "PAIRS") + real, """
<p><b>Market-neutral</b>: quando il rapporto fra due asset si allontana troppo dalla sua media
(|z| del log-ratio ≥ 2), compra la gamba debole e shorta la forte; chiude quando il rapporto
rientra (|z| ≤ 0.75) o dopo 72 barre. Config <b>universale</b> per tutte le coppie (niente tuning
per-coppia = anti-overfit). Correlazione col mercato ~0.05: rende anche quando il mercato è fermo.
Fee su 2 gambe. Senza stop per design → position size ridotto a 0.20 (esposizione ≈ validato).</p>
<p class='sub'><b>BLEND timeframe (nuovo, 2026-06-09)</b>: ETH/BTC gira anche a <b>15m</b> accanto al 1h
(config n=66, |z|≥1.67, exit |z|≤1.0 o 35 barre). Origine: gioco "Blind Traders" (100 agenti ciechi
su dati anonimi); testato col gate PORT06 — <b>decorrelato dal 1h (corr 0.37)</b>, robusto (16/16),
e l'edge regge anche filtrando le candele flat ETH 15m (<b>flat-skip</b>: niente ingresso/uscita su
barre stale O=H=L=C). Worker validato (replay == backtest). A <b>mezza size</b> (blend-tilt prudente
sul caveat slippage): porta il PORT06 a FULL Sharpe ~7.2 / OOS ~9.7.</p>
<p class='sub'>Esecuzione reale a 2 gambe su Deribit testnet (<code>PairsExecutionClient</code>):
open/close long A / short B, leg-risk unwind, mai <code>close_position</code>.</p>""", g_pr, t_pairs)

    c_tsm = card("TSM01 — TSMOM (1d)", B("tsm", "TSM") + sim, """
<p>Long sugli asset con <b>consenso pieno</b> di momentum su 3 orizzonti (3/6/12 mesi),
gross 0.30, cash totale se BTC&lt;SMA100. Mai un anno negativo nel backtest. Non è un motore di
ritorno: è il <b>diversificatore</b> che lavora nei regimi in cui le fade soffrono.
Attualmente flat by-design (risk-off).</p>""", g_tsm, t_tsm)

    c_sh = card("SH01 — Shape-ML (BTC, ETH)", B("shape", "SHAPE") + real, """
<p>Una <b>LogisticRegression</b> legge 17 feature della <i>forma</i> delle ultime 24 barre e
predice il segno del rendimento a 12 barre; entra solo se la probabilità supera 0.58, esce a
orizzonte. Training <b>walk-forward causale</b> (mai dati futuri). Win-rate ~50%: l'edge è
nell'asimmetria, non nella frequenza. <b>Senza stop-loss by design</b> (ogni stop testato rompe
l'edge): la coda si gestisce dimezzando il peso della famiglia (cap 5.88%).</p>
<p class='sub'>Esecuzione reale single-leg su Deribit testnet: niente TP/SL, chiusura a orizzonte
H=12 (market reduce-only), disaster-bracket on-book come unica protezione di coda — è il
diversificatore più decorrelato del portafoglio.</p>
<p class='sub'>Fix punto-10 (2026-06-07): il training live usa la storia COMPLETA dal parquet
locale (il regime corto a 365g non era robusto: trade-rate 22% vs 10% validato).</p>""", g_sh, yearly_table(st_sh))

    c_xs = card("XS01 — Cross-Sectional Reversion (8 asset)", B("xsec", "XSEC") + sim, """
<p><b>Famiglia nuova (2026-06-09)</b>: ogni 12 ore classifica 8 crypto (BTC, ETH, LTC, ADA,
SOL, BNB, XRP, DOGE) per rendimento a 48 ore e va <b>long i perdenti relativi / short i
vincenti</b> (peso ∝ −(ret − media cross-section)), market-neutral gross 1. Cattura il
<i>fattore</i> reversione cross-sezionale — distinto dai pairs (pairwise) e dai fade
(single-asset): correlazione ~0 con entrambi. Gate PORT06: OOS Sharpe 9.66→10.07,
FULL DD 3.68→3.46. Plateau robusto (lb 12-72 × hold 6-24 tutte OOS+).</p>
<p class='sub'><b>Dispersion-gate (2026-06-10, v1.1.20)</b>: entra solo se la dispersione
cross-section del momentum ≥ 0.0313 (mediana TRAIN) — la reversione paga quando c'è
dispersione da far rientrare. Diagnostica monotona TRAIN e OOS, plateau p30-p70,
standalone Sharpe 2.50→3.46 (regge fee 2x), PORT06 OOS 10.07→<b>10.37</b> a DD pari.
Solo path live (backtest canonico non filtrato → il live farà meglio del backtest).</p>
<p class='sub'><b>Phase-tranching (2026-06-11, v1.1.21)</b>: la fase del roll non-sovrapposto
è arbitraria e da sola muove lo Sharpe daily 1.52-2.33 e il DD 14-33% (timing-luck) → live
gira con <b>3 sub-book sfasati</b> di 4 barre su capitale comune (PnL/3 ciascuno): ensemble
di fase SENZA parametri fittati. Gate con plateau (K=2 e K=3 entrambi promossi): PORT06
OOS Sharpe 10.07→10.15, DD 1.48→1.38 a FULL pari. Worker validato esatto (K=1 == backtest;
K=3 == unione fasi).</p>
<p class='sub'>8 gambe → niente esecuzione reale: gira PAPER come i book multi-asset
(il rumore di arrotondamento a €2k dominerebbe). Worker validato (replay == backtest esatto).</p>""",
                g_xs, t_xs)

    html = f"""<!doctype html><html lang="it"><head><meta charset="utf-8">
<title>PythagorasGoal — Strategie attive PORT06</title><style>{CSS}</style></head>
<body><div class="wrap">
<h1>PythagorasGoal — Strategie attive</h1>
<p class="sub">Portafoglio live <b>PORT06</b> — definizione {n_def} sleeve; <b>pool live real-only 15 sleeve</b>
(i 4 book multi-asset TR01/ROT02/TSM01/XS01 girano in statistica, fuori dal capitale-pool).
Capitale pool €2.000, <b>leva 3x</b> (dal 2026-06-12), <b>real-truth ledger</b> ·
v{ver} · generato {now} · backtest canonico: {hdr}</p>
<div class="card">
<p>Famiglie quasi <b>scorrelate</b> fra loro (fade↔honest ~0.05, pairs ~0.02-0.09,
shape ~0.08, xsec ~0): la diversificazione è la leva anti-drawdown. Pesi equal con tetti
per famiglia, ribilancio giornaliero su capitale pool condiviso. Fee Deribit 0.10% round-trip
incluse ovunque; ogni meccanismo live è passato da un gate out-of-sample a livello di portafoglio.</p>
<img src="data:image/png;base64,{g_w}"></div>

<h2>FADE — mean-reversion intraday 15m <span class="sub">(6 sleeve × 5.69%, swap 1h→15m dal 2026-06-12)</span></h2>
<div class="note"><b>Tesi della famiglia:</b> sui perpetui crypto l'edge è la <i>reversione</i>:
i movimenti estremi rientrano (i breakout falliscono — l'intera famiglia squeeze-breakout è stata
scartata come artefatto di look-ahead). Le fade vendono l'eccesso e comprano il panico, con tre
protezioni comuni: <b>filtro trend</b> (salta il segnale se il prezzo è oltre 3·ATR dalla EMA200 —
non si fada un crollo/parabolica), <b>EXIT-16</b> (stop solo sul close confermato, vedi sotto) e
<b>min_tp_frac</b> (salta i micro-trade col TP entro le fee).</div>
{c_mr01}
{c_mr02}
{c_mr07}
{c_e16}
<h2>HONEST — long-only multi-regime <span class="sub">(3 sleeve × 5.69%)</span></h2>
{c_dip}
{c_tr}
{c_rot}
<h2>PAIRS — spread reversion market-neutral <span class="sub">(6 sleeve × 5.26%: 5 coppie 1h + ETH/BTC 15m a mezza size, famiglia ≤33%)</span></h2>
{c_pr}
<h2>TSM — trend-following multi-orizzonte <span class="sub">(1 sleeve × 5.69%)</span></h2>
{c_tsm}
<h2>XSEC — reversione cross-sectional <span class="sub">(1 sleeve × 5.69%)</span></h2>
{c_xs}
<h2>SHAPE — ML morfologico <span class="sub">(2 sleeve × 2.94%, famiglia ≤5.88%)</span></h2>
{c_sh}

<h2>Metodologia</h2>
<div class="card"><ul style="font-size:13.5px;line-height:1.7">
<li><b>Fee sempre incluse</b>: 0.10% round-trip taker Deribit (misurate reali = assunte). Molte operazioni = morte per fee: ogni strategia regge lo stress a fee doppie.</li>
<li><b>Niente look-ahead</b>: direzione e prezzo decisi solo con dati fino al close corrente; barre in formazione escluse (lezione EXIT-16). La famiglia squeeze (accuratezze 76-82%) è stata scartata proprio per questo artefatto.</li>
<li><b>Gate out-of-sample</b>: nessun meccanismo va in produzione senza migliorare (o non degradare) il portafoglio FULL e OOS — robusto individualmente ≠ migliora PORT06.</li>
<li><b>Esecuzione reale shadow (15 sleeve su 19)</b>: i 6 fade + DIP01 (single-leg, TP limit al livello, disaster-stop −30% on-book), i 6 pairs incl. <b>ETH/BTC 15m flat-skip</b> (2 gambe, leg-risk unwind) e i 2 SH01 (single-leg, exit a orizzonte, niente TP/SL) eseguono ordini reali su Deribit testnet. Restano simulati solo i book multi-asset TR01/ROT02/TSM01/XS01 (bloccati dal capitale: serve ~€20k per il rumore di arrotondamento). Fee reali verificate dai trade; divergenze sim/reale ≥100bps alertate su Telegram.</li>
<li><b>Real-truth ledger (2026-06-10)</b>: il capitale dei worker eseguiti si aggiorna col PnL dei <b>fill reali</b> (fee reali incluse), non col sim — equity, pesi e notional derivano dai soldi veri sul conto; il sim resta diagnostica nel log (fallback dichiarato solo se il trade reale non è mai esistito). Le decisioni di entry/exit restano guidate dal feed.</li>
<li><b>Verità d'esecuzione e netting (2026-06-11, v1.1.23-25)</b>: il tocco TP va confermato dal <b>book reale</b> (gate TP_PHANTOM: resting a zero-fill + prezzo lontano dal livello = wick fantasma del feed → exit soppressa); i ledger usano l'amount <b>fillato</b>, mai il richiesto; le chiusure sono <b>netting-aware</b> (residuo reduce-only cappato/respinto dal netting di conto → rieseguito in market puro: niente gambe orfane quando worker opposti condividono lo strumento). Reti di sicurezza: reconciler orario conto-vs-libri (alert <code>ACCOUNT_DRIFT</code>), eventi <code>NET_CLOSE</code>/<code>PAIR_LEG_ORPHAN</code>/<code>REAL_CLOSE_PARTIAL</code>, drift-monitor giornaliero per famiglia (warn sotto il p5 storico).</li>
</ul></div>
<p class="sub">Generato da <code>scripts/analysis/make_strategy_doc.py</code> — grafici da episodi reali sui dati parquet locali.</p>
</div></body></html>"""

    OUT.write_text(html)
    print(f"OK -> {OUT} ({OUT.stat().st_size//1024} KB)")


if __name__ == "__main__":
    main()
