From 55c28e51b23bed8dffe1aafefb75018ed8ef1c7b Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 19 Jun 2026 18:50:36 +0000 Subject: [PATCH] =?UTF-8?q?research:=20verify=20TP01=20(branch=20parallelo?= =?UTF-8?q?)=20col=20gauntlet=20onesto=20=E2=80=94=20REGGE=20il=20hold-out?= =?UTF-8?q?=202025-26?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TP01 (TSMOM 30/90/180 vol-target 20% lev2x long-flat, 50/50 BTC+ETH) passa dove il mio trend 1h era caduto: hold-out 2025-26 +2.8%/DD8% vs buy&hold -39%/DD60%, positivo su ENTRAMBI gli asset, plateau 1h/4h/1d. La chiave e' il vol-targeting (esposizione ~1/vol -> cash nei crash) che non avevo combinato col trend. Edge DIFENSIVO reale (Sharpe full 1.36 vs B&H 0.92, ma CAGR 16.6% vs 48%). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/diary/2026-06-19-tp01-verification.md | 61 +++++++++ scripts/analysis/verify_tp01.py | 149 +++++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 docs/diary/2026-06-19-tp01-verification.md create mode 100644 scripts/analysis/verify_tp01.py diff --git a/docs/diary/2026-06-19-tp01-verification.md b/docs/diary/2026-06-19-tp01-verification.md new file mode 100644 index 0000000..faf59cb --- /dev/null +++ b/docs/diary/2026-06-19-tp01-verification.md @@ -0,0 +1,61 @@ +# 2026-06-19 — Verifica TP01 (branch strategy-research-2026-06) col gauntlet onesto + +Una ricerca PARALLELA (branch `strategy-research-2026-06`, AdrianoDev) dallo stesso baseline +v2.0.0 ha trovato TP01 come "unica vincitrice". La mia linea (Fasi 2-3) aveva bocciato il trend +sul hold-out 2025-26. Ho riprodotto TP01 VERBATIM (`scripts/analysis/verify_tp01.py`) e l'ho +passato al mio gauntlet. **TP01 REGGE — la mia conclusione precedente era incompleta.** + +## TP01 = TSMOM 30/90/180g, **vol-target 20%**, leva cap 2x, **long-flat**, portafoglio 50/50 BTC+ETH (4h) + +## Esiti del gauntlet + +**(A) Multi-TF (4h cherry-picked?) — NO, plateau robusto:** +| TF | FULL Sharpe | HOLD-OUT Sharpe | +|---|---|---| +| 15m | 0.93 | −0.31 | +| 1h | 1.32 | +0.20 | +| **4h** | **1.36** | **+0.27** | +| 1d | 1.30 | +0.31 | +1h/4h/1d danno tutti FULL ~1.3 e hold-out positivo → non è un artefatto di un singolo TF (solo il 15m, fee-sensibile, fallisce). + +**(C) HOLD-OUT 2025-26 (il test che ha ucciso il mio trend 1h) — TP01 PROTEGGE:** +| | Sharpe | ret | DD | +|---|---|---|---| +| **TP01 portfolio** | **+0.27** | **+2.8%** | **8.3%** | +| buy&hold 50/50 | −0.35 | **−39.4%** | 59.8% | + +**(D) Cross-asset nel hold-out — regge su ENTRAMBI** (BTC sleeve +2.9% Sh 0.24, ETH +2.4% Sh 0.24). +A differenza del "vincitore" frattale (+ETH/−BTC), TP01 protegge coerentemente su BTC E ETH. + +**(B) Per anno:** positiva quasi ogni anno 2019-2026 (eccezioni piccole: 2022 −2.4%, 2026-YTD −0.9%), +DD annui 1-12%. Il claim "positiva ogni anno" è lievemente ottimistico ma sostanzialmente vero. + +## Perché TP01 regge dove il MIO trend (Fase 3) è caduto +La differenza chiave è il **VOL-TARGETING** (che NON avevo combinato col trend): TP01 scala +l'esposizione ∝ 1/vol_realizzata → nel crollo 2025-26 la vol è esplosa e TP01 si è messo +quasi in cash, schivando il drawdown. Il mio MA-cross 1h aveva esposizione fissa ed è rimasto +long nel chop → frullato. Concorrono: TSMOM multi-orizzonte (più liscio del MA-cross), long-flat +(niente perdite short), diversificazione 50/50. **La mia "trend = regime-luck" era vera per il +trend NUDO; TP01 = trend + vol-target + portafoglio è un'altra cosa, e robusta.** + +## Cosa È onestamente TP01 (no oversell) +- **Edge DIFENSIVO, non alpha**: FULL Sharpe 1.36 vs buy&hold 0.92 — MA CAGR +16.6% vs +48.1%. + Su tutto il toro il buy&hold ha reso ~8x di più. Il valore di TP01 è il **DD** (13.8% vs 77.5% + full; 8% vs 60% nel hold-out) e la **protezione dai crash**. +- Nel hold-out 2025-26 ha fatto solo +2.8% (Sharpe 0.27, basso): ha **protetto, non profittato**. +- Un solo regime di hold-out, ma il vol-targeting è meccanico (high vol → low expo) → generalizza + per costruzione meglio di un timing fittato. +- Config canonica (30/90/180, vol20%, lev2x) non iper-tunata; 4h non cherry-picked (plateau). + +## VERDETTO +**TP01 è la PRIMA strategia onesta e robusta del progetto post-reset.** Supera il mio gauntlet +(hold-out positivo su entrambi gli asset, plateau multi-TF, causale, fee-aware). È modesta e +difensiva (Sharpe ~1.3, soffitto strutturale dichiarato corretto), ma è reale: migliora il +rischio/rendimento del buy&hold tagliando i drawdown e proteggendo nei crash. La ricerca parallela +ha fatto centro proprio sul pezzo che la mia linea non aveva combinato (vol-target sul trend). + +**Raccomandazione:** integrare il branch su main (modulo `trend_portfolio.py` + paper trader), +trattare TP01 come baseline operativa difensiva. Aspettative oneste verso il target €50/g: a +Sharpe 1.3 / CAGR 16.6% servono molto capitale o leva (con più DD) — TP01 è un fondamento solido, +non una scorciatoia. Prossimo: stress fee2x/slippage, deflated-Sharpe sui loro track A-E, e +walk-forward del vol-target prima di rischiare capitale reale. diff --git a/scripts/analysis/verify_tp01.py b/scripts/analysis/verify_tp01.py new file mode 100644 index 0000000..f36054c --- /dev/null +++ b/scripts/analysis/verify_tp01.py @@ -0,0 +1,149 @@ +"""VERIFICA AVVERSARIALE di TP01 (branch strategy-research-2026-06) col MIO gauntlet onesto. + +TP01 = TSMOM multi-orizzonte (30/90/180g) long-flat, vol-target 20%, leva cap 2x, portafoglio +50/50 BTC+ETH. Codice riprodotto VERBATIM dal branch (src/strategies/trend_portfolio.py). +La loro tesi: 'positiva ogni anno 2019-2026, Sharpe ~1.32'. Il mio test decisivo: il HOLD-OUT +2025-26 (che ha bocciato il mio trend 1h in Fase 3) + cross-asset + multi-TF (cherry-picking 4h?). + + uv run python scripts/analysis/verify_tp01.py +""" +from __future__ import annotations +import sys +from pathlib import Path +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) +import numpy as np +import pandas as pd +from src.data.downloader import load_data + +HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC") +CANONICAL = dict(target_vol=0.20, leverage=2.0, long_only=True, + horizons_days=(30, 90, 180), vol_win_days=30, fee_side=0.0005) + + +# ---- TP01 riprodotto VERBATIM dal branch ---- +def simple_returns(c): + r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0; return r + +def realized_vol(r, win, bpy): + return pd.Series(r).rolling(win, min_periods=win // 2).std().values * np.sqrt(bpy) + +def tsmom_blend(c, horizons): + n = len(c); acc = np.zeros(n); cnt = np.zeros(n) + for h in horizons: + s = np.full(n, np.nan); s[h:] = np.sign(c[h:] / c[:-h] - 1.0) + v = np.isfinite(s); acc[v] += s[v]; cnt[v] += 1 + out = np.zeros(n); nz = cnt > 0; out[nz] = acc[nz] / cnt[nz]; return out + +def target_series(df, p, bpd): + c = df["close"].values.astype(float); bpy = bpd * 365.25 + r = simple_returns(c) + vol = realized_vol(r, p["vol_win_days"] * bpd, bpy) + direction = tsmom_blend(c, tuple(d * bpd for d in p["horizons_days"])) + if p["long_only"]: + direction = np.clip(direction, 0, None) + scal = np.where((vol > 0) & np.isfinite(vol), p["target_vol"] / vol, 0.0) + tgt = np.clip(direction * scal, -p["leverage"], p["leverage"]); tgt[~np.isfinite(tgt)] = 0.0 + return tgt + +def net_returns(df, p, bpd): + c = df["close"].values.astype(float); r = simple_returns(c) + tgt = target_series(df, p, bpd) + pos_held = np.zeros(len(tgt)); pos_held[1:] = tgt[:-1] # decisa a close[t-1], tenuta in t -> causale + gross = pos_held * r + turn = np.abs(np.diff(pos_held, prepend=0.0)) + net = gross - p["fee_side"] * turn; net[0] = 0.0 + return np.clip(net, -0.99, None), pos_held + + +def resample(df_1h, rule): + g = df_1h.copy(); idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True); g.index = idx + out = g.resample(rule, label="left", closed="left").agg( + {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}).dropna(subset=["open"]) + out["timestamp"] = out.index + return out.reset_index(drop=True) + + +def metrics(combo, idx): + rr = combo[np.isfinite(combo)] + if len(rr) < 2 or np.std(rr) == 0: + return dict(sharpe=0, cagr=0, dd=0, ret=0, n=len(rr)) + dt = pd.Series(idx).diff().dt.total_seconds().median() + bpy = 86400 * 365.25 / dt + eq = np.cumprod(1 + rr); peak = np.maximum.accumulate(eq) + years = (idx[-1] - idx[0]).total_seconds() / 86400 / 365.25 + return dict(sharpe=float(np.mean(rr) / np.std(rr) * np.sqrt(bpy)), + cagr=float(eq[-1] ** (1 / years) - 1) if years > 0 else 0, + dd=float(np.max((peak - eq) / peak)), ret=float(eq[-1] - 1), n=len(rr)) + + +def portfolio_combo(tf_rule, bpd): + series = {} + for a in ("BTC", "ETH"): + df = load_data(a, "1h") + if tf_rule: + df = resample(df, tf_rule) + net, _ = net_returns(df, CANONICAL, bpd) + series[a] = pd.Series(net, index=pd.to_datetime(df["timestamp"], unit="ms", utc=True) if not tf_rule + else pd.DatetimeIndex(df["timestamp"])) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + combo = 0.5 * J["BTC"].values + 0.5 * J["ETH"].values + return combo, J.index, J + + +def line(label, combo, idx): + m = metrics(combo, idx) + return f" {label:<22s} Sharpe {m['sharpe']:>5.2f} | ret {m['ret']*100:>+8.1f}% CAGR {m['cagr']*100:>+6.1f}% | DD {m['dd']*100:>5.1f}% | n {m['n']}" + + +def main(): + print("=" * 92) + print(" VERIFICA TP01 (TSMOM 30/90/180 vol-target 20% lev2x long-flat, 50/50 BTC+ETH)") + print(" col gauntlet onesto: FULL vs buy&hold | HOLD-OUT 2025-26 bloccato | per-anno | multi-TF") + print("=" * 92) + + TFS = [("15m", "15min", 96), ("1h", None, 24), ("4h", "4h", 6), ("1d", "1D", 1)] + print("\n (A) MULTI-TF: il 4h e' cherry-picked? FULL + HOLD-OUT per ogni timeframe") + for tf, rule, bpd in TFS: + combo, idx, J = portfolio_combo(rule, bpd) + ho = idx >= HOLDOUT + full = metrics(combo, idx) + hold = metrics(combo[ho], idx[ho]) + tag = " <- canonica" if tf == "4h" else "" + print(f" {tf:<3s} FULL Sh {full['sharpe']:>5.2f} CAGR {full['cagr']*100:>+6.1f}% DD {full['dd']*100:>4.1f}% " + f"| HOLD-OUT Sh {hold['sharpe']:>5.2f} ret {hold['ret']*100:>+6.1f}% DD {hold['dd']*100:>4.1f}%{tag}") + + # focus 4h canonica + combo, idx, J = portfolio_combo("4h", 6) + print("\n (B) 4h CANONICA — per anno (la tesi: positiva OGNI anno 2019-2026)") + s = pd.Series(combo, index=idx) + for y, g in s.groupby(s.index.year): + eq = np.cumprod(1 + g.values); pk = np.maximum.accumulate(eq) + ho_flag = " <- HOLD-OUT (mai usato per scegliere config?)" if y >= 2025 else "" + print(f" {y}: ret {(eq[-1]-1)*100:>+7.1f}% DD {np.max((pk-eq)/pk)*100:>5.1f}%{ho_flag}") + + print("\n (C) HOLD-OUT 2025-26 — TP01 vs buy&hold 50/50 (4h)") + ho = idx >= HOLDOUT + print(line("TP01 portfolio HO", combo[ho], idx[ho])) + # buy&hold 50/50 sullo stesso indice/finestra + bh = {} + for a in ("BTC", "ETH"): + df = resample(load_data(a, "1h"), "4h") + r = simple_returns(df["close"].values.astype(float)) + bh[a] = pd.Series(r, index=pd.DatetimeIndex(df["timestamp"])) + Jb = pd.concat(bh, axis=1, join="inner").reindex(idx).fillna(0.0) + bh_combo = 0.5 * Jb["BTC"].values + 0.5 * Jb["ETH"].values + print(line("buy&hold 50/50 HO", bh_combo[ho], idx[ho])) + print(line("TP01 portfolio FULL", combo, idx)) + print(line("buy&hold 50/50 FULL", bh_combo, idx)) + + print("\n (D) CROSS-ASSET nel HOLD-OUT (lo stesso edge regge su ENTRAMBI?)") + for a in ("BTC", "ETH"): + df = resample(load_data(a, "1h"), "4h") + net, _ = net_returns(df, CANONICAL, 6) + ix = pd.DatetimeIndex(df["timestamp"]); m = ix >= HOLDOUT + print(line(f"TP01 {a} sleeve HO", net[m], ix[m])) + + +if __name__ == "__main__": + main()