#!/usr/bin/env python """r0822d_piano_vero.py — LA TABELLA DEL PIANO CON TUTTE LE CORREZIONI DENTRO, UNA VOLTA SOLA. IL BUCO. L'obiettivo dichiarato del progetto e' 50 EUR/giorno e la domanda operativa e' «quanto ci vuole, versando quanto». La tabella pubblicata che risponde e' stata corretta DUE VOLTE, ogni volta partendo dallo stesso numero LORDO e mai dall'altra correzione: riga «250 EUR/mese», P(traguardo entro 20 anni) * 92% al LORDO (26/07, fattore d'ancora x0.89) * 52% col FISCO d'accumulo (07/08, r0807_piano_netto) * 64% col FUNDING (22/08, r0822d_funding) * ??? con ENTRAMBI <- non esiste Le due lenti sono state applicate SEPARATAMENTE. Questo script produce la lente unica, coerente, con dentro tutte le correzioni MISURATE (nessuna scelta a mano), e dice quanto ci vuole davvero. L'ORDINE DI APPLICAZIONE, e perche' quello. 1. FUNDING — e' un costo sul rendimento LORDO: si paga sul nozionale, ogni ora, prima che esista una plusvalenza. Entra DENTRO la serie dei ritorni, giorno per giorno (`r0822d_funding.book_75_25(rate)`), non come haircut a valle. 2. ANCORA x0.89 — e' una correzione sul DRIFT della serie (fortuna di calendario misurata, r0726_deluck_factor). Si applica alla serie, dopo il funding, come fa `r0822d_funding.sezione4c` — cosi' i numeri restano confrontabili con quelli pubblicati. La convenzione alternativa (de-luckare il lordo e POI togliere il funding) e' riportata come sensibilita': vale 0.24pp di drift. 3. FISCO — tassa la PLUSVALENZA, a fine anno, sul percorso GIA' netto di funding. L'ordine inverso (tassare la plusvalenza lorda e poi sottrarre il funding dal capitale gia' tassato) farebbe pagare il 33% su un guadagno mai incassato. Da qui l'effetto di SECONDO ORDINE che la somma delle due correzioni non contiene: meno drift -> meno plusvalenza -> meno imposta. Il congiunto e' meno severo della somma, e di quanto e' una MISURA (sezione 3), non un'assunzione. COSA NON C'E' DENTRO, DI PROPOSITO. Il rischio di venue. Non e' un costo, e' un rischio di ROVINA: a p=5% annuo il capitale mediano a 20 anni e' ZERO per qualunque calendario di versamenti (26/07). Sta su un asse separato e ci resta; metterlo qui trasformerebbe una tabella di pianificazione in una tabella di sopravvivenza. ASSUNZIONI FISCALI, dichiarate e NON un parere fiscale (le stesse del 27/07 e del 07/08): 33% sulle plusvalenze cripto realizzate, minusvalenze in carry 4 anni, patrimoniale 0.2%/anno sul valore. Il modello tassa la variazione ANNUA di valore = limite superiore, stretto perche' il libro realizza quasi tutto entro l'anno. TRAPPOLE GIA' PAGATE DA QUESTO PROGETTO, evitate qui per costruzione: * un NON-ARRIVO si codifica +inf, mai -1 (con -1 la mediana MIGLIORA quando la colonna peggiora: errore commesso il 07/08). Qui si stampano DUE mediane: quella incondizionata (non-arrivo = +inf, quindi «>25a») e quella condizionata, che non compare MAI senza la sua probabilita' accanto. * un Monte Carlo ha una RISOLUZIONE: misurata su 3 semi e dichiarata (sezione 2). * la serie del libro e' su giorni di CALENDARIO (cripto, 365/anno): l'annualizzazione a 365 e' corretta e `to_daily()` non serve. Sarebbe servito su una serie di giorni di borsa. uv run python scripts/research/r0822d_piano_vero.py uv run python scripts/research/r0822d_piano_vero.py --rebuild # ignora la cache di serie """ from __future__ import annotations import sys import time from pathlib import Path import numpy as np import pandas as pd ROOT = Path("/opt/docker/PythagorasGoal") sys.path.insert(0, str(ROOT / "scripts" / "research")) sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) sys.path.insert(0, str(ROOT)) import r0725_capcurve as CC # noqa: E402 TAX_RATE, EURUSD, _boot_paths, book_series import r0726_capwall_refresh as WR # noqa: E402 perp_and_wall, trajectory (2000 path) import r0727_lumpsum_split as LS # noqa: E402 TARGET pubblicato import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI import r0807_piano_netto as PN # noqa: E402 passo_fiscale, sopravvivenza, accumula # --------------------------------------------------------------------------- # Costanti dichiarate # --------------------------------------------------------------------------- DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor), non a occhio START = 635.0 # il conto Deribit VERO di oggi (le tabelle pubblicate usano 600) START_PUB = 600.0 # solo per la replica N_WALL = 4000 # path per il muro (la taglia a cui furono calcolati i pubblicati) N_TRAJ = 3000 # path per le traiettorie (idem, seed 725) N_DEP = 3000 # path per la bisezione sul versamento BLOCK = 20 SEED_TRAJ = 725 SEED_DEP = 20260822 SEED_WALL = PN.SEED_MURO # 725 ANNI_TRAJ = 25 # 20 troncherebbe la coda: convenzione di WR.trajectory VERSAMENTI = (0, 250, 500, 800, 1_000, 2_000) LUMPS_EUR = (0.0, 2_000.0, 5_000.0, 10_000.0) ORIZZONTI = (10, 15, 20) CONFIDENZE = (0.50, 0.75, 0.90) VINTAGE_PUB = "2026-08-07" # la data in cui furono calcolati $272.061 / $258.338 COSTO_STRUMENTO_VERO = 0.013931 # -1.39%/anno: funding misurato sulla sola finestra >= 2022-03 CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/pianovero") if not CACHE.parent.parent.exists(): CACHE = Path("/tmp/pythagoras_pianovero") REBUILD = "--rebuild" in sys.argv T0 = time.time() def sez(t: str) -> None: print("\n" + "=" * 100) print(f" {t}") print("=" * 100) def clock() -> str: return f"[{time.time() - T0:5.0f}s]" # =========================================================================== # 0) LE SERIE # =========================================================================== def costruisci_serie() -> pd.DataFrame: """Le due serie giornaliere del libro live 75/25: senza e con funding. Nessuna re-implementazione: `r0822d_funding.book_75_25` e' la stessa lente di `CC.book_series('hourly')` e la replica bit-exact e' verificata qui sotto. """ f = CACHE / "book.parquet" if f.exists() and not REBUILD: return pd.read_parquet(f) # Se la cache dello storico funding c'e', si usa quella: nessuna chiamata al venue (il # progetto gira su una VPS con soldi veri e un rate limit Deribit per-IP gia' saturato una # volta, 29/07). Se NON c'e', si lascia che `fetch_funding` la scarichi: cosi' lo script # gira anche da solo, su una macchina pulita. for c in (Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/fund"), Path("/tmp/pythagoras_funding_cache")): if any(c.glob("*.parquet")): sys.argv.append("--no-net") break import r0822d_funding as FD # noqa: E402 (USE_NET si decide all'import) F = {} for instr, (start, _asset, _kind) in FD.INSTR.items(): d = FD.fetch_funding(instr, start).copy() d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True) F[instr] = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt") rate = FD.build_rate(F) A, B = FD.book_75_25(None), FD.book_75_25(rate) out = pd.DataFrame({"nofund": A, "fund": B.reindex(A.index)}) CACHE.mkdir(parents=True, exist_ok=True) out.to_parquet(f) return out def deluck(r: np.ndarray) -> np.ndarray: """Toglie dal DRIFT la fortuna d'ancora misurata. La vol non si tocca: fra le ancore era invariata (7.80% -> 7.76%), la fortuna sta tutta nel drift (r0726_deluck_factor).""" r = np.asarray(r, float) return r - (1.0 - DELUCK) * r.mean() # =========================================================================== # LE LENTI # =========================================================================== class Lente: """Una lente = (serie di ritorni, convenzione di prelievo, regime fiscale d'accumulo). `lordizza=True` -> convenzione PUBBLICATA: il prelievo si lordizza al 33% e il capitale investito non paga mai imposte. Incoerente (r0807), tenuta per replica. `lordizza=False` -> convenzione COERENTE: imposte ogni anno dentro il portafoglio, prelievo gia' netto. """ def __init__(self, nome: str, r: np.ndarray, lordizza: bool, nota: str = ""): self.nome, self.r, self.lordizza, self.nota = nome, np.asarray(r, float), lordizza, nota self.aliq = 0.0 if lordizza else CC.TAX_RATE self.patr = 0.0 if lordizza else TX.PATRIMONIALE self.perp = float("nan") self.muro = float("nan") @property def prelievo(self) -> float: netto = CC.TARGET_EUR_DAY * 365 * CC.EURUSD return netto / (1 - CC.TAX_RATE) if self.lordizza else netto @property def drift(self) -> float: return float(self.r.mean()) * 365.0 @property def vol(self) -> float: return float(self.r.std()) * 365.0 ** 0.5 def muro_di(lente: Lente, n_paths: int = N_WALL, seed: int = SEED_WALL) -> tuple[float, float]: """(rendita perpetua, capitale necessario). Perpetua = il prelievo piu' alto con P(capitale a 20 anni >= capitale iniziale) >= 90% — definizione del 25/07, invariata. Bisezione identica a `PN.muro`, esposta solo per poter cambiare SEME e misurare la risoluzione Monte Carlo. Il controllo positivo (a seme 725 deve dare esattamente `PN.muro`) e' in sezione 2. """ lo, hi = 0.0, 0.40 for _ in range(13): mid = (lo + hi) / 2 s = PN.sopravvivenza(lente.r, 1e6, 1e6 * mid, 20, lente.aliq, lente.patr, n_paths=n_paths, seed=seed) if s["p10_end"] >= 1e6: lo = mid else: hi = mid return lo, (lente.prelievo / lo if lo > 0.002 else float("inf")) # =========================================================================== # lettura onesta di un vettore di primi passaggi # =========================================================================== def leggi(colpito: np.ndarray, anni_sim: int = ANNI_TRAJ) -> dict: """Mediana INCONDIZIONATA (non-arrivo = +inf) + mediana condizionata + P(20 anni). ⚠️ Il non-arrivo e' +inf, MAI -1: con -1 la mediana scende quanto peggio va la colonna (errore commesso il 07/08 e corretto lo stesso giorno). E la mediana condizionata non si stampa mai da sola: descrive solo chi arriva. """ y = np.where(colpito >= 0, colpito / 365.0, np.inf) arr = y[np.isfinite(y)] return dict( med=float(np.median(y)), # +inf se meno di meta' arriva med_cond=float(np.median(arr)) if len(arr) > 100 else float("nan"), p20=float((y <= 20.0).mean()), p_arr=float(np.isfinite(y).mean()), anni_sim=anni_sim) def fmt_med(d: dict) -> str: m = d["med"] return f">{d['anni_sim']}a" if not np.isfinite(m) else f"{m:.1f}a" def fmt_cond(d: dict) -> str: c = d["med_cond"] return "—" if np.isnan(c) else f"{c:.1f}a" # =========================================================================== # bisezione sul versamento (start parametrico: le tabelle pubblicate partono da $600) # =========================================================================== def dep_per_conf(paths: np.ndarray, lente: Lente, conf: float, start: float, lump_eur: float = 0.0) -> tuple[float, float]: """(versamento mensile EUR minimo per P(traguardo entro l'orizzonte) >= conf, totale versato). I path si estraggono UNA volta e si riusano a ogni passo: con campioni diversi per passo la funzione non sarebbe monotona e la bisezione oscillerebbe (lezione WR.solve_deposit). """ def p(dep: float) -> float: c = PN.accumula(paths, dep, lente.muro, lente.aliq, lente.patr, start=start, lump_eur=lump_eur)["colpito"] return float((c >= 0).mean()) lo, hi = 0.0, 6_000.0 if p(hi) < conf: return float("inf"), float("nan") for _ in range(12): mid = (lo + hi) / 2 if p(mid) >= conf: hi = mid else: lo = mid versato = PN.accumula(paths, hi, lente.muro, lente.aliq, lente.patr, start=start, lump_eur=lump_eur)["versato"] return hi, versato # =========================================================================== def main() -> None: print("=" * 100) print(" r0822d — IL PIANO VERO: fisco d'accumulo E funding nella STESSA tabella") print("=" * 100) B = costruisci_serie() idx = pd.DatetimeIndex(B.index) A_raw, F_raw = B["nofund"].values.astype(float), B["fund"].values.astype(float) print(f"\n serie giornaliera del libro LIVE (TP01 75 + SKH01 25, lente 'hourly', path live)") print(f" {len(B)} giorni, dal {idx.min().date()} al {idx.max().date()} {clock()}") print(f" drift lordo {A_raw.mean() * 365:.3%} · drift netto funding {F_raw.mean() * 365:.3%}") print(" ⚠️ giorni di CALENDARIO (cripto): annualizzazione a 365 corretta, `to_daily()` non") print(" serve qui — servirebbe su una serie di giorni di borsa (lezione 25/07 su GTAA01).") # ===================================================================== sez("1) CONTROLLO DI REPLICA — prima di pubblicare un numero nuovo, riprodurre i vecchi") print("\n Regola del progetto (07/08): «prima di pubblicare un numero nuovo, far riprodurre") print(" alla macchina quello vecchio». Cinque controlli, tutti su codice IMPORTATO.\n") ok = {} # (R1) la serie senza funding e' bit-exact la serie del libro gia' pubblicata C = CC.book_series("hourly") d = float(np.max(np.abs(A_raw - C.reindex(idx).values))) ok["R1"] = d == 0.0 print(f" R1 serie 'senza funding' vs CC.book_series('hourly') max|dif| = {d:.3e} " f"{'BIT-EXACT' if ok['R1'] else 'FALLITA'}") # (R2) il costo del funding per finestra print(f"\n R2 costo del funding per finestra (pubblicato 22/08: -2,16% / -1,39% / -0,55%)") fin = [("piena (proxy inverse pre-2022)", None, -0.0216), (">= 2022-03-15 (strumento vero)", "2022-03-15", -0.0139), (">= 2025-01-01", "2025-01-01", -0.0055)] r2 = True for lab, lo, atteso in fin: m = np.ones(len(B), bool) if lo is None else np.asarray(idx >= pd.Timestamp(lo, tz="UTC")) got = -(A_raw[m].mean() - F_raw[m].mean()) * 365 r2 &= abs(got - atteso) < 5e-4 print(f" {lab:<34} n={m.sum():>5} misurato {got:+.4%} atteso {atteso:+.2%}") ok["R2"] = r2 # (R3-R4) i muri pubblicati, sul VINTAGE su cui furono calcolati print(f"\n R3/R4 i muri pubblicati ($272.061 lordo / $258.338 netto fisco) sul vintage") print(f" {VINTAGE_PUB} — la data in cui furono calcolati.") mv = np.asarray(idx <= pd.Timestamp(VINTAGE_PUB, tz="UTC")) Lv0 = Lente("vintage LORDO", deluck(A_raw[mv]), True) Lv1 = Lente("vintage +FISCO", deluck(A_raw[mv]), False) Lv0.perp, Lv0.muro = PN.muro(Lv0.r, Lv0.prelievo, 0.0, 0.0) Lv1.perp, Lv1.muro = PN.muro(Lv1.r, Lv1.prelievo, Lv1.aliq, Lv1.patr) ok["R3"] = abs(Lv0.muro - LS.TARGET) < 1.0 ok["R4"] = abs(Lv1.muro - 258_338.0) < 1.0 print(f" LORDO perpetua {Lv0.perp:.4%} muro ${Lv0.muro:,.0f} atteso $272,061 " f"{'ESATTO AL DOLLARO' if ok['R3'] else 'DIVERSO'}") print(f" NETTO perpetua {Lv1.perp:.4%} muro ${Lv1.muro:,.0f} atteso $258,338 " f"{'ESATTO AL DOLLARO' if ok['R4'] else 'DIVERSO'} {clock()}") # (R5) i numeri del funding, alla SUA taglia MC (2000 path) print(f"\n R5 i numeri del filone funding (22/08), alla sua taglia dichiarata (2000 path):") g = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - CC.TAX_RATE) rA, rF = deluck(A_raw), deluck(F_raw) _, pA, wA = WR.perp_and_wall(rA, 1.0, g) _, pF, wF = WR.perp_and_wall(rF, 1.0, g) tA = WR.trajectory(rA, wA, n_paths=2000)[250] tF = WR.trajectory(rF, wF, n_paths=2000)[250] ok["R5"] = abs(wF / wA - 1 - 0.175) < 0.004 and abs(pF - 0.0914) < 3e-4 print(f" perpetua {pA:.2%} -> {pF:.2%} (pubblicato 10,73% -> 9,14%)") print(f" muro ${wA:,.0f} -> ${wF:,.0f} = {wF / wA - 1:+.1%} (pubblicato $276,6k -> " f"$325,0k = +17,5%)") print(f" traiettoria EUR250/mese {tA['p50']:.1f}a P20 {tA['p20y']:.0%} -> " f"{tF['p50']:.1f}a P20 {tF['p20y']:.0%} (pubblicato 16,4a/90% -> 18,7a/64%) {clock()}") # (R6) la colonna LORDA e la colonna NETTA delle traiettorie, sul vintage, da $600 print(f"\n R6 le due colonne di traiettoria pubblicate, sul vintage {VINTAGE_PUB}, da $600:") rng = np.random.default_rng(SEED_TRAJ) pv = CC._boot_paths(Lv0.r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rng) att_l = {250: 16.3, 500: 12.4, 800: 10.0, 1_000: 9.0, 2_000: 6.0} att_n = {250: 19.8, 500: 14.7, 800: 11.4, 1_000: 10.0, 2_000: 6.4} print(f" {'EUR/m':>7}{'LORDO mis.':>13}{'atteso':>9}{'P20':>7}" f"{' |':>3}{'NETTO mis.':>13}{'atteso':>9}{'P20':>7}") r6 = True for dep in (250, 500, 800, 1_000, 2_000): l = leggi(PN.accumula(pv, dep, Lv0.muro, 0.0, 0.0, start=START_PUB)["colpito"]) n = leggi(PN.accumula(pv, dep, Lv1.muro, Lv1.aliq, Lv1.patr, start=START_PUB)["colpito"]) r6 &= abs(l["med_cond"] - att_l[dep]) < 0.25 and abs(n["med_cond"] - att_n[dep]) < 0.25 print(f" {dep:>7}{fmt_cond(l):>13}{att_l[dep]:>9.1f}{l['p20']:>7.0%}" f"{' |':>3}{fmt_cond(n):>13}{att_n[dep]:>9.1f}{n['p20']:>7.0%}") ok["R6"] = r6 del pv print(f"\n ESITO REPLICA: {sum(ok.values())}/{len(ok)} — " + ", ".join(f"{k}={'OK' if v else 'NO'}" for k, v in ok.items())) if not all(ok.values()): raise SystemExit("REPLICA INCOMPLETA: un numero nuovo sarebbe indistinguibile da un bug.") print(""" ⚠️ E IL FATTO CHE VA DETTO: la replica e' esatta AL DOLLARO solo sul vintage del 07/08. Su TUTTA la serie di oggi lo stesso codice, senza una riga cambiata, da' muri diversi:""") L0t = Lente("oggi LORDO", rA, True) L0t.perp, L0t.muro = PN.muro(L0t.r, L0t.prelievo, 0.0, 0.0) print(f" muro LORDO: ${Lv0.muro:,.0f} (vintage {VINTAGE_PUB}, {mv.sum()} giorni) -> " f"${L0t.muro:,.0f} (oggi, {len(B)} giorni) = {L0t.muro / Lv0.muro - 1:+.1%}") print(" `data/raw/` e' gitignored e il cron lo allunga ogni notte: 15 giorni in piu' di") print(" dati spostano il muro del ~2%, cioe' PIU' della risoluzione Monte Carlo. E' la") print(" stessa lezione del 07/08 sul gate di GTAA01 (un numero cambia senza che il codice") print(" cambi = i dati non sono versionati). Percio' qui sotto ogni confronto fra lenti e'") print(" fatto sullo STESSO vintage: un grado di liberta' per volta.") # ===================================================================== sez("2) LE QUATTRO LENTI E IL MURO — la riga che non esisteva") print(""" Tutte de-luckate x0.89. Le prime due sono le lenti pubblicate; la terza e' quella del filone funding di stanotte; la quarta e' la lente unica con dentro TUTTO.""") Lo = { "L0 LORDO (26/07)": Lente("L0", rA, True, "prelievo lordizzato, nessuna imposta in accumulo"), "L1 +FISCO (07/08)": Lente("L1", rA, False, "imposte ogni anno nel portafoglio"), "L2 +FUNDING (22/08)": Lente("L2", rF, True, "funding esatto, convenzione pubblicata"), "L3 CONGIUNTA": Lente("L3", rF, False, "funding esatto + fisco coerente"), } rFb = deluck(A_raw - COSTO_STRUMENTO_VERO / 365.0) Lo["L3b CONGIUNTA (str. vero)"] = Lente("L3b", rFb, False, "funding -1,39%/anno (finestra >= 2022-03)") print(f"\n {'lente':<27}{'drift':>9}{'vol':>8}{'prelievo/a':>13}{'perpetua':>10}{'muro':>13}") for k, L in Lo.items(): L.perp, L.muro = muro_di(L) print(f" {k:<27}{L.drift:>9.2%}{L.vol:>8.2%}{L.prelievo:>13,.0f}" f"{L.perp:>10.2%}{L.muro:>13,.0f}") print(f" {clock()}") # controllo positivo sulla bisezione con seme + risoluzione Monte Carlo p_ctrl, w_ctrl = PN.muro(Lo["L3 CONGIUNTA"].r, Lo["L3 CONGIUNTA"].prelievo, CC.TAX_RATE, TX.PATRIMONIALE) same = abs(w_ctrl - Lo["L3 CONGIUNTA"].muro) < 1e-6 print(f"\n controllo positivo: `muro_di` a seme {SEED_WALL} == `PN.muro` importato " f"${w_ctrl:,.0f} vs ${Lo['L3 CONGIUNTA'].muro:,.0f} {'OK' if same else 'DIVERGE'}") semi = [Lo["L3 CONGIUNTA"].muro] for s in (2026, 8_22): semi.append(muro_di(Lo["L3 CONGIUNTA"], seed=s)[1]) sp = (max(semi) - min(semi)) / np.mean(semi) print(f" RISOLUZIONE MONTE CARLO del muro, misurata su 3 semi a {N_WALL} path: " + " / ".join(f"${x:,.0f}" for x in semi)) print(f" ampiezza {sp:.1%} -> si citano TRE cifre significative (${Lo['L3 CONGIUNTA'].muro/1000:.0f}k), " f"non il dollaro. {clock()}") # sensibilita' sull'ordine de-luck / funding alt = Lente("alt", deluck(A_raw) - (A_raw - F_raw).mean(), False) alt.perp, alt.muro = muro_di(alt) print(f"\n sensibilita' all'ORDINE de-luck/funding (convenzione alternativa: de-luckare il") print(f" drift LORDO e poi togliere il funding): drift {alt.drift:.2%} contro " f"{Lo['L3 CONGIUNTA'].drift:.2%}, muro ${alt.muro:,.0f} contro " f"${Lo['L3 CONGIUNTA'].muro:,.0f} ({alt.muro / Lo['L3 CONGIUNTA'].muro - 1:+.1%}).") print(" Sotto la risoluzione MC: la scelta di convenzione non decide niente. Si tiene quella") print(" di `r0822d_funding.sezione4c`, per confrontabilita' con i numeri gia' pubblicati.") # ===================================================================== sez("3) LE DUE CORREZIONI SI SOMMANO? — l'effetto di secondo ordine, misurato") print(""" ATTESA DICHIARATA PRIMA DI MISURARE: meno drift -> meno plusvalenza -> meno imposta, quindi il congiunto dovrebbe essere MENO severo della somma delle due correzioni prese da sole. Misurato come interazione di un fattoriale 2x2 (funding OFF/ON x fisco OFF/ON) in TRE monete, perche' un'interazione piccola puo' cambiare SEGNO a seconda di come la si misura.""") # (3a) accumulo puro: il capitale a 20 anni non dipende dal muro -> isola il meccanismo print("\n (3a) SUL CAPITALE ACCUMULATO — la domanda pura sul meccanismo.") print(" Capitale mediano a 20 anni, EUR250/mese, partenza $%.0f. Non dipende dal muro:" % START) print(" qui dentro c'e' SOLO l'accumulo, cioe' l'unico posto dove il meccanismo vive.") capf = {} for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"): L = Lo[k] rr = np.random.default_rng(SEED_DEP) pp = CC._boot_paths(L.r, N_DEP, 20 * 365, BLOCK, rr) capf[k] = float(np.median(PN.accumula(pp, 250, L.muro, L.aliq, L.patr, start=START)["cap"])) del pp print(f"\n {'':<14}{'fisco OFF':>16}{'fisco ON':>16}{'rapporto ON/OFF':>19}") for lab, a, b in (("funding OFF", "L0 LORDO (26/07)", "L1 +FISCO (07/08)"), ("funding ON ", "L2 +FUNDING (22/08)", "L3 CONGIUNTA")): print(f" {lab:<14}{capf[a]:>16,.0f}{capf[b]:>16,.0f}" f"{capf[b] / capf[a]:>19.4f}") dragOFF = capf["L1 +FISCO (07/08)"] / capf["L0 LORDO (26/07)"] dragON = capf["L3 CONGIUNTA"] / capf["L2 +FUNDING (22/08)"] inter_log = (np.log(capf["L3 CONGIUNTA"]) - np.log(capf["L1 +FISCO (07/08)"]) - np.log(capf["L2 +FUNDING (22/08)"]) + np.log(capf["L0 LORDO (26/07)"])) verso_drag = "MENO" if dragON > dragOFF else "DI PIU'" print(f"\n Il fisco toglie il {1 - dragOFF:.1%} del capitale a funding OFF e il " f"{1 - dragON:.1%} a funding ON.") esito_mecc = ("ESISTE ed e' nel verso previsto" if dragON > dragOFF else "NON si vede") print(f" -> il meccanismo {esito_mecc}: con meno drift il fisco morde {verso_drag}.") print(f" Interazione moltiplicativa (log-capitale): {inter_log:+.4f} = " f"{np.expm1(inter_log):+.2%} di capitale.") print(" ⚠️ Ma e' UNA FRAZIONE: il fisco resta la correzione dominante in entrambe le") print(" colonne, e la 'compensazione' non ne recupera che una parte trascurabile.") # (3b) il muro inter_w = (Lo["L3 CONGIUNTA"].muro - Lo["L1 +FISCO (07/08)"].muro - Lo["L2 +FUNDING (22/08)"].muro + Lo["L0 LORDO (26/07)"].muro) print("\n (3b) SUL MURO — la fase di prelievo.") print(f" ${Lo['L0 LORDO (26/07)'].muro:,.0f} (L0) · ${Lo['L1 +FISCO (07/08)'].muro:,.0f} (L1) · " f"${Lo['L2 +FUNDING (22/08)'].muro:,.0f} (L2) · ${Lo['L3 CONGIUNTA'].muro:,.0f} (L3)") print(f" interazione ${inter_w:+,.0f} = {abs(inter_w) / Lo['L3 CONGIUNTA'].muro:.2%} del muro " f"congiunto, cioe' SOTTO la risoluzione Monte Carlo ({sp:.1%}): indistinguibile da zero.") # (3c) il versamento necessario print("\n (3c) SUL VERSAMENTO NECESSARIO — la moneta in cui si decide.") print(f" Orizzonte 20 anni, P=90%, partenza ${START:.0f}. Ogni lente col PROPRIO muro.") # ⚠️ ogni lente ha la SUA serie di ritorni: i path si riestraggono per lente (stesso seme, # quindi stessi INDICI di blocco: il confronto fra lenti resta appaiato) e non si riusano. dep90 = {} for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"): L = Lo[k] rr = np.random.default_rng(SEED_DEP) pp = CC._boot_paths(L.r, N_DEP, 20 * 365, BLOCK, rr) dep90[k], _ = dep_per_conf(pp, L, 0.90, START) del pp b = dep90["L0 LORDO (26/07)"] d_fisco = dep90["L1 +FISCO (07/08)"] - b d_fund = dep90["L2 +FUNDING (22/08)"] - b d_joint = dep90["L3 CONGIUNTA"] - b inter = d_joint - d_fisco - d_fund print(f"\n {'lente':<27}{'EUR/mese per P=90% a 20 anni':>32}{'vs L0':>10}") for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"): print(f" {k:<27}{dep90[k]:>32,.0f}{dep90[k] - b:>+10,.0f}") print(f"\n somma ingenua : {d_fisco:+,.0f} {d_fund:+,.0f} = {d_fisco + d_fund:+,.0f} EUR/mese") print(f" MISURATO : {d_joint:+,.0f} EUR/mese") print(f" INTERAZIONE : {inter:+,.0f} EUR/mese ({inter / (d_fisco + d_fund):+.0%} della somma)") # ⚠️ un'interazione piccola e' una DIFFERENZA DI DIFFERENZE: prima di dichiararne il SEGNO # va misurato il rumore Monte Carlo che la circonda, o si pubblica un segno di rumore. inters = [inter] for sd in (4_242, 90_909): dd = {} for k in ("L0 LORDO (26/07)", "L1 +FISCO (07/08)", "L2 +FUNDING (22/08)", "L3 CONGIUNTA"): L = Lo[k] rr = np.random.default_rng(sd) pp = CC._boot_paths(L.r, N_DEP, 20 * 365, BLOCK, rr) dd[k], _ = dep_per_conf(pp, L, 0.90, START) del pp inters.append(dd["L3 CONGIUNTA"] - dd["L1 +FISCO (07/08)"] - dd["L2 +FUNDING (22/08)"] + dd["L0 LORDO (26/07)"]) segno_ok = all(x > 0 for x in inters) or all(x < 0 for x in inters) et_segno = "STABILE" if segno_ok else "NON stabile: e' rumore" print(" su 3 semi: " + " / ".join(f"{x:+,.0f}" for x in inters) + f" -> segno {et_segno}" + f" (bisezione a {6000 / 2 ** 12:.1f} EUR/m di risoluzione)") verso = "MENO" if np.median(inters) < 0 else "PIU'" print(f"\n 📌 ESITO, e la mia attesa era SBAGLIATA nella moneta che conta. Il meccanismo") print(f" esiste (3a: il fisco morde meno dove il drift e' minore) ma nel VERSAMENTO") if segno_ok: print(f" necessario il congiunto e' {verso} severo della somma ({np.median(inters):+,.0f} EUR/m,") print(" segno stabile su 3 semi): la funzione «versamento -> probabilita'» e' convessa") print(" e ribalta il segno di un'interazione gia' piccola.") else: print(f" necessario l'interazione NON ha segno stabile fra i semi ({inters[0]:+,.0f} / " f"{inters[1]:+,.0f} / {inters[2]:+,.0f} EUR/m):") print(" e' rumore Monte Carlo, e va detto invece di pubblicarne il segno.") print(" Sul MURO l'interazione e' sotto la risoluzione MC.") print(f" REGOLA OPERATIVA: NON contare su una compensazione fra le due correzioni —") print(f" si sommano, e il congiunto resta {d_joint / max(d_fisco, d_fund):.2f}x la piu' grande delle due") print(f" presa da sola. {clock()}") sez(f"4) TRAIETTORIA da ${START:.0f} — anni al capitale-rendita, nelle quattro lenti") print(f""" Partenza ${START:.0f} = il conto Deribit VERO (le tabelle pubblicate partono da $600). Ogni lente usa il PROPRIO muro. {N_TRAJ} path, blocchi da {BLOCK} giorni, orizzonte {ANNI_TRAJ} anni. DUE mediane: 'mediana' = incondizionata (non-arrivo = +inf -> '>{ANNI_TRAJ}a'), 'cond.' = solo su chi arriva — e la seconda non si legge senza la P accanto.""") traj = {} for k in Lo: L = Lo[k] rr = np.random.default_rng(SEED_TRAJ) pp = CC._boot_paths(L.r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rr) traj[k] = {dep: leggi(PN.accumula(pp, dep, L.muro, L.aliq, L.patr, start=START)["colpito"]) for dep in VERSAMENTI} del pp hdr = f" {'EUR/m':>7}" for k in Lo: hdr += f"{k.split()[0]:>22}" print("\n" + hdr) print(f" {'':>7}" + "".join(f"{'mediana cond. P20':>22}" for _ in Lo)) for dep in VERSAMENTI: row = f" {dep:>7}" for k in Lo: d = traj[k][dep] row += f"{fmt_med(d):>9}{fmt_cond(d):>7}{d['p20']:>6.0%}" print(row) print(f" {clock()}") d250 = {k: traj[k][250] for k in Lo} print(f""" 📌 LA RIGA CHE MANCAVA. «EUR250/mese» P(traguardo entro 20 anni): L0 LORDO {d250['L0 LORDO (26/07)']['p20']:>5.0%} (pubblicato 92% — qui {d250['L0 LORDO (26/07)']['p20']:.0%} su serie piu' lunga e da $635) L1 +FISCO {d250['L1 +FISCO (07/08)']['p20']:>5.0%} (pubblicato 52%) L2 +FUNDING {d250['L2 +FUNDING (22/08)']['p20']:>5.0%} (pubblicato 64%) L3 CONGIUNTA {d250['L3 CONGIUNTA']['p20']:>5.0%} <- IL NUMERO ONESTO L3b str. vero {d250['L3b CONGIUNTA (str. vero)']['p20']:>5.0%} (col funding misurato solo dove lo strumento esiste) ⚠️ Le due colonne di funding sono una BANDA, non una scelta: -2,16%/anno usa il proxy inverse sul 40% del campione (conservativa), -1,39%/anno usa solo la finestra in cui il perpetual lineare USDC esiste davvero. La verita' sta fra 14% e 26%, non su un numero. P(traguardo entro i {ANNI_TRAJ} anni simulati), stessa riga: L3 {d250['L3 CONGIUNTA']['p_arr']:.0%} · L3b {d250['L3b CONGIUNTA (str. vero)']['p_arr']:.0%} — cioe' anche allungando di 5 anni l'orizzonte una quota non trascurabile non arriva.""") # --- il lump: i soldi gia' fermi --- sez(f"4-bis) E SE PARTO CON UN VERSAMENTO INIZIALE — ${START:.0f} + un lump, lente CONGIUNTA") print(f""" Il progetto ha misurato (27/07) che EUR10.000 fermi oggi valgono 2,45x, ma quel lump non e' MAI entrato nelle tabelle nette. Qui e' nella lente congiunta. Lo stesso muro, gli stessi path.""") L3 = Lo["L3 CONGIUNTA"] rr = np.random.default_rng(SEED_TRAJ) p3 = CC._boot_paths(L3.r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rr) print(f"\n {'lump EUR':>10}" + "".join(f"{f'EUR{d}/mese':>26}" for d in (0, 250, 500))) print(f" {'':>10}" + "".join(f"{'mediana cond. P20':>26}" for _ in range(3))) for lump in LUMPS_EUR: row = f" {lump:>10,.0f}" for dep in (0, 250, 500): d = leggi(PN.accumula(p3, dep, L3.muro, L3.aliq, L3.patr, start=START, lump_eur=lump)["colpito"]) row += f"{fmt_med(d):>11}{fmt_cond(d):>8}{d['p20']:>7.0%}" print(row) del p3 print(f" {clock()}") print("\n ⚠️ Un lump non e' capitale disponibile se e' il fondo d'emergenza (nota 27/07),") print(" e a EUR10.000 sul conto si riapre la decisione di venue (soglia dichiarata $20k,") print(" ma il piano cambia -> CLAUDE.md dice di riaprire PRIMA, non dopo).") # ===================================================================== sez("5) QUANTO VERSARE PER UN ORIZZONTE DATO — lente CONGIUNTA, e il totale versato accanto") print(f""" bersaglio ${L3.muro:,.0f} (lente congiunta) · partenza ${START:.0f} · nessun lump La colonna 'tot. versato' e' il confronto che conta: quanto del bersaglio lo COMPRI coi bonifici invece di farlo produrre alla strategia.""") L1 = Lo["L1 +FISCO (07/08)"] print(f"\n {'orizzonte':>10}" + "".join(f"{f'P={c:.0%}':>13}" for c in CONFIDENZE) + f"{'tot.versato@90%':>18}{'% bersaglio':>13}{' | solo fisco @90%':>21}") riga10 = None for h in ORIZZONTI: rr = np.random.default_rng(SEED_DEP) ph = CC._boot_paths(L3.r, N_DEP, h * 365, BLOCK, rr) celle, tot90, dep90h = [], float("nan"), float("nan") for c in CONFIDENZE: v, vers = dep_per_conf(ph, L3, c, START) celle.append(f"EUR{v:,.0f}/m" if np.isfinite(v) else "irraggiung.") if c == 0.90: tot90, dep90h = vers, v del ph rr = np.random.default_rng(SEED_DEP) p1 = CC._boot_paths(L1.r, N_DEP, h * 365, BLOCK, rr) v1, _ = dep_per_conf(p1, L1, 0.90, START) del p1 quota = tot90 / L3.muro if np.isfinite(tot90) else float("nan") if h == 10: rr = np.random.default_rng(SEED_DEP) pb = CC._boot_paths(Lo["L3b CONGIUNTA (str. vero)"].r, N_DEP, h * 365, BLOCK, rr) vb, versb = dep_per_conf(pb, Lo["L3b CONGIUNTA (str. vero)"], 0.90, START) del pb riga10 = (dep90h, tot90, quota, vb, versb) print(f" {f'{h} anni':>10}" + "".join(f"{x:>13}" for x in celle) + f"{f'${tot90:,.0f}':>18}{quota:>13.0%}" + f"{f' | EUR{v1:,.0f}/m ({dep90h / v1:.2f}x)':>21}") print(f" {clock()}") print(""" 📌 A 10 anni il rendimento fa la minoranza del lavoro: il resto lo fanno i bonifici. E' la lezione gia' scritta il 26/07, qui con le due correzioni dentro: comprimere l'orizzonte non fa lavorare la strategia, COMPRA il capitale.""") # ===================================================================== sez("6) LA DOMANDA INVERSA — che rendita compra quello che posso permettermi") print(f""" Rendita NETTA mediana in EUR/giorno = capitale x perpetua({L3.perp:.2%}). Le imposte sono gia' dentro la perpetua: il numero e' netto e NON va lordizzato una seconda volta. Lente CONGIUNTA, partenza ${START:.0f}, nessun lump.""") print(f"\n {'EUR/mese':>9}" + "".join(f"{f'{y}a':>12}" for y in (5, 10, 15, 20)) + f"{'P(>=EUR50/g a 20a)':>21}") for dep in VERSAMENTI: celle, p50 = [], 0.0 for y in (5, 10, 15, 20): rr = np.random.default_rng(SEED_DEP) py = CC._boot_paths(L3.r, N_DEP, y * 365, BLOCK, rr) cap = PN.accumula(py, dep, L3.muro, L3.aliq, L3.patr, start=START)["cap"] del py rend = cap * L3.perp / 365.0 / CC.EURUSD celle.append(f"{np.median(rend):,.2f}") if y == 20: p50 = float((rend >= CC.TARGET_EUR_DAY).mean()) print(f" {dep:>9}" + "".join(f"{x:>12}" for x in celle) + f"{p50:>21.1%}") print(f" {clock()}") # ===================================================================== sez("VERDETTO") j250 = traj["L3 CONGIUNTA"][250] d10, t10, q10, d10b, t10b = riga10 print(f""" 1. LA TABELLA ORA ESISTE, ed e' peggiore di entrambe le lenti separate: EUR250/mese, P(entro 20 anni) — LORDO {d250['L0 LORDO (26/07)']['p20']:.0%} · +FISCO {d250['L1 +FISCO (07/08)']['p20']:.0%} · +FUNDING {d250['L2 +FUNDING (22/08)']['p20']:.0%} · CONGIUNTA {j250['p20']:.0%}. Il muro passa da ${Lo['L0 LORDO (26/07)'].muro:,.0f} a ${L3.muro:,.0f} ({L3.muro / Lo['L0 LORDO (26/07)'].muro - 1:+.0%}). 2. LE DUE CORREZIONI SI SOMMANO — la mia attesa («meno drift -> meno imposta -> congiunto meno severo della somma») e' MISURATA e vale poco, e nella moneta che decide NON la conferma. Interazione: {np.expm1(inter_log):+.2%} sul capitale accumulato (li' il meccanismo c'e'), ${inter_w:+,.0f} sul muro (sotto la risoluzione MC), {np.median(inters):+,.0f} EUR/mese sul versamento necessario ({np.median(inters) / (d_fisco + d_fund):+.0%} della somma, segno {"stabile" if segno_ok else "NON stabile"} su 3 semi). Il congiunto resta {d_joint / max(d_fisco, d_fund):.2f}x la piu' grande delle due correzioni presa da sola. 3. COSA NON CAMBIA A NESSUNA LENTE: senza versamenti il capitale-rendita non si raggiunge MAI (P=0% a 20 anni in tutte e cinque le colonne). L'accumulo viene dai bonifici, non dal rendimento — e l'ordine delle leve del piano e' invariato. 4. IL RISCHIO DI VENUE RESTA FUORI, DI PROPOSITO: e' rovina, non costo. A p=5% annuo il capitale mediano a 20 anni e' ZERO per ogni riga di ogni tabella qui sopra. Nessuna di queste righe e' valida se il conto sparisce. 5. L'OBIETTIVO DICHIARATO, senza addolcirlo: EUR50/giorno NON e' raggiungibile con versamenti plausibili in un orizzonte breve. A 10 anni servono EUR{d10:,.0f}/mese (P=90%), cioe' ${t10:,.0f} versati per arrivare a ${L3.muro:,.0f}: il {q10:.0%} del bersaglio lo compri coi bonifici. La tabella utile e' quella INVERSA (sezione 6): non «quando arrivo a 50», ma «cosa compro con quello che ho». """) print(f"\nCON TUTTE LE CORREZIONI, EUR250/MESE DA ${START:.0f}: {fmt_med(j250)[:-1]} anni " f"(mediana incondizionata; cond. all'arrivo {fmt_cond(j250)}), " f"P(20a) {j250['p20']:.0%}") print(f"PER EUR50/GIORNO IN 10 ANNI SERVONO: EUR{d10:,.0f}/mese (P=90%), " f"totale versato ${t10:,.0f}") print(f" [banda di funding: lente conservativa -2,16%/a come sopra; con il funding dello") print(f" strumento vero -1,39%/a -> EUR250/mese fa {fmt_med(d250['L3b CONGIUNTA (str. vero)'])} " f"con P(20a) {d250['L3b CONGIUNTA (str. vero)']['p20']:.0%}, e i 10 anni costano " f"EUR{d10b:,.0f}/mese (${t10b:,.0f})]") print(f"\n{clock()} fine.") if __name__ == "__main__": main()