diff --git a/docs/diary/2026-06-19-tp01-verification.md b/docs/diary/2026-06-19-tp01-verification.md index faf59cb..72247db 100644 --- a/docs/diary/2026-06-19-tp01-verification.md +++ b/docs/diary/2026-06-19-tp01-verification.md @@ -57,5 +57,28 @@ ha fatto centro proprio sul pezzo che la mia linea non aveva combinato (vol-targ **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. +non una scorciatoia. + +## STRESS-TEST (`scripts/analysis/stress_tp01.py`, integrato e rieseguito sul modulo vero) + +| Dimensione | Esito | +|---|---| +| **Sweep fee** | FULL robusto fino a **0.40% RT** (Sh 1.44→1.36→1.28→1.13). HOLD-OUT SOTTILE: +2.8%/Sh0.27 a 0.10% → ~flat (Sh 0.03) a 0.40% | +| **Lag/slippage** | FULL robusto (1.29-1.43). HOLD-OUT si erode: lag1(4h)→Sh0.12, lag2→−0.02, lag1+fee0.20%→0.04 | +| **Plateau parametri** | OTTIMO — target_vol/leva/orizzonti/vol_win tutti reggono o migliorano (orizzonti 20/60/120 → Sh 1.61). **NON un picco cherry-picked** | +| **Deflated-Sharpe** | DSR **0.999** a N=10/40/100 trial → il Sharpe FULL non è artefatto di multiple-testing | + +**Verdetto stress (onesto):** +- **Robustezza FULL-period: FORTE.** TP01 supera fee 0.40%, lag, ampio plateau di parametri, e + deflated-Sharpe. NON è overfit né cherry-picked — la proprietà robusta è il **taglio del + drawdown** (13.8% vs 77.5% full; 8% vs 60% hold-out), invariante a tutto lo stress. +- **Edge di RITORNO nel hold-out: REALE ma SOTTILE e sensibile alla frizione.** Nel 2025-26 ha + schivato il crash in modo affidabile (DD 8% vs 60%) ma ha **protetto più che profittato** (+2.8%, + Sh 0.27), e quel sottile positivo si assottiglia a zero sotto fee2x o lag 2 barre. + +**Conclusione:** la proprietà **deployabile e robusta di TP01 è la PROTEZIONE del drawdown**, non +la generazione di alpha. È una strategia difensiva genuina (prima del progetto a superare gauntlet ++ stress), ma a basso ritorno: il valore è "Sharpe ~1.3 con DD ~6× più piccolo del buy&hold", +non "battere il mercato". Per il capitale reale: il vol-targeting + long-flat sono meccanici e +generalizzano; il rischio residuo è la frizione di esecuzione sul filo del sottile edge di ritorno +nei regimi avversi → da monitorare col paper trader forward-only prima di scalare. diff --git a/scripts/analysis/stress_tp01.py b/scripts/analysis/stress_tp01.py new file mode 100644 index 0000000..d369936 --- /dev/null +++ b/scripts/analysis/stress_tp01.py @@ -0,0 +1,121 @@ +"""STRESS-TEST di TP01 (integrato da strategy-research-2026-06) — robustezza avversariale. + +Usa il modulo VERO integrato (src/strategies/trend_portfolio). Oltre a hold-out/cross-asset/multi-TF +(gia' in verify_tp01.py), qui: sweep FEE (fino 0.40% RT), LAG di esecuzione + slippage, PLATEAU dei +parametri (config cherry-picked?), DEFLATED-SHARPE (multiple-testing track A-E). + + uv run python scripts/analysis/stress_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 scipy.stats import norm, skew, kurtosis +from src.data.downloader import load_data +from src.strategies.trend_portfolio import TrendPortfolio, resample_4h, simple_returns, CANONICAL + +HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC") +DF4H = {a: resample_4h(load_data(a, "1h")) for a in ("BTC", "ETH")} + + +def combo(cfg, lag_bars=0, fee_side=0.0005): + """Rendimenti per-barra del portafoglio 50/50 con config cfg, lag extra e fee dati.""" + tp = TrendPortfolio(**{**cfg, "fee_side": fee_side}) + series = {} + for a in ("BTC", "ETH"): + df = DF4H[a] + r = simple_returns(df["close"].values.astype(float)) + tgt = tp.target_series(df) + held = np.zeros(len(tgt)) + s = 1 + lag_bars + held[s:] = tgt[:-s] # tenuta = decisa s barre prima (causale + lag) + net = held * r - fee_side * np.abs(np.diff(held, prepend=0.0)) + net[0] = 0.0 + series[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"])) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + return 0.5 * J["BTC"].values + 0.5 * J["ETH"].values, J.index + + +def met(combo_r, idx): + rr = combo_r[np.isfinite(combo_r)] + if len(rr) < 2 or np.std(rr) == 0: + return dict(sh=0, ret=0, dd=0) + bpy = 86400 * 365.25 / pd.Series(idx).diff().dt.total_seconds().median() + eq = np.cumprod(1 + rr); pk = np.maximum.accumulate(eq) + return dict(sh=float(np.mean(rr) / np.std(rr) * np.sqrt(bpy)), + ret=float(eq[-1] - 1), dd=float(np.max((pk - eq) / pk))) + + +def full_ho(cfg, lag_bars=0, fee_side=0.0005): + cr, idx = combo(cfg, lag_bars, fee_side) + ho = idx >= HOLDOUT + return met(cr, idx), met(cr[ho], idx[ho]) + + +def main(): + print("=" * 88) + print(" STRESS-TEST TP01 (PORT LF4h canonica) — robustezza avversariale") + print("=" * 88) + + base_f, base_h = full_ho(CANONICAL) + print(f"\n BASELINE (4h, fee 0.10% RT): FULL Sh {base_f['sh']:.2f} ret {base_f['ret']*100:+.0f}% DD {base_f['dd']*100:.1f}%" + f" | HOLD-OUT Sh {base_h['sh']:.2f} ret {base_h['ret']*100:+.1f}% DD {base_h['dd']*100:.1f}%") + + print("\n (1) SWEEP FEE (RT) — regge fino a 0.40%?") + print(f" {'fee RT':<10s}{'FULL Sh':>9s}{'FULL ret':>10s}{'HOLD Sh':>9s}{'HOLD ret':>10s}") + for frt in (0.0, 0.001, 0.002, 0.004): + f, h = full_ho(CANONICAL, fee_side=frt / 2) + print(f" {frt*100:>5.2f}% {f['sh']:>8.2f}{f['ret']*100:>+9.0f}%{h['sh']:>9.2f}{h['ret']*100:>+9.1f}%") + + print("\n (2) LAG di esecuzione + slippage (fee 0.20% per simulare slippage)") + print(f" {'scenario':<22s}{'FULL Sh':>9s}{'HOLD Sh':>9s}{'HOLD ret':>10s}") + for name, lag, frt in [("base", 0, 0.001), ("lag 1 barra (4h)", 1, 0.001), + ("lag 2 barre", 2, 0.001), ("lag1 + fee0.20% slip", 1, 0.002)]: + f, h = full_ho(CANONICAL, lag_bars=lag, fee_side=frt / 2) + print(f" {name:<22s}{f['sh']:>8.2f}{h['sh']:>9.2f}{h['ret']*100:>+9.1f}%") + + print("\n (3) PLATEAU PARAMETRI — la config canonica e' un picco o un altopiano?") + print(f" {'variazione':<26s}{'FULL Sh':>9s}{'HOLD Sh':>9s}") + grid = [ + ("canonica (vt.20 lev2 30/90/180 vw30)", CANONICAL), + ("target_vol 0.15", {**CANONICAL, "target_vol": 0.15}), + ("target_vol 0.25", {**CANONICAL, "target_vol": 0.25}), + ("leverage 1.5", {**CANONICAL, "leverage": 1.5}), + ("leverage 3.0", {**CANONICAL, "leverage": 3.0}), + ("horizons 20/60/120", {**CANONICAL, "horizons_days": (20, 60, 120)}), + ("horizons 60/120/240", {**CANONICAL, "horizons_days": (60, 120, 240)}), + ("vol_win 20", {**CANONICAL, "vol_win_days": 20}), + ("vol_win 45", {**CANONICAL, "vol_win_days": 45}), + ] + sr_trials = [] + for name, cfg in grid: + f, h = full_ho(cfg) + cr, idx = combo(cfg) + sr_trials.append(cr[np.isfinite(cr)].mean() / cr[np.isfinite(cr)].std()) # Sharpe per-barra + print(f" {name:<26s}{f['sh']:>8.2f}{h['sh']:>9.2f}") + + print("\n (4) DEFLATED SHARPE — corregge il multiple-testing (track A-E + sweep). DSR>0.95 = regge") + cr, idx = combo(CANONICAL) + rr = cr[np.isfinite(cr)] + sr = rr.mean() / rr.std(); T = len(rr) + g3 = float(skew(rr)); g4 = float(kurtosis(rr, fisher=False)) + var_sr = float(np.var(sr_trials, ddof=1)) + ge = 0.5772156649 + for N in (10, 40, 100): # N = numero di trial/config provati (conservativo) + z1 = norm.ppf(1 - 1.0 / N); z2 = norm.ppf(1 - 1.0 / (N * np.e)) + sr0 = np.sqrt(var_sr) * ((1 - ge) * z1 + ge * z2) + den = np.sqrt(max(1 - g3 * sr + (g4 - 1) / 4.0 * sr ** 2, 1e-9)) + dsr = float(norm.cdf((sr - sr0) * np.sqrt(T - 1) / den)) + bpy = 86400 * 365.25 / pd.Series(idx).diff().dt.total_seconds().median() + print(f" N={N:>3d} trial -> soglia-max-attesa Sh {sr0*np.sqrt(bpy):.2f} | DSR {dsr:.3f} [{'REGGE' if dsr>0.95 else 'NON regge'}]") + + print("\n" + "=" * 88) + print(" Verdetto: TP01 robusto se regge fee 0.40%+lag (HOLD positivo), plateau (no picco), DSR>0.95.") + print("=" * 88) + + +if __name__ == "__main__": + main()