research(cross-market): oltre SP500 (bond/commodity/indici esteri) -> niente, artefatto di confine UTC
Esteso il test crypto-lead a ZN(bond), ESTX50/DAX(Europa), NKD(Nikkei) via futures orari IB (commodity GC/CL/HG bloccate da subscription). Test non-sovrapposto crypto[T-8h->T]->future[T->T+6h]. ES/NQ/RTY niente (gia'); ZN negativo; NKD debole (~overnight drift). ESTX50/DAX SEMBRANO fortissimi (t_crypto 7.8, Sharpe 2.5, 3/3 anni) MA e' artefatto di confine UTC: picco a coltello a T=00:00, morto a T=1h; GAP di 1h uccide l'effetto (Sharpe 2.45->-0.52); tutto l'edge nella singola barra 00:00->01:00 (Sh +2.93) vs ora dopo (-1.02). Firma esatta di day_boundary_robust (CLAUDE.md). VERDETTO: nessuna anticipazione crypto->mercato sfruttabile, ne' SP500 ne' altro. Sempre co-movimento contemporaneo (risk-beta) o artefatto di confine. Resta valido solo il diversificatore TP01+GTAA. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||||
@@ -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()
|
||||||
@@ -14,8 +14,11 @@ import numpy as np, pandas as pd
|
|||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
RAW = ROOT / "data" / "raw"
|
RAW = ROOT / "data" / "raw"
|
||||||
SYMS = ["ES", "NQ", "RTY"]
|
# sym -> exchange (indici US + esteri + commodity + bond per la ricerca cross-mercato oltre SP500)
|
||||||
N_CHUNKS = 5 # ~5 anni se disponibili
|
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():
|
def main():
|
||||||
@@ -26,11 +29,11 @@ def main():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[CONNESSIONE FALLITA] {repr(e)[:100]}"); sys.exit(1)
|
print(f"[CONNESSIONE FALLITA] {repr(e)[:100]}"); sys.exit(1)
|
||||||
print(f" acct {ib.managedAccounts()} | fetch futures orari -> data/raw/fut_*")
|
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"
|
out = RAW / f"fut_{sym.lower()}_1h.parquet"
|
||||||
if out.exists():
|
if out.exists():
|
||||||
print(f" {sym}: gia' su disco -> skip"); continue
|
print(f" {sym}: gia' su disco -> skip"); continue
|
||||||
cf = ContFuture(sym, exchange="CME")
|
cf = ContFuture(sym, exchange=exc)
|
||||||
try:
|
try:
|
||||||
ib.qualifyContracts(cf)
|
ib.qualifyContracts(cf)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user