research(capitale): il fisco durante l'ACCUMULO non era mai stato contato

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) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-08-07 18:43:38 +00:00
parent f81c783ea7
commit 68c6894d85
+190
View File
@@ -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()