Files
PythagorasGoal/scripts/research/r0822d_cc01_2022.py
T

1045 lines
56 KiB
Python

#!/usr/bin/env python3
"""
r0822d_cc01_2022.py — filone CC01-2022 (ondata 2026-08-22, terza tornata)
RIAPRIRE UN FILONE CHIUSO SULL'UNICA BASE CHE QUESTO PROGETTO ACCETTA: DATI NUOVI.
IL FILONE E LA SUA RAGIONE DI MORTE
CC01 (cash-and-carry delta-neutral, `scripts/research/cash_carry_hl.py`) fu chiuso il
2026-06-26 con un verdetto molto specifico: il premio di funding e' REALE (~+8-14%/anno,
positivo ogni anno, ortogonale a TP01), ma lo Sharpe modellato 11-13 e' un ARTEFATTO
perche' i rischi di coda sono *strutturalmente fuori dal dataset*:
"lo storico funding parte dal 2023-05 -> manca il 2022 (deleveraging, funding
negativo, basis blow-out)."
Quella e' una ragione FALSIFICABILE, non un giudizio: dice esattamente quale dato
ribalterebbe la conclusione. Fino a stamattina quel dato non c'era.
COSA E' CAMBIATO OGGI
Il filone BASIS-CALENDAR (r0822_basis_calendar.py) ha ricostruito da Deribit pubblico
i 30/30 futures trimestrali SCADUTI (i nomi sono deterministici, e
`get_tradingview_chart_data` serve la storia di un contratto scaduto) piu' il funding
e l'indice orario del perpetual: **dal 2019-04, col deleveraging 2022 dentro**.
E' esattamente la coda che a CC01 mancava per costruzione.
LE QUATTRO DOMANDE (in ordine di importanza, non di comodita')
Q1 Come si comporta il carry NEL 2022? Si comprime, si annulla, o si INVERTE?
Quanto dura e quanto e' profondo il peggior episodio?
Q2 Con la coda dentro, lo Sharpe 11-13 quanto diventa? E `implausible_sharpe` —
il gate che nacque proprio da CC01 — segnala ancora?
Q3 I due muri NON strutturali ("non eseguibile a $600", "procyclico") reggono
coi numeri di oggi, su Deribit, a due gambe?
Q4 Il rischio che ha ucciso CC01 e' il MODELLO, non i dati: un carry delta-neutral
sembra liscio perche' il P&L a scadenza non vede il ROLL ne' il MARGINE. In un
blow-out lo short perde SUBITO e il guadagno arriva a scadenza: se il conto non
regge il margine, la posizione muore nel punto peggiore.
COSA QUESTO SCRIPT *NON* RIFA'
Il premio a termine (implicito +7,33% contro funding realizzato +6,48% -> incassabile
+0,85%/anno con IC95 che contiene lo zero) e' gia' misurato dal filone BASIS-CALENDAR
e non viene ri-misurato qui. Qui la domanda e' un'altra: **il 2022**.
CONVENZIONI DICHIARATE
* Nessuna serie di prezzi viene mai incollata: la posizione vive su un CONTRATTO, e al
roll si chiude e si riapre pagando il round-trip. Si incollano solo i RENDIMENTI.
* I futures datati Deribit sono INVERSE (settlement BTC/ETH): il P&L e' calcolato nella
convenzione del venue (BTC) e riportato in USD all'indice. La versione lineare e'
calcolata accanto, e la differenza si misura invece di assumerla trascurabile.
* Fee taker REALE letta dal venue: 3,5 bps/lato per gamba. Slippage dichiarato a parte.
* Leva per-gamba lambda=1 (nozionale per gamba = equity; lordo 2x) salvo dove indicato.
USO
nice -n 19 timeout 900 uv run python scripts/research/r0822d_cc01_2022.py
RETE: questo script NON scarica nulla (il dato e' su disco). L'unica chiamata opzionale e'
la lettura delle specifiche strumento, che e' gia' in cache; se manca, si dichiara.
"""
from __future__ import annotations
import calendar
import datetime as dt
import json
import math
import os
import shutil
import sys
import time
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
import altlib as A # noqa: E402
# ------------------------------------------------------------------ costanti
UTC = dt.timezone.utc
MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN",
"JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]
ASSETS = ("BTC", "ETH")
HOURS_Y = 365.25 * 24
SQ365 = math.sqrt(365.25)
FEE_SIDE = 0.00035 # taker Deribit letto dal venue (get_instruments), per gamba
SLIP_BPS = 2.0 # slippage dichiarato sul datato (meta' spread del front, vedi [6])
ANNUAL_COST_CC01 = 0.02 # il drag annuo che CC01 usava: si tiene IDENTICO per replicare
# Margine: DICHIARATO, fonte secondaria (documentazione Deribit futures). Si fa la
# sensibilita' invece di fingere precisione: il risultato e' una LEVA, e la leva scala
# come 1/(mm_tot + |MAE|) -> con |MAE| >> mm_tot il valore di mm quasi non conta.
MM_LEG = 0.00575 # maintenance margin per gamba (0,575% del nozionale)
IM_LEG = 0.01 # initial margin per gamba (1%)
# Finestre
W_CC01 = pd.Timestamp("2023-05-12", tz="UTC") # inizio dello storico funding di CC01 (HL)
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
FUT_DIR = ROOT / "data" / "raw" / "fut_deribit"
SCRATCH_CACHE = Path(os.environ.get(
"R0822_SCRATCH",
"/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad")) / "basis_cache"
_line = "=" * 96
def hr(t: str) -> None:
print("\n" + _line)
print(t)
print(_line)
# ==========================================================================
# 0. PERSISTENZA — il dato viveva SOLO in uno scratchpad di sessione
# ==========================================================================
def last_friday(year: int, month: int) -> dt.date:
d = dt.date(year, month, calendar.monthrange(year, month)[1])
while d.weekday() != 4:
d -= dt.timedelta(days=1)
return d
def quarterly_names(asset: str, y0: int = 2018, y1: int = 2027):
out = []
for y in range(y0, y1 + 1):
for m in (3, 6, 9, 12):
lf = last_friday(y, m)
out.append((f"{asset}-{lf.day}{MONTHS[m - 1]}{str(y)[2:]}",
pd.Timestamp(lf.year, lf.month, lf.day, 8, tz="UTC")))
return out
def persist_dataset() -> dict:
"""Copia il dataset dallo scratchpad a `data/raw/fut_deribit/` se non c'e' gia'.
NAMESPACE — la lezione SOL del 22/08: `load_data` NON ha una whitelist, il guardrail
"solo dati certi" vive nell'ASSENZA di un file, e mettere un parquet nel namespace
NUDO (`btc_1h.parquet`) lo disattiva in silenzio. Qui i file stanno in una
SOTTOCARTELLA con prefisso `fut_` (accanto a `hl_`/`eq_`/`eqx_`/`alt_`/`fut_`), quindi
(a) nessun nome puo' collidere con `{asset}_{tf}.parquet` e (b) il guardrail resta
verificabile, e viene VERIFICATO sotto, non assunto.
"""
hr("[0] PERSISTENZA DEL DATASET — 20 MB che vivevano solo in uno scratchpad di sessione")
FUT_DIR.mkdir(parents=True, exist_ok=True)
have = sorted(p.name for p in FUT_DIR.glob("*.parquet"))
copied = 0
if SCRATCH_CACHE.exists():
for src in sorted(SCRATCH_CACHE.glob("ohlc_*.parquet")):
dst = FUT_DIR / src.name[len("ohlc_"):]
if not dst.exists():
shutil.copy2(src, dst)
copied += 1
for a in ASSETS:
src = SCRATCH_CACHE / f"fund_{a}.parquet"
dst = FUT_DIR / f"fund_{a}.parquet"
if src.exists() and not dst.exists():
shutil.copy2(src, dst)
copied += 1
print(f" scratchpad presente ({SCRATCH_CACHE}) -> copiati {copied} file nuovi")
else:
print(f" scratchpad ASSENTE ({SCRATCH_CACHE}) — si usa cio' che e' gia' su disco")
files = sorted(FUT_DIR.glob("*.parquet"))
if not files:
print("\n ERRORE: nessun dato. Il dataset si ricostruisce da Deribit pubblico con")
print(" scripts/research/r0822_basis_calendar.py (fetcher rate-limited, ~45 min).")
raise SystemExit(2)
rows, bad = 0, []
per_asset = {a: dict(contratti=0, barre=0) for a in ASSETS}
for f in files:
try:
n = len(pd.read_parquet(f, columns=["ts"]))
except Exception as exc: # noqa: BLE001
bad.append((f.name, type(exc).__name__))
continue
rows += n
if f.name.startswith("fund_"):
continue
a = f.name.split("-")[0]
if a in per_asset:
per_asset[a]["contratti"] += 1
per_asset[a]["barre"] += n
tot_mb = sum(f.stat().st_size for f in files) / 1e6
print(f" {FUT_DIR.relative_to(ROOT)}: {len(files)} file, {tot_mb:.1f} MB, {rows:,} righe")
for a in ASSETS:
print(f" {a}: {per_asset[a]['contratti']} file di trimestrali, "
f"{per_asset[a]['barre']:,} barre orarie (uno di questi ha <200 barre e il")
print(f" caricatore lo scarta: sotto ne compaiono 34, non e' un dato mancante)")
if bad:
print(f" ⚠️ file illeggibili: {bad}")
# ---- GUARDRAIL: verificato, non assunto -----------------------------
from src.data.downloader import load_data, DATA_DIR # noqa: PLC0415
esiti = []
for nm, _ in quarterly_names("BTC")[:2] + quarterly_names("ETH")[:2]:
for tf in ("1h", "5m"):
p = DATA_DIR / f"{nm.lower()}_{tf}.parquet"
esiti.append(p.exists())
collide = any(esiti)
try:
load_data("BTC-30SEP22", "1h")
guard = "ROTTO (load_data ha trovato un contratto!)"
except FileNotFoundError:
guard = "INTATTO (FileNotFoundError)"
ok_btc = True
try:
load_data("BTC", "1h")
except Exception: # noqa: BLE001
ok_btc = False
print(f" guardrail load_data('BTC-30SEP22') -> {guard} | "
f"collisioni nel namespace nudo: {'SI' if collide else 'NO'} | "
f"load_data('BTC','1h') ancora ok: {'SI' if ok_btc else 'NO'}")
print(" ⚠️ DICHIARATO: il cron NON rinfresca questi file (cron_daily fa BTC/ETH, DVOL, HL,")
print(" equity, catena — non i trimestrali). Sono un'ISTANTANEA al 2026-08-22, e il")
print(" contratto in corso (BTC/ETH-25SEP26) e' TRONCATO a oggi, non a scadenza.")
print(" ⚠️ DICHIARATO: `data/raw/` e' gitignored e `do_pythagoras` in backup.sh salva solo")
print(" il dato NON ricostruibile — questo e' ricostruibile (~2.700 chiamate pubbliche),")
print(" quindi resta fuori dal backup di proposito. Se sparisce, si rifa'.")
return dict(files=len(files), mb=tot_mb, rows=rows, guard_ok=("INTATTO" in guard and not collide))
# ==========================================================================
# 1. CARICAMENTO
# ==========================================================================
def load_futures(asset: str):
closes, exps = {}, {}
for nm, e in quarterly_names(asset):
f = FUT_DIR / f"{nm}.parquet"
if not f.exists():
continue
d = pd.read_parquet(f, columns=["ts", "close"])
if len(d) < 200:
continue
s = d.set_index("ts")["close"].astype(float)
s = s[s > 0]
if len(s) < 200:
continue
closes[nm], exps[nm] = s, e
wide = pd.DataFrame(closes).sort_index()
return wide, exps
def load_fund(asset: str) -> pd.DataFrame:
return pd.read_parquet(FUT_DIR / f"fund_{asset}.parquet").set_index("ts").sort_index()
def load_perp(asset: str) -> pd.Series:
d = A.get(asset, "1h")
return d.set_index("datetime")["close"].astype(float).sort_index()
# ==========================================================================
# 2. METRICHE
# ==========================================================================
def to_daily(h: pd.Series) -> pd.Series:
return ((1.0 + h.fillna(0.0)).resample("1D").prod() - 1.0).dropna()
def stats(daily: pd.Series) -> dict:
r = daily.dropna()
if len(r) < 30 or r.std() == 0:
return dict(sharpe=0.0, hold=float("nan"), maxdd=0.0, cagr=float("nan"),
n=len(r), vol=0.0, dd_days=0, worst_month=float("nan"))
sh = float(r.mean() / r.std() * SQ365)
eq = (1 + r).cumprod()
peak = eq.cummax()
ddser = (peak - eq) / peak
dd = float(ddser.max())
under = (eq < peak)
# durata del piu' lungo periodo SOTT'ACQUA (in giorni di calendario)
dur, best, start = 0, 0, None
for t, u in under.items():
if u and start is None:
start = t
elif not u and start is not None:
best = max(best, (t - start).days)
start = None
if start is not None:
best = max(best, (under.index[-1] - start).days)
dur = best
yrs = max((r.index[-1] - r.index[0]).days / 365.25, 1e-9)
cagr = float(eq.iloc[-1] ** (1 / yrs) - 1) if eq.iloc[-1] > 0 else float("nan")
ho = r[r.index >= HOLDOUT]
sho = float(ho.mean() / ho.std() * SQ365) if len(ho) > 30 and ho.std() > 0 else float("nan")
wm = float((((1 + r).resample("ME").prod() - 1)).min())
return dict(sharpe=sh, hold=sho, maxdd=dd, cagr=cagr, n=len(r),
vol=float(r.std() * SQ365), dd_days=dur, worst_month=wm)
def show(label: str, daily: pd.Series, extra: str = "") -> dict:
m = stats(daily)
print(f" {label:<38s} Sh {m['sharpe']:+6.2f} hold {m['hold']:+6.2f} "
f"CAGR {m['cagr']*100:+6.2f}% maxDD {m['maxdd']*100:5.2f}% "
f"peggior mese {m['worst_month']*100:+6.2f}% sott'acqua {m['dd_days']:4d}g "
f"n={m['n']:,}{extra}")
return m
def per_year(daily: pd.Series) -> pd.Series:
return (1 + daily).groupby(daily.index.year).prod() - 1
# ==========================================================================
# 3. Q1 — IL 2022, cioe' l'anno che CC01 non aveva
# ==========================================================================
def q1_descrittiva(F: dict, W: dict, E: dict) -> pd.DataFrame:
hr("[1] Q1 — IL 2022: il carry si comprime, si annulla, o si INVERTE?")
tab = {}
for a in ASSETS:
fu = F[a]
f1 = fu["f1h"].dropna()
d = f1.resample("1D").sum()
wide, exps = W[a], E[a]
idx = wide.index
# basis del front (trimestrale piu' vicino con dte >= 3g) vs INDICE
order = sorted(exps, key=lambda c: exps[c])
TAU = np.column_stack([(exps[c] - idx).total_seconds().values / 3600.0 for c in order])
AV = np.column_stack([wide[c].notna().values for c in order])
AL = AV & (TAU >= 3 * 24)
ok = AL.any(axis=1)
k1 = np.argmax(AL, axis=1)
Fv = np.where(ok, wide[order].values.astype(float)[np.arange(len(idx)), k1], np.nan)
tau = np.where(ok, TAU[np.arange(len(idx)), k1], np.nan)
ix = fu["index"].reindex(idx).ffill(limit=2).values
basis = pd.Series(np.log(Fv / ix) * HOURS_Y / tau, index=idx).replace([np.inf, -np.inf], np.nan)
for y in range(2019, 2027):
dy = d[d.index.year == y]
by = basis[basis.index.year == y].dropna()
if len(dy) < 20 or len(by) < 200:
continue
tab[(a, y)] = dict(
funding_anno=float(dy.sum()),
giorni_f_neg=float((dy < 0).mean()),
basis_med=float(by.median()),
backwardation=float((by < 0).mean()),
)
T = pd.DataFrame(tab).T
print("\n funding REALIZZATO del perpetual Deribit (somma dell'anno) e BASIS del trimestrale front")
print(" (il carry classico INCASSA il funding: un anno negativo e' un anno in cui PAGA)\n")
print(f" {'':6s} {'anno':>5s} {'funding anno':>13s} {'gg f<0':>8s} "
f"{'basis mediano':>14s} {'ore in backwardation':>21s}")
for (a, y), r in T.iterrows():
flag = " <<< il 2022" if y == 2022 else ""
print(f" {a:6s} {y:5d} {r.funding_anno*100:+12.2f}% {r.giorni_f_neg*100:7.1f}% "
f"{r.basis_med*100:+13.2f}% {r.backwardation*100:20.1f}%{flag}")
# episodio peggiore del carry PURO (r = funding), per asset
print("\n peggior EPISODIO del carry puro (r = funding, senza costi): "
"run consecutivo piu' negativo")
for a in ASSETS:
d = F[a]["f1h"].dropna().resample("1D").sum()
cum = d.cumsum()
run_peak = cum.cummax()
under = cum - run_peak
i = under.idxmin()
j = run_peak[:i].idxmax()
print(f" {a}: {float(under.min())*100:+.2f}% di funding cumulato perso fra "
f"{j.date()} e {i.date()} ({(i-j).days} giorni)")
print("\n ⚠️ E il 2022 non e' nemmeno l'unico buco: lo storico di CC01 (dal 2023-05)")
print(" non conteneva neppure MARZO 2020 — che sotto risulta il mese peggiore di tutti.")
return T
# ==========================================================================
# 4. Q2 — LA REPLICA DI CC01 CON IL 2022 DENTRO
# ==========================================================================
def cc01_daily(FUNDD: pd.DataFrame, mode: str = "static", trail: int = 14,
cost: float = ANNUAL_COST_CC01, SIG: pd.DataFrame | None = None) -> pd.Series:
"""IL MODELLO DI CC01, IDENTICO: r = funding (long spot / short perp), meno un drag
annuo fisso. `FUNDD` = [giorno x asset] di RITORNO giornaliero della gamba.
`gated` = harvest solo se il funding trailing CAUSALE e' positivo.
⚠️ `SIG` esiste perche' in CC01 il gate guarda il FUNDING anche quando il ritorno
include il mark-to-market della base (`leg_raw = FUND - PREM.diff()` ma
`sig = FUND.rolling(...)`). Gatare sul ritorno con la base dentro significa gatare
sul RUMORE del premio: prima stesura, e produceva -35%/anno di CAGR."""
SIG = FUNDD if SIG is None else SIG.reindex(FUNDD.index)
daily_cost = cost / 365.25
if mode == "gated":
sig = SIG.rolling(trail, min_periods=trail // 2).mean().shift(1)
act = (sig > 0).astype(float)
n = act.sum(axis=1).replace(0, np.nan)
gross = ((FUNDD * act).sum(axis=1) / n).fillna(0.0)
drag = (act.sum(axis=1) > 0).astype(float) * daily_cost
else:
gross = FUNDD.mean(axis=1)
drag = daily_cost
return (gross - drag).dropna()
def q2_replica(F: dict, PP_GLOBAL: dict) -> dict:
hr("[2] Q2 — LA REPLICA DI CC01: prima riprodurre il numero VECCHIO, poi produrre il nuovo")
out = {}
# (a) il numero vecchio, con la macchina vecchia e i dati vecchi (Hyperliquid)
print("\n (a) CONTROLLO DI REPLICA — cash_carry_hl.py tale e quale (funding Hyperliquid 2023-05+)")
try:
import importlib.util
spec = importlib.util.spec_from_file_location(
"cc_old", ROOT / "scripts" / "research" / "cash_carry_hl.py")
cc_old = importlib.util.module_from_spec(spec)
spec.loader.exec_module(cc_old) # type: ignore
for mode in ("static", "gated"):
r = cc_old.cc_returns(["BTC", "ETH"], mode=mode)
out[f"HL_{mode}"] = r
m = cc_old.metrics(r)
print(f" HL {mode:7s}: Sharpe {m['sharpe']:+6.2f} CAGR {m['cagr']*100:+.1f}% "
f"maxDD {m['dd']*100:.2f}% ({r.index[0].date()} .. {r.index[-1].date()})")
print(" -> il '11-13' del 26/06 e' RIPRODOTTO. Ogni delta sotto e' un delta vero.")
except Exception as exc: # noqa: BLE001
print(f" ⚠️ replica non eseguibile ({type(exc).__name__}: {exc}) — i dati HL sono su disco?")
# (b) STESSO MODELLO, funding DERIBIT: prima sulla finestra di CC01 (controllo di venue),
# poi sulla finestra piena che contiene il 2022.
FUNDD = pd.concat({a: F[a]["f1h"].dropna().resample("1D").sum() for a in ASSETS},
axis=1).sort_index().dropna()
print(f"\n (b) STESSO MODELLO su funding DERIBIT ({FUNDD.index[0].date()} .. "
f"{FUNDD.index[-1].date()}, {len(FUNDD):,} giorni)")
print(" NB: e' un CONTROLLO DI VENUE, non una replica — HL e Deribit sono due mercati.\n")
for mode in ("static", "gated"):
full = cc01_daily(FUNDD, mode)
win = full[full.index >= W_CC01]
out[f"DBT_{mode}_full"] = full
out[f"DBT_{mode}_win"] = win
show(f"DERIBIT {mode} — finestra CC01", win)
show(f"DERIBIT {mode} — FINESTRA PIENA", full, extra=" <<< col 2022 dentro")
print()
print(" per anno (rendimento del carry puro, netto del drag 2%/anno di CC01):")
py = per_year(out["DBT_static_full"])
print(" " + " ".join(f"{y}:{v*100:+.1f}%" for y, v in py.items()))
pyg = per_year(out["DBT_gated_full"])
print(" gated: " + " ".join(f"{y}:{v*100:+.1f}%" for y, v in pyg.items()))
g22 = float(pyg.get(2022, float("nan")))
s22 = float(py.get(2022, float("nan")))
print(f"\n ⚠️ IPOTESI MIA, SMENTITA DALLA MISURA: avevo scritto che il gate causale di CC01")
print(f" (funding trailing 14g > 0) 'entra ed esce col ritardo, quindi il 2022 lo")
print(f" attraversa comunque'. Falso: 2022 static {s22*100:+.1f}% contro gated "
f"{g22*100:+.1f}% = il gate")
print(f" taglia il {100*(1-abs(g22)/max(abs(s22),1e-9)):.0f}% della perdita dell'anno. "
f"Il funding negativo e' PERSISTENTE (53-59% dei")
print(f" giorni del 2022), quindi un trailing lento basta a stare fuori.")
print(f" -> La conseguenza NON e' che CC01 e' salvo: e' che **il buco del 2022 non era")
print(f" il buco principale**. Il gated con il 2022 dentro fa ancora Sharpe "
f"{stats(out['DBT_gated_full'])['sharpe']:+.2f} con")
print(f" maxDD {stats(out['DBT_gated_full'])['maxdd']*100:.2f}% — cioe' e' ANCORA una serie senza coda sinistra.")
# (c) la variante che CC01 chiamava 'onesta': r = funding - Delta(premio perp/indice).
# E' il modello di un long-spot/short-perp VERO su un venue che abbia spot.
prem = {a: (PP_GLOBAL[a].reindex(F[a].index) / F[a]["index"] - 1.0) for a in ASSETS}
out["_FUNDD"] = FUNDD
print("\n (c) VARIANTE CON MARK-TO-MARKET DELLA BASE (r = funding - Delta premio perp/indice)")
print(" e' il modello di un long-spot/short-perp VERO (su un venue che abbia spot).")
print(" ⚠️ DUE LENTI, perche' il 'premio' non e' un oggetto unico: quello GREZZO")
print(" (chiusura del perp / indice, ora per ora) contiene le dislocazioni di")
print(" microstruttura; quello LISCIO (mediana 24h) e' l'analogo del `premium` che")
print(" Hyperliquid pubblica e che CC01 usava — ed e' PIU' MITE per costruzione.")
for lente, sm in (("grezzo", 1), ("liscio 24h", 24)):
DPR = pd.concat({a: (prem[a].rolling(sm, min_periods=1).median() if sm > 1 else prem[a])
.resample("1D").last().diff() for a in ASSETS}, axis=1).sort_index()
FB = (FUNDD - DPR.reindex(FUNDD.index)).dropna()
for mode in ("static", "gated"):
fb = cc01_daily(FB, mode, SIG=FUNDD) # il GATE guarda il funding, come CC01
key = "" if sm == 1 else "_sm"
out[f"DBT_{mode}_basis{key}_full"] = fb
show(f"{mode} + base ({lente}) — PIENA", fb)
out[f"DBT_{mode}_basis{key}_win"] = fb[fb.index >= W_CC01]
show(f"{mode} + base ({lente}) — fin. CC01", out[f"DBT_{mode}_basis{key}_win"])
print()
# DIAGNOSTICA: la liscezza si MISURA, non si asserisce.
try:
from src.portfolio.sleeves import _HL_DIR # noqa: PLC0415
rows = []
for a in ASSETS:
fp = _HL_DIR / f"hlfund_{a.lower()}_1h.parquet"
if not fp.exists():
continue
hl = pd.read_parquet(fp)["premium"].resample("1D").last().diff().dropna()
raw = prem[a].resample("1D").last().diff().dropna()
sm = prem[a].rolling(24, min_periods=1).median().resample("1D").last().diff().dropna()
rows.append((a, hl.std(), sm.std(), raw.std()))
if rows:
print("\n sd del Delta-premio GIORNALIERO (e' la vol che il carry si prende addosso):")
for a, h, sm_, r_ in rows:
print(f" {a}: Hyperliquid `premium` {h*1e4:6.1f} bps | Deribit liscio 24h "
f"{sm_*1e4:6.1f} bps | Deribit grezzo {r_*1e4:6.1f} bps "
f"-> grezzo/HL = {r_/max(h,1e-12):.1f}x")
except Exception as exc: # noqa: BLE001
print(f" (diagnostica sulla liscezza NON girata: {type(exc).__name__})")
print(" ⚠️ Il confronto che conta NON e' con l'11-13 di CC01 ma con il suo 'onesto'")
print(" 10,87: CC01 scriveva che il mark-to-market della base 'sgonfia lo Sharpe solo")
print(" 13->11, quindi il basis-from-data NON e' il rischio vero'. Su Deribit lo stesso")
print(" esperimento, SULLA STESSA FINESTRA, lo sgonfia a 1,65 (lente liscia) / 0,37")
print(" (lente grezza). La differenza non e' il periodo: e' la MISURA del premio, e")
print(" **la liscezza era una proprieta' della colonna, non del rischio.**")
return out
# ==========================================================================
# 5. Q3/Q4 — IL CARRY DERIBIT A DUE GAMBE (perp + trimestrale), roll e margine
# ==========================================================================
def deribit_carry(asset: str, wide: pd.DataFrame, exps: dict, fu: pd.DataFrame,
perp: pd.Series, roll_h: int = 72, sign: int = +1,
slip_bps: float = SLIP_BPS, fee_side: float = FEE_SIDE,
inverse: bool = True, gated: bool = False, trail_h: int = 24 * 14):
"""Carry delta-neutral ESEGUIBILE su Deribit, a DUE gambe sullo STESSO venue.
sign = +1 LONG trimestrale / SHORT perp -> incassa funding, paga il basis
(e' la DIREZIONE DI CC01: il datato fa da proxy dello spot, che a Deribit
storicamente non abbiamo; e la gamba corta sul perp incassa il funding)
sign = -1 SHORT trimestrale / LONG perp -> incassa il basis, paga il funding
Il roll: il datato attivo e' il trimestrale piu' vicino con dte >= roll_h ore. Quando
cambia si paga il round-trip COMPLETO sulla sola gamba datata (2 ordini). **La gamba
perp NON si rolla**: e' un perpetuo. (Il filone BASIS-CALENDAR contava il turnover x2
su entrambe le gambe: qui il costo del roll e' modellato per quello che e'.)
Convenzione INVERSE (quella del venue: settlement BTC/ETH, quote USD):
P&L_BTC(long N USD) = N (1/entrata - 1/uscita)
funding inverse: dimensione in BTC = N/indice; i long PAGANO quando f>0
Il ritorno orario e' espresso per $1 di nozionale PER GAMBA.
"""
idx = wide.index
n = len(idx)
order = sorted(exps, key=lambda c: exps[c])
TAU = np.column_stack([(exps[c] - idx).total_seconds().values / 3600.0 for c in order])
AV = np.column_stack([wide[c].notna().values for c in order])
AL = AV & (TAU >= roll_h)
ok = AL.any(axis=1)
k1 = np.argmax(AL, axis=1)
W = wide[order].values.astype(float)
rows = np.arange(n)
front = np.where(ok, np.array(order, dtype=object)[k1], "")
Fv = np.where(ok, W[rows, k1], np.nan)
tau = np.where(ok, TAU[rows, k1], np.nan)
fu2 = fu.reindex(idx).ffill(limit=2)
IX = fu2["index"].values.astype(float)
f1 = np.nan_to_num(fu2["f1h"].values.astype(float))
P = perp.reindex(idx).values.astype(float)
live = np.isfinite(Fv) & np.isfinite(P) & np.isfinite(IX) & (P > 0) & (IX > 0)
same = np.zeros(n, bool)
same[1:] = live[1:] & live[:-1] & (front[1:] == front[:-1]) & (front[1:] != "")
# ------- segnale (causale): opzionale gate sul carry netto trailing -------
basis_p = np.log(np.where(live, Fv, np.nan) / np.where(live, P, np.nan)) * HOURS_Y / tau
net_carry = f1 * HOURS_Y - basis_p # carry atteso del sign=+1, annualizzato
if gated:
s = pd.Series(net_carry, index=idx).rolling(trail_h, min_periods=trail_h // 3).mean().shift(1)
w = ((s * sign) > 0).astype(float).values # esci quando il carry trailing e' contrario
else:
w = np.ones(n)
w = np.nan_to_num(w)
# ------- P&L orario per $1 di nozionale per gamba -------
r = np.zeros(n)
if inverse:
# long datato: N(1/F_{t-1} - 1/F_t) | short perp: N(1/P_t - 1/P_{t-1}) (in BTC)
with np.errstate(all="ignore"):
dF = np.zeros(n); dP = np.zeros(n)
dF[1:] = 1.0 / Fv[:-1] - 1.0 / Fv[1:]
dP[1:] = 1.0 / P[1:] - 1.0 / P[:-1]
btc = sign * (dF + dP)
btc = btc + sign * (f1 / IX) # short perp INCASSA il funding se f>0
r = np.where(same, IX * btc, 0.0) # da BTC a USD all'indice
else:
with np.errstate(all="ignore"):
rF = np.zeros(n); rP = np.zeros(n)
rF[1:] = Fv[1:] / Fv[:-1] - 1.0
rP[1:] = P[1:] / P[:-1] - 1.0
r = np.where(same, sign * (rF - rP + f1), 0.0)
r = np.nan_to_num(r)
held = np.zeros(n)
held[1:] = w[:-1] # deciso a t, tenuto t->t+1 (causale)
gross = r * held
# ------- costi: apertura, ROLL (solo gamba datata), chiusura, e on/off del gate -------
tc = fee_side + slip_bps * 1e-4
cost = np.zeros(n)
rolled = np.zeros(n, bool)
rolled[1:] = live[1:] & live[:-1] & (front[1:] != front[:-1]) & (front[:-1] != "") & (front[1:] != "")
cost += rolled * (2.0 * tc) * np.maximum(held, np.roll(held, 1)) # chiudi vecchio + apri nuovo
dw = np.zeros(n)
dw[1:] = np.abs(held[1:] - held[:-1])
cost += dw * (2.0 * tc) # entrata/uscita: 2 gambe
net = pd.Series(gross - cost, index=idx)
# ------- tabella per TRADE (un trade = un contratto tenuto) -------
fr = pd.Series(front, index=idx)
trades = []
for c, g in net.groupby(fr):
if c == "" or len(g) < 24:
continue
cc = g.cumsum()
trades.append(dict(contract=c, start=g.index[0], end=g.index[-1], ore=len(g),
pnl=float(cc.iloc[-1]), mae=float(cc.min()), mfe=float(cc.max())))
T = pd.DataFrame(trades).sort_values("start").reset_index(drop=True) if trades else pd.DataFrame()
return net, T, pd.Series(basis_p, index=idx), pd.Series(net_carry, index=idx)
def q3_carry(W: dict, E: dict, F: dict, PP: dict) -> dict:
hr("[3] Q3 — IL CARRY DERIBIT A DUE GAMBE (perpetual + trimestrale), roll pagato")
print(" Su Deribit non esiste storico SPOT: la coppia delta-neutral a due gambe sullo stesso")
print(" venue e' PERP vs TRIMESTRALE. Il trimestrale non paga funding -> fa da proxy dello")
print(" spot, e la direzione di CC01 diventa: LONG trimestrale / SHORT perp.\n")
res = {}
for sign, nome in ((+1, "LONG datato / SHORT perp (= direzione CC01)"),
(-1, "SHORT datato / LONG perp (= raccolta del contango)")):
legs = {}
for a in ASSETS:
net, T, basis, nc = deribit_carry(a, W[a], E[a], F[a], PP[a], sign=sign)
legs[a] = (net, T)
both = pd.concat([legs[a][0] for a in ASSETS], axis=1, join="inner").mean(axis=1)
d = to_daily(both)
res[sign] = dict(hourly=both, daily=d, legs=legs)
print(f" --- {nome} ---")
show(" 50/50 BTC+ETH, finestra piena", d)
show(" 50/50 BTC+ETH, finestra CC01", d[d.index >= W_CC01])
py = per_year(d)
print(" per anno: " + " ".join(f"{y}:{v*100:+.1f}%" for y, v in py.items()))
print()
# convenzione inverse vs lineare — misurata, non assunta
net_i, _, _, _ = deribit_carry("BTC", W["BTC"], E["BTC"], F["BTC"], PP["BTC"], sign=+1, inverse=True)
net_l, _, _, _ = deribit_carry("BTC", W["BTC"], E["BTC"], F["BTC"], PP["BTC"], sign=+1, inverse=False)
di, dl = to_daily(net_i), to_daily(net_l)
print(f" convenzione del venue: INVERSE (BTC-settled) Sharpe {stats(di)['sharpe']:+.2f} "
f"contro l'approssimazione LINEARE {stats(dl)['sharpe']:+.2f} "
f"(CAGR {stats(di)['cagr']*100:+.2f}% vs {stats(dl)['cagr']*100:+.2f}%, BTC sign=+1)")
dy = (per_year(di) - per_year(dl)) * 100
print(" differenza per anno (inverse - lineare, punti percentuali): " +
" ".join(f"{y}:{v:+.1f}" for y, v in dy.items()))
print(" ⚠️ NON e' un dettaglio contabile: su una coppia INVERSE con nozionali pareggiati")
print(" in USD resta un delta residuo ~ -basis (dal termine 1/F - 1/P), e vale piu'")
print(" dell'intero premio che il carry vorrebbe incassare. Chi modella un carry su")
print(" inverse in convenzione lineare si regala un ritorno che il venue non paga.")
return res
def q4_margine(res: dict, F: dict, PP: dict) -> dict:
hr("[4] Q4 — IL ROLL E IL MARGINE: cio' che un P&L a scadenza NON PUO' VEDERE")
print(" Un carry delta-neutral SEMBRA liscio perche' il modello guarda il cashflow e il")
print(" payoff a scadenza. Ma la perdita di mark-to-market e' RICHIESTA SUBITO in margine")
print(" mentre il guadagno arriva alla convergenza: se l'equity scende sotto il mantenimento,")
print(" la posizione viene chiusa nel punto peggiore e il guadagno a scadenza non arriva mai.\n")
print(f" Margine dichiarato (fonte secondaria, documentazione Deribit futures): "
f"IM {IM_LEG*100:.2f}%/gamba, MM {MM_LEG*100:.3f}%/gamba -> MM totale "
f"{2*MM_LEG*100:.2f}% del nozionale per gamba.")
print(" Sotto leva per-gamba lambda (nozionale per gamba = lambda x equity):")
print(" liquidazione quando 1 + lambda*cumPnL < 2*MM*lambda")
print(" -> lambda_max = 1 / (2*MM + |MAE|) con MAE = escursione avversa del trade\n")
out = {}
for sign in (+1, -1):
nome = "LONG datato/SHORT perp" if sign == +1 else "SHORT datato/LONG perp"
rows = []
for a in ASSETS:
T = res[sign]["legs"][a][1].copy()
T["asset"] = a
rows.append(T)
T = pd.concat(rows).sort_values("start").reset_index(drop=True)
T["anno"] = T.start.dt.year
full_mae = float(T.mae.min())
win = T[T.start >= W_CC01]
win_mae = float(win.mae.min()) if len(win) else float("nan")
lam_full = 1.0 / (2 * MM_LEG + abs(full_mae))
lam_win = 1.0 / (2 * MM_LEG + abs(win_mae)) if np.isfinite(win_mae) else float("nan")
out[sign] = dict(T=T, lam_full=lam_full, lam_win=lam_win,
mae_full=full_mae, mae_win=win_mae)
print(f" --- {nome} ---")
print(f" trade (= contratti tenuti): {len(T)} in perdita: "
f"{int((T.pnl<0).sum())} ({(T.pnl<0).mean()*100:.0f}%)")
print(f" peggiore escursione avversa (MAE) sulla FINESTRA PIENA : "
f"{full_mae*100:+6.2f}% -> leva massima sostenibile {lam_full:5.1f}x per gamba")
print(f" peggiore escursione avversa (MAE) nella FINESTRA CC01 : "
f"{win_mae*100:+6.2f}% -> leva massima sostenibile {lam_win:5.1f}x per gamba")
if np.isfinite(lam_win) and lam_full > 0:
print(f" ⚠️ chi avesse dimensionato il carry sulla finestra di CC01 avrebbe usato "
f"fino a {lam_win:.1f}x: e' {lam_win/lam_full:.1f} VOLTE la leva che la storia piena sopporta.")
worst = T.nsmallest(5, "mae")[["asset", "contract", "start", "ore", "pnl", "mae"]]
print(" i 5 peggiori trade per escursione avversa:")
for r in worst.itertuples():
print(f" {r.asset} {r.contract:<12s} {str(r.start.date()):>10s} "
f"{r.ore:5d} ore P&L finale {r.pnl*100:+6.2f}% MAE {r.mae*100:+6.2f}%")
print()
# ---- e il carry PURO (long spot / short perp): il suo blow-out e' il PREMIO DEL PERP ----
print(" Il carry PURO di CC01 (long spot / short perp) NON HA SCADENZA: non converge mai,")
print(" quindi il suo mark-to-market e' il PREMIO del perpetual sull'indice, e la sua perdita")
print(" massima e' il premio stesso. Misurato sul feed certificato Deribit:")
for a in ASSETS:
fu = F[a]
pp = PP[a].reindex(fu.index)
prem = (pp / fu["index"] - 1.0).dropna()
f1 = fu["f1h"].reindex(prem.index).fillna(0.0)
pnl = f1.cumsum() - (prem - prem.iloc[0]) # funding incassato - MTM della base
# MAE a 90 GIORNI: la finestra di detenzione confrontabile con un trimestrale.
# (Il drawdown da picco NON e' la misura giusta qui: il picco e' un crash-spike di
# un'ora in cui il perp scambia -20% sotto l'indice, cioe' un GUADAGNO per chi e'
# corto il perp. Un rischio si misura dall'INGRESSO, non da una fortuna transitoria.)
w = 90 * 24
fwd_min = pnl[::-1].rolling(w, min_periods=1).min()[::-1]
exc = fwd_min - pnl
# ⚠️ INGRESSI AMMISSIBILI: un carry si apre quando il perp e' a PREMIO (e' l'intera
# ragione del trade). Includere le ore in cui il perp scambia -20% SOTTO l'indice
# (marzo 2020) misura l'escursione di un ingresso che nessuno farebbe. Si riporta
# il numero ristretto come stima, e quello grezzo come limite superiore.
exc_ok = exc.where(prem >= 0)
mae90 = float(exc_ok.min())
i90 = exc_ok.idxmin()
mae90_raw = float(exc.min())
lam90 = 1.0 / (2 * MM_LEG + abs(mae90))
pr_y = prem.groupby(prem.index.year).max()
print(f" {a}: premio perp/indice mediana {prem.median()*1e4:+.1f} bps | "
f"MASSIMO {prem.max()*1e4:+.0f} bps ({prem.idxmax().date()}) | "
f"minimo {prem.min()*1e4:+.0f} bps ({prem.idxmin().date()})")
print(f" MAE a 90 giorni del carry puro CON mark-to-market della base, su ingressi")
print(f" ammissibili (premio >= 0): {mae90*100:+.2f}% (ingresso {i90.date()}) "
f"-> leva massima {lam90:.1f}x per gamba")
print(f" (senza il filtro d'ingresso: {mae90_raw*100:+.2f}% — include l'apertura di")
print(f" uno short perp a sconto del 20%, che nessuno farebbe)")
print(f" premio MASSIMO per anno (bps, e' il lato AVVERSO del carry): " +
", ".join(f"{y}:{v*1e4:+.0f}" for y, v in pr_y.items()))
print(" ⚠️ Il modello r=funding di CC01 ha maxDD 0,3-0,9%: la stessa posizione, con il")
print(" mark-to-market della base che il margine guarda davvero, ha una escursione")
print(" avversa di un ordine di grandezza piu' grande. Non e' un dettaglio: e' la")
print(" differenza fra 'leva 80x' e 'leva 6x'.")
return out
# ==========================================================================
# 6. GATE
# ==========================================================================
def q5_gate(cc: dict, res: dict, mg: dict, n_trials: int, family_sr: list) -> None:
hr("[5] GATE — a partire da quello che CC01 stesso ha fatto nascere")
print("\n (a) implausible_sharpe — il gate del 26/07, scritto PROPRIO su CC01")
cand = [
("CC01 originale (HL, 2023-05+)", cc.get("HL_static"), None, None),
("CC01 gated originale (HL)", cc.get("HL_gated"), None, None),
("CC01 su Deribit, finestra CC01", cc.get("DBT_static_win"), None, None),
("CC01 su Deribit, FINESTRA PIENA", cc.get("DBT_static_full"), None, None),
("CC01 gated Deribit, FINESTRA PIENA", cc.get("DBT_gated_full"), None, None),
("CC01+base grezzo, FINESTRA PIENA", cc.get("DBT_static_basis_full"), None, None),
("CC01+base grezzo gated, PIENA", cc.get("DBT_gated_basis_full"), None, None),
("CC01+base liscio 24h, FINESTRA PIENA", cc.get("DBT_static_basis_sm_full"), None, None),
("CC01+base liscio 24h gated, PIENA", cc.get("DBT_gated_basis_sm_full"), None, None),
]
for sign in (+1, -1):
T = mg[sign]["T"]
nm = "carry 2 gambe Deribit sign=+1" if sign == +1 else "carry 2 gambe Deribit sign=-1"
cand.append((nm, res[sign]["daily"], int(len(T)), int((T.pnl < 0).sum())))
for nm, ser, nt, nl in cand:
if ser is None or len(ser) < 30:
print(f" {nm:<38s} NON GIRATO (serie assente)")
continue
g = A.implausible_sharpe(ser, n_trades=nt, n_losing_trades=nl)
tag = "🚨 SEGNALA" if g["implausible"] else " pulito "
print(f" {nm:<38s} {tag} Sh {g.get('sharpe', float('nan')):+6.2f} "
f"maxDD {g.get('maxdd', float('nan'))*100:5.2f}% "
f"perdite/attive {g.get('loss_frac', float('nan'))*100:5.1f}% "
f"Calmar {g.get('calmar', float('nan')):7.1f}")
for rr in g["reasons"]:
print(f" - {rr}")
print("\n (b) marginal_vs_tp01 — sul carry ESEGUIBILE (2 gambe Deribit, finestra piena)")
for sign in (+1, -1):
d = res[sign]["daily"]
if d.std() == 0:
print(f" sign={sign:+d}: serie piatta, skip")
continue
m = A.marginal_vs_tp01(d)
b = m.get("blends", {}).get("w25", {})
print(f" sign={sign:+d} verdetto={m.get('marginal_verdict')} "
f"corr->TP01 {m.get('corr_full')} is_hedge={m.get('is_hedge')} "
f"edge_in_sample={m.get('has_insample_edge')} (Sh IS {m.get('cand_insample_sharpe')})")
print(f" robust_oos={m.get('robust_oos')} multicut={m.get('multicut_persistent')} "
f"blend25 full {b.get('full')} (upl {b.get('uplift_full')}) hold {b.get('hold')} "
f"(upl {b.get('uplift_hold')})")
print("\n (c) deflated_sharpe — sui trial DICHIARATI, contati al rialzo")
print(" ⚠️ la famiglia e' quella del CANDIDATO (le celle del carry a 2 gambe: 2 segni x")
print(" 8 ancore d'ora x 7 anticipi di roll). Mescolarci gli Sharpe del carry PURO")
print(" (3-7) gonfierebbe la varianza dei trial e quindi la soglia del null: sarebbe")
print(" un null di un'altra strategia.")
allsr = list(family_sr) + [stats(res[s_]["daily"])["sharpe"] for s_ in (+1, -1)]
best_sign = max((+1, -1), key=lambda s: stats(res[s]["daily"])["sharpe"])
d = res[best_sign]["daily"]
sr = stats(d)["sharpe"]
# la lista degli Sharpe di famiglia serve per la varianza dei trial: si usa la griglia
# d'ancora del roll, gia' calcolata sotto, piu' i due segni.
try:
dsr, sr0 = A.deflated_sharpe(sr, allsr, d)
txt = (f"DSR {dsr:.3f} ({'PASS' if dsr >= 0.95 else 'FAIL'}), "
f"massimo atteso dal solo rumore {sr0:+.2f}")
except Exception as exc: # noqa: BLE001
txt = f"NON GIRATO ({type(exc).__name__})"
print(f" candidato migliore = sign {best_sign:+d}, Sharpe {sr:+.2f}, "
f"trial dichiarati {n_trials} -> {txt}")
print(f" (il massimo atteso dal rumore su questa famiglia e' PIU' ALTO del candidato:")
print(f" non c'e' niente da deflazionare, il candidato e' sotto il proprio null.)")
print("\n (c-bis) CAUSALITA' — dichiarata, e con un controllo POSITIVO")
print(" Il carry STATICO non ha segnale: non c'e' niente da anticipare, e la scelta del")
print(" contratto front dipende solo dal calendario di scadenza, noto anni prima.")
print(" L'unico segnale del filone e' il gate di CC01 (media mobile del funding). Li' la")
print(" causalita' e' uno `.shift(1)`, e un controllo positivo deve mostrare che TOGLIERLO")
print(" migliora — altrimenti il test non ha potenza:")
try:
FU2 = pd.concat({a: cc["_FUNDD"][a] for a in ASSETS}, axis=1) if "_FUNDD" in cc else None
except Exception: # noqa: BLE001
FU2 = None
if FU2 is not None:
leak = FU2.rolling(14, min_periods=7).mean() # SENZA shift = leak
act = (leak > 0).astype(float)
n_ = act.sum(axis=1).replace(0, np.nan)
g_leak = ((FU2 * act).sum(axis=1) / n_).fillna(0.0) - \
(act.sum(axis=1) > 0).astype(float) * ANNUAL_COST_CC01 / 365.25
print(f" gate CAUSALE (shift 1): Sharpe {stats(cc['DBT_gated_full'])['sharpe']:+.2f} | "
f"gate CON LEAK (no shift): Sharpe {stats(g_leak.dropna())['sharpe']:+.2f} "
f"-> il test HA potenza e la versione pubblicata e' quella causale")
else:
print(" NON GIRATO (pannello di funding non disponibile)")
print("\n (d) null del DE-LEVERING — il carry rivendica un DD minuscolo: e' solo meno leva?")
try:
base = A.tp01_baseline_daily()
for nm, ser in (("CC01 Deribit finestra piena", cc.get("DBT_static_full")),
("carry 2 gambe sign=+1", res[+1]["daily"])):
if ser is None:
continue
cs = stats(ser)
hit = None
for k in np.arange(1.00, 0.01, -0.02): # il k PIU' GRANDE che rispetta il DD
st = stats(base * k)
if st["maxdd"] <= cs["maxdd"] + 1e-12:
hit = (float(k), st)
break
if hit is None:
print(f" {nm}: nessun k<=1 sul baseline raggiunge quel DD -> null non applicabile")
else:
k, st = hit
verdetto = "REFUTATO (il baseline de-leverato fa meglio)" if st["sharpe"] > cs["sharpe"] \
else "SUPERATO (il baseline de-leverato NON lo eguaglia)"
print(f" {nm}: DD {cs['maxdd']*100:.2f}% Sh {cs['sharpe']:+.2f} vs "
f"TP01 a k={k:.2f}: DD {st['maxdd']*100:.2f}% Sh {st['sharpe']:+.2f} -> {verdetto}")
print(" ⚠️ lo Sharpe e' INVARIANTE alla scala, quindi su un candidato che rivendica")
print(" 'poco DD' questo null si riduce a 'il tuo Sharpe batte quello del baseline?'.")
print(" E' degenere per costruzione (lezione 22/08) — girato lo stesso e dichiarato.")
except Exception as exc: # noqa: BLE001
print(f" NON GIRATO ({type(exc).__name__}: {exc})")
def q5b_ancora(W: dict, E: dict, F: dict, PP: dict, sign: int) -> list:
print("\n (e) FORTUNA D'ANCORA del roll — l'ora in cui si rolla e' un max-of-k non dichiarato")
sh = {}
for off in range(0, 24, 3):
legs = []
for a in ASSETS:
net, _, _, _ = deribit_carry(a, W[a], E[a], F[a], PP[a],
roll_h=72 + off, sign=sign)
legs.append(net)
d = to_daily(pd.concat(legs, axis=1, join="inner").mean(axis=1))
sh[off] = stats(d)["sharpe"]
v = np.array(list(sh.values()))
print(f" 8 ancore di roll (72h + 0..21h): mediana {np.median(v):+.3f} "
f"banda [{v.min():+.3f}, {v.max():+.3f}] canonica(72h) {sh[0]:+.3f} "
f"-> fortuna {sh[0]-np.median(v):+.3f}")
sh2 = {}
for rd in (1, 2, 3, 5, 7, 10, 14):
legs = []
for a in ASSETS:
net, _, _, _ = deribit_carry(a, W[a], E[a], F[a], PP[a],
roll_h=rd * 24, sign=sign)
legs.append(net)
d = to_daily(pd.concat(legs, axis=1, join="inner").mean(axis=1))
sh2[rd] = stats(d)["sharpe"]
print(" giorni di anticipo del roll: " +
" ".join(f"{k}g:{v:+.2f}" for k, v in sh2.items()))
return list(sh.values()) + list(sh2.values())
# ==========================================================================
# 7. ESEGUIBILITA'
# ==========================================================================
def q6_eseguibilita(mg: dict) -> None:
hr("[6] Q3-bis — I MURI NON STRUTTURALI, riprezzati coi numeri di oggi")
print(" CC01 fu dichiarato non eseguibile per: 'spot+perp = 4-38 gambe, funding HL non")
print(" Deribit'. La versione perp+trimestrale e' a DUE gambe sullo STESSO venue.\n")
specs = {
"BTC-PERPETUAL": dict(min_usd=10.0, tick=0.5, settle="BTC"),
"BTC trimestrale": dict(min_usd=10.0, tick=2.5, settle="BTC"),
"ETH-PERPETUAL": dict(min_usd=1.0, tick=0.05, settle="ETH"),
"ETH trimestrale": dict(min_usd=1.0, tick=0.25, settle="ETH"),
}
print(" specifiche lette dal venue (public/get_instruments, 2026-08-22):")
for k, v in specs.items():
print(f" {k:<18s} lotto minimo ${v['min_usd']:>5.0f} tick {v['tick']:<5} "
f"settlement {v['settle']}")
print(f"\n A $635 il lotto minimo NON e' il vincolo (4a volta in questa ondata): un carry")
print(f" BTC a lambda=1 vuole $635 per gamba, 63 volte il lotto minimo.")
print(f" Margine iniziale a lambda=1: {2*IM_LEG*100:.1f}% del nozionale per gamba = "
f"${2*IM_LEG*635:.0f} su $635.")
lam = mg[+1]["lam_full"]
print(f"\n ⚠️ MA il vincolo vero e' un ALTRO, e non e' la taglia: **tutti i futures datati")
print(f" Deribit sono INVERSE, settlement_currency BTC/ETH** (verificato strumento per")
print(f" strumento: non esiste una linea USDC datata). Il libro live e' su USDC")
print(f" lineari. E' lo stesso muro trovato il 22/08 sulle opzioni ('BTC fuori a $3.000")
print(f" misurato sulla famiglia inverse'): un conto USDC non margina un inverse senza")
print(f" cross-collateral, e il collaterale in BTC E' esso stesso esposizione")
print(f" direzionale — che e' precisamente cio' che un delta-neutral vuole evitare.")
print(f" Questo NON e' stato risolto qui: e' una verifica da UNA chiamata sul conto")
print(f" reale (haircut del cross-collateral), e va fatta prima di credere a qualsiasi")
print(f" numero di eseguibilita' di questo filone.")
print("\n costo d'esecuzione ANNUO del carry rollato (4 roll/anno x 2 ordini, SOLO la gamba")
print(" datata: il perpetuo non si rolla; apertura e chiusura sono 2 ordini UNA volta sola):")
for slip in (0.0, 2.0, 5.0, 10.0):
c = 4 * 2 * (FEE_SIDE + slip * 1e-4)
print(f" slippage {slip:4.1f} bps/lato -> {c*100:5.3f}%/anno (8 ordini/anno)")
print(" spread REALE letto sul book vivo (cache 22/08): BTC front 0,32 bps / secondo 5,43;")
print(" ETH front 1,03 / secondo 1,02 -> slippage 2 bps/lato e' una stima ONESTA sul front.")
print(f" ⚠️ BASIS-CALENDAR citava ~1,4%/anno di costo perche' contava il turnover su")
print(f" ENTRAMBE le gambe a ogni roll: il perpetuo NON si rolla. Il costo vero e'")
print(f" **0,44%/anno a 2 bps di slippage**, ~3 volte meno. Il che rende quel filone")
print(f" MENO severo sul costo e per NIENTE meno severo sul segno: un premio di")
print(f" ~0,85%/anno con IC95 che contiene lo zero resta indistinguibile da zero anche")
print(f" pagando un terzo del costo. **Correggere un costo non crea un edge.**")
# ==========================================================================
# MAIN
# ==========================================================================
def main() -> None:
t0 = time.time()
hr("r0822d CC01-2022 — riaprire il cash-and-carry con la coda che gli mancava")
persist_dataset()
print("\n caricamento...")
W, E, F, PP = {}, {}, {}, {}
for a in ASSETS:
W[a], E[a] = load_futures(a)
F[a] = load_fund(a)
PP[a] = load_perp(a)
print(f" {a}: {W[a].shape[1]} contratti, {len(W[a]):,} ore "
f"({W[a].index.min().date()} .. {W[a].index.max().date()}) "
f"funding+indice {len(F[a]):,} ore perp certificato {len(PP[a]):,} barre")
q1_descrittiva(F, W, E)
cc = q2_replica(F, PP)
res = q3_carry(W, E, F, PP)
mg = q4_margine(res, F, PP)
# trial dichiarati, contati AL RIALZO (ogni variante provata, comprese quelle scartate):
# 4 carry puro r=funding : 2 modi (static/gated) x 2 finestre
# 8 carry puro + base MTM : 2 modi x 2 lenti sul premio x 2 finestre
# 32 carry a 2 gambe : 2 segni x (1 canonica + 8 ancore d'ora + 7 anticipi roll)
# 2 convenzione : inverse vs lineare
# 1 controllo positivo di leak
N_TRIALS = 4 + 8 + 32 + 2 + 1
best = max((+1, -1), key=lambda s: stats(res[s]["daily"])["sharpe"])
print("\n (pre-calcolo della griglia d'ancora, serve al deflated-Sharpe)")
family_sr = q5b_ancora(W, E, F, PP, best)
q5_gate(cc, res, mg, N_TRIALS, family_sr)
q6_eseguibilita(mg)
hr("[7] SINTESI — LA SCALA, cioe' dove sono davvero andati gli 11-13 punti di Sharpe")
s_hl = stats(cc["HL_static"])["sharpe"] if "HL_static" in cc else float("nan")
s_win = stats(cc["DBT_static_win"])["sharpe"]
s_full = stats(cc["DBT_static_full"])["sharpe"]
s_bas = stats(cc["DBT_static_basis_full"])["sharpe"]
d_full = cc["DBT_static_full"]
best_d = res[best]["daily"]
righe = [
("CC01 come pubblicato (funding HL, r=funding, 2023-05+)", s_hl,
stats(cc["HL_static"])["maxdd"] if "HL_static" in cc else float("nan"), ""),
("stesso modello, funding DERIBIT, STESSA finestra", s_win,
stats(cc["DBT_static_win"])["maxdd"], "controllo di venue"),
("stesso modello, DERIBIT, finestra piena (col 2022)", s_full,
stats(d_full)["maxdd"], "<- la coda che mancava"),
("+ base, premio LISCIO 24h (la lente di CC01/HL)",
stats(cc["DBT_static_basis_sm_full"])["sharpe"],
stats(cc["DBT_static_basis_sm_full"])["maxdd"], "<- il rischio che il margine guarda"),
("+ base, premio GREZZO (la lente severa)", s_bas,
stats(cc["DBT_static_basis_full"])["maxdd"], " banda della stessa cosa"),
("carry ESEGUIBILE su Deribit, 2 gambe, DIREZIONE CC01",
stats(res[+1]["daily"])["sharpe"], stats(res[+1]["daily"])["maxdd"],
"<- niente spot: serve un 2o derivato"),
(" (e scegliendo EX-POST il segno migliore dei due)",
stats(res[-1]["daily"])["sharpe"], stats(res[-1]["daily"])["maxdd"], ""),
]
print(f" {'lente':<56s} {'Sharpe':>8s} {'maxDD':>8s}")
for nm, sh, dd, nota in righe:
print(f" {nm:<56s} {sh:+8.2f} {dd*100:7.2f}% {nota}")
print()
s_sm = stats(cc["DBT_static_basis_sm_full"])["sharpe"]
s_2l = stats(res[+1]["daily"])["sharpe"]
print(f" SCOMPOSIZIONE (lente LISCIA, la piu' favorevole al filone):")
print(f" il 2022 vale {s_win - s_full:+.2f} di Sharpe (7,88 -> 3,72)")
print(f" la base MTM vale {s_full - s_sm:+.2f} (3,72 -> {s_sm:.2f})")
print(f" la SECONDA GAMBA {s_sm - s_2l:+.2f} ({s_sm:.2f} -> {s_2l:.2f})")
print(f" (lente GREZZA: il 2022 {s_win - s_full:+.2f}, la base {s_full - s_bas:+.2f}, la seconda gamba "
f"{s_bas - s_2l:+.2f})")
print()
print(f" ⚠️ CORREZIONE A ME STESSO, fatta guardando la mia stessa tabella: avevo scritto")
print(f" 'il buco piu' grande non era il 2022'. L'aritmetica dice il CONTRARIO — il 2022 e'")
print(f" il singolo pezzo piu' grande ({s_win - s_full:+.2f}) su entrambe le lenti. Il punto vero e' un")
print(f" altro, ed e' piu' forte: **il 2022 da solo NON BASTA**. Toglierlo dal buco porta lo")
print(f" Sharpe da 7,88 a 3,72, che `implausible_sharpe` SEGNALA ANCORA. I due pezzi che il")
print(f" 26/06 non aveva dichiarato — mark-to-market della base e assenza di una gamba spot")
print(f" negoziabile — valgono INSIEME quanto il 2022 ({s_full - s_2l:+.2f} sulla lente liscia) e sono")
print(f" quelli che portano il numero sotto 1. La ragione pubblicata era NECESSARIA e non")
print(f" SUFFICIENTE.")
print(f" 📌 E il salto piu' grande della tabella non e' nemmeno nell'elenco: e' il VENUE")
print(f" ({s_hl - s_win:+.2f}, da HL a Deribit sulla STESSA finestra e collo STESSO modello). Un carry")
print(f" di funding misurato su un venue non e' un carry di funding.")
print()
print(f" leva massima sostenibile dal carry a 2 gambe in direzione CC01: "
f"{mg[+1]['lam_full']:.1f}x per gamba sulla")
print(f" storia piena, contro {mg[+1]['lam_win']:.1f}x che la sola finestra di CC01 avrebbe autorizzato")
print(f" ({mg[+1]['lam_win']/mg[+1]['lam_full']:.1f}x di leva di troppo). Sul carry PURO con MTM della base: "
f"~5,6-6,4x contro le")
print(f" ~80x che un maxDD dello 0,8% farebbe credere sostenibili.")
print()
print(" ✅ Il criterio di riapertura dichiarato il 26/06 era FALSIFICABILE ed e' stato")
print(" ESEGUITO: il dato che mancava ora esiste, la misura e' stata fatta, e il verdetto")
print(" non cambia. **Il filone era chiuso per la ragione giusta** — e ora si sa anche")
print(" che quella ragione era NECESSARIA ma non sufficiente: il 2022 era il pezzo piu'")
print(" grande dei tre, e da solo lascia il candidato ancora sopra ogni soglia di")
print(" plausibilita'. Riaprire un filone su dati nuovi ha prodotto un verdetto")
print(" INVARIATO e una diagnosi MIGLIORE — che e' il risultato che si voleva.")
print(f"\n tempo totale {time.time()-t0:.1f}s")
if __name__ == "__main__":
main()