research(capitale): "5k messi dove" — lo split si ottiene versando di meno

Misura nuova: a $6.050 lo split CON GTAA01 e' impossibile (servirebbe il 50% del conto per
i $3.000 di gamba minima), ma quello in LIQUIDITA' non ha soglie.

Due correzioni prima dei numeri:
- la configurazione vera e' 5.000 + 500/mese (dal commento in config/live.json del 26/07),
  non i 250/mese di tutte le tabelle. Nessuna azione di config: il cap e' gia' armato e
  vale min($3.000, equity_osservata x 0.5) = leva lorda <=1x.
- a $6.050 non si sblocca nulla (GTAA01/XS01/XSR01 tutti fuori portata o sotto gate):
  il book resta TP01+SKH01.

Split in liquidita' (p=1%, 20a): fuori 10% -> P(perso tutto) 18.4% -> 3.5% (0.0% con cassa
in banca), salvati $6.818; 25% -> salvati $17.045, al prezzo di 0.9pp di P(arrivare) e circa
un anno di ritardo mediano.

P(perso tutto) SATURA a qualunque quota > 0: la protezione binaria si compra col fatto di
avere un secondo conto. Cio' che distingue le quote e' il salvataggio.

ERRORE CORRETTO IN SESSIONE: la colonna del salvataggio riportava prima il capitale a 20 anni
condizionato al fallimento ($77k al 10% = 11x il vero), gonfiato dalla convenzione ereditata
dal 26/07 per cui i versamenti si dirottano ai superstiti -> attribuiva allo split il valore
di continuare a versare, che si ottiene comunque aprendo un altro conto. Ora misura il
salvataggio istantaneo, congelato in un test.

simulate() accetta un hazard PER VENUE (una cassa in banca non fallisce come un exchange);
la replica esatta dei numeri del 26/07 e' preservata e testata.

