Files
Adriano Dal Pastro 38c8cdf25b 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>
2026-06-19 18:37:05 +00:00

222 lines
9.7 KiB
Python

"""FASE 2 — esplorazione larga per famiglie su BTC/ETH, harness onesto (research_lab).
Famiglie (serie di posizione, causali, netto fee, vs buy&hold + null p-value):
TSMOM (momentum) | REVERSAL | MA-cross | DONCHIAN breakout | VOL-TARGET overlay |
LEAD-LAG BTC<->ETH | HURST-gated momentum. Multi-TF dove sensato (1h + 15m).
La barra DA BATTERE è il buy&hold (Sharpe ~0.8 su BTC/ETH): una strategia di timing vale solo
se fa MEGLIO net-fee. Per ogni famiglia: scan griglia (FULL Sharpe), poi report onesto sulla
config migliore. Selezionare il best-di-griglia GONFIA -> i gate veri sono OOS-VAL + null p<0.05.
uv run python scripts/analysis/phase2_families.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
from scripts.analysis.research_lab import (
backtest, buy_hold, mc_pvalue, window_mask, ts, VAL_START, HOLDOUT_START, BARS_PER_YEAR,
)
# --------------------------------- famiglie ---------------------------------
def tsmom(df, L, mode="ls"):
c = pd.Series(df["close"].values.astype(float))
pos = np.sign(np.nan_to_num((c / c.shift(L) - 1).values))
return np.maximum(pos, 0) if mode == "lo" else pos
def reversal(df, L, mode="ls"):
c = pd.Series(df["close"].values.astype(float))
pos = -np.sign(np.nan_to_num((c / c.shift(L) - 1).values))
return np.maximum(pos, 0) if mode == "lo" else pos
def ma_cross(df, fast, slow, mode="ls"):
c = pd.Series(df["close"].values.astype(float))
ef = c.ewm(span=fast, adjust=False).mean()
es = c.ewm(span=slow, adjust=False).mean()
pos = np.sign((ef - es).values)
return np.maximum(pos, 0) if mode == "lo" else pos
def donchian(df, L, mode="ls"):
h = pd.Series(df["high"].values.astype(float)).rolling(L).max().shift(1).values
l = pd.Series(df["low"].values.astype(float)).rolling(L).min().shift(1).values
c = df["close"].values.astype(float)
pos = np.zeros(len(c)); cur = 0
for i in range(len(c)):
if not np.isnan(h[i]) and c[i] > h[i]:
cur = 1
elif not np.isnan(l[i]) and c[i] < l[i]:
cur = -1 if mode == "ls" else 0
pos[i] = cur
return pos
def vol_target(df, tf, target=0.6, L=72):
"""Overlay SEMPRE-LONG con esposizione scalata dalla vol realizzata (target vol annua)."""
c = pd.Series(df["close"].values.astype(float))
rv_ann = c.pct_change().rolling(L).std().values * np.sqrt(BARS_PER_YEAR[tf])
pos = np.clip(np.nan_to_num(target / np.where(rv_ann > 0, rv_ann, np.nan), nan=0.0), 0, 1)
return pos
def rolling_hurst(c, W=120, step=6, lags=(2, 4, 8, 16, 32)):
logc = np.log(c); n = len(c); H = np.full(n, np.nan)
lg = np.log(lags)
for i in range(W, n, step):
seg = logc[i - W:i]
tau = [np.std(seg[lag:] - seg[:-lag]) for lag in lags]
if min(tau) > 0:
H[i] = np.polyfit(lg, np.log(tau), 1)[0]
return pd.Series(H).ffill().fillna(0.5).values
def hurst_mom(df, L=48, W=120, mode="ls"):
H = rolling_hurst(df["close"].values.astype(float), W)
return np.where(H > 0.5, tsmom(df, L, mode), 0.0)
def leadlag_df(target_df, other_df, L):
"""Costruisce un df col close del TARGET e la posizione = segno del rendimento a L barre
dell'ALTRO asset (allineato per timestamp). Ritorna (df_merged, pos)."""
a = target_df[["timestamp", "open", "high", "low", "close"]]
b = other_df[["timestamp", "close"]].rename(columns={"close": "other"})
m = a.merge(b, on="timestamp", how="inner").reset_index(drop=True)
o = pd.Series(m["other"].values.astype(float))
pos = np.sign(np.nan_to_num((o / o.shift(L) - 1).values))
return m, pos
# --------------------------------- reporting ---------------------------------
ROWS = []
def summarize(family, asset, tf, df, pos, mc_n=300):
full = backtest(df, pos, tf)
oos = backtest(df, pos, tf, lo=VAL_START, hi=HOLDOUT_START)
bh = buy_hold(df, tf)
gross = backtest(df, pos, tf, fee_rt=0.0).sharpe
_, p, _, _ = mc_pvalue(df, pos, tf, n=mc_n)
beats_bh = full.sharpe > bh.sharpe and oos.sharpe > 0
real = (full.sharpe > 0 and oos.sharpe > 0 and not np.isnan(p) and p < 0.05)
verdict = "★EDGE?" if (real and beats_bh) else ("real?" if real else "rumore")
ROWS.append(dict(fam=family, asset=asset, tf=tf, full=full.sharpe, oos=oos.sharpe,
gross=gross, bh=bh.sharpe, p=p, trd=full.ntrades, verdict=verdict))
print(f" {family:<16s} {asset} {tf:<3s} | FULL {full.sharpe:>5.2f} OOS {oos.sharpe:>5.2f} "
f"gross {gross:>5.2f} | B&H {bh.sharpe:>4.2f} | p {p:>.3f} | trd/y {full.ntrades:>6.0f} | {verdict}")
def scan_best(family, asset, tf, df, fn, grid, label_fn):
"""Scansiona la griglia (FULL Sharpe), stampa la riga compatta, ritorna la pos migliore."""
best = None
line = []
for params in grid:
pos = fn(df, *params)
s = backtest(df, pos, tf).sharpe
line.append(f"{label_fn(params)}={s:>4.1f}")
if best is None or s > best[0]:
best = (s, params, pos)
print(f" {asset} {tf} grid: " + " ".join(line))
return best[2], best[1]
def main():
print("=" * 100)
print(" FASE 2 — esplorazione famiglie BTC/ETH | netto fee 0.10% RT | barra = buy&hold | hold-out bloccato")
print("=" * 100)
D1 = {a: load_data(a, "1h") for a in ("BTC", "ETH")}
D15 = {a: load_data(a, "15m") for a in ("BTC", "ETH")}
def block(title):
print("\n" + "#" * 100 + f"\n {title}\n" + "#" * 100)
# ---- TSMOM (momentum) 1h + 15m, L/S e long-only ----
block("TSMOM (momentum)")
Ls = [(12,), (24,), (48,), (96,), (192,)]
for a in ("BTC", "ETH"):
pos, p = scan_best("TSMOM-LS", a, "1h", D1[a], lambda d, L: tsmom(d, L, "ls"), Ls, lambda x: f"L{x[0]}")
summarize("TSMOM-LS", a, "1h", D1[a], pos)
pos, p = scan_best("TSMOM-LO", a, "1h", D1[a], lambda d, L: tsmom(d, L, "lo"), Ls, lambda x: f"L{x[0]}")
summarize("TSMOM-LO", a, "1h", D1[a], pos)
pos, p = scan_best("TSMOM-LS", a, "15m", D15[a], lambda d, L: tsmom(d, L, "ls"), [(48,),(96,),(192,),(384,)], lambda x: f"L{x[0]}")
summarize("TSMOM-LS", a, "15m", D15[a], pos)
# ---- REVERSAL 1h + 15m ----
block("REVERSAL (mean-reversion breve)")
Lr = [(1,), (3,), (6,), (12,), (24,)]
for a in ("BTC", "ETH"):
pos, p = scan_best("REV-LS", a, "1h", D1[a], lambda d, L: reversal(d, L, "ls"), Lr, lambda x: f"L{x[0]}")
summarize("REV-LS", a, "1h", D1[a], pos)
pos, p = scan_best("REV-LS", a, "15m", D15[a], lambda d, L: reversal(d, L, "ls"), Lr, lambda x: f"L{x[0]}")
summarize("REV-LS", a, "15m", D15[a], pos)
# ---- MA cross ----
block("MA-CROSS (trend)")
g = [(12, 48), (24, 96), (48, 192), (24, 200)]
for a in ("BTC", "ETH"):
pos, p = scan_best("MAX-LS", a, "1h", D1[a], lambda d, f, s: ma_cross(d, f, s, "ls"), g, lambda x: f"{x[0]}/{x[1]}")
summarize("MAX-LS", a, "1h", D1[a], pos)
pos, p = scan_best("MAX-LO", a, "1h", D1[a], lambda d, f, s: ma_cross(d, f, s, "lo"), g, lambda x: f"{x[0]}/{x[1]}")
summarize("MAX-LO", a, "1h", D1[a], pos)
# ---- Donchian breakout ----
block("DONCHIAN breakout")
Ld = [(24,), (48,), (96,), (192,)]
for a in ("BTC", "ETH"):
pos, p = scan_best("DONCH-LS", a, "1h", D1[a], lambda d, L: donchian(d, L, "ls"), Ld, lambda x: f"L{x[0]}")
summarize("DONCH-LS", a, "1h", D1[a], pos)
pos, p = scan_best("DONCH-LO", a, "1h", D1[a], lambda d, L: donchian(d, L, "lo"), Ld, lambda x: f"L{x[0]}")
summarize("DONCH-LO", a, "1h", D1[a], pos)
# ---- Vol-target overlay (vs buy&hold) ----
block("VOL-TARGET overlay (sempre-long scalato) — riduce la vol/DD del buy&hold?")
for a in ("BTC", "ETH"):
pos, p = scan_best("VOLTGT", a, "1h", D1[a], lambda d, t: vol_target(d, "1h", t, 72),
[(0.4,), (0.6,), (0.8,), (1.0,)], lambda x: f"t{x[0]}")
summarize("VOLTGT", a, "1h", D1[a], pos)
# ---- Hurst-gated momentum ----
block("HURST-gated momentum (momentum solo in regime trending H>0.5)")
for a in ("BTC", "ETH"):
pos, p = scan_best("HURST-MOM", a, "1h", D1[a], lambda d, L: hurst_mom(d, L, 120, "ls"),
[(24,), (48,), (96,)], lambda x: f"L{x[0]}")
summarize("HURST-MOM", a, "1h", D1[a], pos)
# ---- Lead-lag BTC<->ETH ----
block("LEAD-LAG BTC<->ETH (posiziona un asset col rendimento passato dell'altro)")
for tgt, oth in (("ETH", "BTC"), ("BTC", "ETH")):
Ll = [1, 3, 6, 12, 24]
best = None; line = []
for L in Ll:
m, pos = leadlag_df(D1[tgt], D1[oth], L)
s = backtest(m, pos, "1h").sharpe
line.append(f"L{L}={s:>4.1f}")
if best is None or s > best[0]:
best = (s, L, m, pos)
print(f" {oth}->{tgt} 1h grid: " + " ".join(line))
_, L, m, pos = best
summarize(f"LL {oth}>{tgt}", tgt, "1h", m, pos)
# ---- classifica finale ----
print("\n" + "=" * 100)
print(" CLASSIFICA — net-fee FULL Sharpe (★EDGE? = batte B&H, OOS>0 e null p<0.05)")
print("=" * 100)
for r in sorted(ROWS, key=lambda r: -r["full"]):
print(f" {r['fam']:<16s} {r['asset']} {r['tf']:<3s} | FULL {r['full']:>5.2f} | OOS {r['oos']:>5.2f} | "
f"B&H {r['bh']:>4.2f} | p {r['p']:>.3f} | {r['verdict']}")
edges = [r for r in ROWS if r["verdict"] == "★EDGE?"]
print(f"\n Candidati che battono il buy&hold net-fee + OOS>0 + null p<0.05: {len(edges)}")
for r in edges:
print(f" -> {r['fam']} {r['asset']} {r['tf']}: FULL {r['full']:.2f} OOS {r['oos']:.2f} p {r['p']:.3f}")
if __name__ == "__main__":
main()