"""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_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"} 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) # ----------------------------------------------------------------- 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-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'' if img_b64 else "" return (f'

{title}

{badges}

' f'{desc_html}{table_html}{img}
') 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() 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() B = lambda f, t: f'{t}' real = B("real", "ESECUZIONE REALE (testnet)") sim = B("sim", "SIMULATO") c_mr01 = card("MR01 — Bollinger Fade (BTC, ETH)", B("fade", "FADE") + real, """

Quando il close chiude fuori dalla banda ±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).

""", g_mr01) c_mr02 = card("MR02 — Donchian Fade (BTC, ETH)", B("fade", "FADE") + real, """

Fada la rottura degli estremi del canale 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.

""", g_mr02) c_mr07 = card("MR07 — Return Reversal (BTC, ETH)", B("fade", "FADE") + real, """

Guarda il rendimento della singola barra: 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).

""", g_mr07) c_e16 = card("EXIT-16 — perché lo stop scatta solo sul CLOSE", B("fade", "MECCANISMO COMUNE"), """

Scoperta chiave della ricerca exit (34 agenti, 23 famiglie): gli stop intrabar da wick sono falsi negativi — 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 close della barra completata sfonda il livello di 0.5·ATR. Il TP intrabar resta. Impatto sul portafoglio: OOS Sharpe 8.82→10.06. Esteso oggi anche a DIP01 (grid 36/36).

""", g_e16) c_dip = card("DIP01 — Dip Buy (BTC)", B("honest", "HONEST") + real, """

Compra il dip: 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).

""", g_dip) c_tr = card("TR01 — Basket Trend (4h)", B("honest", "HONEST") + sim, """

Trend-following difensivo: 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.

""", g_tr) c_rot = card("ROT02 — Dual Momentum (1d)", B("honest", "HONEST") + sim, """

Rotazione: 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<SMA100. Diversificare su 3 asset invece di 2 ha quasi dimezzato il DD (40%→26%) alzando il ritorno.

""", g_rot) c_pr = card("PR01 — Pairs Reversion (ETH/BTC, LTC/ETH, ADA/ETH, BTC/LTC, ETH/SOL)", B("pairs", "PAIRS") + sim, """

Market-neutral: 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 universale 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).

""", g_pr) c_tsm = card("TSM01 — TSMOM (1d)", B("tsm", "TSM") + sim, """

Long sugli asset con consenso pieno di momentum su 3 orizzonti (3/6/12 mesi), gross 0.30, cash totale se BTC<SMA100. Mai un anno negativo nel backtest. Non è un motore di ritorno: è il diversificatore che lavora nei regimi in cui le fade soffrono. Attualmente flat by-design (risk-off).

""", g_tsm) c_sh = card("SH01 — Shape-ML (BTC, ETH)", B("shape", "SHAPE") + sim, """

Una LogisticRegression legge 17 feature della forma 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 walk-forward causale (mai dati futuri). Win-rate ~50%: l'edge è nell'asimmetria, non nella frequenza. Senza stop-loss by design (ogni stop testato rompe l'edge): la coda si gestisce dimezzando il peso della famiglia (cap 5.88%).

Fix di oggi (punto-10): il training live usa la storia COMPLETA dal parquet locale (il regime corto a 365g non era robusto: trade-rate 22% vs 10% validato).

""", g_sh) html = f""" PythagorasGoal — Strategie attive PORT06

PythagorasGoal — Strategie attive

Portafoglio live PORT06 (17 sleeve, capitale pool €2.000, leva 2x) · v{ver} · generato {now} · backtest canonico: FULL Sharpe 6.61 / DD 3.58% — OOS Sharpe 8.77 / DD 1.34%

Tre famiglie principali quasi scorrelate fra loro (fade↔honest ~0.05, pairs ~0.02-0.09, shape ~0.08): 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.

FADE — mean-reversion intraday 1h (6 sleeve × 6.47%)

Tesi della famiglia: sui perpetui crypto l'edge è la reversione: 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: filtro trend (salta il segnale se il prezzo è oltre 3·ATR dalla EMA200 — non si fada un crollo/parabolica), EXIT-16 (stop solo sul close confermato, vedi sotto) e min_tp_frac (salta i micro-trade col TP entro le fee).
{c_mr01} {c_mr02} {c_mr07} {c_e16}

HONEST — long-only multi-regime (3 sleeve × 6.47%)

{c_dip} {c_tr} {c_rot}

PAIRS — spread reversion market-neutral (5 coppie × 5.88%, famiglia ≤33%)

{c_pr}

TSM — trend-following multi-orizzonte (1 sleeve × 6.47%)

{c_tsm}

SHAPE — ML morfologico (2 sleeve × 2.94%, famiglia ≤5.88%)

{c_sh}

Metodologia

Generato da scripts/analysis/make_strategy_doc.py — grafici da episodi reali sui dati parquet locali.

""" OUT.write_text(html) print(f"OK -> {OUT} ({OUT.stat().st_size//1024} KB)") if __name__ == "__main__": main()