"""HARNESS parametrizzato — anticipazione crypto -> mercato (lead-lag eseguibile, onesto). Generalizza l'effetto weekend: la finestra-LEAD e' l'intervallo in cui l'equity e' CHIUSO e la crypto no (prev close 21:00 UTC -> next open 13:30 UTC). Il weekend e' il caso lungo (Ven 21:00 -> Lun 13:30). Per ogni sessione equity D (con sessione precedente P): lead = crypto return su [lead_start, D 13:00] (lead_start = P 21:00 se hours='overnight', else D13:00-hours) predict target: gap = open[D]/close[P]-1 ; intraday = close[D]/open[D]-1 ; full = close[D]/close[P]-1 control = rendimento sessione precedente equity (close[P]/close[P_prev]-1) -> test INCREMENTALE Filtro giorni: all | mon (solo lunedi'/weekend) | nonmon. Output JSON per config: n, corr, beta+t-stat del lead AL NETTO del control (incrementale), Sharpe settimanale/annualizzato del trade eseguibile (sign(lead)*predict - costo) FULL/IS/OOS(2022+), hit-rate, e PER-ANNO (hit e mean) per la robustezza multi-anno. uv run python scripts/research/crypto_lead_harness.py --configs '[{"lead":"BTC","target":"QQQ","day":"mon","predict":"intraday","hours":"overnight"}]' Dati: cache su disco (crypto 1h, ETF eq_*). Nessun IB online. Vettoriale, veloce. """ import sys, json, argparse from pathlib import Path import numpy as np, pandas as pd ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research")) from src.data.downloader import load_data import eqlib OOS_DEFAULT = "2022-01-01" OPEN_H = 13 # ~apertura US 13:30 UTC -> uso barra 13:00 (info nota prima dell'open per il lead) CLOSE_H = 21 # ~chiusura US 21:00 UTC _CRYPTO = {} def crypto_hourly(asset): if asset not in _CRYPTO: s = load_data(asset, "1h").set_index("datetime")["close"].astype(float) full = pd.date_range(s.index[0].floor("h"), s.index[-1].ceil("h"), freq="h", tz="UTC") _CRYPTO[asset] = s.reindex(s.index.union(full)).ffill().reindex(full) return _CRYPTO[asset] def at(series, ts): try: return float(series.asof(ts)) except Exception: return np.nan def evaluate(cfg, cost_rt=0.0004, oos=OOS_DEFAULT): OOS = pd.Timestamp(oos, tz="UTC") lead = cfg["lead"]; tgt = cfg["target"]; day = cfg.get("day", "all") predict = cfg.get("predict", "intraday"); hours = cfg.get("hours", "overnight") bc = crypto_hourly(lead) try: oc = eqlib.load_eq(tgt)["open"].astype(float); cc = eqlib.load_eq(tgt)["close"].astype(float) except Exception as e: return {**cfg, "err": f"no data {tgt}"} idx = cc.index rows = [] for j in range(2, len(idx)): D = idx[j]; P = idx[j-1]; Pp = idx[j-2] if day == "mon" and D.weekday() != 0: continue if day == "nonmon" and D.weekday() == 0: continue d_open = D.normalize() + pd.Timedelta(hours=OPEN_H) p_close = P.normalize() + pd.Timedelta(hours=CLOSE_H) lead_start = p_close if hours == "overnight" else d_open - pd.Timedelta(hours=int(hours)) c1 = at(bc, d_open); c0 = at(bc, lead_start) if not (np.isfinite(c1) and np.isfinite(c0) and c0 > 0): continue ld = c1 / c0 - 1.0 gap = oc[D] / cc[P] - 1.0 intr = cc[D] / oc[D] - 1.0 full = cc[D] / cc[P] - 1.0 ctrl = cc[P] / cc[Pp] - 1.0 rows.append((D, ld, gap, intr, full, ctrl)) if len(rows) < 60: return {**cfg, "err": f"n={len(rows)}"} D_ = pd.DataFrame(rows, columns=["d", "lead", "gap", "intraday", "full", "ctrl"]).set_index("d") y = D_[predict].values; x = D_["lead"].values; ctrl = D_["ctrl"].values def z(a): sd = a.std(); return (a - a.mean()) / sd if sd > 0 else a * 0 corr = float(np.corrcoef(x, y)[0, 1]) # incrementale vs control (OLS standardizzato) X = np.column_stack([np.ones(len(y)), z(x), z(ctrl)]) beta, *_ = np.linalg.lstsq(X, z(y), rcond=None) resid = z(y) - X @ beta dof = max(len(y) - 3, 1) se = np.sqrt(np.sum(resid**2) / dof * np.diag(np.linalg.inv(X.T @ X))) t_inc = float(beta[1] / se[1]) if se[1] > 0 else 0.0 # trade eseguibile: long-short e long-flat su segno del lead, intraday/predict, net costi sign = np.sign(x) def sharpe(r): r = r[np.isfinite(r)] return float(np.mean(r) / np.std(r) * np.sqrt(52)) if len(r) > 5 and np.std(r) > 0 else 0.0 ls = sign * y - cost_rt lf = np.where(x > 0, y, 0.0) - np.where(x > 0, cost_rt, 0.0) yrs = D_.index.year.values def per_year(r): out = {} for yv in sorted(set(yrs)): m = yrs == yv if m.sum() >= 8: out[int(yv)] = round(float(np.mean(np.sign(x[m]) == np.sign(y[m]))), 2) return out is_m = D_.index < OOS; oos_m = D_.index >= OOS py = per_year(ls) return {**cfg, "n": len(D_), "corr": round(corr, 3), "t_incremental": round(t_inc, 2), "hit": round(float(np.mean(sign == np.sign(y))), 3), "sh_ls_full": round(sharpe(ls), 2), "sh_ls_is": round(sharpe(ls[is_m]), 2), "sh_ls_oos": round(sharpe(ls[oos_m]), 2), "sh_lf_full": round(sharpe(lf), 2), "sh_lf_oos": round(sharpe(lf[oos_m]), 2), "ann_ls_pct": round(float(np.nanmean(ls) * 52 * 100), 1), "years_pos": int(sum(1 for v in py.values() if v > 0.5)), "years_tot": len(py), "per_year_hit": py} def main(): ap = argparse.ArgumentParser() ap.add_argument("--configs", required=True) ap.add_argument("--cost", type=float, default=0.0004) ap.add_argument("--oos", default=OOS_DEFAULT) args = ap.parse_args() cfgs = json.loads(args.configs) print(json.dumps([evaluate(c, cost_rt=args.cost, oos=args.oos) for c in cfgs])) if __name__ == "__main__": main()