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
+21
View File
@@ -1373,6 +1373,27 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
= il caso in cui CLAUDE.md dice di riaprire PRIMA.** Materiale pronto; la decisione resta = il caso in cui CLAUDE.md dice di riaprire PRIMA.** Materiale pronto; la decisione resta
dell'operatore. ⚠️ Cosa NON decide: quanto dei €6.043 sia vero fondo d'emergenza (un fondo dell'operatore. ⚠️ Cosa NON decide: quanto dei €6.043 sia vero fondo d'emergenza (un fondo
d'emergenza non e' capitale disponibile). d'emergenza non e' capitale disponibile).
**ADDENDUM "€5k messi dove" (stessa sessione).** (a) ⚠️ La configurazione REALE non era quella
tabulata: `config/live.json` fu alzato il 26/07 *"in previsione del versamento (EUR 5.000 +
500/mese)"* → il piano e' **€500/mese**, non €250. **Nessuna azione di config al deposito**: il
cap e' gia' `min($3.000, equity_osservata × 0.5)` = leva lorda ≤1x a $6.050, protetto dal
watermark. (b) **A $6.050 non si sblocca NIENTE** (GTAA01 vorrebbe il 50% del conto e non e'
deployabile; XS01 $20k; XSR01 sotto gate) → il book resta TP01+SKH01 e l'unica domanda e' quanta
parte NON sta sull'exchange. (c) **Split in liquidita', p=1%, 20a:** fuori 0% → P(arrivare)
89.4% / 11.4a / P(perso tutto) 18.4%; **10% → 89.0% / 11.8a / 3.5% [0.0% con cassa in banca] /
salvati $6.818**; **25% → 88.5% / 12.4a / salvati $17.045**; 40% → 87.8% / 13.3a / $27.272.
⚠️ **P(perso tutto) SATURA a qualunque quota > 0** (3.5% al 10, 25 e 40%): la protezione binaria
si compra col FATTO di avere un secondo conto, non con quanto ci si mette → **quando una metrica
binaria satura, la decisione si sposta sulla metrica continua** (qui il salvataggio).
⚠️ **Errore mio corretto in sessione:** la colonna del salvataggio riportava prima il capitale a
20 anni condizionato al fallimento ($77k al 10% = 11× il vero), gonfiato dalla convenzione
ereditata dal 26/07 per cui **i versamenti si dirottano ai superstiti** (e si scartano se non ne
resta nessuno) → attribuiva allo split il valore di *continuare a versare*, che si ottiene
comunque aprendo un altro conto. Misura onesta = **salvataggio ISTANTANEO**, congelata in
`test_il_salvataggio_e_istantaneo_non_a_scadenza`. (d) ✅ **Lo split non si costruisce spostando
soldi su un secondo venue: si ottiene versandone di meno.** Con €6.043 in XEON, deporne €5.000
lascia fuori ~16% del capitale investito = gia' dentro la banda 10-25%, **a costo operativo
zero**. Prezzo del 25%: 0.9pp di P(arrivare) e ~1 anno di ritardo mediano.
-**FEE WATCH + MONITOR HEALTH — due sorveglianze cablate (2026-07-27).** Nessuna tocca -**FEE WATCH + MONITOR HEALTH — due sorveglianze cablate (2026-07-27).** Nessuna tocca
l'esecuzione; entrambe in `cron_daily.sh`. (1) **`scripts/live/fee_watch.py`** (test 13): il l'esecuzione; entrambe in `cron_daily.sh`. (1) **`scripts/live/fee_watch.py`** (test 13): il
nuovo schema fee Deribit entra il **1° agosto** e l'annuncio non ha numeri → invece di un nuovo schema fee Deribit entra il **1° agosto** e l'annuncio non ha numeri → invece di un
@@ -275,3 +275,63 @@ Aggiunto.
6. **Un rischio non-diversificabile si compra con un secondo conto, non con un secondo sleeve.** 6. **Un rischio non-diversificabile si compra con un secondo conto, non con un secondo sleeve.**
La protezione dalla rovina è identica con GTAA01 o con liquidità: dipende da quanti conti La protezione dalla rovina è identica con GTAA01 o con liquidità: dipende da quanti conti
falliscono, non da cosa ci sta sopra. falliscono, non da cosa ci sta sopra.
---
## 7. Addendum — "€5k messi dove" (stessa sessione)
Domanda diretta dell'operatore dopo il riassunto. Ha richiesto una misura nuova, perché €5.000
cade in una zona che la tabella §1 non copriva: a **$6.050** lo split *con GTAA01* è impossibile
(servirebbe il 50% del conto per fare i $3.000 di gamba minima), ma lo split in **liquidità** non
ha soglie di eseguibilità.
Due correzioni al volo prima dei numeri:
1. **La configurazione vera non è quella che avevo tabulato.** `config/live.json` è stato alzato
il 26/07 *"in previsione del versamento (EUR 5.000 + 500/mese → equity ~$6.050)"*: il piano
dell'operatore è **€500/mese**, non i €250 di tutte le tabelle. Rimisurato su quella.
Conseguenza operativa: **nessuna azione di config al deposito** — il cap è già `min($3.000,
equity_osservata × 0.5)`, quindi a $6.050 vale $3.000/asset = leva lorda ≤1x, ed è protetto dal
watermark contro il caso "cap alto su conto piccolo".
2. **Niente si sblocca a $6.050.** GTAA01 richiede $3.000 *sulla gamba* (50% del conto) e non è
comunque deployabile (PRIIPs); XS01 ~$20.000; XSR01 è sotto gate fino al 23/10. Il book resta
TP01+SKH01 → **l'unica domanda è quanta parte non sta sull'exchange.**
Seconda gamba = liquidità ferma, due letture del suo rischio ([A] rischiosa come Deribit,
conservativa; [B] conto bancario con tutela dei depositi, realistica):
| fuori exchange | P(arrivare) | anni | P(perso tutto) [A] | [B] | salvato se cade |
|---|---|---|---|---|---|
| 0% | 89.4% | 11.4a | 18.4% | 18.4% | $0 |
| 10% | 89.0% | 11.8a | 3.5% | **0.0%** | $6.818 |
| **25%** | **88.5%** | **12.4a** | 3.5% | **0.0%** | **$17.045** |
| 40% | 87.8% | 13.3a | 3.5% | 0.0% | $27.272 |
*(p exchange = 1%; €5.000 + €500/mese, 20 anni)*
⚠️ **Due letture che il tavolo nasconde, e senza le quali si sceglie male.**
**(a) P(perso tutto) SATURA a qualunque quota > 0** (3.5% al 10, 25 e 40%): la protezione binaria
si compra col semplice fatto di *avere* un secondo conto, non con quanto ci si mette. Ciò che
distingue le quote è la colonna del salvataggio.
**(b) ⚠️ Errore mio, corretto in sessione.** La prima versione di quella colonna riportava il
capitale a 20 anni *condizionato al fallimento*, e dava $77k al 10% — undici volte il valore
vero. Artefatto della convenzione ereditata dal 26/07: **alla morte di un venue i versamenti
successivi vengono dirottati ai superstiti, e scartati se non ne resta nessuno.** Quel numero
attribuiva quindi allo split il valore di *continuare a versare*, che si ottiene comunque
aprendo un altro conto dopo il guasto. La misura onesta è il **salvataggio istantaneo**: quanto
resta nel momento in cui l'exchange cade. Congelato in
`test_il_salvataggio_e_istantaneo_non_a_scadenza`.
**Il prezzo, in chiaro:** tenere fuori il 25% costa **0.9pp** di probabilità di arrivare e **circa
un anno** di ritardo mediano (11.4 → 12.4), e compra ~$17k mediani nel 18% di percorsi in cui
l'exchange sparisce (a p=1%), più P(perso tutto) → 0 nella lettura realistica.
**E la parte che non costa nulla:** quei soldi sono *già* fuori dall'exchange, in XEON. Lo split
non si costruisce spostando denaro su un secondo venue — **si ottiene versandone di meno.** Con
€6.043 in XEON, deporne €5.000 lascia fuori ~16% del capitale investito: già dentro la banda
10-25%, a costo operativo zero.
**REGOLA: quando una metrica binaria satura, la decisione va spostata sulla metrica continua** —
qui P(perso tutto) non distingue il 10% dal 40%, il salvataggio sì.
+65 -6
View File
@@ -60,6 +60,10 @@ SEED = 726 # lo stesso di r0726_venue_risk: consente la re
TARGET = VR.TARGET # $272.061 TARGET = VR.TARGET # $272.061
LUMPS_EUR = (0.0, 2_000.0, 5_000.0, 10_000.0) 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) 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: # 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' # 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 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, dep_eur: float = 250.0, n_paths: int = N_PATHS, years: int = YEARS,
seed: int = SEED) -> dict: seed: int = SEED) -> dict:
"""Accumulo con capitale iniziale `START + lump_usd` e jump di venue. """Accumulo con capitale iniziale `START + lump_usd` e jump di venue.
Generalizza `r0726_venue_risk.simulate` sul solo capitale iniziale e sul deposito: la Generalizza `r0726_venue_risk.simulate` sul capitale iniziale, sul deposito e sul rischio
struttura (ordine di consumo dell'RNG compreso) e' identica, cosi' che con lump=0 e PER VENUE: la struttura (ordine di consumo dell'RNG compreso) e' identica, cosi' che con
dep=250 i numeri debbano coincidere ESATTAMENTE con quelli pubblicati il 26/07. 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) rng = np.random.default_rng(seed)
n_days = years * 365 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) w0 = np.array([cfg[k] for k in names], float)
cap = np.outer(np.full(n_paths, START + lump_usd), w0) cap = np.outer(np.full(n_paths, START + lump_usd), w0)
alive = np.ones((n_paths, len(names)), bool) 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 = np.zeros(n_paths, bool)
hit_day = np.full(n_paths, n_days + 1, int) 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): for t in range(n_days):
cap *= (1.0 + R[idx[:, t], :]) cap *= (1.0 + R[idx[:, t], :])
if haz > 0: if (haz > 0).any():
dead = (rng.random((n_paths, len(names))) < haz) & alive dead = (rng.random((n_paths, len(names))) < haz) & alive
cap = np.where(dead, 0.0, cap) cap = np.where(dead, 0.0, cap)
alive &= ~dead 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: if t % 30 == 0 and t > 0 and dep > 0:
wa = alive * w0 wa = alive * w0
tot = wa.sum(axis=1, keepdims=True) 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 hit |= newly
yrs = np.where(hit, hit_day / 365.0, np.nan) yrs = np.where(hit, hit_day / 365.0, np.nan)
fin = yrs[~np.isnan(yrs)] 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()), 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"), median_yrs=float(np.median(fin)) if len(fin) > 50 else float("nan"),
p_all_dead=float((~alive.any(axis=1)).mean()), p_all_dead=float((~alive.any(axis=1)).mean()),
median_end=float(np.median(cap.sum(axis=1))), 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(" proprieta' del tempo di esposizione, non del capitale. Il lump non la peggiora —")
print(" ma moltiplica cio' che quella probabilita' porta via.") 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 # ---------------------------------------------------------------- (3) la soglia
print("\n" + "=" * 104) print("\n" + "=" * 104)
print(" (3) DA QUALE CAPITALE LO SPLIT E' POSSIBILE SENZA DISTORCERE I PESI") print(" (3) DA QUALE CAPITALE LO SPLIT E' POSSIBILE SENZA DISTORCERE I PESI")
+40
View File
@@ -151,3 +151,43 @@ def test_il_bersaglio_e_lo_stesso_per_entrambi_i_bracci():
usassero due bersagli, il confronto smetterebbe di essere leggibile.""" usassero due bersagli, il confronto smetterebbe di essere leggibile."""
import r0726_venue_risk as VR import r0726_venue_risk as VR
assert LS.TARGET == VR.TARGET assert LS.TARGET == VR.TARGET
# ===========================================================================
# rischio per-venue: la cassa non e' un exchange
# ===========================================================================
def test_un_p_per_venue_non_cambia_il_caso_scalare(series):
"""La generalizzazione a hazard per-venue deve lasciare identico il caso gia' pubblicato,
altrimenti la replica del 26/07 vale solo per la versione vecchia della funzione."""
kw = dict(n_paths=300, years=4, seed=9, lump_usd=5_000.0)
cfg = {"Deribit": 0.75, "IB": 0.25}
a = LS.simulate(cfg, series, 0.02, **kw)
b = LS.simulate(cfg, series, {"Deribit": 0.02, "IB": 0.02}, **kw)
assert a["p_reach"] == b["p_reach"] and a["p_all_dead"] == b["p_all_dead"]
def test_una_cassa_che_non_fallisce_azzera_la_rovina(series):
"""P(perso tutto) richiede che cadano TUTTI i conti: se il secondo non puo' cadere, e' zero
a qualunque quota. E' il risultato che rende la protezione indipendente da GTAA01."""
kw = dict(n_paths=400, years=6, seed=13, lump_usd=5_000.0)
zero = {**series, "IB": series["IB"] * 0.0}
r = LS.simulate({"Deribit": 0.9, "IB": 0.1}, zero, {"Deribit": 0.05, "IB": 0.0}, **kw)
assert r["p_all_dead"] == 0.0
def test_il_salvataggio_scala_con_la_quota_tenuta_fuori(series):
"""P(perso tutto) SATURA a qualunque quota > 0: cio' che distingue il 10% dal 40% e' quanto
si salva nel momento del fallimento, e deve crescere con la quota."""
kw = dict(n_paths=600, years=8, seed=17, lump_usd=5_000.0)
zero = {**series, "IB": series["IB"] * 0.0}
sv = [LS.simulate({"Deribit": 1 - q, "IB": q}, zero, {"Deribit": 0.05, "IB": 0.0},
**kw)["median_salvage"] for q in (0.10, 0.25, 0.40)]
assert sv[0] < sv[1] < sv[2]
def test_il_salvataggio_e_istantaneo_non_a_scadenza(series):
"""Deve misurare cio' che resta NEL MOMENTO del fallimento. Il capitale a 20 anni
condizionato al fallimento sarebbe gonfiato dai versamenti dirottati ai superstiti
valore che si ottiene comunque aprendo un altro conto, non merito dello split."""
assert "SALVA nel momento" in LS.simulate.__doc__ or "salvataggio istantaneo" in (
Path(LS.__file__).read_text())