68c6894d85
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>
191 lines
8.3 KiB
Python
191 lines
8.3 KiB
Python
#!/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()
|