#!/usr/bin/env python """r0807_growth_yearly.py — la crescita anno per anno, separando VERSAMENTI e GUADAGNO. DOMANDA DELL'OPERATORE: *"grafico della crescita per anno con versamento e con guadagno"*. COSA FA: prende il piano come sta oggi (€5.000 di versamento iniziale + €500/mese sul conto Deribit) e per ogni anno riporta le due componenti del capitale: capitale(t) = [START + lump + versamenti cumulati fino a t] + [guadagno cumulato] \_______________ deterministico ______________/ \___ dal book ___/ La prima e' esatta (i bonifici sono decisi, non estratti); la seconda e' la MEDIANA fra i path del bootstrap. ⚠️ Quindi la riga "guadagno" NON e' il guadagno di un percorso singolo: e' `mediana(capitale) - versato`, cioe' quanto ha reso il book nel percorso mediano di ogni anno. La banda p25-p75 dice quanto quella riga puo' spostarsi. DUE REGIMI, SUGLI STESSI PATH (confronto appaiato): * `lordo` — il modello pubblicato in CLAUDE.md: l'accumulo compone senza imposte; * `netto` — con l'imposta annua sulle plusvalenze (33%) + patrimoniale 0.2% sul valore, cioe' la correzione misurata il 2026-07-27 (`r0727_tasse.py`), che vale -31% di capitale a 10 anni e -44% a 15. Riusa senza riscrivere: `r0725_capcurve._boot_paths` (block bootstrap), `r0726_venue_risk. venue_series` (serie del book live alle ancore canoniche, gia' de-luckata x0.89) e la contabilita' fiscale di `r0727_tasse.accumula` (stesse assunzioni, stesso carry a 4 anni). ⚠️ Le assunzioni fiscali sono quelle dichiarate in r0727_tasse.py e NON sono un parere fiscale. uv run python scripts/research/r0807_growth_yearly.py [--json PATH] """ from __future__ import annotations import argparse import json 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_venue_risk as VR # noqa: E402 import r0727_lumpsum_split as LS # noqa: E402 import r0727_tasse as TX # noqa: E402 LUMP_EUR = 5_000.0 # il versamento iniziale previsto (config/live.json, nota del 26/07) DEP_EUR = 500.0 # il versamento mensile previsto YEARS = 15 N_PATHS = 4000 BLOCK = 20 SEED = 807 def traiettoria(r: np.ndarray, lump_eur: float, dep_eur: float, years: int, aliquota: float, patrimoniale: float, n_paths: int = N_PATHS, seed: int = SEED) -> list[dict]: """Accumulo giorno per giorno, con fotografia a fine di ogni anno. Stessa meccanica di `r0727_tasse.accumula` (deposito ogni 30 giorni, imposta annua sulla variazione di valore al netto dei versamenti, minusvalenze in carry 4 anni), ma qui interessa la SERIE annuale, non il solo valore finale. """ rng = np.random.default_rng(seed) n_days = years * 365 paths = CC._boot_paths(r, n_paths, n_days, BLOCK, rng) dep = dep_eur * CC.EURUSD cap = np.full(n_paths, LS.START + lump_eur * CC.EURUSD) versato = np.full(n_paths, LS.START + lump_eur * CC.EURUSD) # deterministico, ma tenuto hit = np.zeros(n_paths, bool) # traguardo toccato mai carry = np.zeros((n_paths, TX.CARRY_ANNI)) imposte = np.zeros(n_paths) anno_start = cap.copy() versato_anno = np.zeros(n_paths) out: list[dict] = [] for t in range(n_days): cap *= (1.0 + paths[:, t]) hit |= cap >= LS.TARGET if t % 30 == 0 and t > 0 and dep > 0: cap += dep versato += dep versato_anno += dep if (t + 1) % 365 == 0: plus = cap - anno_start - versato_anno if aliquota > 0: disponibili = carry.sum(axis=1) usate = np.minimum(np.maximum(plus, 0.0), disponibili) imponibile = np.maximum(plus, 0.0) - usate resid = usate.copy() for k in range(TX.CARRY_ANNI - 1, -1, -1): presa = np.minimum(carry[:, k], resid) carry[:, k] -= presa resid -= presa dovuta = imponibile * aliquota cap -= np.minimum(dovuta, cap) imposte += dovuta carry = np.roll(carry, 1, axis=1) carry[:, 0] = np.maximum(-plus, 0.0) if patrimoniale > 0: p = cap * patrimoniale cap -= np.minimum(p, cap) imposte += p anno_start = cap.copy() versato_anno[:] = 0.0 anno = (t + 1) // 365 vers = float(versato[0]) # identico su ogni path: bonifici decisi med = float(np.median(cap)) out.append(dict( anno=anno, versato=vers, capitale_p25=float(np.percentile(cap, 25)), capitale_med=med, capitale_p75=float(np.percentile(cap, 75)), guadagno_med=med - vers, imposte_med=float(np.median(imposte)), p_traguardo=float(hit.mean()), )) return out def _riga(x: dict) -> str: return (f" {x['anno']:>4}{x['versato']:>14,.0f}${x['guadagno_med']:>16,.0f}$" f"{x['capitale_med']:>16,.0f}${x['capitale_p25']:>15,.0f}$" f"{x['capitale_p75']:>15,.0f}${x['p_traguardo']:>13.1%}") def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--json", type=str, default="") ap.add_argument("--lump", type=float, default=LUMP_EUR, help="versamento iniziale in EUR") ap.add_argument("--dep", type=float, default=DEP_EUR, help="versamento mensile in EUR") ap.add_argument("--anni", type=int, default=YEARS) args = ap.parse_args() lump_eur, dep_eur, years = args.lump, args.dep, args.anni r = VR.venue_series()["Deribit"].values.astype(float) print("=" * 108) print(" r0807 — CRESCITA ANNO PER ANNO: quanto e' versamento e quanto e' guadagno") print("=" * 108) print(f" piano: START ${LS.START:,.0f} + lump €{lump_eur:,.0f} + €{dep_eur:,.0f}/mese · " f"orizzonte {years} anni · {N_PATHS:,} path, blocchi {BLOCK}g") print(f" book live TP01+SKH01 75/25 alle ancore canoniche, de-luckato ×{VR.DELUCK} · " f"bersaglio ${LS.TARGET:,.0f}") lordo = traiettoria(r, lump_eur, dep_eur, years, 0.0, 0.0) netto = traiettoria(r, lump_eur, dep_eur, years, 0.33, TX.PATRIMONIALE) for nome, serie in (("LORDO — il modello pubblicato (nessuna imposta durante l'accumulo)", lordo), ("NETTO — plusvalenze 33% annue + patrimoniale 0.2% (r0727_tasse)", netto)): print("\n" + "-" * 108) print(f" {nome}") print("-" * 108) print(f" {'anno':>4}{'versato':>15}{'guadagno med':>17}{'capitale med':>17}" f"{'p25':>16}{'p75':>16}{'P(traguardo)':>14}") for x in serie: print(_riga(x)) print("\n" + "=" * 108) print(" LETTURA") print("=" * 108) for a in [x for x in (5, 10, 15) if x <= years]: lo = next(x for x in lordo if x["anno"] == a) ne = next(x for x in netto if x["anno"] == a) quota_lo = lo["guadagno_med"] / lo["capitale_med"] if lo["capitale_med"] > 0 else 0.0 quota_ne = ne["guadagno_med"] / ne["capitale_med"] if ne["capitale_med"] > 0 else 0.0 print(f" a {a:>2} anni · versato ${lo['versato']:,.0f} → capitale mediano " f"${lo['capitale_med']:,.0f} lordo (guadagno = {quota_lo:.0%}) · " f"${ne['capitale_med']:,.0f} netto fisco (guadagno = {quota_ne:.0%}) · " f"costo del fisco {ne['capitale_med'] / lo['capitale_med'] - 1:+.1%}") print("\n ⚠️ 'guadagno' = mediana(capitale) − versato: e' il rendimento del percorso MEDIANO,") print(" non di un percorso singolo. La banda p25-p75 dice di quanto puo' spostarsi.") if args.json: Path(args.json).write_text(json.dumps( dict(piano=dict(start=LS.START, lump_eur=lump_eur, dep_eur=dep_eur, eurusd=CC.EURUSD, anni=years, n_paths=N_PATHS, deluck=VR.DELUCK, target=LS.TARGET, aliquota=0.33, patrimoniale=TX.PATRIMONIALE), lordo=lordo, netto=netto), indent=1)) print(f"\n scritto {args.json}") if __name__ == "__main__": main()