From d2d535cf6a5d3581ad5f6e42a506d4f1d9e70893 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Mon, 22 Jun 2026 21:55:01 +0000 Subject: [PATCH] research(cross-market): crypto x mercati IB -> trovata ANTICIPAZIONE weekend-crypto -> lunedi' equity Goal: cercare correlazioni/anticipazioni crypto<->IB. Dati cache (BTC/ETH Deribit 1h->1d; ETF eq_*). (1) Correlazione contemporanea: crypto = risk-on (BTC ~0.32 SPY/QQQ/IWM, 0.25 HYG, 0.13 GLD, ~0 TLT). (2) Lead-lag GIORNALIERO: NIENTE (picco k=0, rumore a |k|>=1) -> nessuno anticipa l'altro al daily. (3) EFFETTO WEEKEND (anticipazione pulita): crypto Sab+Dom (equity chiuso) anticipa il lunedi'. GAP lunedi' corr +0.22/0.24 (SPY/QQQ/IWM/HYG), hit 59-62%, si RAFFORZA OOS22+ (+0.30/0.36). Validazione avversariale: (A) INCREMENTALE vs venerdi': beta weekend-crypto significativo (QQQ gap t=+4.7, intr +2.9; SPY +4.4/+2.0; IWM +4.7/+2.7), friday_eq NON signif. -> info crypto-specifica, non momentum equity. (B) TRADABILE (entro Mon open, esco close, net 4bps): QQQ hit 60%, Sharpe 1.46 (OOS 1.33), long-flat OOS 1.91 ~+9%/yr; SPY/IWM piu' deboli ma OOS positivi. VERDETTO: prima anticipazione cross-mercato reale. Crypto = proxy 24/7 risk-sentiment; lunedi' equity recupera la direzione del weekend. Caveat: capacita' bassa (~52 lun/anno), tattico non cornerstone; gap catturabile via futures IB (MNQ domenica sera) da validare. Coerente su 3 ETF (no cherry-pick). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/diary/2026-06-22-crypto-macro-leadlag.md | 53 ++++++++ scripts/research/crypto_macro_leadlag.py | 120 ++++++++++++++++++ scripts/research/crypto_weekend_signal.py | 97 ++++++++++++++ 3 files changed, 270 insertions(+) create mode 100644 docs/diary/2026-06-22-crypto-macro-leadlag.md create mode 100644 scripts/research/crypto_macro_leadlag.py create mode 100644 scripts/research/crypto_weekend_signal.py diff --git a/docs/diary/2026-06-22-crypto-macro-leadlag.md b/docs/diary/2026-06-22-crypto-macro-leadlag.md new file mode 100644 index 0000000..c4385fd --- /dev/null +++ b/docs/diary/2026-06-22-crypto-macro-leadlag.md @@ -0,0 +1,53 @@ +# 2026-06-22 — Crypto × mercati IB: correlazioni e ANTICIPAZIONI (lead-lag) + +## Obiettivo +Cercare correlazioni e soprattutto ANTICIPAZIONI tra crypto e mercati IB: un mercato fa capire +l'andamento dell'altro? Dati: cache su disco (BTC/ETH Deribit 1h->1d UTC; ETF eq_* con OPEN). Nessun +IB online. Disciplina: attenzione ai tranelli di timing daily (crypto chiude 00:00 UTC, US equity +21:00 -> lag-0 contaminato), test del segno + OOS + multiple-testing. +Script: `crypto_macro_leadlag.py`, `crypto_weekend_signal.py`. + +## (1) Correlazione contemporanea +Crypto = asset RISK-ON: corr BTC/ETH ~ **+0.32/0.37** con SPY/QQQ/IWM, **+0.25/0.28** con HYG +(credito), **+0.13** GLD, **~-0.02** TLT (bond). Atteso. + +## (2) Lead-lag giornaliero: NIENTE +corr(BTC_{t-k}, ETF_t) ha picco a **k=0** (~0.32) e crolla a rumore (±0.05) per |k|>=1. Al daily +**nessuno anticipa l'altro** (ne' crypto->equity ne' viceversa). Honest negative. + +## (3) EFFETTO WEEKEND: anticipazione PULITA, significativa, OOS-robusta +La crypto si muove Sab+Dom (azionario chiuso) -> quel movimento e' info PRIOR al lunedi'. +- **Anticipa il GAP del lunedi'**: corr +0.22/+0.24 (SPY/QQQ/IWM/HYG), hit 59-62%, e **si RAFFORZA + OOS (2022+): +0.30/+0.36**. Coerente su 4 ETF (non cherry-pick). +- Intraday del lunedi' (open->close) piu' debole ma presente (corr 0.10-0.15, OOS 0.18-0.22). + +### Validazione avversariale +- **(A) INCREMENTALE vs venerdi'**: regressione `Mon ~ weekend_crypto + friday_eq`. Coeff weekend + crypto significativo ovunque (QQQ gap **t=+4.7**, intr t=+2.9; SPY +4.4/+2.0; IWM +4.7/+2.7); + friday_eq NON significativo. -> e' info CRYPTO-SPECIFICA del weekend, non momentum equity. +- **(B) TRADABILE** (osservo weekend crypto Dom 24:00, entro Monday OPEN, esco CLOSE, net 4bps): + | ETF | hit | Sharpe FULL / IS / OOS22+ | long-flat OOS | ann | + |---|---|---|---|---| + | QQQ | 60% | 1.46 / 1.61 / 1.33 | **1.91** | ~+9-11% | + | SPY | 60% | 0.96 / 0.91 / 1.01 | 1.70 | ~+5% | + | IWM | 56% | 0.89 / 0.73 / 1.04 | 1.07 | ~+6% | + +## Verdetto +**Trovata UNA anticipazione reale**: il weekend crypto anticipa il lunedi' azionario (massimo su QQQ, +risk-on/tech). Significativa (t>4 sul gap), incrementale al venerdi', tradabile net costi, **regge e +si rafforza OOS**, coerente su piu' ETF. Meccanismo economico sensato: crypto = proxy 24/7 del +risk-sentiment; nel weekend l'equity e' chiuso e lunedi' "recupera" la direzione crypto. + +### Caveat onesti +- **Capacita' bassa**: ~52 lunedi'/anno, intraday -> ~+9%/yr sul capitale impiegato il lunedi', non + una macchina da compounding. E' un segnale TATTICO, non un cornerstone. +- Il GAP (t=4.7) e' piu' forte dell'intraday (t=2.9) ma per catturarlo serve entrare PRIMA del Monday + open -> via **futures indice IB (MNQ/MES, aperti la domenica sera)**: enhancement eseguibile da + validare (cattura gap+sessione). +- Multiple-testing 3 ETF x 2 target: ma TUTTI significativi e coerenti -> effetto ampio, non fortuna. +- Niente IB online qui (cache); per il deploy servirebbe il feed crypto live la domenica sera. + +## Prossimo (se si procede) +Validare la variante FUTURES (MNQ domenica sera -> cattura il gap del lunedi') e il sizing a basso +capitale; eventualmente paper-trade. E' la prima ANTICIPAZIONE cross-mercato trovata: crypto come +lead di sentiment sul lunedi' equity. diff --git a/scripts/research/crypto_macro_leadlag.py b/scripts/research/crypto_macro_leadlag.py new file mode 100644 index 0000000..cef583e --- /dev/null +++ b/scripts/research/crypto_macro_leadlag.py @@ -0,0 +1,120 @@ +"""CRYPTO x MERCATI IB — correlazioni e ANTICIPAZIONI (lead-lag). + +Obiettivo: la crypto (24/7) anticipa i mercati IB (azioni/bond/oro/credito), o viceversa? +Disciplina onesta: i tranelli di timing daily sono enormi (crypto chiude 00:00 UTC, US equity 21:00 +UTC -> il lag-0 e' contaminato), quindi (1) allineo i rendimenti sullo STESSO intervallo +(compounding crypto sul grid giorni-di-borsa), (2) guardo i lag >=1 giorno, (3) test del segno con +hit-rate e split in-sample/OOS, (4) flag multiple-testing. + +Ipotesi piu' pulita = EFFETTO WEEKEND: la crypto si muove Sab+Dom (azionario chiuso) -> quel +movimento e' informazione PRIOR al lunedi'. Predice il gap/intraday del lunedi' azionario? +Uso gli OPEN dei parquet eq_ (Monday open noto alle 13:30 UTC, weekend crypto noto alle 00:00 UTC). + +DATI: cache su disco (BTC/ETH Deribit 1h->1d UTC; ETF IB eq_*). Nessun IB online. +""" +import sys +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 + +ETFS = ["SPY", "QQQ", "IWM", "TLT", "GLD", "HYG"] + + +def crypto_daily_close(asset="BTC") -> pd.Series: + df = load_data(asset, "1h").set_index("datetime")["close"].astype(float) + return df.resample("1D").last().dropna() # close ~00:00 UTC del giorno dopo + + +def _ret(s): + return s.pct_change() + + +def _corr_lags(x: pd.Series, y: pd.Series, lags=range(-5, 6)): + """corr(x_{t-k}, y_t): k>0 => x ANTICIPA y di k giorni. Allineati sullo stesso grid.""" + J = pd.concat({"x": x, "y": y}, axis=1, join="inner").dropna() + out = {} + for k in lags: + out[k] = round(float(J["x"].shift(k).corr(J["y"])), 3) + return out + + +def main(): + print("=" * 98) + print(" CRYPTO x MERCATI IB — correlazioni & anticipazioni (lead-lag)") + print("=" * 98) + btc = crypto_daily_close("BTC"); eth = crypto_daily_close("ETH") + btc_r = _ret(btc); eth_r = _ret(eth) + # equity close + grid giorni-di-borsa + eq_close = {s: eqlib.load_eq(s)["close"].astype(float) for s in ETFS} + eq_open = {s: eqlib.load_eq(s)["open"].astype(float) for s in ETFS} + grid = eq_close["SPY"].index + grid = grid[grid >= btc.index[0]] + # crypto compoundato sul grid giorni-di-borsa (stesso intervallo dell'equity ret) + def to_grid(s): + cum = (1 + _ret(s)).cumprod() + return (cum.reindex(cum.index.union(grid)).ffill().reindex(grid)).pct_change() + btc_g = to_grid(btc); eth_g = to_grid(eth) + + print(f" overlap dal {grid[0].date()} ({len(grid)} giorni di borsa)\n") + + print(" --- (1) CORRELAZIONE CONTEMPORANEA (stesso intervallo; lag0 contaminato da timing) ---") + print(f" {'ETF':5} {'corr BTC':>9} {'corr ETH':>9}") + for s in ETFS: + er = _ret(eq_close[s]).reindex(grid) + cb = round(float(pd.concat([btc_g, er], axis=1).dropna().corr().iloc[0, 1]), 3) + ce = round(float(pd.concat([eth_g, er], axis=1).dropna().corr().iloc[0, 1]), 3) + print(f" {s:5} {cb:>9} {ce:>9}") + + print("\n --- (2) LEAD-LAG BTC vs ETF: corr(BTC_{t-k}, ETF_t), k>0 = BTC ANTICIPA ---") + print(f" {'ETF':5} " + " ".join(f"k{ k:+d}" for k in range(-3, 4)) + " picco") + for s in ETFS: + er = _ret(eq_close[s]).reindex(grid) + cl = _corr_lags(btc_g, er, range(-3, 4)) + peak = max(cl, key=lambda k: abs(cl[k])) + row = " ".join(f"{cl[k]:+.2f}" for k in range(-3, 4)) + tag = f"k={peak:+d} ({'BTC->ETF' if peak>0 else 'ETF->BTC' if peak<0 else 'contemp'})" + print(f" {s:5} {row} {tag}") + + print("\n --- (3) EFFETTO WEEKEND: crypto Sab+Dom -> lunedi' azionario (anticipazione pulita) ---") + # weekend crypto = close(Dom 00:00 lun) / close(Ven) - 1 ; calcolato su crypto daily (calendario) + cal = pd.date_range(btc.index[0], btc.index[-1], freq="D", tz="UTC") + bc = btc.reindex(cal).ffill() + for s in ["SPY", "QQQ", "IWM", "HYG"]: + oc = eq_open[s]; cc = eq_close[s] + rows = [] + for mon in grid: + if mon.weekday() != 0: # solo lunedi' + continue + fri = mon - pd.Timedelta(days=3) + if fri not in cc.index: # venerdi' non di borsa (festa) -> salta + continue + wk = float(bc.get(mon, np.nan) / bc.get(fri + pd.Timedelta(days=0), np.nan) - 1) if fri in bc.index else np.nan + # weekend crypto: da venerdi 00:00(close ven) a lunedi 00:00 -> usa bc[fri]..bc[mon] + wk = float(bc.loc[mon] / bc.loc[fri] - 1) if (mon in bc.index and fri in bc.index) else np.nan + gap = float(oc.loc[mon] / cc.loc[fri] - 1) if (mon in oc.index and fri in cc.index) else np.nan + intr = float(cc.loc[mon] / oc.loc[mon] - 1) if mon in oc.index else np.nan + rows.append((mon, wk, gap, intr)) + D = pd.DataFrame(rows, columns=["mon", "wk", "gap", "intr"]).dropna().set_index("mon") + if len(D) < 50: + print(f" {s}: pochi dati ({len(D)})"); continue + def stat(col): + c = float(D["wk"].corr(D[col])) + hit = float((np.sign(D["wk"]) == np.sign(D[col])).mean()) + return c, hit + cg, hg = stat("gap"); ci, hi = stat("intr") + # OOS: split 2022 + Dh = D[D.index >= pd.Timestamp("2022-01-01", tz="UTC")] + cg_o = float(Dh["wk"].corr(Dh["gap"])); ci_o = float(Dh["wk"].corr(Dh["intr"])) + print(f" {s}: n={len(D)} | weekend-crypto -> Mon GAP corr {cg:+.2f} hit {hg*100:.0f}% (OOS22+ {cg_o:+.2f}) " + f"| Mon INTRADAY corr {ci:+.2f} hit {hi*100:.0f}% (OOS {ci_o:+.2f})") + + print("\n NB: lag-0/contemporanea contaminata dal timing (crypto chiude 00:00, equity 21:00 UTC).") + print(" Il GAP del lunedi' e' il test pulito (weekend crypto = info prior all'apertura).") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/crypto_weekend_signal.py b/scripts/research/crypto_weekend_signal.py new file mode 100644 index 0000000..d69f082 --- /dev/null +++ b/scripts/research/crypto_weekend_signal.py @@ -0,0 +1,97 @@ +"""WEEKEND CRYPTO -> LUNEDI' AZIONARIO — validazione avversariale dell'anticipazione. + +L'analisi lead-lag ha trovato UNA anticipazione pulita: il movimento crypto del weekend (Sab+Dom, +azionario chiuso) anticipa il lunedi' azionario (gap corr ~0.24, OOS piu' forte). Prima di crederci, +due test scettici: + (A) INCREMENTALE: aggiunge info OLTRE il rendimento del VENERDI'? (o e' solo momentum equity?) + regressione Mon ~ weekend_crypto + friday_equity ; il coeff del crypto resta significativo? + (B) TRADABILE: segnale eseguibile = osservo weekend crypto (noto Dom 24:00 UTC), entro al Monday + OPEN, esco al Monday CLOSE. Net di costi (4 bps RT ETF). Sharpe/hit/OOS vs sempre-long lunedi'. + +DATI: cache su disco (BTC Deribit 1h->1d; ETF eq_* con OPEN). Nessun IB online. +""" +import sys +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 = pd.Timestamp("2022-01-01", tz="UTC") +COST_RT = 0.0004 # 4 bps round-trip ETF (entry open + exit close) + + +def _sh_weekly(r): + r = np.asarray(pd.Series(r).dropna(), float) + return float(np.mean(r) / np.std(r) * np.sqrt(52)) if len(r) > 2 and np.std(r) > 0 else 0.0 + + +def build(asset_etf="QQQ"): + btc = load_data("BTC", "1h").set_index("datetime")["close"].astype(float).resample("1D").last() + cal = pd.date_range(btc.index[0], btc.index[-1], freq="D", tz="UTC") + bc = btc.reindex(cal).ffill() + oc = eqlib.load_eq(asset_etf)["open"].astype(float) + cc = eqlib.load_eq(asset_etf)["close"].astype(float) + rows = [] + for mon in cc.index: + if mon.weekday() != 0: + continue + fri = mon - pd.Timedelta(days=3) + thu = mon - pd.Timedelta(days=4) + if fri not in cc.index or fri not in bc.index or mon not in bc.index: + continue + wk = bc.loc[mon] / bc.loc[fri] - 1.0 # weekend crypto (Ven 00:00 -> Lun 00:00) + fri_eq = (cc.loc[fri] / cc.loc[thu] - 1.0) if thu in cc.index else np.nan # rendimento venerdi' + gap = oc.loc[mon] / cc.loc[fri] - 1.0 + intr = cc.loc[mon] / oc.loc[mon] - 1.0 # tradabile: open->close lunedi' + rows.append((mon, wk, fri_eq, gap, intr)) + return pd.DataFrame(rows, columns=["mon", "wk", "fri_eq", "gap", "intr"]).dropna().set_index("mon") + + +def main(): + print("=" * 92) + print(" WEEKEND CRYPTO -> LUNEDI' AZIONARIO — validazione avversariale") + print("=" * 92) + for etf in ("QQQ", "SPY", "IWM"): + D = build(etf) + print(f"\n ===== {etf} (n={len(D)} lunedi', {D.index[0].date()}..{D.index[-1].date()}) =====") + + # (A) INCREMENTALE vs venerdi' — regressione OLS standardizzata, t-stat su weekend_crypto + for tgt in ("gap", "intr"): + y = (D[tgt] - D[tgt].mean()) / D[tgt].std() + x1 = (D["wk"] - D["wk"].mean()) / D["wk"].std() + x2 = (D["fri_eq"] - D["fri_eq"].mean()) / D["fri_eq"].std() + X = np.column_stack([np.ones(len(D)), x1.values, x2.values]) + beta, *_ = np.linalg.lstsq(X, y.values, rcond=None) + resid = y.values - X @ beta + se = np.sqrt(np.sum(resid**2) / (len(D) - 3) * np.diag(np.linalg.inv(X.T @ X))) + t_wk = beta[1] / se[1]; t_fri = beta[2] / se[2] + partial = float(pd.Series(resid).corr(x1)) # ~ contributo crypto al netto del resto + print(f" [{tgt:4}] beta_weekendCrypto {beta[1]:+.3f} (t={t_wk:+.1f}) | " + f"beta_fridayEq {beta[2]:+.3f} (t={t_fri:+.1f}) -> crypto {'INCREMENTALE' if abs(t_wk)>2 else 'non signif.'}") + + # (B) TRADABILE: long lunedi' intraday se weekend crypto > 0, short se < 0 (net costi) + sig = np.sign(D["wk"].values) + gross = sig * D["intr"].values + net = gross - COST_RT + D2 = D.assign(net=net) + full = D2["net"]; oos = D2[D2.index >= OOS]["net"]; ins = D2[D2.index < OOS]["net"] + bh = D["intr"] # baseline: sempre-long lunedi' intraday + hit = float((np.sign(gross) > 0).mean()) if False else float((sig == np.sign(D["intr"].values)).mean()) + print(f" TRADE (long/short Mon intraday su segno weekend-crypto, net {COST_RT*1e4:.0f}bps):") + print(f" hit-rate segno {hit*100:.0f}% | Sharpe(sett.) FULL {_sh_weekly(full):.2f} IS {_sh_weekly(ins):.2f} OOS22+ {_sh_weekly(oos):.2f}") + print(f" ritorno medio/lun {full.mean()*1e4:+.1f}bps (net) | baseline sempre-long {bh.mean()*1e4:+.1f}bps | " + f"ann.~{full.mean()*52*100:+.1f}%") + # long-flat (piu' realistico: long se crypto su, altrimenti cash) + lf = np.where(D["wk"].values > 0, D["intr"].values, 0.0) - np.where(D["wk"].values > 0, COST_RT, 0.0) + lfs = pd.Series(lf, index=D.index) + print(f" variante LONG-FLAT (long se crypto su, else cash): Sharpe FULL {_sh_weekly(lfs):.2f} " + f"OOS {_sh_weekly(lfs[lfs.index>=OOS]):.2f} ann.~{lfs.mean()*52*100:+.1f}%") + + print("\n NB: ~52 lunedi'/anno -> Sharpe settimanale; OOS = 2022+. Multiple-testing: 3 ETF x 2 target.") + + +if __name__ == "__main__": + main()