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
+149
View File
@@ -94,6 +94,77 @@ def trajectory(r: np.ndarray, cap_needed: float, n_paths: int = 3000,
return out
def focus(r: np.ndarray, cap_needed: float, dep_eur: float, n_paths: int = 5000,
years: int = 30, seed: int = 726) -> dict:
"""Drill-down su UN livello di deposito: percentili del tempo, probabilita' cumulate,
capitale raggiunto per anno, e quanto del capitale finale e' DEPOSITO vs rendimento."""
rng = np.random.default_rng(seed)
n_days = 365 * years
paths = CC._boot_paths(r, n_paths, n_days, 20, rng)
dep = dep_eur * CC.EURUSD
cap = np.full(n_paths, 600.0)
hit = np.full(n_paths, -1, int)
paid = np.full(n_paths, 600.0) # capitale immesso (iniziale + depositi)
paid_at_hit = np.full(n_paths, np.nan)
snaps, snap_paid = {}, {}
for t in range(n_days):
cap = cap * (1.0 + paths[:, t])
if t % 30 == 0 and t > 0:
cap += dep
# ⚠️ il contatore deve seguire il capitale: `cap` riceve il deposito SEMPRE, quindi
# anche `paid` accumula sempre. Congelarlo al traguardo (come faceva la prima
# stesura) rende il rapporto capitale/versato gonfiato dopo il traguardo, perche'
# il capitale conterrebbe depositi che il contatore non conta. Il valore AL
# traguardo si cattura a parte, con lo snapshot `paid_at_hit`.
paid = paid + dep
newly = (hit < 0) & (cap >= cap_needed)
hit[newly] = t
paid_at_hit[newly] = paid[newly]
if (t + 1) % 365 == 0:
snaps[(t + 1) // 365] = cap.copy()
snap_paid[(t + 1) // 365] = paid.copy()
yrs = np.where(hit >= 0, hit / 365.0, np.nan)
fin = yrs[~np.isnan(yrs)]
return dict(yrs=yrs, fin=fin, snaps=snaps, snap_paid=snap_paid,
paid_at_hit=paid_at_hit[~np.isnan(paid_at_hit)], n=n_paths)
def _p_hit(paths: np.ndarray, cap_needed: float, dep_usd: float, n_days: int) -> float:
"""P(il capitale tocca `cap_needed` entro n_days) partendo da $600 con deposito mensile."""
cap = np.full(paths.shape[0], 600.0)
hit = np.zeros(paths.shape[0], bool)
for t in range(n_days):
cap = cap * (1.0 + paths[:, t])
if t % 30 == 0 and t > 0:
cap += dep_usd
hit |= cap >= cap_needed
return float(hit.mean())
def solve_deposit(r: np.ndarray, cap_needed: float, years: int, p_target: float,
n_paths: int = 4000, seed: int = 726) -> tuple[float, float]:
"""Deposito mensile (EUR) perche' P(arrivare entro `years`) >= p_target.
Il bootstrap si estrae UNA VOLTA e si riusa a ogni passo della ricerca binaria: con
campioni diversi per passo la funzione non sarebbe monotona e la bisezione oscillerebbe.
"""
rng = np.random.default_rng(seed)
n_days = 365 * years
paths = CC._boot_paths(r, n_paths, n_days, 20, rng)
lo, hi = 0.0, 20_000.0
if _p_hit(paths, cap_needed, hi * CC.EURUSD, n_days) < p_target:
return float("nan"), float("nan")
for _ in range(16):
mid = (lo + hi) / 2
if _p_hit(paths, cap_needed, mid * CC.EURUSD, n_days) >= p_target:
hi = mid
else:
lo = mid
dep = hi
versato = 600.0 + dep * CC.EURUSD * (years * 365 // 30)
return dep, versato
def main() -> None:
print("=" * 100)
print(" r0726 — MURI DI CAPITALE col fattore de-luck MISURATO (era ×0.6 a occhio)")
@@ -180,6 +251,84 @@ def main() -> None:
print(" capitale-rendita non si raggiunge, e la mediana e' una MEDIANA (meta' dei")
print(" path arriva dopo, e una quota non arriva affatto).")
# ------------------------------------------------------- drill-down su un deposito
DEP = 250.0
print("\n" + "=" * 100)
print(f" DRILL-DOWN — EUR {DEP:,.0f}/mese (5000 path, 30 anni)")
print("=" * 100)
print(f" E' la soglia dove il verdetto si ribalta: P(entro 20a) 8% (×0.60) -> 91% (×0.89).")
r_new = (base - (1.0 - 0.89) * mu).values.astype(float)
r_old = (base - (1.0 - 0.60) * mu).values.astype(float)
F = focus(r_new, cap_new, DEP)
F_old = focus(r_old, cap_old, DEP)
print(f"\n A) QUANDO si tocca il capitale-rendita (${cap_new:,.0f})")
print(f" {'':<14}{'p10':>9}{'p25':>9}{'MEDIANA':>10}{'p75':>9}{'p90':>9}")
for lab, D in (("26/07 (×0.89)", F), ("25/07 (×0.60)", F_old)):
f_ = D["fin"]
if len(f_) < 100:
print(f" {lab:<14}{'':>9}{'':>9}{'':>10}{'':>9}{'':>9}")
continue
q = np.percentile(f_, [10, 25, 50, 75, 90])
print(f" {lab:<14}" + "".join(f"{v:8.1f}a" for v in q[:2]) +
f"{q[2]:9.1f}a" + "".join(f"{v:8.1f}a" for v in q[3:]))
print(f"\n B) PROBABILITA' di averlo toccato entro N anni")
print(f" {'':<14}" + "".join(f"{y:>9}a" for y in (5, 10, 15, 20, 25, 30)))
for lab, D in (("26/07 (×0.89)", F), ("25/07 (×0.60)", F_old)):
y = D["yrs"]
print(f" {lab:<14}" + "".join(f"{float((y <= n).sum())/D['n']:9.0%}"
for n in (5, 10, 15, 20, 25, 30)))
print(f"\n C) CAPITALE lungo la strada (×0.89) — e quanto ne hai MESSO tu")
print(f" {'anno':>5}{'versato':>12}{'p10':>13}{'MEDIANA':>13}{'p90':>13}"
f"{' mediana/versato':>18}")
for y in (5, 10, 15, 20, 25, 30):
c = F["snaps"][y]
pd_ = float(np.median(F["snap_paid"][y]))
q = np.percentile(c, [10, 50, 90])
print(f" {y:>5}{'$'+format(pd_, ',.0f'):>12}"
+ "".join(f"{'$'+format(v, ',.0f'):>13}" for v in q)
+ f"{q[1]/pd_:>17.1f}x")
if len(F["paid_at_hit"]) > 100:
med_paid = float(np.median(F["paid_at_hit"]))
print(f"\n D) AL TRAGUARDO (mediana dei path che arrivano)")
print(f" versato di tasca: ${med_paid:,.0f} "
f"(= EUR {med_paid/CC.EURUSD:,.0f} in ~{np.median(F['fin']):.0f} anni)")
print(f" capitale: ${cap_new:,.0f}")
print(f" quota da RENDIMENTO: {(1 - med_paid/cap_new)*100:.0f}% "
f"(il resto sono i tuoi versamenti)")
print(f" rendita che paga: EUR {CC.TARGET_EUR_DAY:.0f}/giorno netti")
# -------------------------------------------- quanto versare per un ORIZZONTE dato
print("\n" + "=" * 100)
print(f" QUANTO VERSARE PER ARRIVARCI IN N ANNI (bersaglio ${cap_new:,.0f}, ×0.89)")
print("=" * 100)
print(" 'Arrivarci in N anni' non e' un numero: dipende da quanto vuoi esserne sicuro.")
print(" La colonna e' la PROBABILITA' di ORRIVARE entro l'orizzonte.".replace("ORRIVARE", "arrivare"))
print(f"\n {'orizzonte':<12}{'P=50%':>12}{'P=75%':>12}{'P=90%':>12}{'P=95%':>12}")
sol = {}
for years in (10, 15, 20):
row = f" {str(years) + ' anni':<12}"
for p in (0.50, 0.75, 0.90, 0.95):
dep, versato = solve_deposit(r_new, cap_new, years, p)
sol[(years, p)] = (dep, versato)
row += (f"{'EUR ' + format(dep, ',.0f'):>12}" if np.isfinite(dep)
else f"{'irragg.':>12}")
print(row)
print(f"\n {'':12}{'(totale versato di tasca su tutto l orizzonte)':<48}")
for years in (10, 15, 20):
row = f" {str(years) + ' anni':<12}"
for p in (0.50, 0.75, 0.90, 0.95):
_d, v = sol[(years, p)]
row += (f"{'$' + format(v, ',.0f'):>12}" if np.isfinite(v) else f"{'':>12}")
print(row)
print("\n ⚠️ A orizzonti corti il versamento domina il rendimento: si sta comprando")
print(" il capitale, non generandolo. Il confronto onesto e' con la colonna")
print(" 'totale versato' — se si avvicina al bersaglio, il book non sta facendo")
print(" il lavoro, lo stanno facendo i bonifici.")
if __name__ == "__main__":
main()