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) <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user