a5a61ac7e3
Espansione universo (su input utente "storico da cerbero"): il Cerbero MCP col token MAINNET serve Hyperliquid (230 perp REALI, storia nativa dal 2024). fetch_hyperliquid.py certifica 19 alt liquidi a 1d (flat 0%, cross-venue 4-9 bps vs Binance) -> data/raw/hl_*_1d.parquet. Abilita le strategie CROSS-SECTIONAL (impossibili a 2 asset). XS01 = cross-sectional momentum market-neutral (long 5 forti / short 5 deboli su ret 30g, ogni 10g, vol-target 20%). Validato onesto: plateau (config/k/subset), fee-robusto (0.3% RT), scorrelato a TP01 (-0.06), positivo OGNI anno 2024-26, meccanismo complementare (lavora nella dispersione quando TP01 e' in cash). Diverso dal regime-luck RV bocciato (19 asset, plateau, ogni anno+). Contributo al portafoglio (outer-join + pesi rinormalizzati per sleeve a date diverse): TP01-solo FULL 1.30 / HOLD 0.31 -> TP01 70% + XS01 30%: FULL 1.41 / HOLD 1.15, DD giu', ~ogni anno+. -> XS01 BATTE il portafoglio esistente: inserito in active_sleeves. Caveat (documentati): storia XS ~2.5 anni; STAT-MODE (book 19 gambe non eseguibile a 2k -> ~20k), sleeve diagnostico/forward-monitor. portfolio.combine ora outer-join+renorm. 12 test passano. Diario 2026-06-19-hyperliquid-xsec.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
110 lines
4.7 KiB
Python
110 lines
4.7 KiB
Python
"""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 <signal_file.py> <BTC|ETH> <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()
|