#!/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()