Test: 4 nuovi (508 totali), tutti verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-27 14:58:50 +00:00
parent fd05307c96
commit 17b0f2cb47
4 changed files with 186 additions and 6 deletions
+65 -6
View File
@@ -60,6 +60,10 @@ SEED = 726 # lo stesso di r0726_venue_risk: consente la re
TARGET = VR.TARGET # $272.061
LUMPS_EUR = (0.0, 2_000.0, 5_000.0, 10_000.0)
P_GRID = (0.0, 0.005, 0.01, 0.02, 0.05)
# La configurazione realmente pianificata dall'operatore: il cap di config/live.json e' stato
# alzato il 26/07 "in previsione del versamento (EUR 5.000 + 500/mese -> equity ~$6.050)".
LUMP_FOCUS = 5_000.0
DEP_FOCUS = 500.0
# Split: la quota su IB parte dal 25% raccomandato da r0725_ib10k (oltre, la rendita cala:
# 0% -> €2.16/g, 50% -> €1.54/g su $11.5k). Ma la quota MINIMA e' imposta dall'eseguibilita'
@@ -74,14 +78,18 @@ IB_SHARE_MAX = 0.35
IB_HAIRCUT_YR = 0.008 + 0.001
def simulate(cfg: dict, series: dict, p_annual: float, lump_usd: float = 0.0,
def simulate(cfg: dict, series: dict, p_annual: float | dict, lump_usd: float = 0.0,
dep_eur: float = 250.0, n_paths: int = N_PATHS, years: int = YEARS,
seed: int = SEED) -> dict:
"""Accumulo con capitale iniziale `START + lump_usd` e jump di venue.
Generalizza `r0726_venue_risk.simulate` sul solo capitale iniziale e sul deposito: la
struttura (ordine di consumo dell'RNG compreso) e' identica, cosi' che con lump=0 e
dep=250 i numeri debbano coincidere ESATTAMENTE con quelli pubblicati il 26/07.
Generalizza `r0726_venue_risk.simulate` sul capitale iniziale, sul deposito e sul rischio
PER VENUE: la struttura (ordine di consumo dell'RNG compreso) e' identica, cosi' che con
lump=0, dep=250 e `p` scalare i numeri debbano coincidere ESATTAMENTE con quelli del 26/07.
`p_annual` puo' essere un dict {venue: p}: un conto bancario/liquidita' NON fallisce come un
exchange (tutela dei depositi fino a €100k), e attribuirgli lo stesso hazard e' una scelta
CONSERVATIVA contro lo split — che va poi misurata, non assunta.
"""
rng = np.random.default_rng(seed)
n_days = years * 365
@@ -94,15 +102,22 @@ def simulate(cfg: dict, series: dict, p_annual: float, lump_usd: float = 0.0,
w0 = np.array([cfg[k] for k in names], float)
cap = np.outer(np.full(n_paths, START + lump_usd), w0)
alive = np.ones((n_paths, len(names)), bool)
haz = 1.0 - (1.0 - p_annual) ** (1.0 / 365.0)
p_vec = np.array([p_annual.get(k, 0.0) if isinstance(p_annual, dict) else p_annual
for k in names], float)
haz = 1.0 - (1.0 - p_vec) ** (1.0 / 365.0) # (V,), broadcast sulle estrazioni
hit = np.zeros(n_paths, bool)
hit_day = np.full(n_paths, n_days + 1, int)
main_i = int(np.argmax(w0)) # il venue che porta il book
salvage = np.full(n_paths, np.nan) # cio' che resta NEL MOMENTO in cui il main cade
for t in range(n_days):
cap *= (1.0 + R[idx[:, t], :])
if haz > 0:
if (haz > 0).any():
dead = (rng.random((n_paths, len(names))) < haz) & alive
cap = np.where(dead, 0.0, cap)
alive &= ~dead
jm = dead[:, main_i]
if jm.any():
salvage[jm] = cap[jm].sum(axis=1)
if t % 30 == 0 and t > 0 and dep > 0:
wa = alive * w0
tot = wa.sum(axis=1, keepdims=True)
@@ -114,7 +129,15 @@ def simulate(cfg: dict, series: dict, p_annual: float, lump_usd: float = 0.0,
hit |= newly
yrs = np.where(hit, hit_day / 365.0, np.nan)
fin = yrs[~np.isnan(yrs)]
# Cio' che distingue una quota dall'altra NON e' P(perso tutto) — che satura a qualunque
# quota > 0 — ma quanto si SALVA nel momento del fallimento. ⚠️ Volutamente NON si misura il
# capitale a 20 anni condizionato al fallimento: dopo la morte di un venue questo modello
# dirotta i versamenti ai superstiti (e li scarta se non ne restano), quindi quel numero
# attribuirebbe allo split il valore di continuare a versare — che si puo' fare comunque
# aprendo un altro conto. Il salvataggio istantaneo e' l'unica parte che lo split compra.
sv = salvage[~np.isnan(salvage)]
return dict(p_reach=float(hit.mean()),
median_salvage=float(np.median(sv)) if len(sv) > 20 else float("nan"),
median_yrs=float(np.median(fin)) if len(fin) > 50 else float("nan"),
p_all_dead=float((~alive.any(axis=1)).mean()),
median_end=float(np.median(cap.sum(axis=1))),
@@ -264,6 +287,42 @@ def main() -> None:
print(" proprieta' del tempo di esposizione, non del capitale. Il lump non la peggiora —")
print(" ma moltiplica cio' che quella probabilita' porta via.")
# ---------------------------------------------------------------- (2-ter) dove mettere 5k
print("\n" + "=" * 104)
print(f" (2-ter) DOVE METTERE €{LUMP_FOCUS:,.0f} — la configurazione realmente pianificata")
print(f" (€{LUMP_FOCUS:,.0f} + €{DEP_FOCUS:,.0f}/mese; il cap di `live.json` e' gia'")
print(" armato per questo caso dal 26/07)")
print("=" * 104)
tot5 = START + LUMP_FOCUS * CC.EURUSD
print(f"\n Capitale iniziale ${tot5:,.0f}. Cosa si sblocca a questa taglia: NIENTE.")
print(f" GTAA01 richiede ${GTAA_MIN_CAPITAL:,.0f} SULLA GAMBA "
f"(= {GTAA_MIN_CAPITAL / tot5:.0%} del conto: oltre ogni quota sensata) e non e'")
print(" deployabile comunque (PRIIPs); XS01 ~$20.000; XSR01 e' sotto gate fino al 23/10.")
print(" -> il book resta TP01+SKH01, e la sola domanda e' QUANTA parte NON sta sull'exchange.")
print("\n Seconda gamba = LIQUIDITA' ferma (nessuna soglia di eseguibilita', ~0% reale).")
print(" Due letture del rischio della liquidita', perche' non e' un exchange:")
print(" [A] cassa rischiosa come Deribit (conservativa contro lo split)")
print(" [B] cassa che non fallisce (conto bancario con tutela dei depositi)")
for p in (0.005, 0.01, 0.02, 0.05):
print(f"\n p(exchange) = {p:.1%}")
print(f" {'fuori exchange':>15}{'P(arrivare)':>13}{'anni':>8}"
f"{'P(perso tutto) [A]':>21}{'[B]':>8}{'salvato se cade':>17}")
for share in (0.0, 0.10, 0.25, 0.40):
cfg5 = {"Deribit": 1.0 - share, "IB": share} if share > 0 else {"Deribit": 1.0}
ser5 = {**series, "IB": series["IB"] * 0.0}
a = simulate(cfg5, ser5, p, lump_usd=LUMP_FOCUS * CC.EURUSD, dep_eur=DEP_FOCUS)
b = simulate(cfg5, ser5, {"Deribit": p, "IB": 0.0},
lump_usd=LUMP_FOCUS * CC.EURUSD, dep_eur=DEP_FOCUS)
yrs = f"{a['median_yrs']:.1f}a" if not np.isnan(a["median_yrs"]) else "mai"
resta = b["median_salvage"]
print(f" {share:>14.0%}{a['p_reach']:>13.1%}{yrs:>8}"
f"{a['p_all_dead']:>20.1%}{b['p_all_dead']:>8.1%}{resta:>16,.0f}$")
print("\n ⚠️ La colonna [B] e' quella realistica per un conto bancario in UE (tutela dei")
print(" depositi fino a €100k): tenere fuori una quota qualsiasi porta P(perso tutto)")
print(" a ZERO, perche' 'perso tutto' richiede che falliscano ENTRAMBI i conti.")
print(" La [A] e' il caso in cui anche la cassa e' a rischio: e' li' che si vede il")
print(" prezzo vero della protezione, e resta grande.")
# ---------------------------------------------------------------- (3) la soglia
print("\n" + "=" * 104)
print(" (3) DA QUALE CAPITALE LO SPLIT E' POSSIBILE SENZA DISTORCERE I PESI")