1142 lines
66 KiB
Python
1142 lines
66 KiB
Python
#!/usr/bin/env python3
|
|
"""r0822e_funding_avoid.py — FUNDING-AVOID: il costo di tenere aperta una posizione si puo'
|
|
EVITARE cambiando STRUMENTO, senza toccare il segnale?
|
|
|
|
CONTESTO (ondata 2026-08-22, sezioni 34-35 di docs/research/RESULTS-0822.md)
|
|
(1) Stanotte e' stato MISURATO che il libro paga **2,16%/anno di funding** (TP01 -2,10%,
|
|
SKH01 -2,32%), mai modellato prima. E' una tassa sul DRIFT: il maxDD non si muove
|
|
(+0,3pp) ma la rendita perpetua scende 10,73% -> 9,14% e il muro di capitale sale del
|
|
+17,5%. Su una rendita, il drift e' la moneta.
|
|
(2) Stanotte e' stato PERSISTITO in `data/raw/fut_deribit/` lo storico dei futures DATATI
|
|
Deribit: 34 trimestrali per asset, BTC 240.150 barre orarie / ETH 233.334, 2018-09 ->
|
|
2026-08, piu' 64.110 ore di funding e indice dal 2019-04.
|
|
|
|
LA DOMANDA, MAI POSTA: **un future DATATO non paga funding.** Il carry e' incorporato nel
|
|
PREZZO (contango = compri sopra lo spot e convergi a scadenza). Quindi:
|
|
|
|
esprimere TP01 (e/o SKH01) su un future datato invece che sul perpetual
|
|
costa MENO o PIU' del funding che paghiamo oggi?
|
|
|
|
E' una domanda sull'ESECUZIONE, non sul segnale: il segnale resta congelato (TSMOM 30/90/180
|
|
su BTC/ETH, vol-target 20%, `CANONICAL`), cambia solo lo strumento su cui lo si esprime.
|
|
Un grado di liberta' per volta, replica di controllo prima di ogni delta.
|
|
|
|
LA TESI DA VERIFICARE O FALSIFICARE (la ragione per cui il filone puo' restituire qualcosa)
|
|
Il funding e' caro PROPRIO QUANDO SIAMO LONG: misurato stanotte, il tasso che il libro paga
|
|
davvero e' 1,86-2,55x l'incondizionato, perche' esposizione e funding sono positivamente
|
|
correlati. Il basis di un datato invece e' FISSATO ALL'INGRESSO e non si riprezza contro di
|
|
te mentre tieni. Se il rapporto condizionale/incondizionale del basis e' piu' BASSO di
|
|
quello del funding, il datato converte un costo condizionale e avverso in uno noto.
|
|
|
|
SEZIONI
|
|
0 IL DATO — 68 contratti trimestrali, copertura, roll; specifiche e LIQUIDITA' lette dal
|
|
venue (cache in scratchpad, `--no-net` per girare offline).
|
|
1 REPLICA DI CONTROLLO — il -2,16%/anno va riprodotto PRIMA di confrontarci qualsiasi cosa.
|
|
2 L'ESTIMATORE — livello vs flusso. La differenza ARITMETICA dei rendimenti fra due
|
|
strumenti sullo stesso sottostante NON e' il carry: contiene un termine di varianza
|
|
(Jensen) che qui vale 1,5-2,6 punti percentuali l'anno, cioe' 2-4x l'effetto cercato.
|
|
3 IL CONFRONTO APPAIATO — per giorno e per segnale, condizionale vs incondizionale.
|
|
4 IL ROLL E LA LIQUIDITA' — quante volte si rolla, quanto costa attraversare, e a $635.
|
|
5 LE SERIE — libro nelle 5 varianti + banda d'ancora (23 offset, differenze APPAIATE).
|
|
6 I MURI — drift -> rendita perpetua -> capitale, con la macchineria di r0726_capwall.
|
|
7 IL MURO CHE NON C'E' — «tutti i datati Deribit sono INVERSE» e' FALSO da maggio 2026.
|
|
8 SOTTOPRODOTTO — lo SPOT, che ha carry ZERO e oggi fee ZERO, e la sua domanda aperta.
|
|
9 GATE + VERDETTO.
|
|
|
|
VINCOLI: nessun file di produzione toccato (config/, src/, scripts/live/, cron INVARIATI);
|
|
nessun commit; output a video + cache in scratchpad. Runtime ~2-4 min.
|
|
|
|
USO
|
|
nice -n 19 timeout 900 uv run python scripts/research/r0822e_funding_avoid.py
|
|
... --no-net usa solo la cache del venue (nessuna richiesta a Deribit)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
import json
|
|
import sys
|
|
import time
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
ROOT = Path("/opt/docker/PythagorasGoal")
|
|
for _p in (ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt", ROOT):
|
|
sys.path.insert(0, str(_p))
|
|
|
|
# r0822d_funding legge `--no-net` da sys.argv AL MOMENTO DELL'IMPORT: qui non si scarica il
|
|
# funding (la sua cache esiste gia'), quindi glielo si impone. Le uniche chiamate di rete di
|
|
# QUESTO script sono le sue, sotto, ed hanno il proprio flag.
|
|
_NET = "--no-net" not in sys.argv
|
|
if "--no-net" not in sys.argv:
|
|
sys.argv.append("--no-net")
|
|
|
|
import altlib as al # noqa: E402
|
|
import r0702_anchor_skh01 as r02 # noqa: E402
|
|
import r0725_capcurve as CC # noqa: E402
|
|
import r0726_capwall_refresh as WR # noqa: E402
|
|
import r0822d_funding as FD # noqa: E402
|
|
from src.portfolio.portfolio import combine_outer # noqa: E402
|
|
|
|
T0 = time.time()
|
|
ASSETS = ("BTC", "ETH")
|
|
FUT = ROOT / "data" / "raw" / "fut_deribit"
|
|
_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
|
|
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/fa")
|
|
SCRATCH = _SP if _SP.parent.exists() else Path("/tmp/pythagoras_favoid")
|
|
API = "https://www.deribit.com/api/v2/public/"
|
|
MON = {"JAN": 1, "FEB": 2, "MAR": 3, "APR": 4, "MAY": 5, "JUN": 6,
|
|
"JUL": 7, "AUG": 8, "SEP": 9, "OCT": 10, "NOV": 11, "DEC": 12}
|
|
|
|
BUF_CANON = 5 # si rolla 5 giorni prima della scadenza (dichiarato a priori)
|
|
BUF_GRID = (2, 5, 10, 20) # sensibilita' del parametro d'esecuzione
|
|
ALLOC = 600.0 # il capitale su cui gira book_series (conto reale $635)
|
|
DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor)
|
|
W_TP, W_SKH = 0.75, 0.25
|
|
LO_BOOK = pd.Timestamp("2019-03-14", tz="UTC")
|
|
FEE_REALE = 0.00035 # taker Deribit letto dal venue (3,5 bps/lato)
|
|
# il modello di CC.tp01_realistic usa 5 bps/lato (convenzione conservativa del progetto)
|
|
FEE_MODELLO = CC.DERIBIT_FEE_SIDE
|
|
|
|
|
|
def sez(t: str) -> None:
|
|
print("\n" + "=" * 100)
|
|
print(f" {t}")
|
|
print("=" * 100)
|
|
|
|
|
|
def clock() -> str:
|
|
return f"[{time.time() - T0:5.0f}s]"
|
|
|
|
|
|
def expdate(nm: str) -> pd.Timestamp:
|
|
d = nm.split("-")[1]
|
|
return pd.Timestamp(dt.datetime(2000 + int(d[5:]), MON[d[2:5]], int(d[:2]), 8, 0),
|
|
tz="UTC")
|
|
|
|
|
|
# ===========================================================================
|
|
# 0) IL DATO
|
|
# ===========================================================================
|
|
def _pace() -> None:
|
|
"""Mai nei minuti :07 (cron_book) e :25-:29 (cron_chain); <=2 req/s."""
|
|
while dt.datetime.now(dt.timezone.utc).minute in (7, 25, 26, 27, 28, 29):
|
|
time.sleep(20)
|
|
time.sleep(0.6)
|
|
|
|
|
|
def _get(path: str):
|
|
_pace()
|
|
return json.load(urllib.request.urlopen(API + path, timeout=30))["result"]
|
|
|
|
|
|
VENUE_CACHE = SCRATCH / "venue_snapshot.json"
|
|
TICKETS = (100, 240, 500) # $ per ordine: 240 = nozionale max per asset di TP01 a $635
|
|
|
|
|
|
def venue_snapshot() -> dict | None:
|
|
"""Specifiche + libro degli strumenti candidati. UNA istantanea, non una distribuzione:
|
|
e' esattamente cio' che si puo' dire e va detto cosi'."""
|
|
if VENUE_CACHE.exists():
|
|
old = json.loads(VENUE_CACHE.read_text())
|
|
else:
|
|
old = None
|
|
if not _NET:
|
|
return old
|
|
try:
|
|
out = {"ts": pd.Timestamp.now(tz="UTC").isoformat(), "instruments": {}, "books": {}}
|
|
for cur in ("BTC", "ETH", "USDC"):
|
|
for kind in ("future", "spot"):
|
|
try:
|
|
for x in _get(f"get_instruments?currency={cur}&kind={kind}&expired=false"):
|
|
out["instruments"][x["instrument_name"]] = dict(
|
|
settlement=x.get("settlement_currency"),
|
|
contract_size=x.get("contract_size"),
|
|
min_trade_amount=x.get("min_trade_amount"),
|
|
tick=x.get("tick_size"), taker=x.get("taker_commission"),
|
|
maker=x.get("maker_commission"), kind=kind, currency=cur,
|
|
created=x.get("creation_timestamp"),
|
|
expiry=x.get("expiration_timestamp"))
|
|
except Exception:
|
|
pass
|
|
for i in ("BTC_USDC-PERPETUAL", "BTC_USDC-25SEP26", "BTC_USDC-25DEC26", "BTC_USDC",
|
|
"BTC-25SEP26", "BTC-PERPETUAL",
|
|
"ETH_USDC-PERPETUAL", "ETH_USDC-25SEP26", "ETH_USDC-25DEC26", "ETH_USDC",
|
|
"ETH-25SEP26", "ETH-PERPETUAL"):
|
|
try:
|
|
d = _get(f"get_order_book?instrument_name={i}&depth=20")
|
|
out["books"][i] = dict(bid=d.get("best_bid_price"), ask=d.get("best_ask_price"),
|
|
index=d.get("index_price"), oi=d.get("open_interest"),
|
|
bids=d.get("bids"), asks=d.get("asks"),
|
|
vol24=(d.get("stats") or {}).get("volume"),
|
|
vol24usd=(d.get("stats") or {}).get("volume_usd"))
|
|
except Exception:
|
|
pass
|
|
SCRATCH.mkdir(parents=True, exist_ok=True)
|
|
VENUE_CACHE.write_text(json.dumps(out))
|
|
return out
|
|
except Exception as e: # rete KO: si dichiara
|
|
print(f" ⚠️ venue non raggiungibile ({type(e).__name__}) -> uso la cache se c'e'.")
|
|
return old
|
|
|
|
|
|
def walk_cost_bps(book: dict, usd: float, side: str) -> float | None:
|
|
"""Costo effettivo vs mid di un ordine da `usd` dollari, camminando il libro.
|
|
Ritorna None se il libro non basta (e allora si dice, non si estrapola)."""
|
|
if not book or not book.get("bid") or not book.get("ask"):
|
|
return None
|
|
mid = (book["bid"] + book["ask"]) / 2.0
|
|
lv = book.get("asks" if side == "buy" else "bids") or []
|
|
need, got, cost = usd / mid, 0.0, 0.0
|
|
for p, q in lv:
|
|
take = min(q, need - got)
|
|
got += take
|
|
cost += take * p
|
|
if got >= need - 1e-12:
|
|
break
|
|
if got < need - 1e-12:
|
|
return None
|
|
eff = cost / got
|
|
return (eff / mid - 1.0) * 1e4 * (1.0 if side == "buy" else -1.0)
|
|
|
|
|
|
def sezione0(V: dict | None) -> dict:
|
|
sez("0) IL DATO — 68 trimestrali datati su disco, e le specifiche LETTE dal venue")
|
|
tab = {}
|
|
for a in ASSETS:
|
|
rows = []
|
|
for p in sorted(FUT.glob(f"{a}-*.parquet")):
|
|
d = pd.read_parquet(p, columns=["ts", "close", "volume"])
|
|
if len(d) < 200:
|
|
continue
|
|
e = expdate(p.stem)
|
|
d = d[(d.ts <= e) & (d.close > 0)]
|
|
rows.append((p.stem, e, d.ts.min(), d.ts.max(), len(d), float(d.volume.sum())))
|
|
R = pd.DataFrame(rows, columns=["nm", "exp", "t0", "t1", "n", "vol"]).sort_values("exp")
|
|
tab[a] = R
|
|
print(f" {a}: {len(R)} contratti trimestrali usabili, scadenze da {R.exp.min().date()} "
|
|
f"a {R.exp.max().date()}, {int(R.n.sum()):,} barre orarie")
|
|
print(" ⚠️ Istantanea al 22/08 (il cron NON rinfresca questi file) e il contratto in corso")
|
|
print(" e' troncato. Il guardrail «solo dati certi» regge perche' vivono in una")
|
|
print(" sottocartella con prefisso, fuori dal namespace nudo di `load_data`.")
|
|
|
|
if not V:
|
|
print("\n ⚠️ SPECIFICHE DEL VENUE: NON MISURATE (nessuna rete, nessuna cache).")
|
|
return dict(tab=tab, spec=None)
|
|
|
|
print(f"\n Istantanea del venue del {V['ts'][:19]}Z — UNA lettura, non una distribuzione.")
|
|
print(f" {'strumento':<22} {'settl':>6} {'min':>9} {'tick':>7} {'taker':>7} "
|
|
f"{'spread':>8} {'vol 24h $':>13} {'OI':>14}")
|
|
for i in ("BTC_USDC-PERPETUAL", "BTC_USDC-25SEP26", "BTC_USDC-25DEC26", "BTC-25SEP26",
|
|
"BTC_USDC", "ETH_USDC-PERPETUAL", "ETH_USDC-25SEP26", "ETH_USDC-25DEC26",
|
|
"ETH-25SEP26", "ETH_USDC"):
|
|
s = V["instruments"].get(i, {})
|
|
b = V["books"].get(i, {})
|
|
sp = float("nan")
|
|
if b.get("bid") and b.get("ask"):
|
|
mid = (b["bid"] + b["ask"]) / 2
|
|
sp = (b["ask"] - b["bid"]) / mid * 1e4
|
|
print(f" {i:<22} {str(s.get('settlement')):>6} {str(s.get('min_trade_amount')):>9} "
|
|
f"{str(s.get('tick')):>7} {str(s.get('taker')):>7} {sp:>7.2f}b "
|
|
f"{(b.get('vol24usd') or float('nan')):>13,.0f} "
|
|
f"{(b.get('oi') or float('nan')):>14,.0f}")
|
|
return dict(tab=tab, spec=V)
|
|
|
|
|
|
# ===========================================================================
|
|
# 1) IL CARRY DEI DATATI — costruzione della serie
|
|
# ===========================================================================
|
|
def aligned_index(a: str, grid: pd.DatetimeIndex) -> np.ndarray:
|
|
"""Indice Deribit ALLINEATO alla convenzione delle barre orarie.
|
|
|
|
⚠️ TRAPPOLA MISURATA IN QUESTA SESSIONE, e non e' un dettaglio: `fund_{a}.parquet`
|
|
etichetta il record T con l'indice ALL'ISTANTE T, mentre `al.get(a,'1h')` etichetta la
|
|
barra all'INIZIO (la sua chiusura e' il prezzo a T+1h). Appaiare le due serie per
|
|
etichetta le sfasa di UN'ORA: corr(r_perp_t, r_index_t) = -0,02 e corr(r_perp_t,
|
|
r_index_t+1) = +0,996, la base perp/indice esce con sd 68-86 bps invece di pochi bps, e
|
|
la differenza di rendimento eredita 97-122 bps/ora di rumore INVENTATO.
|
|
Nessuna eccezione, nessun NaN: due serie plausibili che non parlano dello stesso istante.
|
|
(Stessa famiglia dell'errore di RISOLUZIONE `[ms]/[us]/[s]` catturato ieri notte in
|
|
r0822d_funding, e del `DatetimeIndex.view("int64")` codificato in CLAUDE.md il 01/07:
|
|
li' era la scala, qui e' l'ETICHETTA. Il sintomo e' lo stesso — un numero credibile.)
|
|
"""
|
|
fu = pd.read_parquet(FUT / f"fund_{a}.parquet").set_index("ts").sort_index()
|
|
return fu["index"].reindex(grid).shift(-1).values.astype(float)
|
|
|
|
|
|
def verifica_allineamento(grid: dict) -> None:
|
|
"""Controllo POSITIVO dell'allineamento: deve fallire se lo si sbaglia, o non e' un test."""
|
|
print("\n [ALLINEAMENTO] controllo positivo indice-vs-barre (corr dei log-rendimenti):")
|
|
for a in ASSETS:
|
|
fu = pd.read_parquet(FUT / f"fund_{a}.parquet").set_index("ts").sort_index()
|
|
raw = fu["index"].reindex(grid[a]).values.astype(float)
|
|
fix = aligned_index(a, grid[a])
|
|
pp = al.get(a, "1h").set_index("datetime")["close"].astype(float)
|
|
pp.index = pd.DatetimeIndex(pp.index)
|
|
pv = pp.reindex(grid[a]).values.astype(float)
|
|
out = []
|
|
for lbl, v in (("per etichetta", raw), ("allineato", fix)):
|
|
rP = np.diff(np.log(pv)); rI = np.diff(np.log(v))
|
|
m = np.isfinite(rP) & np.isfinite(rI)
|
|
b = pv / v
|
|
out.append((lbl, float(np.corrcoef(rP[m], rI[m])[0, 1]),
|
|
float(np.nanstd(b - 1.0)) * 1e4))
|
|
print(f" {a}: per etichetta corr {out[0][1]:+.4f} sd base {out[0][2]:6.1f} bps -> "
|
|
f"allineato corr {out[1][1]:+.4f} sd base {out[1][2]:5.1f} bps")
|
|
assert out[1][1] > 0.99 > out[0][1], f"allineamento non riparato su {a}"
|
|
print(" -> il controllo ha POTENZA: con l'etichetta grezza la correlazione e' ~0.")
|
|
|
|
|
|
def dated_rate(a: str, grid: pd.DatetimeIndex, buf: int = BUF_CANON,
|
|
log: bool = True) -> dict:
|
|
"""Pseudo-`rate` compatibile con `FD.cumF` / `FD.daily_funding`: al posto del funding
|
|
orario mette il CARRY del future datato front, cioe' quanto il datato sotto-rende il
|
|
perpetual in quell'ora (>0 in contango = costo per un long).
|
|
|
|
Il contratto tenuto durante l'ora t e' quello scelto alla chiusura di t-1 (nessuna
|
|
decisione usa il prezzo dell'ora che si sta valutando), e nell'ora di ROLL il rendimento
|
|
lo fa il contratto VECCHIO: cosi' non si perde nessuna ora e non si incolla nessun salto
|
|
di prezzo (convenzione dichiarata in r0822d_cc01_2022: si incollano i RENDIMENTI).
|
|
"""
|
|
n = len(grid)
|
|
idxp = aligned_index(a, grid)
|
|
perp = al.get(a, "1h").set_index("datetime")["close"].astype(float)
|
|
perp.index = pd.DatetimeIndex(perp.index)
|
|
pp = perp.reindex(grid).values.astype(float)
|
|
|
|
C = {}
|
|
for p in sorted(FUT.glob(f"{a}-*.parquet")):
|
|
d = pd.read_parquet(p, columns=["ts", "close", "volume"])
|
|
if len(d) < 200:
|
|
continue
|
|
e = expdate(p.stem)
|
|
d = d[(d.ts <= e) & (d.close > 0)].set_index("ts").sort_index()
|
|
C[p.stem] = (e, d["close"].reindex(grid).values.astype(float),
|
|
d["volume"].reindex(grid).values.astype(float))
|
|
order = sorted(C, key=lambda x: C[x][0])
|
|
|
|
px = np.full(n, np.nan); nmi = np.full(n, -1); dte = np.full(n, np.nan)
|
|
vol = np.full(n, np.nan)
|
|
for k, nme in enumerate(order):
|
|
e, s, v = C[nme]
|
|
dd = (e - grid).total_seconds().values / 86400.0
|
|
m = (dd >= buf) & np.isfinite(s) & np.isnan(px)
|
|
px[m] = s[m]; nmi[m] = k; dte[m] = dd[m]; vol[m] = v[m]
|
|
|
|
def ret(v: np.ndarray) -> np.ndarray:
|
|
r = np.full(n, np.nan)
|
|
r[1:] = (np.log(v[1:]) - np.log(v[:-1])) if log else (v[1:] / v[:-1] - 1.0)
|
|
return r
|
|
|
|
rP, rF = ret(pp), ret(px)
|
|
c = np.zeros(n); val = np.zeros(n, bool)
|
|
same = np.concatenate([[False], nmi[1:] == nmi[:-1]])
|
|
m = same & np.isfinite(rF) & np.isfinite(rP)
|
|
c[m] = rP[m] - rF[m]; val[m] = True
|
|
|
|
roll = np.zeros(n, bool)
|
|
ch = np.where((nmi[1:] != nmi[:-1]) & (nmi[1:] >= 0) & (nmi[:-1] >= 0))[0] + 1
|
|
for i in ch:
|
|
s = C[order[nmi[i - 1]]][1]
|
|
if np.isfinite(s[i]) and np.isfinite(s[i - 1]) and np.isfinite(rP[i]):
|
|
old = (np.log(s[i]) - np.log(s[i - 1])) if log else (s[i] / s[i - 1] - 1.0)
|
|
c[i] = rP[i] - old; val[i] = True
|
|
roll[ch] = True
|
|
|
|
return dict(ts=FD.to_ms(grid), i1h=c, cum=np.concatenate([[0.0], np.cumsum(c)]),
|
|
series=pd.Series(c, index=grid), switch=grid[0], valid=val, roll=roll,
|
|
bann=(px / idxp - 1.0) / (dte / 365.25), dte=dte, vol=vol, grid=grid,
|
|
nmi=nmi, order=order, px=px, idx=idxp, pp=pp)
|
|
|
|
|
|
def spot_rate(a: str, grid: pd.DatetimeIndex, log: bool = True) -> dict:
|
|
"""Stessa forma, ma il carry di esprimere la posizione sullo SPOT: quanto il perpetual
|
|
sovra-rende l'INDICE (= il costo che si evita non usando il perpetual). Lo spot vero di
|
|
Deribit non ha storia in questo progetto: si usa l'INDICE, dichiarato come tale."""
|
|
n = len(grid)
|
|
ix = aligned_index(a, grid)
|
|
perp = al.get(a, "1h").set_index("datetime")["close"].astype(float)
|
|
perp.index = pd.DatetimeIndex(perp.index)
|
|
pp = perp.reindex(grid).values.astype(float)
|
|
|
|
def ret(v):
|
|
r = np.full(n, np.nan)
|
|
r[1:] = (np.log(v[1:]) - np.log(v[:-1])) if log else (v[1:] / v[:-1] - 1.0)
|
|
return r
|
|
c = np.nan_to_num(ret(pp) - ret(ix))
|
|
return dict(ts=FD.to_ms(grid), i1h=c, cum=np.concatenate([[0.0], np.cumsum(c)]),
|
|
series=pd.Series(c, index=grid), switch=grid[0],
|
|
valid=np.isfinite(c), roll=np.zeros(n, bool), grid=grid)
|
|
|
|
|
|
# ===========================================================================
|
|
# 2) REPLICA DI CONTROLLO
|
|
# ===========================================================================
|
|
def book(rate_or_none, off: int = 0, tp_series: pd.Series | None = None) -> pd.Series:
|
|
"""Libro LIVE 75/25 alla lente `hourly`. ⚠️ `FD._SKH` e' cachato su
|
|
`(asset, off, mode, rate is not None)`: due `rate` DIVERSI collidono. Si svuota sempre."""
|
|
FD._SKH.clear()
|
|
s = FD.skh_book(off, "hourly", rate_or_none)
|
|
if s.index.tz is None:
|
|
s.index = s.index.tz_localize("UTC")
|
|
tp = tp_series if tp_series is not None else FD.tp01_realistic(ALLOC * W_TP,
|
|
rate_or_none)[0]
|
|
return combine_outer({"TP01": tp, "SKH01": s}, {"TP01": W_TP, "SKH01": W_SKH}, lo=LO_BOOK)
|
|
|
|
|
|
def sezione1(rate: dict) -> dict:
|
|
sez("1) REPLICA DI CONTROLLO — il -2,16%/anno prima di confrontarci qualsiasi cosa")
|
|
a0, a1 = FD.stat(book(None)), FD.stat(book(rate))
|
|
t0, t1 = FD.stat(FD.tp01_realistic(ALLOC * W_TP, None)[0]), \
|
|
FD.stat(FD.tp01_realistic(ALLOC * W_TP, rate)[0])
|
|
FD._SKH.clear(); s0 = FD.stat(FD.skh_book(0, "hourly", None))
|
|
FD._SKH.clear(); s1 = FD.stat(FD.skh_book(0, "hourly", rate))
|
|
print(f" {'serie':<26} {'ShFULL':>8} {'ShHOLD':>8} {'maxDD':>7} {'CAGR':>8} {'drift':>8}")
|
|
for nm, x, y in (("LIBRO 75/25 path live", a0, a1), ("TP01 realistic", t0, t1),
|
|
("SKH01 50/50 path live", s0, s1)):
|
|
print(f" {nm:<26} {x['full']:>8.3f} {x['hold']:>8.3f} {x['dd']:>7.1%} "
|
|
f"{x['cagr']:>8.2%} {x['drift']:>8.2%}")
|
|
print(f" {' + funding':<26} {y['full']:>8.3f} {y['hold']:>8.3f} {y['dd']:>7.1%} "
|
|
f"{y['cagr']:>8.2%} {y['drift']:>8.2%}")
|
|
print(f" {' DELTA':<26} {y['full']-x['full']:>+8.3f} {y['hold']-x['hold']:>+8.3f} "
|
|
f"{y['dd']-x['dd']:>+7.1%} {y['cagr']-x['cagr']:>+8.2%} "
|
|
f"{y['drift']-x['drift']:>+8.2%}")
|
|
d = a1["drift"] - a0["drift"]
|
|
ok = abs(d + 0.0216) < 0.0005
|
|
print(f"\n drift del libro {a0['drift']:.2%} -> {a1['drift']:.2%} = {d:+.3%}/anno "
|
|
f"contro il -2,16% pubblicato -> {'REPLICATO' if ok else '⚠️ NON REPLICA'}")
|
|
assert ok, "la replica del funding non torna: nessun delta oltre questo punto"
|
|
return dict(book0=a0, book1=a1, tp0=t0, tp1=t1, skh0=s0, skh1=s1)
|
|
|
|
|
|
# ===========================================================================
|
|
# 3) L'ESTIMATORE — livello vs flusso, in una veste nuova
|
|
# ===========================================================================
|
|
def sezione2(rate: dict, grid: dict) -> None:
|
|
sez("2) L'ESTIMATORE — la differenza ARITMETICA dei rendimenti NON e' il carry")
|
|
print(" Il costo di tenere un datato e' MOLTIPLICATIVO (il basis si consuma in")
|
|
print(" proporzione), il funding e' un FLUSSO di cassa (tasso x nozionale). Chi misura il")
|
|
print(" primo con una differenza aritmetica di rendimenti si porta dentro un termine di")
|
|
print(" VARIANZA: media(rA) - media(rB) = [log-drift] + (var(rA)-var(rB))/2. Fra due")
|
|
print(" strumenti sullo STESSO sottostante il log-drift e' il carry e la differenza di")
|
|
print(" varianza e' microstruttura (il datato e' meno scambiato, la sua barra oraria e'")
|
|
print(" piu' stantia, la varianza misurata piu' bassa).")
|
|
print("\n CONTROLLO che rende visibile l'artefatto senza discuterne: il PERPETUAL contro")
|
|
print(" l'INDICE. La sua base e' stazionaria intorno a ~3 bps, quindi il carry vero e' ~0.")
|
|
print(f" {'asset':>6} {'aritmetico':>12} {'log':>10} {'artefatto':>11} {'base perp media':>17}")
|
|
for a in ASSETS:
|
|
sa = spot_rate(a, grid[a], log=False)["i1h"]
|
|
sl = spot_rate(a, grid[a], log=True)["i1h"]
|
|
ix = aligned_index(a, grid[a])
|
|
pp = al.get(a, "1h").set_index("datetime")["close"].astype(float)
|
|
pp.index = pd.DatetimeIndex(pp.index)
|
|
bas = float(np.nanmean(pp.reindex(grid[a]).values / ix - 1.0)) * 1e4
|
|
print(f" {a:>6} {sa.mean()*24*365.25:>+11.3%} {sl.mean()*24*365.25:>+9.3%} "
|
|
f"{(sa.mean()-sl.mean())*24*365.25:>+10.3%} {bas:>16.1f}b")
|
|
print(" -> l'aritmetico attribuisce al perpetual +0,4/+0,5%/anno di 'sovra-rendimento'")
|
|
print(" su un indice da cui non si stacca mai: e' varianza, non carry. Il log da' ~0.")
|
|
print("\n Lo stesso artefatto sul DATATO, che e' l'oggetto della domanda:")
|
|
print(f" {'asset':>6} {'carry aritm.':>13} {'carry LOG':>11} {'artefatto':>11} "
|
|
f"{'funding':>9}")
|
|
for a in ASSETS:
|
|
da = dated_rate(a, grid[a], log=False)
|
|
dl = dated_rate(a, grid[a], log=True)
|
|
f = rate[a]["i1h"]
|
|
print(f" {a:>6} {da['i1h'][da['valid']].mean()*24*365.25:>+12.3%} "
|
|
f"{dl['i1h'][dl['valid']].mean()*24*365.25:>+10.3%} "
|
|
f"{(da['i1h'][da['valid']].mean()-dl['i1h'][dl['valid']].mean())*24*365.25:>+10.3%} "
|
|
f"{f.mean()*24*365.25:>+8.3%}")
|
|
print(" 🚨 L'artefatto vale 1,5-2,6 punti percentuali l'anno, cioe' DA 2 A 4 VOLTE")
|
|
print(" l'effetto che questo filone sta cercando (~0,7%/anno). Con l'estimatore")
|
|
print(" aritmetico il datato sembra costare 4,1-5,2%/anno contro un funding del")
|
|
print(" 6,4-7,3%: un vantaggio enorme, e per meta' inventato dalla formula.")
|
|
print(" 📌 REGOLA: fra due strumenti sullo stesso sottostante il carry si misura sul")
|
|
print(" LOG del rapporto dei prezzi (telescopico: cum(t) = ln(P_t/F_t) - ln(P_0/F_0)),")
|
|
print(" mai sulla media delle differenze di rendimento. E' la stessa forma dell'errore")
|
|
print(" gia' codificato il 26/07 sui veicoli UCITS («la deviazione fra due veicoli")
|
|
print(" sullo stesso indice si misura sul RAPPORTO CUMULATO»): li' erano due ETF, qui")
|
|
print(" due contratti, ed e' passato lo stesso.")
|
|
print(" Da qui in avanti: LOG. L'aritmetico non compare piu' in nessun numero.")
|
|
|
|
|
|
# ===========================================================================
|
|
# 4) IL CONFRONTO APPAIATO
|
|
# ===========================================================================
|
|
def sezione3(rate: dict, DR: dict) -> dict:
|
|
sez("3) IL CONFRONTO APPAIATO — per GIORNO e per SEGNALE, e la tesi condizionale")
|
|
_, dg = FD.tp01_realistic(ALLOC * W_TP, rate)
|
|
out = {}
|
|
print(" Segnale CONGELATO (stesso `pos` in entrambe le colonne): cambia solo lo strumento.")
|
|
print(f" {'asset':>5} {'esposiz.':>9} {'costo perp':>11} {'costo datato':>13} "
|
|
f"{'delta':>9} {'t':>6} {'IC95 (bootstrap)':>22}")
|
|
rng = np.random.default_rng(822)
|
|
for a in ASSETS:
|
|
idx = pd.DatetimeIndex(dg[a]["idx"])
|
|
pos = pd.Series(dg[a]["pos"], index=idx)
|
|
fD = pd.Series(FD.daily_funding(rate[a], idx), index=idx)
|
|
cD = pd.Series(FD.daily_funding(DR[a], idx), index=idx)
|
|
m = idx >= pd.Timestamp("2019-05-01", tz="UTC")
|
|
cp, cd = (pos * fD)[m], (pos * cD)[m]
|
|
d = (cd - cp).values
|
|
t = d.mean() / d.std() * np.sqrt(len(d))
|
|
# bootstrap a blocchi (20 giorni): il carry e' persistente, l'IID sottostima l'errore
|
|
nb = max(1, len(d) // 20)
|
|
bs = []
|
|
for _ in range(2000):
|
|
st = rng.integers(0, max(1, len(d) - 20), nb)
|
|
bs.append(np.mean(np.concatenate([d[s:s + 20] for s in st])))
|
|
lo, hi = np.percentile(bs, [2.5, 97.5]) * 365.25
|
|
out[a] = dict(perp=cp.mean() * 365.25, dat=cd.mean() * 365.25,
|
|
delta=d.mean() * 365.25, t=t, lo=lo, hi=hi, pos=pos, m=m)
|
|
print(f" {a:>5} {pos[m].mean():>9.4f} {out[a]['perp']:>10.3%} {out[a]['dat']:>12.3%} "
|
|
f"{out[a]['delta']:>+8.3%} {t:>+6.2f} [{lo:>+7.3%}, {hi:>+7.3%}]")
|
|
|
|
print("\n LA TESI: il funding e' caro PROPRIO QUANDO SIAMO LONG. Lo e' anche il basis?")
|
|
print(f" {'asset':>5} | {'funding incond':>14} {'condiz.':>9} {'rapporto':>9} "
|
|
f"| {'basis incond':>13} {'condiz.':>9} {'rapporto':>9}")
|
|
for a in ASSETS:
|
|
idx = pd.DatetimeIndex(dg[a]["idx"])
|
|
pos, m = out[a]["pos"], out[a]["m"]
|
|
bann = pd.Series(DR[a]["bann"], index=DR[a]["grid"]).resample("1D").mean().reindex(idx)
|
|
fann = pd.Series(rate[a]["i1h"], index=rate[a]["series"].index)\
|
|
.resample("1D").sum().reindex(idx) * 365.25
|
|
w = pos[m]
|
|
fu_, fc = float(fann[m].mean()), float((w * fann[m]).sum() / w.sum())
|
|
bu, bc = float(bann[m].mean()), float((w * bann[m]).sum() / w.sum())
|
|
out[a].update(f_unc=fu_, f_con=fc, b_unc=bu, b_con=bc)
|
|
print(f" {a:>5} | {fu_:>14.2%} {fc:>9.2%} {fc/fu_:>8.2f}x "
|
|
f"| {bu:>13.2%} {bc:>9.2%} {bc/bu:>8.2f}x")
|
|
print(" 📌 LA TESI REGGE, e in modo appaiato: il funding condizionale e' 1,9-2,6x")
|
|
print(" l'incondizionato (replica indipendente del 1,86-2,55x misurato stanotte), il")
|
|
print(" basis condizionale solo 1,5-1,7x. Il datato NON e' piu' economico in assoluto")
|
|
print(" — sui livelli incondizionati i due tassi sono quasi identici (6,4-7,5% contro")
|
|
print(" 6,6-7,3%, e BASIS-CALENDAR aveva gia' misurato che lo scarto contiene lo zero)")
|
|
print(" — lo diventa CONDIZIONATAMENTE, perche' si riprezza di meno contro di noi.")
|
|
print(" Il vantaggio non e' un premio: e' la parte di premio che il perpetual")
|
|
print(" ri-tariffa OGNI ORA e il datato ha gia' fissato all'ingresso.")
|
|
|
|
print("\n ⚠️ MA IL VANTAGGIO NON E' STAZIONARIO — delta per anno (>0 = il datato costa DI PIU'):")
|
|
print(f" {'anno':>6} {'delta BTC':>11} {'delta ETH':>11} {'funding medio':>15} "
|
|
f"{'basis medio':>13}")
|
|
for y in range(2019, 2027):
|
|
cells = []
|
|
for a in ASSETS:
|
|
idx = pd.DatetimeIndex(dg[a]["idx"])
|
|
pos = out[a]["pos"]
|
|
fD = pd.Series(FD.daily_funding(rate[a], idx), index=idx)
|
|
cD = pd.Series(FD.daily_funding(DR[a], idx), index=idx)
|
|
g = (pos * (cD - fD))
|
|
g = g[g.index.year == y]
|
|
cells.append(float(g.mean()) * 365.25 if len(g) > 30 else float("nan"))
|
|
fy = np.mean([rate[a]["series"][rate[a]["series"].index.year == y].mean()
|
|
for a in ASSETS]) * 24 * 365.25
|
|
by = np.nanmean([pd.Series(DR[a]["bann"], index=DR[a]["grid"])[
|
|
pd.Series(DR[a]["bann"], index=DR[a]["grid"]).index.year == y].mean()
|
|
for a in ASSETS])
|
|
print(f" {y:>6} {cells[0]:>+10.3%} {cells[1]:>+10.3%} {fy:>15.2%} {by:>13.2%}")
|
|
return out
|
|
|
|
|
|
# ===========================================================================
|
|
# 5) IL ROLL E LA LIQUIDITA'
|
|
# ===========================================================================
|
|
def sezione4(rate: dict, DR: dict, V: dict | None, grid: dict) -> dict:
|
|
sez("4) IL ROLL E LA LIQUIDITA' — il costo che il perpetual NON ha")
|
|
_, dg = FD.tp01_realistic(ALLOC * W_TP, rate)
|
|
print(" Il perpetuo non si rolla. Il datato si', e il roll e' FORZATO (data di scadenza),")
|
|
print(" non scelto. Quanto costa dipende da tre cose che si MISURANO: quante volte, su")
|
|
print(" quanto nozionale, e quanto costa attraversare il libro di un datato.\n")
|
|
print(f" {'asset':>5} {'roll/anno':>10} {'turnover TP01':>14} {'turnover SKH01':>15} "
|
|
f"{'nozion. medio TP01':>19}")
|
|
tov = {}
|
|
for a in ASSETS:
|
|
idx = pd.DatetimeIndex(dg[a]["idx"]); pos = dg[a]["pos"]
|
|
yrs = (idx[-1] - idx[0]).days / 365.25
|
|
to_tp = float(np.abs(np.diff(pos, prepend=0.0)).sum()) / yrs
|
|
rl = pd.Series(DR[a]["roll"].astype(float), index=DR[a]["grid"])\
|
|
.resample("1D").max().reindex(idx).fillna(0.0).values
|
|
FD._SKH.clear()
|
|
tr = FD.skh_daily(a, 0, "hourly", rate)[1]
|
|
yrs_s = (max(t["t_out"] for t in tr) - min(t["t_in"] for t in tr)) / 1000 / 86400 / 365.25
|
|
to_sk = 2 * len(tr) / yrs_s
|
|
tov[a] = dict(tp=to_tp, sk=to_sk, roll_year=float(rl.sum()) / yrs,
|
|
roll_turn=float((rl * np.abs(pos)).sum()) * 2 / yrs, pos=pos, idx=idx,
|
|
rl=rl, yrs=yrs)
|
|
print(f" {a:>5} {tov[a]['roll_year']:>10.2f} {to_tp:>13.2f}x {to_sk:>14.2f}x "
|
|
f"{pos.mean()*ALLOC*W_TP/2:>18,.2f}$")
|
|
print(" (turnover = somma di |Δnozionale| per anno, in multipli del capitale d'asset)")
|
|
|
|
print("\n QUANTO COSTA ATTRAVERSARE — libro reale, ordine da $100/$240/$500 ($240 = il")
|
|
print(" nozionale massimo per asset di TP01 a $635). UNA istantanea; se il libro non basta")
|
|
print(" si dice, non si estrapola:")
|
|
xs = {}
|
|
if V:
|
|
print(f" {'strumento':<22} " + " ".join(f"{f'${u}':>10}" for u in TICKETS))
|
|
for i in ("BTC_USDC-PERPETUAL", "BTC_USDC-25SEP26", "BTC_USDC",
|
|
"ETH_USDC-PERPETUAL", "ETH_USDC-25SEP26", "ETH_USDC"):
|
|
b = V["books"].get(i)
|
|
cells, vals = [], []
|
|
for u in TICKETS:
|
|
cb = walk_cost_bps(b, u, "buy"); cs = walk_cost_bps(b, u, "sell")
|
|
if cb is None or cs is None:
|
|
cells.append(f"{'LIBRO KO':>10}")
|
|
else:
|
|
v = (cb + cs) / 2
|
|
vals.append(v); cells.append(f"{v:>9.2f}b")
|
|
xs[i] = float(np.mean(vals)) if vals else None
|
|
print(f" {i:<22} " + " ".join(cells))
|
|
print(" ⚠️ Il datato USDC ha un libro 45x piu' sottile del perpetual USDC per volume")
|
|
print(" 24h, ma a $240 di ticket si resta dentro i primi livelli: a QUESTA taglia")
|
|
print(" la liquidita' non e' il vincolo, il MEZZO SPREAD si'.")
|
|
else:
|
|
print(" ⚠️ NON MISURATO (nessuna istantanea del venue).")
|
|
|
|
ov = {}
|
|
if xs:
|
|
print(f"\n SOVRACOSTO D'ESECUZIONE, per sleeve e per anno (fee reale {FEE_REALE*1e4:.1f}")
|
|
print(" bps/lato su perp e datato, 0 sullo spot — letta dal venue; mezzo spread misurato):")
|
|
print(f" {'':<24} {'BTC':>12} {'ETH':>12} {'sleeve 50/50':>14}")
|
|
for lbl, ip, sfee in (("datato vs perp", "-25SEP26", FEE_REALE),
|
|
("spot vs perp", "", 0.0)):
|
|
per = {}
|
|
for a in ASSETS:
|
|
base = xs.get(f"{a}_USDC-PERPETUAL")
|
|
alt = xs.get(f"{a}_USDC{ip}" if ip else f"{a}_USDC")
|
|
if base is None or alt is None:
|
|
per[a] = float("nan"); continue
|
|
dside = (alt * 1e-4 + sfee) - (base * 1e-4 + FEE_REALE)
|
|
extra = tov[a]["tp"] * dside
|
|
if ip: # il roll aggiunge turnover solo al datato
|
|
extra += tov[a]["roll_turn"] * (alt * 1e-4 + sfee)
|
|
per[a] = extra
|
|
ov[lbl] = per
|
|
print(f" {'TP01 ' + lbl:<24} {per['BTC']:>+11.3%} {per['ETH']:>+11.3%} "
|
|
f"{np.nanmean(list(per.values())):>+13.3%}")
|
|
per = {}
|
|
for a in ASSETS:
|
|
base, alt = xs.get(f"{a}_USDC-PERPETUAL"), xs.get(f"{a}_USDC-25SEP26")
|
|
per[a] = tov[a]["sk"] * ((alt * 1e-4 + FEE_REALE) - (base * 1e-4 + FEE_REALE)) \
|
|
if base is not None and alt is not None else float("nan")
|
|
ov["SKH datato"] = per
|
|
print(f" {'SKH01 datato vs perp':<24} {per['BTC']:>+11.3%} {per['ETH']:>+11.3%} "
|
|
f"{np.nanmean(list(per.values())):>+13.3%}")
|
|
print(" 📌 Il turnover di SKH01 e' 48-57x l'anno contro i 6,6-8,4x di TP01: lo stesso")
|
|
print(" mezzo spread costa a SKH01 ~7 volte tanto. Un cambio di strumento si")
|
|
print(" giudica per SLEEVE, non per libro.")
|
|
|
|
print("\n NETTING — oggi TP01 e SKH01 si compensano sullo STESSO strumento; separarli lo")
|
|
print(" perde. Quanto vale? (griglia giornaliera, approssimazione dichiarata)")
|
|
for a in ASSETS:
|
|
FD._SKH.clear()
|
|
tr = FD.skh_daily(a, 0, "hourly", rate)[1]
|
|
idx = tov[a]["idx"]
|
|
ps = pd.Series(0.0, index=idx)
|
|
for t in tr:
|
|
t0 = pd.Timestamp(t["t_in"], unit="ms", tz="UTC").floor("D")
|
|
t1 = pd.Timestamp(t["t_out"], unit="ms", tz="UTC").floor("D")
|
|
ps[(idx >= t0) & (idx <= t1)] += t["dir"]
|
|
ps = ps.clip(-1, 1)
|
|
nT = W_TP * pd.Series(tov[a]["pos"], index=idx); nS = W_SKH * ps
|
|
sep = float((nT.diff().abs().fillna(abs(nT.iloc[0]))
|
|
+ nS.diff().abs().fillna(abs(nS.iloc[0]))).sum()) / tov[a]["yrs"]
|
|
net = float((nT + nS).diff().abs().fillna(abs(nT.iloc[0] + nS.iloc[0])).sum()) / tov[a]["yrs"]
|
|
print(f" {a}: turnover separato {sep:.2f}x/anno · nettato {net:.2f}x/anno -> il netting "
|
|
f"vale {1-net/sep:.1%}")
|
|
print(" -> trascurabile: i due sleeve tradano quasi sempre in momenti diversi. Mettere")
|
|
print(" TP01 e SKH01 su due strumenti NON e' un argomento contro.")
|
|
return dict(tov=tov, xs=xs, ov=ov)
|
|
|
|
|
|
# ===========================================================================
|
|
# 6) LE SERIE + BANDA D'ANCORA
|
|
# ===========================================================================
|
|
def overlay(series: pd.Series, tov: dict, per_asset_bps: dict, roll: bool) -> pd.Series:
|
|
"""Sottrae il sovracosto d'esecuzione GIORNO PER GIORNO (|Δpos| e roll), non come media
|
|
annua: e' un costo di turnover, e il turnover non e' uniforme nel tempo."""
|
|
tot = None
|
|
for a in ASSETS:
|
|
if not np.isfinite(per_asset_bps.get(a, float("nan"))):
|
|
return series
|
|
pos, idx, rl = tov[a]["pos"], tov[a]["idx"], tov[a]["rl"]
|
|
c = np.abs(np.diff(pos, prepend=0.0)) * per_asset_bps[a]
|
|
if roll:
|
|
c = c + 2.0 * np.abs(pos) * rl * per_asset_bps[a]
|
|
s = pd.Series(0.5 * c, index=idx)
|
|
tot = s if tot is None else tot.add(s, fill_value=0.0)
|
|
return series.sub(tot.reindex(series.index).fillna(0.0))
|
|
|
|
|
|
def sezione5(rate: dict, DR: dict, SR: dict, ex: dict) -> dict:
|
|
sez("5) LE SERIE — il libro nelle varianti, netto tutto, e la banda d'ancora")
|
|
tov, xs = ex["tov"], ex["xs"]
|
|
ds = {a: (xs[f"{a}_USDC-25SEP26"] - xs[f"{a}_USDC-PERPETUAL"]) * 1e-4
|
|
for a in ASSETS} if xs else {a: float("nan") for a in ASSETS}
|
|
ss = {a: (xs[f"{a}_USDC"] * 1e-4 + 0.0) - (xs[f"{a}_USDC-PERPETUAL"] * 1e-4 + FEE_REALE)
|
|
for a in ASSETS} if xs else {a: float("nan") for a in ASSETS}
|
|
|
|
tp = {k: FD.tp01_realistic(ALLOC * W_TP, r)[0] for k, r in
|
|
(("nessun carry", None), ("perp", rate), ("datato", DR), ("spot", SR))}
|
|
tp["datato+esec"] = overlay(tp["datato"], tov, ds, roll=True)
|
|
tp["spot+esec"] = overlay(tp["spot"], tov, ss, roll=False)
|
|
|
|
var = {}
|
|
var["A) perp (IL LIBRO DI OGGI)"] = book(rate)
|
|
var["B) tutto sui datati"] = book(DR)
|
|
var["C) TP01 datato + SKH01 perp"] = book(rate, tp_series=tp["datato"])
|
|
var["D) C + sovracosto d'esecuzione"] = book(rate, tp_series=tp["datato+esec"])
|
|
var["E) TP01 spot + SKH01 perp"] = book(rate, tp_series=tp["spot+esec"])
|
|
var["F) nessun carry (irreale)"] = book(None)
|
|
|
|
print(f" {'variante':<32} {'ShFULL':>8} {'ShHOLD':>8} {'maxDD':>7} {'vol':>7} "
|
|
f"{'CAGR':>8} {'drift':>8} {'Δdrift':>8}")
|
|
base = FD.stat(var["A) perp (IL LIBRO DI OGGI)"])
|
|
st = {}
|
|
for k, s in var.items():
|
|
m = FD.stat(s); st[k] = m
|
|
print(f" {k:<32} {m['full']:>8.3f} {m['hold']:>8.3f} {m['dd']:>7.1%} {m['vol']:>7.2%} "
|
|
f"{m['cagr']:>8.2%} {m['drift']:>8.2%} {m['drift']-base['drift']:>+8.2%}")
|
|
|
|
print("\n 📌 QUELLO CHE LA COLONNA `Δdrift` DA SOLA NASCONDE: il datato aggiunge drift E")
|
|
print(" VOLATILITA'. Il basis non e' costante — si riprezza — e quella oscillazione")
|
|
print(" entra nel rendimento della posizione. Il funding invece e' un flusso di cassa")
|
|
print(" che NON aggiunge varianza al prezzo. Per questo lo Sharpe del libro PEGGIORA")
|
|
print(" mentre il drift migliora. Chi cita solo il drift sta vendendo un cambio di")
|
|
print(" strumento come un miglioramento quando e' uno SCAMBIO.")
|
|
|
|
print("\n SLEEVE TP01 da solo (dove il cambio ha senso), stessa lente:")
|
|
print(f" {'':<26} {'ShFULL':>8} {'ShHOLD':>8} {'maxDD':>7} {'vol':>7} {'drift':>8}")
|
|
for k in ("nessun carry", "perp", "datato", "datato+esec", "spot", "spot+esec"):
|
|
m = FD.stat(tp[k])
|
|
print(f" {k:<26} {m['full']:>8.3f} {m['hold']:>8.3f} {m['dd']:>7.1%} {m['vol']:>7.2%} "
|
|
f"{m['drift']:>8.2%}")
|
|
|
|
print("\n FINESTRE — il numero va citato con la finestra (regola di stanotte):")
|
|
print(f" {'variante':<32} {'FULL':>9} {'>=2022-03':>11} {'2025+ (hold-out)':>18}")
|
|
lo1, lo2 = pd.Timestamp("2022-03-09", tz="UTC"), pd.Timestamp("2025-01-01", tz="UTC")
|
|
A = var["A) perp (IL LIBRO DI OGGI)"]
|
|
for k in ("B) tutto sui datati", "C) TP01 datato + SKH01 perp",
|
|
"D) C + sovracosto d'esecuzione", "E) TP01 spot + SKH01 perp"):
|
|
B = var[k]
|
|
cells = []
|
|
for lo in (None, lo1, lo2):
|
|
a_, b_ = (A, B) if lo is None else (A[A.index >= lo], B[B.index >= lo])
|
|
cells.append((float(b_.mean()) - float(a_.mean())) * 365.25)
|
|
print(f" {k:<32} {cells[0]:>+8.2%} {cells[1]:>+10.2%} {cells[2]:>+17.2%}")
|
|
|
|
print(f"\n{clock()} BANDA D'ANCORA — 23 offset della griglia 230m/690m di SKH01, mediana")
|
|
print(" delle DIFFERENZE APPAIATE (mai la differenza delle mediane).")
|
|
bands = {}
|
|
for k, tps, rr in (("C) TP01 datato", tp["datato"], rate),
|
|
("D) C + esecuzione", tp["datato+esec"], rate),
|
|
("E) TP01 spot", tp["spot+esec"], rate)):
|
|
d = {"sh": [], "hold": [], "dd": [], "drift": []}
|
|
for off in r02.OFFSETS:
|
|
a_ = FD.stat(book(rate, off=off))
|
|
b_ = FD.stat(book(rr, off=off, tp_series=tps))
|
|
d["sh"].append(b_["full"] - a_["full"]); d["hold"].append(b_["hold"] - a_["hold"])
|
|
d["dd"].append(b_["dd"] - a_["dd"]); d["drift"].append(b_["drift"] - a_["drift"])
|
|
bands[k] = {m: np.array(v) for m, v in d.items()}
|
|
print(f" {k}:")
|
|
for m, lbl in (("drift", "Δdrift"), ("sh", "ΔShFULL"), ("hold", "ΔShHOLD"),
|
|
("dd", "ΔmaxDD")):
|
|
v = bands[k][m]
|
|
flat = (v.max() - v.min()) < 1e-6
|
|
print(f" {lbl:<9} mediana appaiata {np.median(v):>+8.4f} positiva in "
|
|
f"{np.mean(v>0):>5.0%} dei 23 offset [min {v.min():+.4f}, max {v.max():+.4f}]"
|
|
f"{' <- COSTANTE: nessuna potenza, vedi sotto' if flat else ''}")
|
|
print(" ⚠️ Il Δdrift e' COSTANTE su tutti e 23 gli offset, e non e' robustezza: e'")
|
|
print(" un'identita'. Cambia solo la gamba TP01, che non dipende dall'offset di SKH01,")
|
|
print(" e il drift di una somma pesata e' la somma pesata dei drift -> «positivo a")
|
|
print(" 23/23» qui NON dice niente. La banda ha potenza sullo SHARPE e sul maxDD (che")
|
|
print(" dipendono dalla covarianza fra le gambe), e li' il verdetto e' netto:")
|
|
print(" l'hold-out del datato e' NEGATIVO a 0/23 offset, con e senza sovracosto.")
|
|
print(" (Un controllo che non puo' fallire e' esattamente cio' che questo progetto")
|
|
print(" chiama «un test senza potenza»: si dichiara, non si conta come conferma.)")
|
|
return dict(var=var, st=st, tp=tp, bands=bands, ds=ds, ss=ss)
|
|
|
|
|
|
# ===========================================================================
|
|
# 7) I MURI
|
|
# ===========================================================================
|
|
def sezione6(res5: dict) -> None:
|
|
sez("6) I MURI DI CAPITALE — la moneta giusta, perche' la rendita vive sul DRIFT")
|
|
need = CC.TARGET_EUR_DAY * 365.0 * CC.EURUSD / (1 - CC.TAX_RATE)
|
|
print(f" bersaglio 50 EUR/g netti = ${need:,.0f}/anno lordi (aliquota {CC.TAX_RATE:.0%}),")
|
|
print(f" de-luck del drift x{DELUCK} (MISURATO), MC {WR.N_PATHS} path -> risoluzione ~1%:")
|
|
print(" si citano 3 cifre significative, non il dollaro.\n")
|
|
print(f" {'variante':<32} {'drift':>8} {'vol':>7} {'perpetua':>9} {'muro':>12} {'Δmuro':>8}")
|
|
ref = None
|
|
for k in ("A) perp (IL LIBRO DI OGGI)", "D) C + sovracosto d'esecuzione",
|
|
"E) TP01 spot + SKH01 perp", "F) nessun carry (irreale)"):
|
|
r = res5["var"][k].values.astype(float)
|
|
r = r - (1.0 - DELUCK) * float(r.mean())
|
|
_, perp, cap = WR.perp_and_wall(r, 1.0, need)
|
|
if ref is None:
|
|
ref = cap
|
|
print(f" {k:<32} {float(r.mean())*365.25:>8.2%} {float(r.std())*np.sqrt(365.25):>7.2%} "
|
|
f"{perp:>9.2%} ${cap:>11,.0f} {cap/ref-1:>+7.1%}")
|
|
print("\n 📌 Il muro pubblicato stanotte e' $325,0k col funding dentro (era $276,6k senza).")
|
|
print(" Qui si misura il DELTA con la stessa macchineria, non un muro nuovo: la")
|
|
print(" differenza di livello viene dal de-luck e dal MC, non dal cambio di strumento.")
|
|
|
|
|
|
# ===========================================================================
|
|
# 8) IL MURO CHE NON C'E'
|
|
# ===========================================================================
|
|
def sezione7(V: dict | None) -> None:
|
|
sez("7) IL MURO CHE NON C'E' — «tutti i datati Deribit sono INVERSE» e' FALSO")
|
|
print(" La sezione 34 di RESULTS-0822 chiude cosi': «tutti i futures datati Deribit sono")
|
|
print(" INVERSE: non esiste una linea USDC datata — stesso muro delle opzioni, e il")
|
|
print(" collaterale in BTC e' esso stesso esposizione direzionale. Non risolto.»")
|
|
print(" Il briefing di questo filone lo eredita come «probabilmente il muro che uccide il")
|
|
print(" filone». E' l'unica cosa che si poteva verificare con UNA chiamata pubblica.\n")
|
|
if not V:
|
|
print(" ⚠️ NON VERIFICATO in questa corsa (nessuna rete, nessuna cache).")
|
|
return
|
|
ins = V["instruments"]
|
|
usdc_dated = sorted(n for n, s in ins.items()
|
|
if s.get("settlement") == "USDC" and s.get("kind") == "future"
|
|
and "PERPETUAL" not in n and (n.startswith("BTC_USDC")
|
|
or n.startswith("ETH_USDC")))
|
|
print(f" MISURATO: {len(usdc_dated)} futures DATATI con settlement **USDC** su BTC/ETH.")
|
|
for n in usdc_dated:
|
|
s = ins[n]
|
|
cre = dt.datetime.utcfromtimestamp(s["created"] / 1000).date()
|
|
exp = dt.datetime.utcfromtimestamp(s["expiry"] / 1000).date()
|
|
print(f" {n:<20} creato {cre} scade {exp} min {s['min_trade_amount']} "
|
|
f"tick {s['tick']} taker {s['taker']}")
|
|
q = [n for n in usdc_dated if n.endswith(("SEP26", "DEC26", "MAR27", "JUN27"))]
|
|
print(f"\n ✅ Il muro NON esiste: le due gambe del libro possono restare in USDC. I")
|
|
print(" trimestrali USDC hanno lo STESSO min_trade_amount del perpetual lineare")
|
|
print(" (0,0001 BTC ~ $7,7 · 0,001 ETH ~ $2,4) e la STESSA fee taker: a $635")
|
|
print(" l'eseguibilita' non e' il vincolo (6a volta in questa ondata).")
|
|
print(" ⚠️ E il collaterale in cripto non era nemmeno l'argomento giusto: se lo fosse")
|
|
print(" stato, il costo non sarebbe il carry ma il fatto che un conto denominato in")
|
|
print(" BTC e' long 1,0x anche da FLAT — e TP01 sta flat nel 44% dei giorni, cioe' si")
|
|
print(" sarebbe distrutto esattamente cio' per cui TP01 esiste (il taglio del DD).")
|
|
print("\n ⚠️ IL MURO VERO E' UN ALTRO, ed e' una DATA: la linea USDC datata e' nata il")
|
|
print(f" 2026-05-07 ({len(q)} trimestrali esistono, il secondo creato il 2026-08-06)")
|
|
print(" -> ~3,5 mesi di vita e ZERO roll trimestrali mai eseguiti su quella linea.")
|
|
print(" Tutta la misura di questo filone gira sui trimestrali INVERSE, che hanno 7,3")
|
|
print(" anni: e' un PROXY, esattamente come l'inverse lo era per il funding pre-2022.")
|
|
print(" ✅ E il proxy si VALIDA invece di assumerlo — i due contratti sulla stessa")
|
|
print(" scadenza devono prezzare lo stesso carry, e lo fanno:")
|
|
for a in ASSETS:
|
|
bi, bu = V["books"].get(f"{a}-25SEP26"), V["books"].get(f"{a}_USDC-25SEP26")
|
|
if not bi or not bu or not bi.get("index"):
|
|
continue
|
|
mi = (bi["bid"] + bi["ask"]) / 2; mu = (bu["bid"] + bu["ask"]) / 2
|
|
print(f" {a}-25SEP26 (inverse) base {(mi/bi['index']-1)*1e4:+.1f} bps vs "
|
|
f"{a}_USDC-25SEP26 (lineare) base {(mu/bu['index']-1)*1e4:+.1f} bps "
|
|
f"-> scarto {(mu/bu['index']-mi/bi['index'])*1e4:+.1f} bps")
|
|
print(" 📌 REGOLA: un muro ereditato da un'altra sessione e' un'IPOTESI, e prima di")
|
|
print(" costruirci sopra un verdetto si ri-verifica. Qui e' costato una GET pubblica,")
|
|
print(" e la frase era diventata falsa 3 mesi prima che venisse scritta.")
|
|
print(" (Stessa forma della lezione del 26/07: «un blocco dichiarato e' un'ipotesi».)")
|
|
|
|
|
|
# ===========================================================================
|
|
# 9) SOTTOPRODOTTO — LO SPOT
|
|
# ===========================================================================
|
|
def sezione8(V: dict | None, res5: dict, rate: dict, SR: dict) -> None:
|
|
sez("8) SOTTOPRODOTTO — lo strumento a carry ZERO esiste, si chiama SPOT")
|
|
print(" La domanda del filone era «datato o perpetual». Cercando le specifiche e' saltato")
|
|
print(" fuori il terzo: Deribit quota SPOT BTC_USDC/ETH_USDC. Uno spot non paga funding,")
|
|
print(" non ha basis e non si rolla: il suo carry e' ZERO per costruzione.")
|
|
if V:
|
|
for i in ("BTC_USDC", "ETH_USDC"):
|
|
s = V["instruments"].get(i, {})
|
|
b = V["books"].get(i, {})
|
|
sp = (b["ask"] - b["bid"]) / ((b["ask"] + b["bid"]) / 2) * 1e4 if b.get("bid") else float("nan")
|
|
print(f" {i:<10} min {s.get('min_trade_amount')} taker {s.get('taker')} "
|
|
f"maker {s.get('maker')} spread {sp:.2f} bps vol 24h "
|
|
f"${(b.get('vol24usd') or float('nan')):,.0f}")
|
|
print(" ⚠️ La fee ZERO e' quella di OGGI ed e' dichiarata temporanea nell'annuncio del")
|
|
print(" 1° agosto («spot a zero fino al collegamento con Coinbase»): e' un tier che")
|
|
print(" puo' sparire, e `fee_watch` sorveglia i perpetual, non lo spot.")
|
|
print("\n Perche' funziona SOLO su TP01, e la misura che lo dimostra:")
|
|
_, dg = FD.tp01_realistic(ALLOC * W_TP, rate)
|
|
for a in ASSETS:
|
|
pos = dg[a]["pos"]
|
|
print(f" {a}: TP01 e' long-flat e il suo target per asset supera 1,0x nel "
|
|
f"{np.mean(pos > 1.0):.3%} dei giorni (massimo {pos.max():.3f}) -> il tetto")
|
|
print(f" strutturale dello spot (niente leva, niente short) e' quasi mai binding.")
|
|
print(" SKH01 e' long/SHORT a nozionale 1,0x: sullo spot non e' esprimibile. Meta' dei")
|
|
print(" suoi trade sono short e li' lo spot non esiste come strumento.")
|
|
print("\n DECOMPOSIZIONE del vantaggio dello spot su TP01 — due pezzi, non uno:")
|
|
mnc = FD.stat(res5["tp"]["nessun carry"]); msp = FD.stat(res5["tp"]["spot"])
|
|
mpe = FD.stat(res5["tp"]["perp"])
|
|
print(f" (1) funding evitato {mnc['drift']-mpe['drift']:>+7.2%}/anno "
|
|
f"(perp {mpe['drift']:.2%} -> senza-carry {mnc['drift']:.2%})")
|
|
print(f" (2) base del perpetual {msp['drift']-mnc['drift']:>+7.2%}/anno "
|
|
f"(senza-carry {mnc['drift']:.2%} -> indice {msp['drift']:.2%})")
|
|
print(" ❌ IPOTESI MIA, NATA E REFUTATA NELLA STESSA SESSIONE: mi aspettavo che il")
|
|
print(" secondo pezzo fosse una TERZA conferma della tesi condizionale — «quando siamo")
|
|
print(" long il perpetual e' a premio e quel premio decade». La prima stesura lo")
|
|
print(" misurava a +0,32%/anno e ci avrei creduto: era il MISALLINEAMENTO di un'ora")
|
|
print(" fra indice e barre (vedi [ALLINEAMENTO] in testa). Allineato, il pezzo vale")
|
|
print(" +0,07%/anno: il VERSO e' quello della tesi e l'IC95 esclude lo zero per un")
|
|
print(" soffio su entrambi gli asset, ma la TAGLIA e' il 3% del pezzo (1). Un verso")
|
|
print(" giusto a un trentesimo della grandezza non e' una conferma, e' un residuo:")
|
|
print(" tutto il vantaggio dello spot e' il funding evitato, e va detto cosi'.")
|
|
print(" 📌 E il modo in cui l'artefatto si e' fatto vedere e' la cosa da ricordare: non")
|
|
print(" dal numero, che era plausibile e nel verso atteso, ma dalla VOLATILITA' — la")
|
|
print(" variante spot mostrava +1,07pp di vol su una strategia identica, e uno strumento")
|
|
print(" che traccia lo stesso sottostante non puo' aggiungere un punto di vol.")
|
|
rngz = np.random.default_rng(823)
|
|
for a in ASSETS:
|
|
idx = pd.DatetimeIndex(dg[a]["idx"]); pos = pd.Series(dg[a]["pos"], index=idx)
|
|
c = pd.Series(FD.daily_funding(SR[a], idx), index=idx)
|
|
d = (pos * c).values
|
|
nb = max(1, len(d) // 20)
|
|
bs = [np.mean(np.concatenate([d[x:x+20] for x in
|
|
rngz.integers(0, max(1, len(d)-20), nb)]))
|
|
for _ in range(1000)]
|
|
lo, hi = np.percentile(bs, [2.5, 97.5]) * 365.25
|
|
print(f" {a}: base del perpetual pagata da TP01 {d.mean()*365.25:+.3%}/anno "
|
|
f"IC95 a blocchi [{lo:+.3%}, {hi:+.3%}]")
|
|
|
|
m0 = FD.stat(res5["tp"]["perp"]); m1 = FD.stat(res5["tp"]["spot+esec"])
|
|
print(f"\n TP01: drift {m0['drift']:.2%} -> {m1['drift']:.2%} ({m1['drift']-m0['drift']:+.2%}/anno), "
|
|
f"Sharpe {m0['full']:.3f} -> {m1['full']:.3f}, maxDD {m0['dd']:.1%} -> {m1['dd']:.1%}")
|
|
print(" 📌 E' l'unica variante che alza il drift SENZA alzare la volatilita' (vol di TP01")
|
|
print(" 12,16% sul perpetual e 12,16% sullo spot, alla terza cifra): lo spot non ha una")
|
|
print(" base che si riprezza. Il datato invece scambia carry contro varianza, ed e' per")
|
|
print(" questo che il suo Sharpe non migliora mentre il suo drift si'.")
|
|
print("\n ⚠️ MA C'E' UNA DOMANDA APERTA CHE VALE PIU' DEL RISPARMIO, e non e' risolvibile")
|
|
print(" con un backtest. CLAUDE.md la registra gia' (07/08): i DERIVATI su cripto")
|
|
print(" potrebbero qualificarsi `c-quater` (26%) invece di `c-sexies` (33%). Se e'")
|
|
print(" cosi', spostare TP01 dal perpetual (derivato) allo SPOT (cripto-attivita')")
|
|
print(" sposta il suo P&L nel comparto al 33%.")
|
|
cagr = FD.stat(res5["var"]["A) perp (IL LIBRO DI OGGI)"])["cagr"]
|
|
print(f" Ordine di grandezza: 7 punti di aliquota su un CAGR di libro del {cagr:.1%} "
|
|
f"valgono ~{0.07*cagr:.2%}/anno,")
|
|
print(" cioe' PIU' del risparmio di carry che lo spot porta. La risposta e' del")
|
|
print(" commercialista, non del backtest — ed e' esattamente la stessa domanda gia'")
|
|
print(" aperta nel progetto, che ora ha un secondo motivo per essere posta.")
|
|
if V:
|
|
xs = {}
|
|
for i in (f"{a}_USDC" for a in ASSETS):
|
|
xs[i] = np.mean([x for x in (walk_cost_bps(V["books"].get(i), u, sd)
|
|
for u in TICKETS for sd in ("buy", "sell"))
|
|
if x is not None]) if V["books"].get(i) else float("nan")
|
|
for i in (f"{a}_USDC-PERPETUAL" for a in ASSETS):
|
|
xs[i] = np.mean([x for x in (walk_cost_bps(V["books"].get(i), u, sd)
|
|
for u in TICKETS for sd in ("buy", "sell"))
|
|
if x is not None]) if V["books"].get(i) else float("nan")
|
|
_, dgz = FD.tp01_realistic(ALLOC * W_TP, rate)
|
|
now, later = [], []
|
|
for a in ASSETS:
|
|
to = float(np.abs(np.diff(dgz[a]["pos"], prepend=0.0)).sum()) / \
|
|
((pd.DatetimeIndex(dgz[a]["idx"])[-1]
|
|
- pd.DatetimeIndex(dgz[a]["idx"])[0]).days / 365.25)
|
|
base = xs[f"{a}_USDC-PERPETUAL"] * 1e-4 + FEE_REALE
|
|
now.append(to * (xs[f"{a}_USDC"] * 1e-4 + 0.0 - base))
|
|
later.append(to * (xs[f"{a}_USDC"] * 1e-4 + FEE_REALE - base))
|
|
print(f"\n ⚠️ E se la fee spot smettesse di essere zero: il sovracosto d'esecuzione di")
|
|
print(f" TP01 passa da {np.mean(now):+.3%}/anno a {np.mean(later):+.3%}/anno, cioe' "
|
|
f"mangia il {(np.mean(later)-np.mean(now))/0.0207:.0%} del vantaggio")
|
|
print(f" (che a livello di sleeve vale ~2,07%/anno). Sopravviverebbe, ma va")
|
|
print(f" sorvegliato: `fee_watch` oggi guarda i perpetual, non lo spot.")
|
|
print(" ⚠️ Secondo limite dichiarato: qui lo spot e' modellato con l'INDICE Deribit, non")
|
|
print(" col libro spot vero (che non ha storia in questo progetto). Sul carry e'")
|
|
print(" corretto per costruzione (l'indice E' il sottostante); sull'esecuzione no, e")
|
|
print(" infatti il mezzo spread e' preso dall'istantanea del libro vero.")
|
|
|
|
|
|
# ===========================================================================
|
|
# 10) GATE
|
|
# ===========================================================================
|
|
def sezione9(rate: dict, DR: dict, res5: dict, grid: dict) -> None:
|
|
sez("9) GATE — esito di ciascuno; quelli non girati sono DICHIARATI, non omessi")
|
|
|
|
# causalita': controllo POSITIVO (un test che non puo' fallire non e' un test)
|
|
a = "BTC"
|
|
_, dg = FD.tp01_realistic(ALLOC * W_TP, rate)
|
|
idx = pd.DatetimeIndex(dg[a]["idx"]); pos = pd.Series(dg[a]["pos"], index=idx)
|
|
c = pd.Series(FD.daily_funding(DR[a], idx), index=idx)
|
|
base = float((pos * c).mean()) * 365.25
|
|
ahead = float((pos * c.shift(-1).fillna(0.0)).mean()) * 365.25
|
|
lag = float((pos * c.shift(1).fillna(0.0)).mean()) * 365.25
|
|
print(f" causality : il carry dell'ora t si applica alla posizione tenuta in t,")
|
|
print(f" e il contratto e' scelto alla chiusura di t-1. Controllo")
|
|
print(f" POSITIVO: usando il carry di DOMANI {ahead:.3%} e di IERI")
|
|
print(f" {lag:.3%} contro {base:.3%} -> il test ha POTENZA "
|
|
f"({abs(ahead-base)*1e4:.0f}/{abs(lag-base)*1e4:.0f} bps di scarto).")
|
|
|
|
# sensibilita' al parametro d'esecuzione (griglia dichiarata)
|
|
print(f" griglia del roll : BUF in {BUF_GRID} giorni (parametro d'ESECUZIONE, e un")
|
|
print(" parametro d'esecuzione scelto guardando il risultato e'")
|
|
print(" selezione come ogni altra — regola 27/07). Canonico BUF=5")
|
|
print(" dichiarato PRIMA. Δdrift di libro per cella:")
|
|
for b in BUF_GRID:
|
|
DRb = {x: dated_rate(x, grid[x], buf=b) for x in ASSETS}
|
|
tpb = FD.tp01_realistic(ALLOC * W_TP, DRb)[0]
|
|
d = (float(book(rate, tp_series=tpb).mean())
|
|
- float(res5["var"]["A) perp (IL LIBRO DI OGGI)"].mean())) * 365.25
|
|
print(f" BUF={b:>2}g -> Δdrift {d:>+7.3%}"
|
|
f"{' <- canonico' if b == BUF_CANON else ''}")
|
|
print(" -> piatto: la scelta non e' dove vive il risultato.")
|
|
|
|
# de-levering null
|
|
A = res5["var"]["A) perp (IL LIBRO DI OGGI)"]
|
|
D = res5["var"]["D) C + sovracosto d'esecuzione"]
|
|
sa, sd = FD.stat(A), FD.stat(D)
|
|
k = sa["vol"] / sd["vol"]
|
|
rk = D.values.astype(float) * k
|
|
shk = float(np.mean(rk) / np.std(rk) * np.sqrt(365.25))
|
|
print(f" null del de-levering : NON APPLICABILE nella forma consueta (nessun claim di")
|
|
print(f" «meno drawdown»: il maxDD si muove di {sd['dd']-sa['dd']:+.2%}).")
|
|
print(f" Girato il suo SPECULARE — a ISO-VOLATILITA' il candidato")
|
|
print(f" deve restare migliore: k={k:.3f} -> Sharpe {shk:.3f} contro")
|
|
print(f" {sa['full']:.3f} del baseline -> "
|
|
f"{'PASSA' if shk > sa['full'] else 'FALLISCE'} (il datato compra drift PAGANDOLO in")
|
|
print(" varianza, ed e' esattamente quello che il gate deve vedere).")
|
|
|
|
E = res5["var"]["E) TP01 spot + SKH01 perp"]
|
|
se = FD.stat(E)
|
|
ke = sa["vol"] / se["vol"]
|
|
rke = E.values.astype(float) * ke
|
|
she = float(np.mean(rke) / np.std(rke) * np.sqrt(365.25))
|
|
print(f" Stesso gate sulla variante SPOT: k={ke:.3f} -> Sharpe "
|
|
f"{she:.3f} contro {sa['full']:.3f} -> "
|
|
f"{'PASSA' if she > sa['full'] else 'FALLISCE'} (lo spot alza il drift senza")
|
|
print(" comprare varianza: e' l'unica delle tre che sopravvive).")
|
|
|
|
ns = int(sum(len(FD.skh_daily(x, 0, "hourly", rate)[1]) for x in ASSETS))
|
|
nl = int(sum(np.sum(np.array([t["gross"] for t in FD.skh_daily(x, 0, "hourly", rate)[1]]) <= 0)
|
|
for x in ASSETS))
|
|
ip = al.implausible_sharpe(D, n_trades=ns, n_losing_trades=nl)
|
|
print(f" implausible_sharpe : implausible={ip['implausible']} (Sharpe {ip['sharpe']:.2f}, "
|
|
f"maxDD {ip['maxdd']:.1%}, Calmar {ip['calmar']:.2f}) -> {ip['reasons'] or 'nessuna firma'}")
|
|
print(f" anchor_luck (banda) : GIRATO, 23 offset, mediana delle differenze APPAIATE "
|
|
f"(sez. 5).")
|
|
print(" replica bit-exact : PASSATA — il -2,16%/anno del funding e' riprodotto in sez. 1")
|
|
print(" e l'assert ferma lo script se non torna.")
|
|
print(" marginal_vs_tp01 / deflated_sharpe / study_family_honest / weights_tilt_null:")
|
|
print(" NON GIRATI, e non per budget. Qui non c'e' un CANDIDATO:")
|
|
print(" il segnale e' congelato, i pesi non si toccano, non c'e'")
|
|
print(" una famiglia da cui selezionare. C'e' un COSTO che si")
|
|
print(" sposta cambiando strumento. Un `marginal_vs_tp01` su TP01")
|
|
print(" eseguito diversamente misurerebbe TP01 contro se stesso.")
|
|
|
|
|
|
def main() -> None:
|
|
print("=" * 100)
|
|
print("r0822e — FUNDING-AVOID: il 2,16%/anno si evita cambiando strumento?")
|
|
print(f"branch research/wave-0822 · {pd.Timestamp.now(tz='UTC')} · rete={'SI' if _NET else 'NO'}")
|
|
print("=" * 100)
|
|
print(" IPOTESI A PRIORI, registrata prima di misurare: il basis di un datato e il funding")
|
|
print(" di un perpetual sono lo STESSO premio (arbitraggio), quindi in livello devono")
|
|
print(" costare uguale; se il datato vince e' solo perche' il funding si riprezza contro")
|
|
print(" chi e' long e il basis no. Attesa: differenza piccola, forse nulla, e comunque")
|
|
print(" mangiata dal roll o dall'ineseguibilita'.")
|
|
|
|
V = venue_snapshot()
|
|
d0 = sezione0(V)
|
|
|
|
F = {}
|
|
for instr, (start, asset, kind) in FD.INSTR.items():
|
|
d = FD.fetch_funding(instr, start).copy()
|
|
d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True)
|
|
F[instr] = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt")
|
|
rate = FD.build_rate(F)
|
|
grid = {a: pd.DatetimeIndex(rate[a]["series"].index) for a in ASSETS}
|
|
|
|
verifica_allineamento(grid)
|
|
|
|
print(f"\n{clock()} sezione 1...")
|
|
sezione1(rate)
|
|
|
|
DR = {a: dated_rate(a, grid[a], buf=BUF_CANON, log=True) for a in ASSETS}
|
|
SR = {a: spot_rate(a, grid[a], log=True) for a in ASSETS}
|
|
print(f"\n copertura del front datato sulla griglia del funding: " + " · ".join(
|
|
f"{a} {DR[a]['valid'].mean():.4f}" for a in ASSETS))
|
|
|
|
print(f"\n{clock()} sezione 2...")
|
|
sezione2(rate, grid)
|
|
print(f"\n{clock()} sezione 3...")
|
|
sezione3(rate, DR)
|
|
print(f"\n{clock()} sezione 4...")
|
|
ex = sezione4(rate, DR, V, grid)
|
|
print(f"\n{clock()} sezione 5...")
|
|
res5 = sezione5(rate, DR, SR, ex)
|
|
print(f"\n{clock()} sezione 6...")
|
|
sezione6(res5)
|
|
sezione7(V)
|
|
sezione8(V, res5, rate, SR)
|
|
print(f"\n{clock()} sezione 9...")
|
|
sezione9(rate, DR, res5, grid)
|
|
|
|
sez("SINTESI")
|
|
A = FD.stat(res5["var"]["A) perp (IL LIBRO DI OGGI)"])
|
|
for k in ("B) tutto sui datati", "C) TP01 datato + SKH01 perp",
|
|
"D) C + sovracosto d'esecuzione", "E) TP01 spot + SKH01 perp"):
|
|
m = res5["st"][k]
|
|
print(f" {k:<32} Δdrift {m['drift']-A['drift']:>+7.2%} ΔShFULL "
|
|
f"{m['full']-A['full']:>+7.3f} ΔShHOLD {m['hold']-A['hold']:>+7.3f} "
|
|
f"ΔmaxDD {m['dd']-A['dd']:>+6.2%}")
|
|
c_perp = res5["st"]["F) nessun carry (irreale)"]["drift"] - A["drift"]
|
|
print(f"\n costo del carry per il libro, sul perpetual: {c_perp:.2%}/anno "
|
|
f"(il -2,16% replicato)")
|
|
dD = res5["st"]["D) C + sovracosto d'esecuzione"]["drift"] - A["drift"]
|
|
tot = c_perp - dD
|
|
print(f" ... e su TP01-datato/SKH01-perp, tutto compreso (carry + mezzo spread + roll):"
|
|
f" {tot:.2%}/anno")
|
|
mD = res5["st"]["D) C + sovracosto d'esecuzione"]
|
|
print(f" risparmio netto: {dD:+.2%}/anno di drift, pagato con "
|
|
f"{mD['full']-A['full']:+.3f} di Sharpe FULL e {mD['hold']-A['hold']:+.3f} di hold-out.")
|
|
D_ = res5["st"]["D) C + sovracosto d'esecuzione"]
|
|
E_ = res5["st"]["E) TP01 spot + SKH01 perp"]
|
|
costo_dat = (c_perp - dD) * 100
|
|
sez("VERDETTO")
|
|
print(" IPOTESI: esprimere il segnale congelato su un future DATATO invece che sul")
|
|
print(" perpetual costa meno del funding, perche' il basis e' fissato all'ingresso")
|
|
print(" mentre il funding si riprezza contro chi e' long. Attesa PRIMA di misurare:")
|
|
print(" differenza piccola o nulla, e comunque mangiata dal roll o dall'ineseguibilita'.")
|
|
print(" GRIGLIA DICHIARATA (al RIALZO): 2 estimatori (aritmetico/log) x 4 buffer di roll")
|
|
print(" (2/5/10/20 g) x 3 strumenti (perp/datato/spot) x 2 sleeve = 48 valutazioni di")
|
|
print(" carry; 6 varianti di libro x 23 offset d'ancora = 138 valutazioni di libro;")
|
|
print(" 4 muri x 26 passi di bisezione x 2.000 path. NESSUNA e' una SELEZIONE: il")
|
|
print(" segnale e i pesi sono congelati, non c'e' una cella scelta guardando il")
|
|
print(" risultato -> nessun deflated-Sharpe da calcolare.")
|
|
print(f" NUMERI (netti fee, a $600 di allocazione, lente `hourly` del libro live):")
|
|
print(f" A) perpetual, oggi Sh {A['full']:.3f} / hold {A['hold']:.3f} / "
|
|
f"maxDD {A['dd']:.1%} / CAGR {A['cagr']:.2%} / drift {A['drift']:.2%}")
|
|
print(f" D) TP01 datato, tutto dentro Sh {D_['full']:.3f} / hold {D_['hold']:.3f} / "
|
|
f"maxDD {D_['dd']:.1%} / CAGR {D_['cagr']:.2%} / drift {D_['drift']:.2%}")
|
|
print(f" E) TP01 spot, tutto dentro Sh {E_['full']:.3f} / hold {E_['hold']:.3f} / "
|
|
f"maxDD {E_['dd']:.1%} / CAGR {E_['cagr']:.2%} / drift {E_['drift']:.2%}")
|
|
print(f" haircut a $600: gia' dentro (le serie girano a $600 con min-order $5); il")
|
|
print(f" ticket massimo per asset e' $240 e resta nei primi livelli del libro datato.")
|
|
print(" VERDETTO: **SCARTATO** per i datati (il risparmio di drift si paga in varianza:")
|
|
print(f" hold-out {D_['hold']-A['hold']:+.3f}, negativo a 0/23 offset, e il gate")
|
|
print(" iso-volatilita' fallisce) — **LEAD** per lo SPOT su TP01, con la domanda")
|
|
print(" fiscale aperta che vale piu' del risparmio.")
|
|
print(" COSA MI SMENTIREBBE: (a) sui datati — un campione in cui il basis condizionale")
|
|
print(" NON si riprezza meno del funding, cioe' un regime in cui l'hold-out smette di")
|
|
print(" essere negativo: qui il vantaggio e' tutto 2019-2023 e vale ~0 dal 2024;")
|
|
print(" (b) sullo spot — che il fisco tratti i derivati come le cripto-attivita' (33%")
|
|
print(" entrambi): allora il LEAD diventa un candidato d'esecuzione; oppure che la fee")
|
|
print(" spot 0 sparisca col collegamento Coinbase (misurato in sez. 8: il vantaggio")
|
|
print(" si riduce ma non si annulla);")
|
|
print(" (c) su entrambi — che il mio proxy inverse per il carry dei datati non valga:")
|
|
print(" si smentisce raccogliendo qualche mese di quote della linea USDC datata e")
|
|
print(" confrontando i basis, oggi a 0,4-0,9 bps l'uno dall'altro.")
|
|
print(" SCRIPT: scripts/research/r0822e_funding_avoid.py")
|
|
print()
|
|
print(f"ESPRIMERE IL LIBRO SUI DATATI: COSTA {costo_dat:.2f} %/ANNO CONTRO 2,16% DEL PERP "
|
|
f"-> NON CONVIENE, perche' i {dD*100:.2f} punti risparmiati sul drift si pagano con "
|
|
f"{D_['hold']-A['hold']:+.3f} di Sharpe hold-out (negativo a 0/23 offset d'ancora) e "
|
|
f"il gate iso-volatilita' fallisce; NON e' INESEGUIBILE (il muro «tutti i datati "
|
|
f"sono INVERSE» e' FALSO: 16 datati USDC esistono da 2026-05-07, min-order e fee "
|
|
f"identici al perpetual) ma la linea USDC ha 3,5 mesi di vita e zero roll mai "
|
|
f"eseguiti.")
|
|
print(f"\n{clock()} fine.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|