"""FASE 3 — conferma avversariale del SOLO candidato reale: trend-following long-only (MA-cross). Protocollo onesto: 1. SELEZIONE config SOLO sul pre-hold-out (< 2025-01-01). Niente sbirciate al hold-out. 2. HOLD-OUT 2025-26 sbloccato UNA volta (la prova del nove, mai usato in ricerca). 3. Breakdown PER ANNO vs buy&hold: il trend-LO deve "schivare" i bear (2018/2022). 4. STRESS: fee 2x, lag di esecuzione (1 barra), slippage. 5. DEFLATED SHARPE (Bailey & López de Prado): lo Sharpe regge alla correzione per multiple-testing? uv run python scripts/analysis/phase3_confirm.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 scripts.analysis.research_lab import ( backtest, buy_hold, window_mask, ts, _net_series, HOLDOUT_START, BARS_PER_YEAR, ) from scripts.analysis.phase2_families import ma_cross GRID = [(12, 48), (24, 96), (48, 192), (24, 200), (96, 288)] # MA-cross griglia (fast/slow) REPR = (24, 96) # config rappresentativa PRE-COMMITTATA TF = "1h" def lag(pos, k=1): """Esecuzione in ritardo di k barre (agisci k barre dopo la decisione).""" return np.concatenate([np.zeros(k), pos[:-k]]) def per_year(df, pos, tf): c = df["close"].values.astype(float) net, _, fwd, _ = _net_series(df, pos) yrs = ts(df).dt.year.values out = {} for y in sorted(set(yrs)): m = yrs == y if m.sum() < 2: continue strat = float(np.prod(1 + net[m]) - 1) * 100 bh = float(np.prod(1 + fwd[m]) - 1) * 100 expo = float(np.mean(np.abs(pos[m]))) out[y] = (strat, bh, expo) return out def deflated_sharpe(net, sr_trials_perbar, N): """DSR: prob. che il vero Sharpe > la soglia attesa-massima sotto N trial (multiple testing). Tutto in Sharpe PER BARRA. >0.95 = significativo dopo correzione.""" sr = net.mean() / net.std() T = len(net) g3 = float(skew(net)); g4 = float(kurtosis(net, fisher=False)) var_sr = float(np.var(sr_trials_perbar, ddof=1)) if len(sr_trials_perbar) > 1 else 0.0 ge = 0.5772156649 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) # Sharpe atteso-massimo sotto null, N trial 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 = BARS_PER_YEAR[TF] return dsr, sr * np.sqrt(bpy), sr0 * np.sqrt(bpy) def main(): print("=" * 96) print(" FASE 3 — conferma avversariale: TREND-following long-only (MA-cross) BTC/ETH") print("=" * 96) data = {a: load_data(a, TF) for a in ("BTC", "ETH")} # ---------- 1) selezione SOLO pre-hold-out ---------- print(f"\n (1) SELEZIONE su pre-hold-out (< {HOLDOUT_START}) — Sharpe per config (plateau = robusto)") for a in ("BTC", "ETH"): line = [] for f, s in GRID: pos = ma_cross(data[a], f, s, "lo") sh = backtest(data[a], pos, TF, hi=HOLDOUT_START).sharpe line.append(f"{f}/{s}={sh:>4.2f}") print(f" {a}: " + " ".join(line)) print(f" -> config rappresentativa PRE-COMMITTATA per i test seguenti: {REPR[0]}/{REPR[1]}") # ---------- 2) HOLD-OUT 2025-26 (sbloccato una volta) ---------- print(f"\n (2) HOLD-OUT {HOLDOUT_START}+ — LA PROVA DEL NOVE (mai usato in ricerca)") for a in ("BTC", "ETH"): bh = buy_hold(data[a], TF, lo=HOLDOUT_START) print(f" {a}: buy&hold hold-out Sh {bh.sharpe:>5.2f} ret {bh.ret*100:>+7.1f}% DD {bh.maxdd*100:>4.1f}%") for f, s in GRID: pos = ma_cross(data[a], f, s, "lo") r = backtest(data[a], pos, TF, lo=HOLDOUT_START) star = " <-REPR" if (f, s) == REPR else "" print(f" {f}/{s:<3d} Sh {r.sharpe:>5.2f} ret {r.ret*100:>+7.1f}% DD {r.maxdd*100:>4.1f}% expo {r.exposure:.2f}{star}") # ---------- 3) per anno vs buy&hold (schiva i bear?) ---------- print(f"\n (3) PER ANNO — strat {REPR[0]}/{REPR[1]} vs buy&hold (expo = quanto è long; bear test 2018/2022)") for a in ("BTC", "ETH"): pos = ma_cross(data[a], *REPR, "lo") py = per_year(data[a], pos, TF) print(f" {a}:") for y, (st, bh, ex) in py.items(): flag = " <- BEAR" if bh < -20 else "" print(f" {y}: strat {st:>+7.0f}% | buy&hold {bh:>+7.0f}% | expo {ex:.2f}{flag}") # ---------- 4) stress ---------- print(f"\n (4) STRESS — strat {REPR[0]}/{REPR[1]} | FULL e HOLD-OUT Sharpe") print(f" {'scenario':<24s}{'BTC FULL':>10s}{'BTC HO':>9s}{'ETH FULL':>10s}{'ETH HO':>9s}") scen = [ ("base fee0.10%", dict(fee_rt=0.001), False), ("fee 0.20% (2x)", dict(fee_rt=0.002), False), ("lag 1 barra", dict(fee_rt=0.001), True), ("fee2x + lag", dict(fee_rt=0.002), True), ] for name, kw, do_lag in scen: row = [name] for a in ("BTC", "ETH"): pos = ma_cross(data[a], *REPR, "lo") if do_lag: pos = lag(pos, 1) full = backtest(data[a], pos, TF, **kw).sharpe ho = backtest(data[a], pos, TF, lo=HOLDOUT_START, **kw).sharpe row += [f"{full:>9.2f}", f"{ho:>8.2f}"] print(f" {row[0]:<24s}{row[1]:>10s}{row[2]:>9s}{row[3]:>10s}{row[4]:>9s}") # ---------- 5) deflated Sharpe ---------- print(f"\n (5) DEFLATED SHARPE — corregge il multiple-testing (DSR>0.95 = regge)") # trial set = TUTTE le config trend long-only provate (proxy del numero di tentativi) N_TRIALS = 60 # stima conservativa dei backtest provati in Fase 2 (tutte le famiglie/asset/TF) for a in ("BTC", "ETH"): trials = [backtest(data[a], ma_cross(data[a], f, s, "lo"), TF, hi=HOLDOUT_START) for f, s in GRID] sr_trials = [] for f, s in GRID: net, _, _, _ = _net_series(data[a], ma_cross(data[a], f, s, "lo")) m = window_mask(data[a], hi=HOLDOUT_START) sr_trials.append(net[m].mean() / net[m].std()) net, _, _, _ = _net_series(data[a], ma_cross(data[a], *REPR, "lo")) m = window_mask(data[a], hi=HOLDOUT_START) dsr, sr_ann, sr0_ann = deflated_sharpe(net[m], sr_trials, N_TRIALS) verdict = "REGGE" if dsr > 0.95 else "NON regge" print(f" {a} (pre-hold-out): Sharpe {sr_ann:.2f} vs soglia-max-attesa(N={N_TRIALS}) {sr0_ann:.2f} " f"-> DSR {dsr:.3f} [{verdict}]") print("\n" + "=" * 96) print(" VERDETTO: edge ONESTO solo se (2) hold-out positivo, (3) schiva i bear, (4) regge lo") print(" stress, (5) DSR>0.95. Altrimenti: anche il trend era sample-luck del mercato toro.") print("=" * 96) if __name__ == "__main__": main()