d916520f2c
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>
127 lines
5.6 KiB
Python
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()
|