Files
PythagorasGoal/scripts/research/crypto_lead_harness.py
Adriano Dal Pastro d916520f2c research(cross-market): sweep 65-agenti crypto->mercati IB -> fenomeno gap robustissimo ma NON edge
Goal: >=50 agenti, migliore soluzione, diversi mercati e timing, su piu' anni.
Setup: 26 ETF certificati (IB) + BTC/ETH 1h; harness parametrizzato (lead overnight crypto ->
gap/intraday equity, t-incrementale, Sharpe IS/OOS, hit per-anno); workflow 416 config = 52 sweep +
12 verifica avversariale + 1 sintesi = 65 agenti.

RISULTATO: cluster fortissimo crypto-overnight -> GAP apertura equity (tutti i target risk-on).
Migliori: ETH->IWM/QQQ/XLK gap (Sh OOS 2.4-2.5, t 17), BTC->QQQ gap (Sh OOS 2.31, t 15, 9/9 ANNI).
Regge stress 10bps e OOS recente. MA due killer (verificatori concordi):
  1. NON tradabile via ETF (gap gia' all'open) -> serve future overnight (MNQ/MES), fuori dal
     capitale $0.5-2k (margin/liquidazione);
  2. e' RISK-BETA non alpha: finestra-lead ~contemporanea al gap (stesso shock macro), forza solo
     negli anni alta-vol (2022), beta implicito ~37%.
Unico ETF-tradabile (ETH->XLE intraday) crolla a 10bps (0.48->0.15), t 2.38 sotto Bonferroni/416.

VERDETTO: nessun edge proprietario deployabile a basso capitale. Migliore FENOMENO da forward-monitor
= BTC->QQQ gap overnight (9/9 anni). Coerente col soffitto del progetto. Valore: aver classificato il
fenomeno (risk-beta overnight) invece di scambiarlo per alpha.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:32:28 +00:00

127 lines
5.6 KiB
Python

"""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()