research: verify TP01 (branch parallelo) col gauntlet onesto — REGGE il hold-out 2025-26
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user