From 17b0f2cb4745dcc49660b3eb1db6a12c78cc6ac2 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Mon, 27 Jul 2026 14:58:50 +0000 Subject: [PATCH] =?UTF-8?q?research(capitale):=20"5k=20messi=20dove"=20?= =?UTF-8?q?=E2=80=94=20lo=20split=20si=20ottiene=20versando=20di=20meno?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CLAUDE.md | 21 ++++++ docs/diary/2026-07-27-lumpsum-venue-gates.md | 60 +++++++++++++++++ scripts/research/r0727_lumpsum_split.py | 71 ++++++++++++++++++-- tests/test_lumpsum_split.py | 40 +++++++++++ 4 files changed, 186 insertions(+), 6 deletions(-) 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())