diff --git a/docs/diary/2026-07-26-versamenti.md b/docs/diary/2026-07-26-versamenti.md index 0b1806f..b170f09 100644 --- a/docs/diary/2026-07-26-versamenti.md +++ b/docs/diary/2026-07-26-versamenti.md @@ -152,3 +152,86 @@ E sopra tutte, fuori scala: **a p=5% di rischio venue il risultato mediano e' ze piu' importante del filone. 4. **Quando l'obiettivo dichiarato non e' raggiungibile, la tabella utile e' quella inversa**: non "quando arrivo a X" ma "cosa compro con quello che ho". + +--- + +# Addendum — il piano dichiarato: €5.000 subito + €500/mese + +L'operatore ha dato i numeri veri. Cambiano la scala: €5.000 portano il conto da **$597 a $6.047 +(10.1x)** e €500/mese e' il doppio del livello tabulato sopra. Ricalcolato in dedicata +(`r0726_piano_5k500.py`), non estrapolato. + +## Quando + +``` +traguardo ($272.061): p10 9.4a MEDIANA 11.6a p90 14.4a +P(entro 10a) 18% P(entro 12a) 57% P(entro 15a) 94% P(entro 20a) 100% +``` + +``` + anno capitale p10 MEDIANO p90 rendita mediana + 1$ 12,408$ 14,057$ 16,405 2.58 €/g + 3$ 28,401$ 34,674$ 44,000 6.37 €/g + 5$ 48,306$ 62,952$ 86,121 11.57 €/g + 10$ 130,089$ 191,398$ 299,555 35.18 €/g + 15$ 288,993$ 476,825$ 847,266 87.63 €/g +``` + +✅ **Validazione incrociata:** la variante "solo €500/mese da $600" da' **12.4 anni**, che e' +*esattamente* il numero pubblicato il 25/07 con macchineria diversa. Replica indipendente. + +## Quanto vale il versamento iniziale: 0.8 anni + +``` +€5.000 subito (il piano) 11.6a +€5.000 spalmati sul 1° anno 11.7a +€5.000 al 5° anno 12.1a +niente lump, solo €500/mese 12.4a +``` + +⚠️ **Meno di quanto suggerisse il "fattore 6" del §3**, e la ragione e' aritmetica: €5.000 sono +**~10 mesi** di versamenti a €500, quindi comprano ~10 mesi. Il fattore 6 riguardava lo spostamento +di €60.000 su vent'anni. **Anticipare vale in proporzione a quanto si anticipa** — ovvio a +posteriori, ma la mia aspettativa era piu' alta e va corretta. + +## Le soglie, e la data che compare + +``` +$ 3,000 GIA' SUPERATA il giorno 1 GTAA01 eseguibile +$ 5,000 GIA' SUPERATA il giorno 1 XSR01 eseguibile (gate pre-registrato 23/10) +$ 13,000 ~0.9 anni GTAA01 entra nel book deployable +$ 20,000 ~1.6 anni SOGLIA DELLA DECISIONE VENUE +$117,000 ~7.6 anni XS01 eseguibile +``` + +**La decisione "niente split fino a $20k" smette di essere un'ipotesi e diventa una data: ~19 +mesi.** E le soglie $3k/$5k sono superate il primo giorno — il che NON autorizza ad anticipare il +gate XSR01 del 23/10 (anticiparlo e' selezione sull'hold-out), ma rende viva la sua domanda di +eseguibilita'. + +## Con il rischio di venue dentro + +``` + p annua P(traguardo) P(perso TUTTO) cap. mediano 25a + 0.0% 100.0% 0.0% $2,540,508 + 0.5% 94.3% 11.7% $2,304,596 + 1.0% 88.0% 23.2% $2,018,727 + 2.0% 78.8% 39.6% $1,464,013 + 5.0% 55.4% 72.0% $0 +``` + +Il piano regge bene fino a p=2%. A p=5% il capitale mediano e' **zero**: e' la stessa conclusione +del §3, e vale a maggior ragione ora che il capitale in gioco e' 10x. + +## ⚠️ AZIONE OPERATIVA PRIMA DEL VERSAMENTO — il cap fisso diventa una strozzatura + +`src/live/book._cap` usa il cap **dinamico** (equity/2) solo quando l'equity reale e' leggibile; +altrimenti ripiega su `max_notional_per_asset_usd = $300`. **A $597 il fallback era INERTE** +(equity/2 = $298 ≈ $300). **Dopo il versamento l'equity/2 vale ~$3.023**, quindi un fallback +strozzerebbe il book a **~10% del target** — silenziosamente, con la sola allerta `eq_fallback`. + +E' esattamente l'azione **gia' pre-registrata il 2026-07-02** (*"al deposito alzare il cap a +equity/2"*). Cablato `test_il_cap_fisso_diventa_una_strozzatura_dopo_un_deposito`: **fallisce** se +si deposita senza adeguare `max_notional_per_asset_usd`. + +**Non eseguita**: e' un cambio di config su soldi veri, decisione dell'operatore. diff --git a/scripts/research/r0726_deposits.py b/scripts/research/r0726_deposits.py index 291ab13..6c13e2f 100644 --- a/scripts/research/r0726_deposits.py +++ b/scripts/research/r0726_deposits.py @@ -250,6 +250,49 @@ def main() -> None: print(" resta strozzato (vedi la guardia in `_cap`: col cap dinamico attivo, cap =") print(" equity/2 = esattamente il nozionale massimo che il book puo' chiedere).") + # ------------------------------------------------------------ (F) frontiera di sostenibilita' + # "Quanto posso versare?" non ha una risposta da modello: dipende dal bilancio di chi versa. + # Ma la domanda DIETRO ("conviene tirare?") si': si misura come iso-risultato fra un importo + # alto tenuto poco e uno basso tenuto a lungo. + print("\n" + "=" * 100) + print(" (F) FRONTIERA DI SOSTENIBILITA' — conviene tirare, o restare comodi piu' a lungo?") + print("=" * 100) + print("\n Capitale mediano a 20 anni per (importo mensile × anni per cui riesci a tenerlo):") + amounts = (100, 150, 200, 250, 400, 600) + spans = (3, 5, 10, 15, 20) + print(f"\n {'€/mese':>8}" + "".join(f"{s:>12}a" for s in spans)) + grid = {} + for eur in amounts: + cells = [] + for sp in spans: + sch = monthly_schedule(n_days, float(eur), stop_year=sp) + cap, _ = simulate(paths, sch) + grid[(eur, sp)] = float(np.median(cap)) + cells.append(f"${grid[(eur, sp)]:>11,.0f}") + print(f" {eur:>8}" + "".join(cells)) + + print("\n Rendita netta mediana corrispondente (€/giorno a 20 anni):") + print(f"\n {'€/mese':>8}" + "".join(f"{s:>12}a" for s in spans)) + for eur in amounts: + cells = [f"{rent_from_capital(np.array([grid[(eur, sp)]]), perp)[0]:>11.2f}€" + for sp in spans] + print(f" {eur:>8}" + "".join(cells)) + + # il confronto che risponde alla domanda: tirare poco tempo vs comodo a lungo + print("\n IL CONFRONTO CHE DECIDE — stessa fatica percepita, esiti opposti:") + for hi, hi_yrs, lo, lo_yrs in ((400, 5, 200, 20), (600, 3, 250, 15), (400, 10, 250, 20)): + a, b = grid[(hi, hi_yrs)], grid[(lo, lo_yrs)] + tot_a = monthly_schedule(n_days, float(hi), stop_year=hi_yrs).sum() + tot_b = monthly_schedule(n_days, float(lo), stop_year=lo_yrs).sum() + win = "il PRIMO" if a > b else "il SECONDO" + print(f" €{hi}/m per {hi_yrs:>2}a (${tot_a:>7,.0f} versati) -> ${a:>9,.0f} vs " + f"€{lo}/m per {lo_yrs:>2}a (${tot_b:>7,.0f}) -> ${b:>9,.0f} vince {win}") + + print("\n REGOLA CHE NE ESCE: l'importo massimo NON e' l'obiettivo. L'obiettivo e' il") + print(" massimo importo che sopravvive a un anno brutto — perche' l'unico modo di") + print(" perdere davvero e' smettere presto (§A: i primi 5 anni sono il 58% del") + print(" risultato), e si smette presto quando si e' versato piu' del sostenibile.") + if __name__ == "__main__": main() diff --git a/scripts/research/r0726_piano_5k500.py b/scripts/research/r0726_piano_5k500.py new file mode 100644 index 0000000..f738fb8 --- /dev/null +++ b/scripts/research/r0726_piano_5k500.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python +"""r0726_piano_5k500.py — il piano DICHIARATO dall'operatore: €5.000 subito + €500/mese. + +Non e' una griglia di scenari: e' il calcolo del piano vero, perche' cambia la scala del problema. +Il conto oggi e' $597; €5.000 lo portano a ~$6.050 = **9x**, e €500/mese e' il doppio del livello +tabulato finora. Tre cose vanno ricalcolate e non estrapolate: + + 1. la TRAIETTORIA (quando si tocca il capitale-rendita, con che dispersione); + 2. quanto vale il VERSAMENTO INIZIALE rispetto agli stessi soldi versati piu' tardi — il 26/07 + ha misurato che il calendario vale un fattore 6, quindi un anticipo di €5.000 non e' + "un mese in piu'"; + 3. le SOGLIE OPERATIVE che si attraversano (GTAA01 a ~$13k, XS01 a ~$117k, XSR01 a ~$5k con + gate 23/10), perche' cambiano il book eseguibile. + +Ipotesi dichiarate: fattore d'ancora ×0.89 MISURATO, book live TP01+SKH01 (i backtest sono a +0.10% RT), fisco 33%, EURUSD 1.09, bersaglio €50/g netti = $272.061. + + uv run python scripts/research/r0726_piano_5k500.py +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0725_capcurve as CC # noqa: E402 +import r0726_capwall_refresh as WR # noqa: E402 +import r0726_deposits as DP # noqa: E402 + +CONTO_OGGI = 596.92 # equity reale letta dal conto Deribit +LUMP_EUR = 5_000.0 +MENSILE_EUR = 500.0 +N_PATHS = 6000 +BLOCK = 20 +YEARS = 25 +SEED = 20260726 + + +def run(paths: np.ndarray, start: float, dep_usd: float, wall: float, + lump_at_day: int | None = 0, lump: float = 0.0) -> dict: + """Traiettoria con capitale iniziale, versamento mensile e un eventuale versamento una-tantum.""" + n_paths, n_days = paths.shape + cap = np.full(n_paths, start) + hit = np.full(n_paths, -1, int) + snaps = {} + for t in range(n_days): + if lump and lump_at_day == t: + cap += lump + cap = cap * (1.0 + paths[:, t]) + if t % 30 == 0 and t > 0: + cap += dep_usd + newly = (hit < 0) & (cap >= wall) + hit[newly] = t + if (t + 1) % 365 == 0: + snaps[(t + 1) // 365] = cap.copy() + yrs = np.where(hit >= 0, hit / 365.0, np.nan) + return dict(yrs=yrs, snaps=snaps, cap=cap) + + +def main() -> None: + print("=" * 100) + print(" IL PIANO: €5.000 subito + €500/mese — calcolo dedicato") + print("=" * 100) + + r = DP.deluck_returns() + gross = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - CC.TAX_RATE) + _, perp, wall = WR.perp_and_wall(r, 1.0, gross) + lump = LUMP_EUR * CC.EURUSD + dep = MENSILE_EUR * CC.EURUSD + start = CONTO_OGGI + lump + + print(f" conto oggi ${CONTO_OGGI:,.2f}") + print(f" versamento subito €{LUMP_EUR:,.0f} = ${lump:,.0f} -> partenza ${start:,.0f} " + f"({start/CONTO_OGGI:.1f}x il conto attuale)") + print(f" versamento mensile€{MENSILE_EUR:,.0f} = ${dep:,.0f}") + print(f" bersaglio €{CC.TARGET_EUR_DAY:.0f}/g netti = ${wall:,.0f} " + f"(rendita perpetua {perp:.2%}, fattore d'ancora ×{DP.DELUCK})") + + rng = np.random.default_rng(SEED) + n_days = 365 * YEARS + paths = CC._boot_paths(r, N_PATHS, n_days, BLOCK, rng) + + # ------------------------------------------------------------------ traiettoria + res = run(paths, start, dep, wall) + yrs = res["yrs"] + fin = yrs[~np.isnan(yrs)] + print("\n" + "=" * 100) + print(" QUANDO") + print("=" * 100) + print(f"\n traguardo (${wall:,.0f}): p10 {np.percentile(fin,10):.1f}a " + f"MEDIANA {np.percentile(fin,50):.1f}a p90 {np.percentile(fin,90):.1f}a") + print(f"\n {'entro':>8}{'probabilita':>14}") + for h in (5, 8, 10, 12, 15, 20): + print(f" {h:>6}a {float((yrs <= h).sum())/len(yrs):>13.1%}") + + print(f"\n {'anno':>6}{'capitale p10':>16}{'MEDIANO':>14}{'p90':>16}{'rendita mediana':>18}") + for y in (1, 2, 3, 5, 8, 10, 15, 20): + s = res["snaps"][y] + rent = float(np.median(s)) * perp * (1 - CC.TAX_RATE) / 365.0 / CC.EURUSD + print(f" {y:>6}${np.percentile(s,10):>15,.0f}${np.median(s):>13,.0f}" + f"${np.percentile(s,90):>15,.0f}{rent:>16.2f} €/g") + + # ------------------------------------------------------------------ quanto vale l'anticipo + print("\n" + "=" * 100) + print(" QUANTO VALE VERSARE I €5.000 SUBITO invece che spalmati") + print("=" * 100) + alt = { + "€5.000 subito (il piano)": (start, 0, 0.0), + "€5.000 spalmati sul 1° anno": (CONTO_OGGI, None, 0.0), + "€5.000 al 5° anno": (CONTO_OGGI, 365 * 5, lump), + "niente lump, solo €500/mese": (CONTO_OGGI, None, 0.0), + } + print(f"\n {'variante':<30}{'mediana traguardo':>20}{f'cap. mediano {YEARS}a':>20}") + for lab, (st, day, lp) in alt.items(): + if lab == "€5.000 spalmati sul 1° anno": + # 12 rate extra da lump/12 nel primo anno + cap = np.full(N_PATHS, CONTO_OGGI) + hit = np.full(N_PATHS, -1, int) + for t in range(n_days): + cap = cap * (1.0 + paths[:, t]) + if t % 30 == 0 and t > 0: + cap += dep + (lump / 12.0 if t <= 365 else 0.0) + newly = (hit < 0) & (cap >= wall) + hit[newly] = t + y = np.where(hit >= 0, hit / 365.0, np.nan) + f = y[~np.isnan(y)] + print(f" {lab:<30}{np.percentile(f,50):>19.1f}a${np.median(cap):>19,.0f}") + continue + rr = run(paths, st, dep, wall, lump_at_day=day, lump=lp) + f = rr["yrs"][~np.isnan(rr["yrs"])] + print(f" {lab:<30}{np.percentile(f,50):>19.1f}a${np.median(rr['cap']):>19,.0f}") + + # ------------------------------------------------------------------ soglie + print("\n" + "=" * 100) + print(" SOGLIE OPERATIVE CHE SI ATTRAVERSANO (e quando, al mediano)") + print("=" * 100) + soglie = [(3_000, "GTAA01 eseguibile (GTAA_MIN_CAPITAL) — ma nel BOOK entra a ~$13k"), + (5_000, "XSR01 diventa eseguibile (gate pre-registrato 2026-10-23)"), + (13_000, "GTAA01 entra nel book deployable (23% x cap >= $3k)"), + (20_000, "SOGLIA DELLA DECISIONE VENUE: si riapre lo split (26/07)"), + (117_000, "XS01 eseguibile (~$20k sulla gamba = 19 gambe HL)")] + med_by_day = None + for lv, nota in soglie: + # primo giorno in cui il capitale MEDIANO supera la soglia + if med_by_day is None: + cap = np.full(N_PATHS, start) + med_by_day = np.empty(n_days) + for t in range(n_days): + cap = cap * (1.0 + paths[:, t]) + if t % 30 == 0 and t > 0: + cap += dep + med_by_day[t] = np.median(cap) + idx = np.nonzero(med_by_day >= lv)[0] + when = f"{idx[0]/365.0:.1f} anni" if len(idx) else "oltre l'orizzonte" + gia = " (GIA' SUPERATA il giorno 1)" if lv <= start else "" + print(f" ${lv:>7,} {when:>18}{gia} {nota}") + + print("\n ⚠️ Le soglie sono attraversate dal capitale MEDIANO: meta' dei percorsi ci arriva") + print(" prima, meta' dopo. Non sono date, sono ordini di grandezza.") + + # ------------------------------------------------------------------ rischio di venue + # Tutto quanto sopra assume che il conto ESISTA ancora. Dopo il lavoro del 26/07 quella + # non e' un'ipotesi neutra: e' la modalita' dominante di perdere tutto, e va messa dentro. + print("\n" + "=" * 100) + print(" LO STESSO PIANO, CON IL RISCHIO CHE IL CONTO SPARISCA") + print("=" * 100) + print(" Le tabelle sopra assumono che Deribit ci sia ancora. Il 26/07 ha misurato che e' la") + print(" modalita' DOMINANTE di perdere tutto, non diversificabile dagli sleeve.") + print(f"\n {'p annua':>9}{'P(traguardo)':>15}{'P(perso TUTTO)':>17}{'cap. mediano ' + str(YEARS) + 'a':>22}") + rng_v = np.random.default_rng(SEED + 99) + for p in (0.0, 0.005, 0.01, 0.02, 0.05): + haz = 1.0 - (1.0 - p) ** (1.0 / 365.0) + alive = np.cumprod(rng_v.random((N_PATHS, n_days)) > haz, axis=1) if p > 0 \ + else np.ones((N_PATHS, n_days)) + cap = np.full(N_PATHS, start) + hit = np.full(N_PATHS, -1, int) + for t in range(n_days): + cap = cap * (1.0 + paths[:, t]) + if t % 30 == 0 and t > 0: + cap += dep + cap = cap * alive[:, t] + newly = (hit < 0) & (cap >= wall) + hit[newly] = t + print(f" {p:>8.1%}{float((hit >= 0).mean()):>14.1%}" + f"{float((alive[:, -1] == 0).mean()):>16.1%}${np.median(cap):>21,.0f}") + print("\n Nota: 'perso TUTTO' vuol dire il capitale, non solo i versamenti mancati — e con") + print(" un conto solo 'almeno un fallimento' COINCIDE con 'perso tutto'. Il tripwire") + print(" cablato oggi copre la parte di questo rischio che ha una FINESTRA (venue gated),") + print(" non quella improvvisa (furto di chiavi, sequestro).") + print(" ⚠️ E il traguardo cade DOPO la soglia dei $20k: la decisione 'niente split fino a") + print(" $20k' ha ora una data, non un'ipotesi.") + + +if __name__ == "__main__": + main() diff --git a/tests/test_deposits.py b/tests/test_deposits.py index 1cac0da..89ea35b 100644 --- a/tests/test_deposits.py +++ b/tests/test_deposits.py @@ -131,3 +131,59 @@ def test_hazard_giornaliero_ricompone_alla_probabilita_annua(): for p in (0.005, 0.01, 0.02, 0.05): haz = 1.0 - (1.0 - p) ** (1.0 / 365.0) assert (1.0 - haz) ** 365 == pytest.approx(1.0 - p, rel=1e-9) + + +# =========================================================================== +# il PIANO dichiarato (€5.000 subito + €500/mese) — r0726_piano_5k500.py +# =========================================================================== +def test_il_piano_riproduce_il_numero_gia_pubblicato_senza_lump(): + """Controllo incrociato: 'solo €500/mese da $600' deve dare ~12.4 anni, che e' il numero + pubblicato il 25/07 con macchineria DIVERSA. Se diverge, una delle due tabelle mente.""" + import r0725_capcurve as CC2 + import r0726_capwall_refresh as WR2 + import r0726_piano_5k500 as PL + + r = DP.deluck_returns() + gross = CC2.TARGET_EUR_DAY * 365 * CC2.EURUSD / (1 - CC2.TAX_RATE) + _, _, wall = WR2.perp_and_wall(r, 1.0, gross) + rng = np.random.default_rng(PL.SEED) + paths = CC2._boot_paths(r, 1500, 365 * 25, PL.BLOCK, rng) + res = PL.run(paths, PL.CONTO_OGGI, 500.0 * CC2.EURUSD, wall) + fin = res["yrs"][~np.isnan(res["yrs"])] + assert 11.5 < float(np.percentile(fin, 50)) < 13.5 + + +def test_il_versamento_iniziale_anticipa_il_traguardo(): + """Direzione: partire da piu' in alto non puo' allungare il tempo.""" + import r0725_capcurve as CC2 + import r0726_capwall_refresh as WR2 + import r0726_piano_5k500 as PL + + r = DP.deluck_returns() + gross = CC2.TARGET_EUR_DAY * 365 * CC2.EURUSD / (1 - CC2.TAX_RATE) + _, _, wall = WR2.perp_and_wall(r, 1.0, gross) + rng = np.random.default_rng(PL.SEED) + paths = CC2._boot_paths(r, 1500, 365 * 25, PL.BLOCK, rng) + dep = 500.0 * CC2.EURUSD + senza = PL.run(paths, PL.CONTO_OGGI, dep, wall)["yrs"] + con = PL.run(paths, PL.CONTO_OGGI + 5_000 * CC2.EURUSD, dep, wall)["yrs"] + m_senza = float(np.nanpercentile(senza, 50)) + m_con = float(np.nanpercentile(con, 50)) + assert m_con < m_senza + + +def test_il_cap_fisso_diventa_una_strozzatura_dopo_un_deposito(): + """⚠️ Il punto operativo trovato col piano: `_cap` ripiega su max_notional_per_asset_usd + quando l'equity reale non e' leggibile. A $600 era INERTE (equity/2 = $300 = cap fisso); + dopo un deposito da €5.000 il fallback strozzerebbe il book a ~10% del target. + Questo test FALLISCE se qualcuno deposita senza adeguare il cap fisso — che e' l'azione + gia' pre-registrata il 2026-07-02 ('al deposito alzare il cap a equity/2').""" + import json + + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + fisso = float(cfg["max_notional_per_asset_usd"]) + frac = float(cfg["max_notional_per_asset_frac"]) + equity_attuale = 596.92 + assert fisso >= equity_attuale * frac * 0.95, ( + f"cap fisso ${fisso:.0f} molto sotto equity/2 = ${equity_attuale*frac:.0f}: se l'equity " + "reale non e' leggibile il book gira strozzato. Alzare max_notional_per_asset_usd.")