diff --git a/docs/diary/2026-06-23-crossmarket-beyond-sp500.md b/docs/diary/2026-06-23-crossmarket-beyond-sp500.md new file mode 100644 index 0000000..92834ff --- /dev/null +++ b/docs/diary/2026-06-23-crossmarket-beyond-sp500.md @@ -0,0 +1,38 @@ +# 2026-06-23 — Cross-market crypto-lead OLTRE l'SP500: bond, commodity, indici esteri -> niente + +## Obiettivo +Estendere il test "crypto anticipa il mercato?" oltre SP500/azionario USA: commodity, bond, indici +ESTERI (Europa/Asia, fasi orarie diverse = il caso a priori piu' favorevole a un lead vero). + +## Dati (IB, orari, cache fut_*_1h) +ES/NQ/RTY (gia'); + ZN (T-note 10y), ESTX50 (Euro Stoxx50), DAX, NKD (Nikkei). Storia ~2-2.4y (2024+). +Commodity GC/CL/HG: VUOTE (market-data subscription COMEX/NYMEX mancante sul paper) -> non testate. + +## Test (`fut_leadlag_generic.py`): crypto[T-8h->T] -> future[T->T+6h], non-sovrapposto, controllo=moto proprio future +- ES/NQ/RTY: nessun edge (gia' noto). +- ZN (bond): NEGATIVO (0/3 anni). +- NKD (Nikkei): debole (t_crypto 0.2, Sharpe 0.66 ~ overnight drift, non crypto). +- **ESTX50 / DAX: forte all'apparenza** — BTC->T0h: t_crypto ~7.8, Sharpe 2.5, ann ~22%, 3/3 anni. + +## Ma e' un ARTEFATTO DI CONFINE UTC (deep-dive `eu_overnight_deepdive.py`) +- **Picco a coltello a T=00:00 UTC**: t/Sharpe salgono T20->T0 (2.5->7.8 / 0.24->2.45) e CROLLANO a + T=1h (t 1.3, Sharpe -0.09). Un lead vero non e' a coltello su una sola ora. +- **GAP test**: inserendo 1h tra fine-segnale (00:00) e inizio-cattura, l'effetto MUORE + (Sharpe 2.45 -> -0.52, t 7.8 -> 1.6). +- **Singola ora**: T=0h/H=1h (cattura 00:00->01:00) Sharpe +2.93 (t 8.7); T=1h/H=1h (01:00->02:00) + Sharpe -1.02. L'INTERO "edge" e' la barra di confine 00:00->01:00. +- vs SEMPRE-LONG: always-long overnight e' negativo (-0.8/-1.2), quindi non e' overnight-drift; ma + l'uplift del crypto e' tutto nella barra di confine. +-> Firma esatta di `day_boundary_robust` (CLAUDE.md): effetto che vive/muore spostando il confine del + giorno UTC di poche ore = etichettatura/contaminazione, NON anticipazione economica. + +## Verdetto +NIENTE di tradabile oltre l'SP500 nemmeno. Su TUTTI i mercati il legame crypto->X e' o co-movimento +contemporaneo (risk-beta) o artefatto di confine. L'anticipazione crypto->altri-mercati sfruttabile +NON esiste su dati onesti (finestre non-sovrapposte + boundary-robust + gap). Conferma definitiva del +soffitto del progetto, ora anche cross-mercato. + +## Cosa resta di valore (immutato) +La diversificazione TP01(crypto)+GTAA(equity), corr 0.21 -> Sharpe portafoglio ~1.5, DD dimezzato. +Quello e' strutturale e deployabile; l'anticipazione cross-mercato no. +Script: fetch_ib_futures.py (multi-exchange), fut_leadlag_generic.py, eu_overnight_deepdive.py. diff --git a/scripts/research/eu_overnight_deepdive.py b/scripts/research/eu_overnight_deepdive.py new file mode 100644 index 0000000..af487ec --- /dev/null +++ b/scripts/research/eu_overnight_deepdive.py @@ -0,0 +1,86 @@ +"""DEEP-DIVE: crypto[sessione USA] -> indice EUROPEO[overnight successivo]. Reale o artefatto? + +Lo sweep ha trovato un forte segnale: crypto[T-8h->T] (T~00:00 UTC, fine sessione USA) predice il +future europeo (ESTX50/DAX)[T->T+6h] con t_crypto incrementale ~8, Sharpe 2.5, 3/3 anni. Verifiche: + (1) ROBUSTEZZA su T (ora entrata): un effetto vero non e' a coltello su una sola ora. + (2) vs SEMPRE-LONG: l'overnight ha un drift? il crypto AGGIUNGE oltre il long incondizionato? + (3) GAP 1-2h tra fine-segnale e inizio-cattura: se sopravvive, niente contaminazione di bordo. + (4) vs FUTURE-OWN: il crypto batte il momentum proprio del future (gia' nel t incrementale). +Dati: cache (fut europei + crypto 1h). sqrt(252), net 2bps. +""" +import sys +from pathlib import Path +import numpy as np, pandas as pd + +ROOT = Path(__file__).resolve().parents[2]; RAW = ROOT / "data" / "raw" +sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research")) +from crypto_lead_harness import crypto_hourly, at +COST = 0.0002 + + +def fut_hourly(sym): + d = pd.read_parquet(RAW / f"fut_{sym.lower()}_1h.parquet") + return pd.Series(d["close"].astype(float).values, index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)).sort_index() + + +def _sh(r): + r = np.asarray(r, float); r = r[np.isfinite(r)] + return float(np.mean(r) / np.std(r) * np.sqrt(252)) if len(r) > 5 and np.std(r) > 0 else 0.0 + + +def series(fut, bc, T, S=8, H=6, gap=0): + days = pd.date_range(fut.index[0].normalize(), fut.index[-1].normalize(), freq="D", tz="UTC") + rows = [] + for D in days: + te = D + pd.Timedelta(hours=T) + f0, f1 = at(fut, te - pd.Timedelta(hours=S)), at(fut, te) + fcs, fce = at(fut, te + pd.Timedelta(hours=gap)), at(fut, te + pd.Timedelta(hours=gap + H)) + c0, c1 = at(bc, te - pd.Timedelta(hours=S)), at(bc, te) + if not all(np.isfinite(v) and v > 0 for v in (f0, f1, fcs, fce, c0, c1)): + continue + rows.append((D, c1/c0 - 1, f1/f0 - 1, fce/fcs - 1)) + Dd = pd.DataFrame(rows, columns=["d", "csig", "fctrl", "cap"]).set_index("d") + return Dd[Dd["cap"].abs() > 1e-9] + + +def stats(Dd): + x, ctrl, y = Dd["csig"].values, Dd["fctrl"].values, Dd["cap"].values + def z(a): + s = a.std(); return (a - a.mean()) / s if s > 0 else a * 0 + 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_c = float(beta[1]/se[1]) if se[1] > 0 else 0.0 + crypto = np.sign(x)*y - COST + futown = np.sign(ctrl)*y - COST + always = y - COST + yrs = Dd.index.year.values + py = {int(v): round(_sh(crypto[yrs==v]),2) for v in sorted(set(yrs)) if (yrs==v).sum()>=30} + return dict(n=len(Dd), t_crypto=round(t_c,2), sh_crypto=round(_sh(crypto),2), + sh_futown=round(_sh(futown),2), sh_always=round(_sh(always),2), + ann_crypto=round(float(np.nanmean(crypto)*252*100),1), per_year=py) + + +def main(): + print("="*96); print(" DEEP-DIVE crypto -> indice EUROPEO overnight (reale o artefatto?)"); print("="*96) + bcs = {l: crypto_hourly(l) for l in ("BTC","ETH")} + for sym in ("ESTX50","DAX"): + fut = fut_hourly(sym) + print(f"\n ===== {sym} =====") + print(f" (1) ROBUSTEZZA su T (lead=BTC, S=8h H=6h gap=0): t_crypto | Sh crypto vs futown vs always-long") + for T in (20,21,22,23,0,1,2,3,4): + s = stats(series(fut, bcs["BTC"], T)) + print(f" T={T:>2}h: n={s['n']} t {s['t_crypto']:+.1f} | crypto {s['sh_crypto']:+.2f} futown {s['sh_futown']:+.2f} always {s['sh_always']:+.2f} ann {s['ann_crypto']:+.1f}% {s['per_year']}") + print(f" (3) GAP test (T=0h, cattura inizia +1h e +2h dopo il segnale):") + for g in (0,1,2): + s = stats(series(fut, bcs["BTC"], 0, gap=g)) + print(f" gap={g}h: t {s['t_crypto']:+.1f} | crypto {s['sh_crypto']:+.2f} always {s['sh_always']:+.2f}") + print(f" (lead ETH, T=0h): ", end="") + s = stats(series(fut, bcs["ETH"], 0)); print(f"t {s['t_crypto']:+.1f} crypto {s['sh_crypto']:+.2f} always {s['sh_always']:+.2f} {s['per_year']}") + print("\n LETTURA: se 'crypto' >> 'always' E >> 'futown' E regge su T e col gap -> lead REALE (crypto") + print(" anticipa il catch-up europeo). Se crypto ~ always -> e' solo overnight-drift europeo.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/fetch_ib_futures.py b/scripts/research/fetch_ib_futures.py index bc65be0..0c192bf 100644 --- a/scripts/research/fetch_ib_futures.py +++ b/scripts/research/fetch_ib_futures.py @@ -14,8 +14,11 @@ import numpy as np, pandas as pd ROOT = Path(__file__).resolve().parents[2] RAW = ROOT / "data" / "raw" -SYMS = ["ES", "NQ", "RTY"] -N_CHUNKS = 5 # ~5 anni se disponibili +# sym -> exchange (indici US + esteri + commodity + bond per la ricerca cross-mercato oltre SP500) +SYMS = {"ES": "CME", "NQ": "CME", "RTY": "CME", + "GC": "COMEX", "CL": "NYMEX", "HG": "COMEX", "ZN": "CBOT", + "ESTX50": "EUREX", "DAX": "EUREX", "NKD": "CME"} +N_CHUNKS = 5 # (non usato: ContFuture non accetta endDateTime -> chiamata singola) def main(): @@ -26,11 +29,11 @@ def main(): except Exception as e: print(f"[CONNESSIONE FALLITA] {repr(e)[:100]}"); sys.exit(1) print(f" acct {ib.managedAccounts()} | fetch futures orari -> data/raw/fut_*") - for sym in SYMS: + for sym, exc in SYMS.items(): out = RAW / f"fut_{sym.lower()}_1h.parquet" if out.exists(): print(f" {sym}: gia' su disco -> skip"); continue - cf = ContFuture(sym, exchange="CME") + cf = ContFuture(sym, exchange=exc) try: ib.qualifyContracts(cf) except Exception as e: diff --git a/scripts/research/fut_leadlag_generic.py b/scripts/research/fut_leadlag_generic.py new file mode 100644 index 0000000..70a7793 --- /dev/null +++ b/scripts/research/fut_leadlag_generic.py @@ -0,0 +1,108 @@ +"""LEAD-LAG GENERICO non-sovrapposto: crypto[T-S -> T] predice future[T -> T+H]? (ogni mercato/fuso). + +Market-agnostic. Per ogni giorno, entrata a ora T (UTC): segnale = crypto nella finestra [T-S, T] +(finisce all'entrata), cattura = future nella finestra SUCCESSIVA [T, T+H] (non sovrapposta). +Controllo = moto PROPRIO del future [T-S, T] -> isola se il crypto AGGIUNGE (anticipa) oltre il +momentum del future. Sweep su T (copre apertura Europa ~07h, USA ~13h, Asia ~00h) e H. + +TRADE: sign(crypto[T-S,T]) * future[T,T+H] - costo. Sharpe sqrt(252), per-anno, OOS 2024+. +Dati: data/raw/fut_*_1h.parquet (UTC) + crypto 1h. Solo cache, nessun IB. +""" +import sys, glob +from pathlib import Path +import numpy as np, pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +RAW = ROOT / "data" / "raw" +sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research")) +from crypto_lead_harness import crypto_hourly, at + +OOS = pd.Timestamp("2024-01-01", tz="UTC"); COST = 0.0002 + + +def fut_hourly(sym): + d = pd.read_parquet(RAW / f"fut_{sym.lower()}_1h.parquet") + return pd.Series(d["close"].astype(float).values, index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)).sort_index() + + +def _sh(r): + r = np.asarray(r, float); r = r[np.isfinite(r)] + return float(np.mean(r) / np.std(r) * np.sqrt(252)) if len(r) > 5 and np.std(r) > 0 else 0.0 + + +def run(fut, bc, T, S, H): + days = pd.date_range(fut.index[0].normalize(), fut.index[-1].normalize(), freq="D", tz="UTC") + rows = [] + for D in days: + te = D + pd.Timedelta(hours=T) + ts = te - pd.Timedelta(hours=S) + tx = te + pd.Timedelta(hours=H) + f0, f1 = at(fut, ts), at(fut, te); f2 = at(fut, tx) + c0, c1 = at(bc, ts), at(bc, te) + if not all(np.isfinite(v) and v > 0 for v in (f0, f1, f2, c0, c1)): + continue + rows.append((D, c1/c0 - 1, f1/f0 - 1, f2/f1 - 1)) + if len(rows) < 120: + return None + Dd = pd.DataFrame(rows, columns=["d", "csig", "fctrl", "cap"]).set_index("d") + # filtra giorni in cui la cattura e' identicamente 0 (mercato chiuso, prezzo stale) + Dd = Dd[Dd["cap"].abs() > 1e-9] + if len(Dd) < 120: + return None + x, ctrl, y = Dd["csig"].values, Dd["fctrl"].values, Dd["cap"].values + + def z(a): + s = a.std(); return (a - a.mean()) / s if s > 0 else a * 0 + 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_c = float(beta[1] / se[1]) if se[1] > 0 else 0.0 + C = np.sign(x) * y - COST + m = Dd.index >= OOS + yrs = Dd.index.year.values + py = {int(v): round(_sh(C[yrs == v]), 2) for v in sorted(set(yrs)) if (yrs == v).sum() >= 30} + pos = sum(1 for v in py.values() if v > 0) + return dict(T=T, H=H, n=len(Dd), t_crypto=round(t_c, 2), sh_full=round(_sh(C), 2), + sh_oos=round(_sh(C[m]), 2), ann=round(float(np.nanmean(C) * 252 * 100), 1), + years_pos=pos, years_tot=len(py), per_year=py) + + +def main(): + syms = sorted(p.name[4:-11].upper() for p in RAW.glob("fut_*_1h.parquet")) + print("=" * 98) + print(f" LEAD-LAG GENERICO non-sovrapposto — futures: {syms}") + print("=" * 98) + print(" crypto[T-8h -> T] -> future[T -> T+6h], controllo=moto proprio future. net 2bps, OOS2024+\n") + bcs = {l: crypto_hourly(l) for l in ("BTC", "ETH")} + winners = [] + for sym in syms: + fut = fut_hourly(sym) + best = None + for lead in ("BTC", "ETH"): + for T in (0, 4, 8, 12, 16, 20): + r = run(fut, bcs[lead], T, 8, 6) + if not r: + continue + r["sym"] = sym; r["lead"] = lead + if best is None or r["sh_oos"] > best["sh_oos"]: + best = r + # raccogli i forti (crypto significativo E robusto) + if r["t_crypto"] >= 2.5 and r["sh_oos"] > 0.5 and r["years_pos"] == r["years_tot"]: + winners.append(r) + if best: + print(f" {sym:7} miglior: {best['lead']}->T{best['T']}h: t_crypto {best['t_crypto']:+.1f} " + f"Sh full {best['sh_full']:+.2f} OOS {best['sh_oos']:+.2f} ann {best['ann']:+.1f}% " + f"{best['years_pos']}/{best['years_tot']}y {best['per_year']}") + print("\n --- CANDIDATI FORTI (t_crypto>=2.5, OOS>0.5, tutti gli anni positivi) ---") + if not winners: + print(" NESSUNO. -> nessuna anticipazione crypto->future robusta oltre il rumore/beta.") + else: + winners.sort(key=lambda r: r["sh_oos"], reverse=True) + for w in winners[:10]: + print(f" {w['sym']:7} {w['lead']}->T{w['T']}h H{w['H']}: t_crypto {w['t_crypto']} " + f"Sh OOS {w['sh_oos']} full {w['sh_full']} ann {w['ann']}% {w['years_pos']}/{w['years_tot']}y {w['per_year']}") + + +if __name__ == "__main__": + main()