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>
This commit is contained in:
Adriano Dal Pastro
2026-07-26 17:57:53 +00:00
parent ff3ec6795a
commit 326229188a
5 changed files with 615 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
"""Test per r0726_venue_risk.py e per le aggiunte a r0726_capwall_refresh.py.
Due invarianti che, se rotte, darebbero numeri plausibili e sbagliati:
* l'hazard GIORNALIERO deve comporre esattamente alla probabilita' ANNUA dichiarata —
un errore qui sposta l'intera tabella di sensibilita' senza che nulla sembri storto;
* il contatore dei versamenti deve seguire il capitale (bug catturato in sessione: `paid`
si congelava al traguardo mentre `cap` continuava a ricevere i depositi -> rapporto
capitale/versato gonfiato del 70-80% a 25-30 anni).
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
def _flat(vals, start="2020-01-01"):
return pd.Series(np.asarray(vals, float),
index=pd.date_range(start, periods=len(vals), freq="1D", tz="UTC"))
# ===========================================================================
# hazard: giornaliero -> annuo
# ===========================================================================
@pytest.mark.parametrize("p", [0.005, 0.01, 0.02, 0.05, 0.2])
def test_hazard_giornaliero_compone_alla_probabilita_annua(p):
"""(1-(1-p)^(1/365))^ -> su 365 giorni la sopravvivenza deve tornare (1-p)."""
haz = 1.0 - (1.0 - p) ** (1.0 / 365.0)
assert (1.0 - haz) ** 365 == pytest.approx(1.0 - p, rel=1e-9)
def test_p_zero_non_uccide_nessun_venue():
import r0726_venue_risk as VR
S = {"A": _flat([0.001] * 400), "B": _flat([0.001] * 400)}
r = VR.simulate({"A": 0.5, "B": 0.5}, S, 0.0, n_paths=200, years=1)
assert r["p_any_fail"] == 0.0
assert r["p_all_dead"] == 0.0
def test_p_certo_uccide_tutto_e_azzera_il_capitale():
"""Controllo POSITIVO: se il gate non mordesse mai, il test a p=0 passerebbe comunque."""
import r0726_venue_risk as VR
S = {"A": _flat([0.001] * 400), "B": _flat([0.001] * 400)}
r = VR.simulate({"A": 0.5, "B": 0.5}, S, 1.0, n_paths=200, years=1)
assert r["p_all_dead"] == 1.0
assert r["median_end"] == 0.0
def test_concentrato_perde_tutto_piu_spesso_del_diversificato():
"""L'asimmetria che motiva l'intero studio: a parita' di p, 'perso TUTTO' e' molto piu'
probabile con un solo venue, anche se 'almeno un fallimento' e' piu' probabile con tre."""
import r0726_venue_risk as VR
S = {"A": _flat([0.0005] * 800), "B": _flat([0.0005] * 800), "C": _flat([0.0005] * 800)}
conc = VR.simulate({"A": 1.0}, S, 0.05, n_paths=800, years=10)
split = VR.simulate({"A": 0.34, "B": 0.33, "C": 0.33}, S, 0.05, n_paths=800, years=10)
assert conc["p_all_dead"] > split["p_all_dead"]
assert split["p_any_fail"] > conc["p_any_fail"]
def test_venue_series_applica_il_deluck_sul_drift():
import r0726_venue_risk as VR
S = VR.venue_series()
assert set(S) == {"Deribit", "Hyperliquid", "IB"}
for v in S.values():
assert isinstance(v.index, pd.DatetimeIndex) and len(v) > 100
# ===========================================================================
# capwall: il contatore dei versamenti
# ===========================================================================
def test_focus_versato_segue_il_capitale():
"""`paid` deve valere 600 + dep*n_versamenti, deterministico e indipendente dal path.
Se si congelasse al traguardo (il bug), i path che arrivano presto mostrerebbero un
versato piu' basso e il rapporto capitale/versato uscirebbe gonfiato."""
import r0725_capcurve as CC
import r0726_capwall_refresh as CW
r = np.full(2000, 0.0004)
F = CW.focus(r, cap_needed=1e12, dep_eur=250.0, n_paths=50, years=5, seed=1)
for y, paid in F["snap_paid"].items():
n_dep = (y * 365) // 30
atteso = 600.0 + 250.0 * CC.EURUSD * n_dep
assert np.allclose(paid, atteso, rtol=0, atol=1e-6), f"anno {y}: {paid[0]} != {atteso}"
def test_focus_bersaglio_irraggiungibile_non_produce_traguardi():
import r0726_capwall_refresh as CW
F = CW.focus(np.full(2000, 0.0001), cap_needed=1e15, dep_eur=100.0,
n_paths=50, years=3, seed=2)
assert len(F["fin"]) == 0
assert len(F["paid_at_hit"]) == 0
def test_solve_deposit_e_monotono_nella_confidenza():
"""Piu' certezza -> piu' soldi. Se la ricerca binaria riusasse campioni diversi a ogni
passo, la funzione non sarebbe monotona e questo test lo direbbe."""
import r0726_capwall_refresh as CW
r = np.random.default_rng(3).normal(0.0006, 0.006, 1500)
d50, _ = CW.solve_deposit(r, 100_000.0, 10, 0.50, n_paths=400, seed=5)
d90, _ = CW.solve_deposit(r, 100_000.0, 10, 0.90, n_paths=400, seed=5)
assert d90 >= d50
def test_solve_deposit_ritorna_nan_se_irraggiungibile():
import r0726_capwall_refresh as CW
r = np.full(1500, -0.01) # book che perde l'1% al giorno
dep, versato = CW.solve_deposit(r, 1e12, 5, 0.5, n_paths=200, seed=6)
assert np.isnan(dep) and np.isnan(versato)