From 68c6894d85052c0120a46d031aa38febd4b27045 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 7 Aug 2026 18:43:38 +0000 Subject: [PATCH] research(capitale): il fisco durante l'ACCUMULO non era mai stato contato MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit r0807_growth_yearly.py: crescita anno per anno separando versamenti e guadagno, con e senza l'imposta d'accumulo. Nasce da "grafico della crescita per anno con versamento e con guadagno". TAX_RATE compariva in UN SOLO punto del progetto: la lordizzazione del bersaglio in fase di PRELIEVO. L'accumulo componeva al lordo per dieci o vent'anni. Costo (lump 5k + 500/mese): -15.7% a 5 anni, -29.8% a 10, -42.6% a 15 — replica coerente col 27/07 su lump 10k (-16.8/-31/-44%). L'errore e' COMPOSTO. Conseguenza: tutte le tabelle a 15-20 anni pubblicate in CLAUDE.md sono al lordo. Contro-intuitivo e misurato: versare di piu' RITARDA il sorpasso (l'anno in cui il guadagno cumulato supera il versato) — 7o anno a 500/mese, 8o a 800 — perche' alza l'asticella. I 300 in piu' comprano il traguardo (12o anno invece del 15o, P(bersaglio) a 15a da 73.2% a 99.0%), non il sorpasso. Riusa senza riscriverle la contabilita' fiscale di r0727_tasse.accumula e il block bootstrap di r0725_capcurve. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/research/r0807_growth_yearly.py | 190 ++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 scripts/research/r0807_growth_yearly.py diff --git a/scripts/research/r0807_growth_yearly.py b/scripts/research/r0807_growth_yearly.py new file mode 100644 index 0000000..effd72c --- /dev/null +++ b/scripts/research/r0807_growth_yearly.py @@ -0,0 +1,190 @@ +#!/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()