Files
PythagorasGoal/scripts/research/r0726_venue_risk.py
T
Adriano Dal Pastro 326229188a research: rischio di VENUE — mai prezzato, e non e' diversificabile dagli sleeve
Domanda dell'operatore: "tutto su Deribit?". Ha scoperto un buco, non un dettaglio.

IL BUCO. Il progetto ha prezzato con ossessione fee, slippage, min-order, pavimento IB,
haircut small-cap, fortuna d'ancora, degrado d'esecuzione, look-ahead, backfill, split
non aggiustati — e MAI la probabilita' che l'exchange sparisca col saldo dentro. E
TP01+SKH01+VRP01 stanno tutti sullo stesso conto: tre sleeve quasi-ortogonali sui
ritorni, PERFETTAMENTE CORRELATI sul fallimento del venue. La matrice di correlazione
del book non lo vede per costruzione.

LIMITE DEI MURI DEL 25-26/07 (dichiarato): book_series gira a alloc=$600 col book a 2
sleeve, quindi portarlo a $272k assumeva gia' "tutto su Deribit" senza dirlo — e
assumeva anche di girare il book da $600 a $272k (falso: a $3k GTAA01, $5k XSR01, $20k
XS01 -> il muro vero e' piu' basso).

LA MISURA (accumulo da $600, 250 EUR/m, 20a, bersaglio $272k, x0.89, jump di venue;
CONC = 100% Deribit vs SPLIT = Deribit 65 / HL 15 / IB 20; bersaglio identico ->
conservativo CONTRO lo split):

  p annua    P(arrivare) CONC / SPLIT    P(perso TUTTO) CONC / SPLIT
  0.5%              87% / 90%                    10% / 0%
  1.0%              81% / 83%                    18% / 0%
  2.0%              69% / 71%                    34% / 4%
  5.0%              42% / 45%                    64% / 27%

Capitale mediano CONC a 20a: $544k (p=0) -> $0 (p=5%).

LA COLONNA CHE CONTA NON E' LA PRIMA. Sulla probabilita' di ARRIVARE la concentrazione
costa 1-3pp; sulla ROVINA fino a 64pp. Motivo strutturale: con un conto solo "almeno un
fallimento" COINCIDE con "perso tutto". Lo SPLIT viene colpito 2.5x piu' spesso ed e'
molto piu' sicuro -> "quante volte vieni colpito" non e' una misura di rischio.

