research(cross-market): "monitor Deribit/trade IB" gap = LOOK-AHEAD, edge tradabile ~0
L'idea (segnale crypto overnight -> trade indice IB) sembrava Sharpe 3.6-5.9 ma e' look-ahead: la finestra segnale crypto [P21:00->D13:00] e il gap equity [Pclose->Dopen] coprono le stesse ore. All'entrata (D13:00, pre-open) il gap e' gia' avvenuto -> non catturabile con l'ETF. Decomposizione (net 2bps, sqrt252): OVERLAP gap Sh ~3.6-4.0 (artefatto) vs TRADABILE intraday Sh -0.03..0.25 (reale, ~0, muore a costi). Conferma/rafforza "non deployabile" del workflow. Resta possibile solo la versione futures mid-overnight (finestre non sovrapposte) -> serve dato intraday ES/NQ, non in cache. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
# 2026-06-23 — "Monitor Deribit / trade IB": il gap crypto->equity e' LOOK-AHEAD
|
||||||
|
|
||||||
|
## Idea testata (utente)
|
||||||
|
Guardare crypto live su Deribit (24/7) e tradare l'indice su IB sul segnale del gap overnight.
|
||||||
|
|
||||||
|
## Trappola trovata
|
||||||
|
Il segnale crypto [P 21:00 -> D 13:00 UTC] e il "gap" equity [P close -> D open 13:30] coprono QUASI
|
||||||
|
LE STESSE ORE. Condizionare il gap sul crypto-overnight = correlare due ritorni dello STESSO intervallo
|
||||||
|
notturno -> look-ahead. All'entrata (D 13:00, pre-open) il gap e' GIA' avvenuto: non catturabile.
|
||||||
|
|
||||||
|
## Prova (net 2bps, sqrt(252))
|
||||||
|
| target | OVERLAP gap (look-ahead) | TRADABILE intraday (post-entrata) |
|
||||||
|
|---|---|---|
|
||||||
|
| SPY | Sharpe 3.60 (OOS 5.23) | -0.03 (OOS 0.12) |
|
||||||
|
| QQQ | Sharpe 4.01 (OOS 5.47) | 0.25 (OOS 0.43) |
|
||||||
|
| IWM | Sharpe 3.98 (OOS 5.72) | 0.15 (OOS 0.44) |
|
||||||
|
|
||||||
|
Lo "Sharpe 5" e' artefatto. L'edge REALE tradabile via ETF (intraday, entri all'open) ~0, muore a costi.
|
||||||
|
NB: anche i Sharpe "gap" del workflow 65-agenti erano (a) look-ahead di overlap e (b) sotto-annualizzati
|
||||||
|
(sqrt(52) invece di sqrt(252)); il verdetto "non deployabile" resta, rafforzato.
|
||||||
|
|
||||||
|
## Cosa resta possibile (non testato, serve dato)
|
||||||
|
L'unica versione onesta dell'idea: entrare a META' notte via FUTURES IB e vedere se crypto [P21:00->T]
|
||||||
|
predice il future indice [T->open] su finestre NON sovrapposte (crypto come sensore di rischio piu'
|
||||||
|
veloce). Richiede dati INTRADAY dei futures (ES/NQ/RTY), non in cache -> data step se si vuole indagare.
|
||||||
|
|
||||||
|
## Lezione
|
||||||
|
Un risultato "troppo bello" (Sharpe 5) e' un test di disciplina: era overlap di finestre. Catturato.
|
||||||
|
Script: crypto_overnight_equity.py (versione artefatto), crypto_overnight_honest.py (decomposizione).
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""DERIBIT-monitor -> IB-trade: il segnale crypto overnight AGGIUNGE al long-overnight indice?
|
||||||
|
|
||||||
|
Idea utente: guardare crypto live su Deribit (24/7) e tradare l'indice su IB. Il GAP di apertura =
|
||||||
|
movimento OVERNIGHT dei futures (MES/MNQ/M2K, tradati di notte) -> catturabile, net ~2bps.
|
||||||
|
|
||||||
|
DOMANDA DECISIVA (test onesto): l'azionario ha gia' un OVERNIGHT PREMIUM noto (il drift positivo
|
||||||
|
notturno). Quindi "long indice overnight" rende di suo. Il segnale crypto MIGLIORA quel baseline,
|
||||||
|
o e' solo overnight-premium + beta? Confronto:
|
||||||
|
A) ALWAYS-LONG overnight (incondizionato) = cattura il premio notturno puro
|
||||||
|
B) LONG se crypto-overnight>0, else FLAT = usa il crypto come filtro
|
||||||
|
C) LONG/SHORT sul segno del crypto = il segnale pieno
|
||||||
|
La metrica chiave e' B,C VS A (l'uplift del crypto sul puro premio notturno), non A in assoluto.
|
||||||
|
|
||||||
|
Dati: cache su disco (crypto 1h Deribit; ETF eq_* = proxy del future indice). net 2bps RT (futures).
|
||||||
|
"""
|
||||||
|
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"))
|
||||||
|
import eqlib
|
||||||
|
from crypto_lead_harness import crypto_hourly, at, OPEN_H, CLOSE_H
|
||||||
|
|
||||||
|
OOS = pd.Timestamp("2022-01-01", tz="UTC")
|
||||||
|
COST = 0.0002 # ~2bps RT micro-future (commissione + spread)
|
||||||
|
|
||||||
|
|
||||||
|
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 build(target="SPY", lead="BTC"):
|
||||||
|
bc = crypto_hourly(lead)
|
||||||
|
oc = eqlib.load_eq(target)["open"].astype(float); cc = eqlib.load_eq(target)["close"].astype(float)
|
||||||
|
idx = cc.index; rows = []
|
||||||
|
for j in range(1, len(idx)):
|
||||||
|
D = idx[j]; P = idx[j-1]
|
||||||
|
d_open = D.normalize() + pd.Timedelta(hours=OPEN_H)
|
||||||
|
p_close = P.normalize() + pd.Timedelta(hours=CLOSE_H)
|
||||||
|
c1 = at(bc, d_open); c0 = at(bc, p_close)
|
||||||
|
if not (np.isfinite(c1) and np.isfinite(c0) and c0 > 0):
|
||||||
|
continue
|
||||||
|
crypto = c1 / c0 - 1.0
|
||||||
|
overnight = oc[D] / cc[P] - 1.0 # gap = ritorno overnight del future indice (catturabile)
|
||||||
|
rows.append((D, crypto, overnight))
|
||||||
|
return pd.DataFrame(rows, columns=["d", "crypto", "on"]).set_index("d")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 92)
|
||||||
|
print(" CRYPTO overnight -> LONG INDICE OVERNIGHT (Deribit-monitor / IB-trade): aggiunge al premio?")
|
||||||
|
print("=" * 92)
|
||||||
|
print(f" net {COST*1e4:.0f}bps RT (micro-future). overnight = open[D]/close[P]-1 (= move notturno future).\n")
|
||||||
|
for tgt in ("SPY", "QQQ", "IWM"):
|
||||||
|
for lead in ("BTC", "ETH"):
|
||||||
|
D = build(tgt, lead)
|
||||||
|
up = D[D["crypto"] > 0]["on"]; dn = D[D["crypto"] <= 0]["on"]
|
||||||
|
# strategie
|
||||||
|
A = D["on"].values - COST # always-long
|
||||||
|
B = np.where(D["crypto"] > 0, D["on"], 0.0) - np.where(D["crypto"] > 0, COST, 0.0) # long se crypto su
|
||||||
|
C = np.sign(D["crypto"]) * D["on"] - COST # long/short
|
||||||
|
shA, shB, shC = _sh(A), _sh(B), _sh(C)
|
||||||
|
# OOS
|
||||||
|
m = D.index >= OOS
|
||||||
|
print(f" {tgt} <- {lead}: n={len(D)} | notti crypto-SU {len(up)} ret medio {up.mean()*1e4:+.1f}bps "
|
||||||
|
f"| crypto-GIU {len(dn)} ret medio {dn.mean()*1e4:+.1f}bps (spread {(up.mean()-dn.mean())*1e4:+.1f}bps)")
|
||||||
|
print(f" Sharpe: A always-long {shA:.2f} | B long-if-cryptoUp {shB:.2f} | C long/short {shC:.2f} "
|
||||||
|
f"-> UPLIFT crypto B-A {shB-shA:+.2f}, C-A {shC-shA:+.2f}")
|
||||||
|
print(f" OOS22+: A {_sh(A[m]):.2f} | B {_sh(B[m]):.2f} | C {_sh(C[m]):.2f} | "
|
||||||
|
f"ann: A {np.nanmean(A)*252*100:+.1f}% B {np.nanmean(B)*252*100:+.1f}% C {np.nanmean(C)*252*100:+.1f}%")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# focus SPY-BTC: per-anno A vs C, e sketch deploy
|
||||||
|
D = build("SPY", "BTC")
|
||||||
|
A = D["on"].values - COST; C = np.sign(D["crypto"]) * D["on"] - COST
|
||||||
|
print(" --- SPY<-BTC per-anno: Sharpe A(always-long) vs C(crypto long/short) ---")
|
||||||
|
for y in sorted(set(D.index.year)):
|
||||||
|
mm = D.index.year == y
|
||||||
|
if mm.sum() >= 40:
|
||||||
|
print(f" {y}: A {_sh(A[mm]):+.2f} C {_sh(C[mm]):+.2f} (n={mm.sum()})")
|
||||||
|
print("\n NB: se C-A ~ 0, il crypto NON aggiunge al premio overnight (e' solo il premio + beta).")
|
||||||
|
print(" Se C-A >> 0 e A gia' alto, il crypto e' un filtro REALE sul rischio notturno.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"""ONESTA' DI TIMING: il 'Sharpe 5' del crypto->overnight equity e' look-ahead?
|
||||||
|
|
||||||
|
Test: separare la versione OVERLAP (segnale e ritorno coprono le stesse ore = look-ahead) dalla
|
||||||
|
versione TRADABILE (segnale finisce all'ENTRATA, catturo solo il moto SUCCESSIVO).
|
||||||
|
segnale crypto = BTC [P 21:00 -> D 13:00 UTC] (noto solo a D 13:00, poco prima dell'open 13:30)
|
||||||
|
- OVERLAP capture = gap = open[D]/close[P]-1 [P 21:00 -> D 13:30] <-- sovrappone il segnale
|
||||||
|
- TRADABILE capture = intra = close[D]/open[D]-1 [D 13:30 -> D 21:00] <-- DOPO l'entrata, pulito
|
||||||
|
Se OVERLAP >> TRADABILE, il numero alto e' artefatto: all'open il gap e' gia' avvenuto, non lo catturi.
|
||||||
|
Annualizzazione CORRETTA sqrt(252) (giornaliero), non sqrt(52).
|
||||||
|
"""
|
||||||
|
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"))
|
||||||
|
import eqlib
|
||||||
|
from crypto_lead_harness import crypto_hourly, at, OPEN_H, CLOSE_H
|
||||||
|
|
||||||
|
OOS = pd.Timestamp("2022-01-01", tz="UTC"); COST = 0.0002
|
||||||
|
|
||||||
|
|
||||||
|
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 build(target="SPY", lead="BTC"):
|
||||||
|
bc = crypto_hourly(lead)
|
||||||
|
oc = eqlib.load_eq(target)["open"].astype(float); cc = eqlib.load_eq(target)["close"].astype(float)
|
||||||
|
idx = cc.index; rows = []
|
||||||
|
for j in range(1, len(idx)):
|
||||||
|
D = idx[j]; P = idx[j-1]
|
||||||
|
c1 = at(bc, D.normalize() + pd.Timedelta(hours=OPEN_H)) # crypto a D 13:00 (entrata)
|
||||||
|
c0 = at(bc, P.normalize() + pd.Timedelta(hours=CLOSE_H)) # crypto a P 21:00
|
||||||
|
if not (np.isfinite(c1) and np.isfinite(c0) and c0 > 0):
|
||||||
|
continue
|
||||||
|
crypto = c1 / c0 - 1.0
|
||||||
|
gap = oc[D] / cc[P] - 1.0 # OVERLAP col segnale
|
||||||
|
intra = cc[D] / oc[D] - 1.0 # DOPO l'entrata (tradabile)
|
||||||
|
rows.append((D, crypto, gap, intra))
|
||||||
|
return pd.DataFrame(rows, columns=["d", "crypto", "gap", "intra"]).set_index("d")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 90)
|
||||||
|
print(" TIMING ONESTO: OVERLAP (look-ahead) vs TRADABILE — crypto overnight -> equity")
|
||||||
|
print("=" * 90)
|
||||||
|
print(" segnale noto a D13:00. OVERLAP=gap [copre il segnale]. TRADABILE=intraday [dopo entrata]. net 2bps, sqrt(252)\n")
|
||||||
|
for tgt in ("SPY", "QQQ", "IWM"):
|
||||||
|
D = build(tgt, "BTC")
|
||||||
|
for name, col in (("OVERLAP gap (look-ahead)", "gap"), ("TRADABILE intraday", "intra")):
|
||||||
|
C = np.sign(D["crypto"]) * D[col] - COST
|
||||||
|
m = D.index >= OOS
|
||||||
|
print(f" {tgt} {name:26}: Sharpe long/short {_sh(C):.2f} (OOS {_sh(C[m]):.2f}) ann {np.nanmean(C)*252*100:+.1f}%")
|
||||||
|
print()
|
||||||
|
print(" --- VERDETTO ---")
|
||||||
|
D = build("QQQ", "BTC")
|
||||||
|
cg = _sh(np.sign(D["crypto"]) * D["gap"] - COST)
|
||||||
|
ci = _sh(np.sign(D["crypto"]) * D["intra"] - COST)
|
||||||
|
print(f" QQQ: gap(overlap) Sharpe {cg:.2f} vs intraday(tradabile) Sharpe {ci:.2f}")
|
||||||
|
print(f" -> il moto che il crypto 'predice' avviene NELLA finestra del segnale (overnight), non dopo.")
|
||||||
|
print(f" All'entrata (D13:00, pre-open) il gap e' gia' realizzato -> NON catturabile con l'ETF.")
|
||||||
|
print(f" Cio' che resta da catturare (intraday) ha Sharpe ~{ci:.1f}: l'edge tradabile e' quello.")
|
||||||
|
print(f" Per catturare PARTE dell'overnight servirebbe entrare a META' notte via FUTURES IB e")
|
||||||
|
print(f" testare se il crypto [P21:00->T] predice il future [T->open] (finestre NON sovrapposte):")
|
||||||
|
print(f" richiede dati intraday dei futures indice (ES/NQ), che NON abbiamo in cache -> data step.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user