Files
Adriano Dal Pastro 68c6894d85 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>
2026-08-07 18:43:38 +00:00

191 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()