"""EVALUATOR STANDARD per i segnali della ricerca multi-agente (Fase frattale, v2.0.0). Ogni agente scrive SOLO una funzione `signal(df, asset, tf) -> np.ndarray` (posizione per barra in [-1,1], decisa entro close[i]) in un file. Questo evaluator la valuta in modo UNIFORME e ONESTO sull'harness research_lab, e — cruciale — esegue un GUARD ANTI-LOOK-AHEAD automatico: ricalcola il segnale su prefissi del df e verifica che pos[i] non dipenda da barre future (leak>0 = sospetto). uv run python scripts/analysis/eval_signal.py <5m|15m|1h> [--holdout] Stampa una riga "RESULT_JSON:{...}" con tutte le metriche (gli agenti riportano quei campi esatti). """ from __future__ import annotations import sys import json import importlib.util from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) import numpy as np from src.data.downloader import load_data from scripts.analysis.research_lab import backtest, buy_hold, mc_pvalue, VAL_START, HOLDOUT_START def load_signal(path): spec = importlib.util.spec_from_file_location("usig", path) m = importlib.util.module_from_spec(spec) spec.loader.exec_module(m) if not hasattr(m, "signal"): raise AttributeError("il file non definisce signal(df, asset, tf)") return m.signal def causality_guard(signal, df, asset, tf, k=12): """Ricalcola il segnale su prefissi df[:i+1] e confronta pos[i] col run completo. Se differiscono -> il segnale usa dati FUTURI (look-ahead). Ritorna #violazioni (0 = pulito).""" full = np.asarray(signal(df, asset, tf), float) n = len(df) if len(full) != n: return -1 rng = np.random.default_rng(0) idx = rng.integers(int(n * 0.6), n - 1, size=k) bad = 0 for i in idx: try: p = np.asarray(signal(df.iloc[:i + 1].copy(), asset, tf), float) except Exception: bad += 1; continue if len(p) != i + 1 or not np.isclose(np.nan_to_num(p[i]), np.nan_to_num(full[i]), atol=1e-6): bad += 1 return bad def main(): args = sys.argv[1:] holdout = "--holdout" in args args = [a for a in args if a != "--holdout"] sigfile, asset, tf = args[0], args[1].upper(), args[2] res = {"asset": asset, "tf": tf, "sigfile": sigfile} try: signal = load_signal(sigfile) df = load_data(asset, tf) pos = np.asarray(signal(df, asset, tf), float) res["n"] = int(len(df)) res["len_ok"] = bool(len(pos) == len(df)) if not res["len_ok"]: res["error"] = f"len(pos)={len(pos)} != len(df)={len(df)}" print("RESULT_JSON:" + json.dumps(res)); return res["finite"] = bool(np.isfinite(np.nan_to_num(pos, nan=0.0)).all()) res["leak"] = int(causality_guard(signal, df, asset, tf)) full = backtest(df, pos, tf) oos = backtest(df, pos, tf, lo=VAL_START, hi=HOLDOUT_START) bh = buy_hold(df, tf) _, p, _, _ = mc_pvalue(df, pos, tf, n=250) res.update( implemented=True, full_sharpe=round(full.sharpe, 3), full_ret=round(full.ret, 3), full_dd=round(full.maxdd, 3), oos_sharpe=round(oos.sharpe, 3), bh_sharpe=round(bh.sharpe, 3), gross_sharpe=round(backtest(df, pos, tf, fee_rt=0.0).sharpe, 3), fee02_sharpe=round(backtest(df, pos, tf, fee_rt=0.002).sharpe, 3), turnover=round(full.ntrades, 1), exposure=round(full.exposure, 3), null_p=round(p, 4), beats_bh=bool(full.sharpe > bh.sharpe and oos.sharpe > 0), ) if holdout: ho = backtest(df, pos, tf, lo=HOLDOUT_START) res["holdout_sharpe"] = round(ho.sharpe, 3) res["holdout_ret"] = round(ho.ret, 3) res["holdout_dd"] = round(ho.maxdd, 3) except Exception as e: res["implemented"] = False res["error"] = f"{type(e).__name__}: {str(e)[:200]}" print("RESULT_JSON:" + json.dumps(res)) if __name__ == "__main__": main()