research(v2.0.0): honest harness + fasi 0-3 + ricerca frattale 63 agenti — nessun edge robusto su BTC/ETH
Harness onesto research_lab.py (serie di posizione causale, fee-aware, null model a rotazione circolare, hold-out 2025+ bloccato; self-test cheat/noise che valida il banco). - Fase 1: triage superstiti (DIP, shape-ML) -> morti net-fee. - Fase 2: esplorazione famiglie (reversal morta; solo trend long-only/MA-cross passa i gate base). - Fase 3: conferma avversariale del trend -> regime-luck del toro, bocciato sul hold-out 2025-26. - Ricerca frattale multi-agente (Workflow, 63 agenti, 52 ipotesi dai due documenti) con guard anti-look-ahead (eval_signal.py) + hold-out + test cross-asset -> 0 edge robusto (l'unico "confermato" su ETH fallisce su BTC con lo stesso codice). - Analisi options: VRP reale +10/+14 vol pt ma finestra 6 sett. regime unico -> non validabile; ruolo solo overlay tail-cap, tenere cerbero-bite ad accumulare. Quinta conferma indipendente: su BTC/ETH-solo-prezzo non c'e' un edge facile. Il processo disciplinato ha evitato un falso "+49% vs -49%" che sul vecchio feed contaminato sarebbe finito in produzione. Diari docs/diary/2026-06-19-research-phase0-1 / -phase2-options / -phase3-confirm / -fractal-multiagent-search. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user