diff --git a/CLAUDE.md b/CLAUDE.md index f1bfc0e..4f73c31 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 dell'operatore. ⚠️ Cosa NON decide: quanto dei €6.043 sia vero fondo d'emergenza (un fondo 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 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 diff --git a/docs/diary/2026-07-27-lumpsum-venue-gates.md b/docs/diary/2026-07-27-lumpsum-venue-gates.md index c9018a3..8418c95 100644 --- a/docs/diary/2026-07-27-lumpsum-venue-gates.md +++ b/docs/diary/2026-07-27-lumpsum-venue-gates.md @@ -275,3 +275,63 @@ Aggiunto. 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 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ì. diff --git a/scripts/research/r0727_lumpsum_split.py b/scripts/research/r0727_lumpsum_split.py index 4096829..c5b043e 100644 --- a/scripts/research/r0727_lumpsum_split.py +++ b/scripts/research/r0727_lumpsum_split.py @@ -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") diff --git a/tests/test_lumpsum_split.py b/tests/test_lumpsum_split.py index 6ff71a2..8576096 100644 --- a/tests/test_lumpsum_split.py +++ b/tests/test_lumpsum_split.py @@ -151,3 +151,43 @@ def test_il_bersaglio_e_lo_stesso_per_entrambi_i_bracci(): usassero due bersagli, il confronto smetterebbe di essere leggibile.""" import r0726_venue_risk as VR 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())