d2d535cf6a
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>
98 lines
5.1 KiB
Python
98 lines
5.1 KiB
Python
"""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()
|