ONESTA': p NON e' stimato (sensibilita', non previsione); i fallimenti sono assunti
indipendenti, ottimistico per Deribit-HL -> la parte solida dello split e' IB, altra
classe di rischio; a $600 lo split e' impossibile, la concentrazione e' forzata.

RISPOSTA: no, ma la domanda ha una DATA. Prima soglia vera ~$3k (GTAA01 su IB). E
converge col 25/07: "max 25% su IB, COSTA ~0.08 EUR/g" era detto su basi di solo
rendimento; sull'asse della rovina quello stesso 25% e' la mossa principale — non e' il
prezzo di un peggioramento, e' il premio di un'assicurazione.

Incluse le aggiunte a r0726_capwall_refresh: drill-down 250 EUR/m e solve_deposit
(10 anni @P=90% = 1.178 EUR/m, $155.923 versati su $272k -> il rendimento fa il 43%,
contro il 77% a 20 anni). Bug catturato: il contatore dei versamenti si congelava al
traguardo mentre il capitale continuava a riceverli -> rapporto gonfiato (46x vs 25x a
30 anni); test di regressione cablato.

Book, pesi, cron, config INVARIATI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:57:53 +00:00

169 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
"""r0726_venue_risk.py — "tutto su Deribit?": il rischio di VENUE, mai prezzato dal progetto.
CONTESTO. I muri di capitale (25/07, corretti il 26/07) sono calcolati su `book_series("hourly")`
= **TP01+SKH01 75/25 a alloc=$600**, cioe' il book a 2 sleeve eseguibile a bassa taglia. Portare
quella serie fino a $272.061 assume implicitamente due cose mai dichiarate:
(a) che a $272k si continui a girare il book da $600 (falso: a ~$3k si accende GTAA01, a ~$5k
XSR01, a ~$20k XS01 — il book a 5 sleeve ha Sharpe piu' alto, quindi il muro vero e' PIU'
BASSO di quello che ho pubblicato);
(b) che TUTTO il capitale stia su **un solo exchange**, per 10-20 anni.
Il progetto ha prezzato con cura fee, slippage, min-order, pavimenti di commissione, fortuna
d'ancora e degrado d'esecuzione — e **mai** la probabilita' che il venue sparisca col saldo dentro.
Per un piano di rendita ventennale su un exchange offshore quello e' un rischio di prim'ordine, e
non e' diversificabile con gli sleeve: TP01, SKH01 e VRP01 stanno **tutti** sullo stesso conto.
COSA FA. Monte Carlo dell'accumulo con **jump di venue**: ogni venue fallisce indipendentemente
con probabilita' annua `p` e in quel caso la sua quota di capitale va a **zero** (i depositi futuri
vanno ai superstiti). Confronta:
* CONC = tutto su Deribit (TP01+SKH01), cioe' l'ipotesi implicita dei muri;
* SPLIT = Deribit 65% (TP01+SKH01) / Hyperliquid 15% (XS01) / IB 20% (GTAA01),
cioe' il book attivo mappato sui venue dove gira davvero.
Bersaglio FISSO ($272.061) in entrambi i casi, per confrontabilita': lo SPLIT ha in realta' un
muro piu' basso (Sharpe piu' alto), quindi il confronto e' conservativo CONTRO lo split.
NON stima `p`. Il record storico degli exchange crypto (Mt.Gox, QuadrigaCX, FTX...) dice che non
e' trascurabile, ma un numero preciso qui sarebbe inventato: si riporta la SENSIBILITA' e la
scelta di `p` resta dichiarata dall'operatore.
uv run python scripts/research/r0726_venue_risk.py
"""
from __future__ import annotations
import sys
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"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
import r0725_capcurve as CC # noqa: E402
import r0726_loo_deluck as L # noqa: E402
from src.portfolio.portfolio import combine_outer # noqa: E402
TARGET = 272_061.0
DEP_EUR = 250.0
YEARS = 20
N_PATHS = 4000
P_GRID = (0.0, 0.005, 0.01, 0.02, 0.05)
DELUCK = 0.89
def venue_series() -> dict:
"""Serie giornaliere per VENUE, alle ancore canoniche, de-luckate sul drift."""
S = L.build_all()
can = {n: S[n][L.SPEC[n][2]] for n in L.SPEC}
deribit = combine_outer({k: can[k] for k in ("TP01_trend_1d", "SKH01_skyhook")},
{"TP01_trend_1d": 0.75, "SKH01_skyhook": 0.25})
out = {"Deribit": deribit, "Hyperliquid": can["XS01_xsec_hl"], "IB": can["GTAA01_eq_trend"]}
return {k: v - (1.0 - DELUCK) * float(v.mean()) for k, v in out.items()}
def simulate(cfg: dict, series: dict, p_annual: float, n_paths: int = N_PATHS,
years: int = YEARS, seed: int = 726) -> dict:
"""Accumulo da $600 con deposito mensile e jump di venue.
`cfg`: {venue: peso}. Ogni venue fallisce indipendentemente (hazard giornaliero costante);
al fallimento la sua quota va a zero e il venue esce (depositi ai superstiti, pesi
rinormalizzati). Le serie dei venue sono allineate su indice comune per estrarre blocchi
CONGIUNTI: i venue non sono indipendenti fra loro sui RITORNI, solo sui fallimenti.
"""
rng = np.random.default_rng(seed)
n_days = years * 365
names = list(cfg)
J = pd.concat({k: series[k] for k in names}, axis=1, join="inner").fillna(0.0)
R = J.values.astype(float) # (T, V) ritorni congiunti
idx = CC._boot_paths(np.arange(len(R), dtype=float), n_paths, n_days, 20, rng).astype(int)
dep = DEP_EUR * CC.EURUSD
w0 = np.array([cfg[k] for k in names], float)
cap = np.outer(np.full(n_paths, 600.0), w0) # (P, V) capitale per venue
alive = np.ones((n_paths, len(names)), bool)
haz = 1.0 - (1.0 - p_annual) ** (1.0 / 365.0)
hit = np.zeros(n_paths, bool)
hit_day = np.full(n_paths, n_days + 1, int)
for t in range(n_days):
cap *= (1.0 + R[idx[:, t], :])
if haz > 0:
dead = (rng.random((n_paths, len(names))) < haz) & alive
cap = np.where(dead, 0.0, cap)
alive &= ~dead
if t % 30 == 0 and t > 0:
wa = alive * w0 # depositi ripartiti fra i superstiti
tot = wa.sum(axis=1, keepdims=True)
share = np.divide(wa, tot, out=np.zeros_like(wa), where=tot > 0)
cap += dep * share
tot_cap = cap.sum(axis=1)
newly = (~hit) & (tot_cap >= TARGET)
hit_day[newly] = t
hit |= newly
yrs = np.where(hit, hit_day / 365.0, np.nan)
fin = yrs[~np.isnan(yrs)]
return dict(p_reach=float(hit.mean()),
median_yrs=float(np.median(fin)) if len(fin) > 50 else float("nan"),
p_any_fail=float((~alive.all(axis=1)).mean()),
p_all_dead=float((~alive.any(axis=1)).mean()),
median_end=float(np.median(cap.sum(axis=1))))
def main() -> None:
print("=" * 100)
print(" r0726 — \"TUTTO SU DERIBIT?\" — il rischio di venue, mai prezzato")
print("=" * 100)
print(f" accumulo da $600, EUR {DEP_EUR:.0f}/mese, orizzonte {YEARS} anni, "
f"bersaglio ${TARGET:,.0f}, de-luck ×{DELUCK}")
S = venue_series()
for k, v in S.items():
print(f" {k:<12} {len(v):>5} barre {v.index[0].date()} -> {v.index[-1].date()} "
f"drift {v.mean()*365.25*100:5.1f}%/a vol {v.std()*np.sqrt(365.25)*100:5.1f}%")
CONC = {"Deribit": 1.0}
SPLIT = {"Deribit": 0.65, "Hyperliquid": 0.15, "IB": 0.20}
print("\n" + "-" * 100)
print(" P(raggiungere il bersaglio entro 20 anni), al variare della probabilita' ANNUA")
print(" che un venue sparisca col saldo dentro")
print("-" * 100)
print(f" {'p annua':<12}{'CONC (100% Deribit)':>24}{'SPLIT (65/15/20)':>22}"
f"{'differenza':>14}")
res = {}
for p in P_GRID:
a = simulate(CONC, S, p)
b = simulate(SPLIT, S, p)
res[p] = (a, b)
print(f" {p:<12.1%}{a['p_reach']:>23.0%} {b['p_reach']:>21.0%}"
f"{b['p_reach']-a['p_reach']:>13.0%}")
print("\n" + "-" * 100)
print(" Dettaglio: quanto spesso si viene colpiti, e cosa resta")
print("-" * 100)
print(f" {'p annua':<10}{'':<6}{'P(almeno 1 fallimento)':>24}"
f"{'P(perso TUTTO)':>18}{'capitale mediano a 20a':>26}")
for p in P_GRID:
for lab, r in (("CONC ", res[p][0]), ("SPLIT", res[p][1])):
print(f" {p:<10.1%}{lab:<6}{r['p_any_fail']:>23.0%}"
f"{r['p_all_dead']:>17.0%}{'$'+format(r['median_end'], ',.0f'):>26}")
print()
print("=" * 100)
print(" LETTURA")
print("=" * 100)
print(" * A p=0 lo SPLIT e' gia' diverso dal CONC per composizione (Sharpe/drift diversi),")
print(" non per il rischio di venue: quella riga e' il termine di paragone.")
print(" * La differenza CONC-vs-SPLIT CRESCE con p: e' il prezzo della concentrazione.")
print(" * 'P(perso TUTTO)' e' la riga che conta per una RENDITA: nel CONC coincide con")
print(" 'almeno 1 fallimento' — non c'e' nulla dietro. Nello SPLIT serve che spariscano")
print(" tre venue indipendenti.")
print(" * Confronto CONSERVATIVO contro lo split: bersaglio uguale per entrambi, mentre")
print(" il book diversificato ha Sharpe piu' alto -> muro piu' basso -> arriverebbe prima.")
if __name__ == "__main__":
main()