"""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 import pandas as pd from scripts.analysis.research_lab import backtest, buy_hold, mc_pvalue, load_tf, ts, _net_series, 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_tf(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), ) # breadth per-anno (pre-hold-out): % anni positivi, anni rossi consecutivi net, _, _, _ = _net_series(df, pos) s = pd.Series(net, index=ts(df)) s = s[s.index < pd.Timestamp(HOLDOUT_START, tz="UTC")] yr = {int(y): float((1 + g).prod() - 1) for y, g in s.groupby(s.index.year)} vals = list(yr.values()) max_consec_red = 0; cur = 0 for v in vals: cur = cur + 1 if v < 0 else 0 max_consec_red = max(max_consec_red, cur) res["per_year_preho"] = {y: round(v, 3) for y, v in yr.items()} res["pct_years_pos"] = round(sum(v > 0 for v in vals) / len(vals), 2) if vals else 0.0 res["max_consec_red_years"] = int(max_consec_red) 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()