research(capitale): il piano rifatto AL NETTO — il muro si sposta poco, i versamenti molto
Il 07/08 era stato misurato che l'accumulo composto al lordo sovrastima il capitale del 30% a 10 anni, ma i numeri del piano non erano stati rifatti. Qui lo sono. Il muro usava una convenzione ASIMMETRICA: prelievo lordizzato (€50/g netti → $29.690 lordi) ma capitale che compone senza mai pagare imposte. Coerente (imposte annue dentro il portafoglio, prelievo gia' netto): perpetua 10.91% → 7.70%, muro $272.061 → $258.338 (-5.0%). I due errori vanno in versi opposti e si compensano quasi — per caso, non per costruzione. E' sulle traiettorie che il fisco morde, e si legge nella PROBABILITA': €250/mese P(entro 20a) 92% → 52%; €500/mese 100% → 99%. Il versamento necessario a P=90% passa da €237 a €371/mese a 20 anni (+57%), da €509 a €672 a 15 (+32%), da €1.178 a €1.323 a 10 (+12%): l'errore era composto, quindi cresce con l'orizzonte. Rendita a 20 anni con €250/mese: 91.30 → 46.61 €/g, P(€50/g) 90% → 42%. Controllo di replica superato prima di guardare i numeri nuovi: a fisco spento la macchina riproduce $272.061 al dollaro (implementazione separata) e la colonna LORDA riproduce 4 righe su 4 della tabella pubblicata. Trovato per strada: perp_and_wall gira a 2000 path e a quella taglia da' $269.648 — la terza cifra del muro e' rumore Monte Carlo. Errore mio catturato prima di pubblicare: la mediana degli anni calcolata sull'INTERO vettore coi non-arrivi a -1 faceva risultare €250/mese PIU' VELOCE col fisco (15.7 → 15.3 anni) mentre P crollava. Un non-arrivo va codificato +inf, mai -1. Book/pesi/cron/config INVARIATI: non tocca la produzione. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,164 @@
|
|||||||
|
# 2026-08-07 — Il piano rifatto al netto: muro, traiettorie, versamenti
|
||||||
|
|
||||||
|
**Book, pesi, cron, config: INVARIATI.** Non tocca la produzione: rifa' i numeri del *piano di
|
||||||
|
accumulo*, che erano al lordo del fisco.
|
||||||
|
|
||||||
|
Script: `scripts/research/r0807_piano_netto.py`. Test: `tests/test_piano_netto.py` (16).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Il debito
|
||||||
|
|
||||||
|
Il 07/08 (`r0727_tasse.py`, `r0807_growth_yearly.py`) era stato misurato che l'accumulo composto al
|
||||||
|
lordo sovrastima il capitale del **15.7% a 5 anni, 29.8% a 10, 42.6% a 15**. Il fatto era stato
|
||||||
|
scritto in memoria — *"TUTTE le tabelle a 15-20 anni pubblicate sopra sono al LORDO"* — ma i numeri
|
||||||
|
non erano stati rifatti. Sono numeri che sono serviti a decidere: la traiettoria da $600, la
|
||||||
|
tabella «quanto versare per un orizzonte dato», la rendita €/g.
|
||||||
|
|
||||||
|
**E il muro stesso non era neutro.** `$272.061` viene da una convenzione **asimmetrica**: il
|
||||||
|
prelievo viene lordizzato (€50/g netti → $29.690/anno lordi al 33%) ma il capitale che resta
|
||||||
|
investito compone **senza mai pagare imposte**, in accumulo come in prelievo. Le due meta' del
|
||||||
|
conto non usano lo stesso fisco.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## (0) Il controllo di replica, che ha trovato subito qualcosa
|
||||||
|
|
||||||
|
Prima regola: a fisco spento tutto deve riprodurre i numeri pubblicati, altrimenti un numero
|
||||||
|
diverso non si distingue da un bug.
|
||||||
|
|
||||||
|
| | perpetua | muro |
|
||||||
|
|---|---|---|
|
||||||
|
| costante pubblicata (`VR.TARGET`, citata ovunque) | — | **$272.061** |
|
||||||
|
| questa implementazione, fisco OFF, 4000 path | 10.9131% | **$272.061** |
|
||||||
|
| `r0726_capwall_refresh.perp_and_wall` ai suoi **2000** path | 11.0107% | $269.648 |
|
||||||
|
| lo stesso, portato a 4000 path | 10.9131% | **$272.061** |
|
||||||
|
|
||||||
|
Replica esatta al dollaro fra due implementazioni separate. ⚠️ **Ma trovato per strada:** la
|
||||||
|
funzione che ha prodotto il muro gira di default a **2000 path**, e a quella taglia da' $269.648 —
|
||||||
|
**0.9% di rumore Monte Carlo**. Il muro pubblicato e' corretto (fu calcolato a 4000), ma **la sua
|
||||||
|
terza cifra significativa non e' un'informazione**: si cita **$272k**, non $272.061.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## (1) Il muro, nella convenzione coerente
|
||||||
|
|
||||||
|
Imposte pagate ogni anno **dentro** il portafoglio (stessa meccanica dell'accumulo: plusvalenza
|
||||||
|
annua al netto dei movimenti di cassa, carry 4 anni, patrimoniale 0.2%), prelievo gia' netto:
|
||||||
|
|
||||||
|
| convenzione | prelievo/anno | perpetua | muro |
|
||||||
|
|---|---|---|---|
|
||||||
|
| (a) pubblicata — fisco solo sul prelievo | $29.690 | 10.91% | **$272.061** |
|
||||||
|
| (b) coerente, aliquota 33% | $19.892 | **7.70%** | **$258.338** |
|
||||||
|
| (b) coerente, aliquota 26% | $19.892 | 8.42% | $236.310 |
|
||||||
|
|
||||||
|
**−5.0%.** I due errori della convenzione (a) vanno in versi opposti e **si compensano quasi**:
|
||||||
|
lordizzare il prelievo alza il muro, non tassare il capitale investito lo abbassa. Ma si
|
||||||
|
compensano *per caso*, non per costruzione — e' il tipo di errore che si vede solo rifacendo il
|
||||||
|
conto in modo coerente. Cio' che resta scoperto e' la patrimoniale e la non-linearita' della
|
||||||
|
rendita perpetua nel drift.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## (2) Le traiettorie — ed e' qui che il fisco morde
|
||||||
|
|
||||||
|
Orizzonte 25 anni, 3000 path, seed 725: **esattamente la macchina che ha prodotto la tabella
|
||||||
|
pubblicata**, quindi la colonna LORDA e' una replica di controllo.
|
||||||
|
|
||||||
|
| €/mese | versato in 20a | LORDO: anni | P(20a) | NETTO: anni | P(20a) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 0 | $600 | mai | 0% | mai | 0% |
|
||||||
|
| **250** | $66.818 | **16.3a** *(pubbl. 16.2)* | **92%** *(pubbl. 92%)* | **19.8a** | **52%** |
|
||||||
|
| 500 | $133.035 | 12.4a *(pubbl. 12.4)* | 100% | 14.7a | 99% |
|
||||||
|
| 800 | $212.496 | 10.0a | 100% | 11.4a | 100% |
|
||||||
|
| 1000 | $265.470 | 9.0a *(pubbl. 9.0)* | 100% | 10.0a | 100% |
|
||||||
|
| 2000 | $530.340 | 6.0a *(pubbl. 6.0)* | 100% | 6.4a | 100% |
|
||||||
|
|
||||||
|
**Il livello €250/mese — quello con cui il 26/07 il piano risultava «P(entro 20a) 92%» — al netto
|
||||||
|
diventa una moneta: 52%.**
|
||||||
|
|
||||||
|
⚠️ **La colonna «anni» e' la mediana CONDIZIONATA all'arrivo** (convenzione di
|
||||||
|
`r0726_capwall_refresh.trajectory`, quindi confrontabile con la tabella pubblicata) e va letta
|
||||||
|
insieme alla probabilita' accanto.
|
||||||
|
|
||||||
|
> **Errore mio, catturato prima di pubblicare.** La prima stesura calcolava la mediana
|
||||||
|
> sull'**intero** vettore, con i non-arrivi codificati `-1`. Risultato: €250/mese risultava passare
|
||||||
|
> da 15.7 a **15.3** anni col fisco — *piu' veloce* — mentre la probabilita' crollava da 91% a 53%.
|
||||||
|
> Con meta' dei path a `-1` la mediana del vettore cade sui **primi** arrivi. **Un non-arrivo va
|
||||||
|
> messo a +∞, non a −1: messo a −1 il numero migliora tanto piu' quanto peggio va la colonna**, ed
|
||||||
|
> era abbastanza plausibile da finire in un verdetto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## (3) Quanto versare per un orizzonte dato — al netto
|
||||||
|
|
||||||
|
Bersaglio $258.338. Fra parentesi il numero pubblicato (lordo, bersaglio $272.061).
|
||||||
|
|
||||||
|
| orizzonte | P=50% | P=75% | P=90% | P=95% | tot. versato @P=90% |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| **10 anni** | €998/m | €1.162/m | **€1.323/m** *(€1.178)* | €1.424/m | **$175.058** *($155.923)* |
|
||||||
|
| 15 anni | €470/m | €570/m | **€672/m** *(€509)* | €725/m | $133.983 *($101.643)* |
|
||||||
|
| 20 anni | €245/m | €306/m | **€371/m** *(€237)* | €417/m | $98.762 *($63.406)* |
|
||||||
|
|
||||||
|
**Il fisco costa +12% al mese a 10 anni, +32% a 15, +57% a 20.** Cresce con l'orizzonte perche'
|
||||||
|
l'errore era composto: piu' tempo, piu' interessi mai maturati sulle imposte non pagate.
|
||||||
|
|
||||||
|
E la lettura del 26/07 si **rafforza**: a 10 anni versi **$175k** per arrivare a $258k (il
|
||||||
|
rendimento fa il 32%), a 20 anni ne versi **$99k** (il rendimento fa il 62%). *A orizzonti corti
|
||||||
|
non fai lavorare la strategia, compri il capitale coi bonifici* — al netto ancora di piu'.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## (4) Rendita €/g mediana — al netto
|
||||||
|
|
||||||
|
Le imposte sono gia' dentro la perpetua (7.70%): il numero e' netto e **non va lordizzato una
|
||||||
|
seconda volta**.
|
||||||
|
|
||||||
|
| €/mese | 5a | 10a | 15a | 20a | P(€50/g a 20a) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 0 | 0.20 | 0.34 | 0.59 | 1.01 | 0.0% |
|
||||||
|
| **250** | 4.38 | 11.82 | 24.61 | **46.61** *(pubbl. 91.30)* | **42.0%** *(pubbl. 90.0%)* |
|
||||||
|
| 500 | 8.56 | 23.31 | 48.65 | 92.11 | 97.6% |
|
||||||
|
| 800 | 13.58 | 37.08 | 77.47 | 146.78 | 100.0% |
|
||||||
|
| 1000 | 16.93 | 46.26 | 96.67 | 183.18 | 100.0% |
|
||||||
|
| 2000 | 33.66 | 92.18 | 192.79 | 365.24 | 100.0% |
|
||||||
|
|
||||||
|
A 20 anni la rendita di €250/mese si **dimezza** (91.30 → 46.61 €/g) e P(€50/g) passa da **90% a
|
||||||
|
42%**. La non-linearita' resta: da 15 a 20 anni la rendita raddoppia a ogni livello.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cosa cambia e cosa no
|
||||||
|
|
||||||
|
**Cambia:** il livello di versamento che il piano dichiarava sufficiente. €250/mese al lordo
|
||||||
|
sembrava «P 92%, il piano funziona»; al netto e' 52%, e per tornare a ~90% servono **€371/mese**
|
||||||
|
a 20 anni. Il muro si sposta poco (−5%), i **versamenti** si spostano molto (+57% a 20 anni).
|
||||||
|
|
||||||
|
**Non cambia:**
|
||||||
|
- senza versamenti il capitale-rendita non si raggiunge **mai**, a qualunque lente fiscale;
|
||||||
|
- l'ordine di importanza delle leve (versare > quando > quanto presto si smette > piatto vs
|
||||||
|
crescente > frequenza) e' invariato: il fisco colpisce tutte le colonne allo stesso modo;
|
||||||
|
- il rischio di venue resta fuori scala rispetto a tutto questo (a p=5% il risultato mediano e'
|
||||||
|
zero comunque).
|
||||||
|
|
||||||
|
⚠️ **Assunzioni fiscali dichiarate, NON un parere fiscale** (33%, sensibilita' a 26%, minusvalenze
|
||||||
|
in carry 4 anni, patrimoniale 0.2%/anno). Il modello tassa la variazione **annua** di valore =
|
||||||
|
**limite superiore** rispetto alla pura realizzazione, stretto perche' TP01 ribilancia ogni giorno
|
||||||
|
e SKH01 chiude round-trip discreti.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regole
|
||||||
|
|
||||||
|
1. **Un modello che tassa una meta' del conto e non l'altra non e' conservativo, e' incoerente** —
|
||||||
|
e i due errori possono compensarsi quasi esattamente, il che li rende invisibili finche' non si
|
||||||
|
rifa' il conto in modo simmetrico.
|
||||||
|
2. **Un non-arrivo va codificato +∞, mai −1.** Con −1 la mediana *migliora* quanto piu' la colonna
|
||||||
|
peggiora.
|
||||||
|
3. **Una mediana condizionata va sempre stampata accanto alla sua probabilita'**, o dice il
|
||||||
|
contrario di quello che sembra.
|
||||||
|
4. **Prima di pubblicare un numero nuovo, far riprodurre alla macchina quello vecchio** — qui la
|
||||||
|
colonna LORDA riproduce 4 righe su 4 della tabella pubblicata, ed e' l'unica ragione per cui la
|
||||||
|
colonna NETTA e' leggibile.
|
||||||
|
5. **Un Monte Carlo ha una risoluzione, e va detta.** $272.061 e' esatto quanto $269.648: la
|
||||||
|
differenza fra i due e' la taglia del campione, non un'informazione sul piano.
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""r0807_piano_netto.py — il piano di accumulo rifatto AL NETTO: muro, traiettorie, versamenti.
|
||||||
|
|
||||||
|
IL DEBITO. Il 07/08 `r0727_tasse.py` / `r0807_growth_yearly.py` hanno misurato che l'accumulo
|
||||||
|
composto al lordo sovrastima il capitale del **15.7% a 5 anni, 29.8% a 10, 42.6% a 15**. Il fatto
|
||||||
|
e' stato scritto in memoria ma i NUMERI del piano non sono stati rifatti — e sono numeri che sono
|
||||||
|
serviti a decidere: la traiettoria da $600 (\"€250/mese -> 16.2 anni, P(entro 20a) 92%\"), la
|
||||||
|
tabella \"quanto versare per un orizzonte dato\", la rendita €/g per livello di versamento.
|
||||||
|
|
||||||
|
⚠️ E IL MURO STESSO NON E' NEUTRO. `$272.061` viene da una convenzione ASIMMETRICA: il prelievo
|
||||||
|
viene lordizzato (`€50/g netti -> $27.375/anno lordi` a fisco 33%) ma il capitale che resta
|
||||||
|
investito compone **senza mai pagare imposte**, in accumulo come in prelievo. Le due meta' del
|
||||||
|
conto non usano lo stesso fisco. Qui si misura la convenzione COERENTE — le imposte si pagano
|
||||||
|
dentro il portafoglio, ogni anno, e cio' che si preleva e' gia' netto — e si guarda di quanto il
|
||||||
|
muro si sposta. I due errori vanno in versi OPPOSTI (la lordizzazione alza il muro, l'assenza di
|
||||||
|
imposte in prelievo lo abbassa), quindi il segno del netto non e' deducibile: va misurato.
|
||||||
|
|
||||||
|
COSA C'E' QUI
|
||||||
|
(0) CONTROLLO DI REPLICA — a fisco spento tutto deve riprodurre i numeri pubblicati. Senza
|
||||||
|
questo, un numero diverso non si distingue da un bug.
|
||||||
|
(1) IL MURO, nelle due convenzioni.
|
||||||
|
(2) TRAIETTORIE da $600 per livello di versamento, lorde vs nette, sugli STESSI path.
|
||||||
|
(3) QUANTO VERSARE per un orizzonte dato, al netto.
|
||||||
|
(4) RENDITA €/g mediana per livello di versamento, al netto.
|
||||||
|
|
||||||
|
⚠️ Le assunzioni fiscali sono quelle DICHIARATE il 27/07 e NON sono un parere fiscale: 33%
|
||||||
|
(sensibilita' a 26%), minusvalenze in carry 4 anni, patrimoniale 0.2%/anno, e il modello tassa la
|
||||||
|
variazione ANNUA di valore — cioe' un LIMITE SUPERIORE rispetto alla pura realizzazione, vicino
|
||||||
|
al vero perche' TP01 ribilancia ogni giorno e SKH01 chiude round-trip discreti.
|
||||||
|
|
||||||
|
uv run python scripts/research/r0807_piano_netto.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
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_capwall_refresh as WR # noqa: E402
|
||||||
|
import r0726_deposits as DP # noqa: E402
|
||||||
|
import r0727_lumpsum_split as LS # noqa: E402
|
||||||
|
import r0727_tasse as TX # noqa: E402
|
||||||
|
|
||||||
|
N_PATHS = 4000
|
||||||
|
BLOCK = 20
|
||||||
|
SEED = 20260807
|
||||||
|
SEED_MURO = 725 # lo stesso di CC.survival: consente la replica esatta
|
||||||
|
ALIQUOTA = CC.TAX_RATE # 0.33
|
||||||
|
VERSAMENTI = (0, 250, 500, 800, 1_000, 2_000)
|
||||||
|
ORIZZONTI = (10, 15, 20)
|
||||||
|
CONFIDENZE = (0.50, 0.75, 0.90, 0.95)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# il passo fiscale, uno solo, riusato ovunque
|
||||||
|
# ===========================================================================
|
||||||
|
def passo_fiscale(cap: np.ndarray, anno_start: np.ndarray, flusso_anno: np.ndarray,
|
||||||
|
carry: np.ndarray, aliquota: float, patrimoniale: float) -> np.ndarray:
|
||||||
|
"""Imposte di fine anno, in loco su `cap`. Ritorna l'imposta pagata.
|
||||||
|
|
||||||
|
`flusso_anno` = versamenti MENO prelievi dell'anno: la plusvalenza e' la variazione di valore
|
||||||
|
depurata dai movimenti di cassa, quindi la stessa formula serve l'accumulo e il prelievo.
|
||||||
|
Identica meccanica a `r0807_growth_yearly.traiettoria` (carry a 4 anni, piu' vecchie prima).
|
||||||
|
"""
|
||||||
|
imposta = np.zeros(len(cap))
|
||||||
|
plus = cap - anno_start - flusso_anno
|
||||||
|
if aliquota > 0:
|
||||||
|
usate = np.minimum(np.maximum(plus, 0.0), carry.sum(axis=1))
|
||||||
|
imponibile = np.maximum(plus, 0.0) - usate
|
||||||
|
resid = usate.copy()
|
||||||
|
for k in range(carry.shape[1] - 1, -1, -1):
|
||||||
|
presa = np.minimum(carry[:, k], resid)
|
||||||
|
carry[:, k] -= presa
|
||||||
|
resid -= presa
|
||||||
|
dovuta = imponibile * aliquota
|
||||||
|
cap -= np.minimum(dovuta, cap)
|
||||||
|
imposta += 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)
|
||||||
|
imposta += p
|
||||||
|
return imposta
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# (1) il muro: sopravvivenza in prelievo, con e senza fisco dentro
|
||||||
|
# ===========================================================================
|
||||||
|
def sopravvivenza(r: np.ndarray, cap0: float, prelievo_anno: float, anni: int,
|
||||||
|
aliquota: float, patrimoniale: float, n_paths: int = N_PATHS,
|
||||||
|
seed: int = SEED_MURO) -> dict:
|
||||||
|
"""Gemella di `CC.survival` con il fisco DENTRO il portafoglio.
|
||||||
|
|
||||||
|
Con aliquota=0 e patrimoniale=0 deve dare esattamente `CC.survival` — e' il controllo che
|
||||||
|
rende credibile la riga col fisco (test `test_piano_netto.py`).
|
||||||
|
"""
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
n_days = int(anni * 365)
|
||||||
|
paths = CC._boot_paths(r, n_paths, n_days, BLOCK, rng)
|
||||||
|
cap = np.full(n_paths, cap0, float)
|
||||||
|
vivo = np.ones(n_paths, bool)
|
||||||
|
giorno = prelievo_anno / 365.0
|
||||||
|
carry = np.zeros((n_paths, TX.CARRY_ANNI))
|
||||||
|
anno_start = cap.copy()
|
||||||
|
flusso = np.zeros(n_paths)
|
||||||
|
for t in range(n_days):
|
||||||
|
cap = np.where(vivo, cap * (1.0 + paths[:, t]) - giorno, cap)
|
||||||
|
flusso = np.where(vivo, flusso - giorno, flusso)
|
||||||
|
vivo &= cap > 0
|
||||||
|
if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0):
|
||||||
|
vivi = vivo.copy()
|
||||||
|
passo_fiscale(cap, anno_start, flusso, carry, aliquota, patrimoniale)
|
||||||
|
cap = np.where(vivi, cap, 0.0)
|
||||||
|
vivo &= cap > 0
|
||||||
|
anno_start = cap.copy()
|
||||||
|
flusso[:] = 0.0
|
||||||
|
fine = np.where(vivo, cap, 0.0)
|
||||||
|
return dict(p_survive=float(vivo.mean()), median_end=float(np.median(fine)),
|
||||||
|
p10_end=float(np.percentile(fine, 10)))
|
||||||
|
|
||||||
|
|
||||||
|
def muro(r: np.ndarray, prelievo_netto_anno: float, aliquota: float,
|
||||||
|
patrimoniale: float) -> tuple[float, float]:
|
||||||
|
"""(tasso di rendita perpetua, capitale necessario). Perpetua = il prelievo piu' alto con
|
||||||
|
P(capitale a 20 anni >= capitale iniziale) >= 90% — stessa definizione del 25/07."""
|
||||||
|
lo, hi = 0.0, 0.40
|
||||||
|
for _ in range(13):
|
||||||
|
mid = (lo + hi) / 2
|
||||||
|
s = sopravvivenza(r, 1e6, 1e6 * mid, 20, aliquota, patrimoniale)
|
||||||
|
if s["p10_end"] >= 1e6:
|
||||||
|
lo = mid
|
||||||
|
else:
|
||||||
|
hi = mid
|
||||||
|
return lo, (prelievo_netto_anno / lo if lo > 0.002 else float("inf"))
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# (2-4) accumulo
|
||||||
|
# ===========================================================================
|
||||||
|
def accumula(paths: np.ndarray, dep_eur: float, bersaglio: float, aliquota: float,
|
||||||
|
patrimoniale: float, start: float = LS.START,
|
||||||
|
lump_eur: float = 0.0) -> dict:
|
||||||
|
"""Accumulo su path GIA' ESTRATTI (confronto appaiato fra regimi fiscali e versamenti).
|
||||||
|
|
||||||
|
Ritorna il capitale finale, il giorno di primo passaggio al bersaglio e il totale versato.
|
||||||
|
"""
|
||||||
|
n_paths, n_days = paths.shape
|
||||||
|
dep = dep_eur * CC.EURUSD
|
||||||
|
cap = np.full(n_paths, start + lump_eur * CC.EURUSD)
|
||||||
|
versato = start + lump_eur * CC.EURUSD
|
||||||
|
carry = np.zeros((n_paths, TX.CARRY_ANNI))
|
||||||
|
anno_start = cap.copy()
|
||||||
|
flusso = np.zeros(n_paths)
|
||||||
|
colpito = np.full(n_paths, -1, int)
|
||||||
|
for t in range(n_days):
|
||||||
|
cap *= (1.0 + paths[:, t])
|
||||||
|
if t % 30 == 0 and t > 0 and dep > 0:
|
||||||
|
cap += dep
|
||||||
|
flusso += dep
|
||||||
|
versato += dep
|
||||||
|
nuovi = (colpito < 0) & (cap >= bersaglio)
|
||||||
|
colpito[nuovi] = t
|
||||||
|
if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0):
|
||||||
|
passo_fiscale(cap, anno_start, flusso, carry, aliquota, patrimoniale)
|
||||||
|
anno_start = cap.copy()
|
||||||
|
flusso[:] = 0.0
|
||||||
|
return dict(cap=cap, colpito=colpito, versato=versato)
|
||||||
|
|
||||||
|
|
||||||
|
def anni_al_traguardo(colpito: np.ndarray) -> tuple[float, float]:
|
||||||
|
"""(anni mediani al traguardo CONDIZIONATI all'arrivo, P(traguardo entro 20 anni)).
|
||||||
|
|
||||||
|
⚠️ La mediana e' condizionata — e' la convenzione di `r0726_capwall_refresh.trajectory`, quindi
|
||||||
|
i due numeri sono confrontabili con la tabella pubblicata. Va letta insieme alla probabilita':
|
||||||
|
«15 anni» su meta' dei path che arrivano non e' «15 anni». Prendere la mediana sull'INTERO
|
||||||
|
vettore (con i non-arrivi a -1) darebbe un numero piu' BASSO quanto peggio va la colonna, cioe'
|
||||||
|
esattamente il contrario di cio' che si vuole leggere.
|
||||||
|
"""
|
||||||
|
arrivati = colpito[colpito >= 0]
|
||||||
|
med = float(np.percentile(arrivati, 50)) / 365.0 if len(arrivati) > 100 else float("nan")
|
||||||
|
entro20 = float(((colpito >= 0) & (colpito <= 20 * 365)).mean())
|
||||||
|
return med, entro20
|
||||||
|
|
||||||
|
|
||||||
|
def versamento_per_confidenza(paths: np.ndarray, bersaglio: float, conf: float,
|
||||||
|
aliquota: float, patrimoniale: float) -> float:
|
||||||
|
"""Il versamento mensile minimo con P(traguardo entro l'orizzonte) >= `conf`.
|
||||||
|
Bisezione: P e' monotona nel versamento."""
|
||||||
|
lo, hi = 0.0, 6_000.0
|
||||||
|
if accumula(paths, hi, bersaglio, aliquota, patrimoniale)["colpito"].__ge__(0).mean() < conf:
|
||||||
|
return float("inf")
|
||||||
|
for _ in range(12):
|
||||||
|
mid = (lo + hi) / 2
|
||||||
|
p = float((accumula(paths, mid, bersaglio, aliquota, patrimoniale)["colpito"] >= 0).mean())
|
||||||
|
if p >= conf:
|
||||||
|
hi = mid
|
||||||
|
else:
|
||||||
|
lo = mid
|
||||||
|
return hi
|
||||||
|
|
||||||
|
|
||||||
|
def sezione(t: str) -> None:
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print(f" {t}")
|
||||||
|
print("=" * 100)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("=" * 100)
|
||||||
|
print(" r0807 — IL PIANO AL NETTO: muro, traiettorie e versamenti col fisco d'accumulo dentro")
|
||||||
|
print("=" * 100)
|
||||||
|
r = DP.deluck_returns()
|
||||||
|
netto_anno = CC.TARGET_EUR_DAY * 365 * CC.EURUSD # €50/g NETTI, senza lordizzare
|
||||||
|
lordo_anno = netto_anno / (1 - ALIQUOTA)
|
||||||
|
print(f"\n book live de-luckato ×{DP.DELUCK} · {len(r)} giorni · drift {r.mean() * 365:.1%} · "
|
||||||
|
f"vol {r.std() * 365 ** 0.5:.1%}")
|
||||||
|
print(f" bersaglio di rendita: €{CC.TARGET_EUR_DAY:.0f}/g netti = ${netto_anno:,.0f}/anno "
|
||||||
|
f"(${lordo_anno:,.0f} se lordizzati al {ALIQUOTA:.0%})")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ (0)
|
||||||
|
sezione("(0) CONTROLLO DI REPLICA — a fisco spento deve uscire il numero pubblicato")
|
||||||
|
perp_mio, muro_mio = muro(r, lordo_anno, 0.0, 0.0)
|
||||||
|
n_orig = WR.N_PATHS
|
||||||
|
_, perp_2k, muro_2k = WR.perp_and_wall(r, 1.0, lordo_anno) # default dello script, 2000
|
||||||
|
try:
|
||||||
|
WR.N_PATHS = N_PATHS
|
||||||
|
_, perp_4k, muro_4k = WR.perp_and_wall(r, 1.0, lordo_anno) # alla taglia pubblicata
|
||||||
|
finally:
|
||||||
|
WR.N_PATHS = n_orig
|
||||||
|
print(f"\n costante pubblicata (VR.TARGET, citata ovunque) :"
|
||||||
|
f"{'':>13}muro ${LS.TARGET:,.0f}")
|
||||||
|
print(f" questa implementazione, fisco OFF, {N_PATHS} path : perpetua {perp_mio:.4%} · "
|
||||||
|
f"muro ${muro_mio:,.0f}")
|
||||||
|
print(f" r0726_capwall_refresh ai suoi {n_orig} path : perpetua {perp_2k:.4%} · "
|
||||||
|
f"muro ${muro_2k:,.0f}")
|
||||||
|
print(f" r0726_capwall_refresh portato a {N_PATHS} path : perpetua {perp_4k:.4%} · "
|
||||||
|
f"muro ${muro_4k:,.0f}")
|
||||||
|
ok = abs(muro_mio - LS.TARGET) < 1.0 and abs(perp_mio - perp_4k) < 1e-9
|
||||||
|
print(f" -> {'REPLICA ESATTA' if ok else 'NON REPLICA — fermarsi qui'}: due implementazioni "
|
||||||
|
"separate, stesso dollaro")
|
||||||
|
print(f"\n ⚠️ Trovato per strada: `perp_and_wall` gira a {n_orig} path e a quella taglia da'")
|
||||||
|
print(f" ${muro_2k:,.0f} invece di ${LS.TARGET:,.0f} — {abs(muro_2k / LS.TARGET - 1):.1%} di rumore Monte Carlo.")
|
||||||
|
print(" Il muro pubblicato e' corretto (fu calcolato a 4000), ma la sua TERZA cifra")
|
||||||
|
print(" significativa non e' un'informazione: si cita $272k, non $272.061.")
|
||||||
|
if not ok:
|
||||||
|
raise SystemExit("la replica non torna: qualunque numero col fisco sarebbe indistinguibile da un bug")
|
||||||
|
muro_pub, perp_pub = LS.TARGET, perp_mio
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ (1)
|
||||||
|
sezione("(1) IL MURO — la convenzione pubblicata e quella coerente")
|
||||||
|
print("\n (a) PUBBLICATA: prelievo lordizzato, capitale che compone senza mai pagare imposte.")
|
||||||
|
print(" (b) COERENTE : imposte pagate ogni anno dentro il portafoglio, prelievo gia' netto.")
|
||||||
|
print(f"\n {'convenzione':<34}{'prelievo/anno':>15}{'perpetua':>11}{'muro':>14}")
|
||||||
|
print(f" {'(a) pubblicata (fisco solo sul prelievo)':<34}{lordo_anno:>15,.0f}"
|
||||||
|
f"{perp_pub:>10.2%}{muro_pub:>14,.0f}")
|
||||||
|
righe = {}
|
||||||
|
for aliq in (ALIQUOTA, 0.26):
|
||||||
|
p, w = muro(r, netto_anno, aliq, TX.PATRIMONIALE)
|
||||||
|
righe[aliq] = (p, w)
|
||||||
|
print(f" {f'(b) coerente, aliquota {aliq:.0%}':<34}{netto_anno:>15,.0f}{p:>10.2%}{w:>14,.0f}")
|
||||||
|
perp_n, muro_n = righe[ALIQUOTA]
|
||||||
|
print(f"\n Il muro coerente e' ${muro_n:,.0f} contro ${muro_pub:,.0f} pubblicati "
|
||||||
|
f"({muro_n / muro_pub - 1:+.1%}).")
|
||||||
|
print(" I due errori della convenzione (a) si compensano quasi: lordizzare il prelievo")
|
||||||
|
print(" ALZA il muro, non tassare il capitale investito lo ABBASSA. Restano scoperti la")
|
||||||
|
print(f" patrimoniale ({TX.PATRIMONIALE:.1%}/anno) e la non-linearita' della rendita perpetua")
|
||||||
|
print(" nel drift, che e' cio' che si vede nella differenza.")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ (2)
|
||||||
|
sezione(f"(2) TRAIETTORIE DA ${LS.START:.0f} — anni al traguardo, lordo vs netto, sugli STESSI path")
|
||||||
|
anni = 25 # come `r0726_capwall_refresh.trajectory`: 20 troncherebbe la coda
|
||||||
|
rng = np.random.default_rng(725) # stesso seed della tabella
|
||||||
|
paths = CC._boot_paths(r, 3_000, anni * 365, BLOCK, rng) # e stessa taglia
|
||||||
|
print(f"\n orizzonte {anni} anni · 3000 path · seed 725 — cioe' ESATTAMENTE la macchina che")
|
||||||
|
print(" ha prodotto la tabella pubblicata: la colonna LORDA e' una replica di controllo.")
|
||||||
|
print(f" (LORDO usa il muro pubblicato ${muro_pub:,.0f}, NETTO quello coerente ${muro_n:,.0f})")
|
||||||
|
print(f"\n {'€/mese':>8}{'versato in 20a':>16} |{'LORDO: anni':>13}{'P(20a)':>9}"
|
||||||
|
f" |{'NETTO: anni':>13}{'P(20a)':>9}{'ritardo':>10}")
|
||||||
|
tab2 = []
|
||||||
|
for dep in VERSAMENTI:
|
||||||
|
L = accumula(paths, dep, muro_pub, 0.0, 0.0)
|
||||||
|
N = accumula(paths, dep, muro_n, ALIQUOTA, TX.PATRIMONIALE)
|
||||||
|
aL, pL = anni_al_traguardo(L["colpito"])
|
||||||
|
aN, pN = anni_al_traguardo(N["colpito"])
|
||||||
|
rit = f"{aN - aL:+.1f}a" if not (np.isnan(aL) or np.isnan(aN)) else "—"
|
||||||
|
f = lambda a: f"{a:.1f}a" if not np.isnan(a) else "mai" # noqa: E731
|
||||||
|
versato_20a = LS.START + dep * CC.EURUSD * (20 * 365 // 30)
|
||||||
|
print(f" {dep:>8}{versato_20a:>16,.0f} |{f(aL):>13}{pL:>9.0%} "
|
||||||
|
f"|{f(aN):>13}{pN:>9.0%}{rit:>10}")
|
||||||
|
tab2.append(dict(dep=dep, anni_lordo=aL, p_lordo=pL, anni_netto=aN, p_netto=pN))
|
||||||
|
print("\n ⚠️ La colonna «anni» e' la mediana CONDIZIONATA all'arrivo: va letta insieme alla")
|
||||||
|
print(" probabilita' accanto. Dove P scende molto (€250/mese al netto) l'anno mediano")
|
||||||
|
print(" resta basso perche' descrive solo chi arriva — non e' un miglioramento.")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ (3)
|
||||||
|
sezione("(3) QUANTO VERSARE PER UN ORIZZONTE DATO — al netto")
|
||||||
|
print(f"\n bersaglio ${muro_n:,.0f} · versamento mensile minimo per la confidenza indicata")
|
||||||
|
print(f"\n {'orizzonte':>10}" + "".join(f"{f'P={c:.0%}':>12}" for c in CONFIDENZE)
|
||||||
|
+ f"{'tot. versato @P=90%':>22}")
|
||||||
|
for anni_h in ORIZZONTI:
|
||||||
|
rng_h = np.random.default_rng(SEED)
|
||||||
|
ph = CC._boot_paths(r, N_PATHS, anni_h * 365, BLOCK, rng_h)
|
||||||
|
celle, tot90 = [], 0.0
|
||||||
|
for c in CONFIDENZE:
|
||||||
|
v = versamento_per_confidenza(ph, muro_n, c, ALIQUOTA, TX.PATRIMONIALE)
|
||||||
|
celle.append(f"€{v:,.0f}/m" if np.isfinite(v) else "irraggiung.")
|
||||||
|
if c == 0.90 and np.isfinite(v):
|
||||||
|
tot90 = accumula(ph, v, muro_n, ALIQUOTA, TX.PATRIMONIALE)["versato"]
|
||||||
|
print(f" {f'{anni_h} anni':>10}" + "".join(f"{x:>12}" for x in celle)
|
||||||
|
+ f"{f'${tot90:,.0f}':>22}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ (4)
|
||||||
|
sezione("(4) RENDITA €/g MEDIANA — al netto, per livello di versamento")
|
||||||
|
print(f"\n rendita = capitale × perpetua({perp_n:.2%}); le imposte sono gia' dentro la")
|
||||||
|
print(" perpetua, quindi il numero e' NETTO e non va lordizzato una seconda volta.")
|
||||||
|
print(f"\n {'€/mese':>8}" + "".join(f"{f'{y}a':>12}" for y in (5, 10, 15, 20))
|
||||||
|
+ f"{'P(€50/g a 20a)':>17}")
|
||||||
|
for dep in VERSAMENTI:
|
||||||
|
celle, p50 = [], 0.0
|
||||||
|
for y in (5, 10, 15, 20):
|
||||||
|
rng_y = np.random.default_rng(SEED)
|
||||||
|
py = CC._boot_paths(r, N_PATHS, y * 365, BLOCK, rng_y)
|
||||||
|
cap = accumula(py, dep, muro_n, ALIQUOTA, TX.PATRIMONIALE)["cap"]
|
||||||
|
rend = cap * perp_n / 365.0 / CC.EURUSD
|
||||||
|
celle.append(f"{np.median(rend):,.2f}")
|
||||||
|
if y == 20:
|
||||||
|
p50 = float((rend >= CC.TARGET_EUR_DAY).mean())
|
||||||
|
print(f" {dep:>8}" + "".join(f"{x:>12}" for x in celle) + f"{p50:>17.1%}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ verdetto
|
||||||
|
sezione("VERDETTO — cosa cambia nelle conclusioni gia' usate per decidere")
|
||||||
|
d250 = next(x for x in tab2 if x["dep"] == 250)
|
||||||
|
d500 = next(x for x in tab2 if x["dep"] == 500)
|
||||||
|
print(f"""
|
||||||
|
1. Il MURO si sposta poco: ${muro_n:,.0f} contro ${muro_pub:,.0f} ({muro_n / muro_pub - 1:+.1%}).
|
||||||
|
La convenzione pubblicata sbagliava DUE volte in versi opposti e le due meta' quasi si
|
||||||
|
cancellano — ma per caso, non per costruzione: e' il tipo di errore che si scopre solo
|
||||||
|
rifacendo il conto in modo coerente.
|
||||||
|
|
||||||
|
2. E' sulle TRAIETTORIE che il fisco morde, e si legge nella PROBABILITA' piu' che negli anni:
|
||||||
|
€250/mese passa da P(entro 20a) {d250['p_lordo']:.0%} a {d250['p_netto']:.0%} (mediana condizionata
|
||||||
|
{d250['anni_lordo']:.1f} -> {d250['anni_netto']:.1f} anni); €500/mese da {d500['p_lordo']:.0%} a {d500['p_netto']:.0%}
|
||||||
|
({d500['anni_lordo']:.1f} -> {d500['anni_netto']:.1f} anni). Il livello di versamento che il 26/07 dava come
|
||||||
|
«P 92%, il piano funziona» e' quello che al netto diventa una moneta.
|
||||||
|
|
||||||
|
3. Cio' che NON cambia: senza versamenti il capitale-rendita non si raggiunge mai, a
|
||||||
|
qualunque lente fiscale. L'ordine di importanza delle leve (versare > quando > quanto
|
||||||
|
presto si smette > piatto vs crescente > frequenza) e' invariato, perche' il fisco
|
||||||
|
colpisce tutte le colonne allo stesso modo.
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
"""Il piano di accumulo al NETTO del fisco (scripts/research/r0807_piano_netto.py).
|
||||||
|
|
||||||
|
Il numero che questo script deve poter cambiare — le traiettorie e i versamenti pubblicati — e'
|
||||||
|
stato usato per decidere. Percio' l'ordine dei test e' quello del metodo: prima si dimostra che
|
||||||
|
la macchina riproduce cio' che sostituisce (a fisco spento), poi si guarda cosa dice col fisco
|
||||||
|
acceso. Un numero nuovo prodotto da una macchina non validata e' indistinguibile da un bug.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||||
|
|
||||||
|
import r0725_capcurve as CC # noqa: E402
|
||||||
|
import r0727_lumpsum_split as LS # noqa: E402
|
||||||
|
import r0727_tasse as TX # noqa: E402
|
||||||
|
import r0807_piano_netto as PN # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def carry_vuoto(n: int) -> np.ndarray:
|
||||||
|
return np.zeros((n, TX.CARRY_ANNI))
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# il passo fiscale
|
||||||
|
# ===========================================================================
|
||||||
|
def test_una_plusvalenza_viene_tassata_all_aliquota():
|
||||||
|
cap = np.array([110.0])
|
||||||
|
imp = PN.passo_fiscale(cap, np.array([100.0]), np.array([0.0]), carry_vuoto(1), 0.33, 0.0)
|
||||||
|
assert imp[0] == pytest.approx(10 * 0.33)
|
||||||
|
assert cap[0] == pytest.approx(110 - 3.3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_versamenti_dell_anno_non_sono_plusvalenza():
|
||||||
|
"""Chi versa €1.000 e finisce l'anno con €1.000 in piu' non ha guadagnato nulla."""
|
||||||
|
cap = np.array([1_100.0])
|
||||||
|
imp = PN.passo_fiscale(cap, np.array([100.0]), np.array([1_000.0]), carry_vuoto(1), 0.33, 0.0)
|
||||||
|
assert imp[0] == 0.0
|
||||||
|
assert cap[0] == 1_100.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_prelievi_dell_anno_non_sono_minusvalenza():
|
||||||
|
"""Simmetrico del precedente, ed e' il motivo per cui la stessa funzione serve accumulo e
|
||||||
|
prelievo: chi preleva 100 e chiude pari ha guadagnato 100, non zero."""
|
||||||
|
cap = np.array([1_000.0])
|
||||||
|
imp = PN.passo_fiscale(cap, np.array([1_000.0]), np.array([-100.0]), carry_vuoto(1), 0.33, 0.0)
|
||||||
|
assert imp[0] == pytest.approx(100 * 0.33)
|
||||||
|
|
||||||
|
|
||||||
|
def test_una_minusvalenza_non_produce_credito_ma_carry():
|
||||||
|
cap = np.array([90.0])
|
||||||
|
c = carry_vuoto(1)
|
||||||
|
imp = PN.passo_fiscale(cap, np.array([100.0]), np.array([0.0]), c, 0.33, 0.0)
|
||||||
|
assert imp[0] == 0.0 and cap[0] == 90.0
|
||||||
|
assert c[0, 0] == pytest.approx(10.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_carry_abbatte_l_imponibile_dell_anno_dopo():
|
||||||
|
cap = np.array([90.0])
|
||||||
|
c = carry_vuoto(1)
|
||||||
|
PN.passo_fiscale(cap, np.array([100.0]), np.array([0.0]), c, 0.33, 0.0) # -10
|
||||||
|
cap[0] = 120.0
|
||||||
|
imp = PN.passo_fiscale(cap, np.array([90.0]), np.array([0.0]), c, 0.33, 0.0) # +30, -10 di carry
|
||||||
|
assert imp[0] == pytest.approx(20 * 0.33)
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_carry_scade_dopo_quattro_anni():
|
||||||
|
c = carry_vuoto(1)
|
||||||
|
cap = np.array([90.0])
|
||||||
|
PN.passo_fiscale(cap, np.array([100.0]), np.array([0.0]), c, 0.33, 0.0) # minus di 10
|
||||||
|
for _ in range(TX.CARRY_ANNI): # 4 anni in pari
|
||||||
|
cap[0] = 100.0
|
||||||
|
PN.passo_fiscale(cap, np.array([100.0]), np.array([0.0]), c, 0.33, 0.0)
|
||||||
|
cap[0] = 130.0
|
||||||
|
imp = PN.passo_fiscale(cap, np.array([100.0]), np.array([0.0]), c, 0.33, 0.0)
|
||||||
|
assert imp[0] == pytest.approx(30 * 0.33), "la minusvalenza doveva essere scaduta"
|
||||||
|
|
||||||
|
|
||||||
|
def test_le_minusvalenze_piu_vecchie_si_usano_per_prime():
|
||||||
|
"""Se si consumassero le piu' recenti, quelle vecchie scadrebbero e il modello sarebbe
|
||||||
|
pessimista sul fisco senza dirlo."""
|
||||||
|
c = carry_vuoto(1)
|
||||||
|
c[0, 3] = 10.0 # vecchia, all'ultimo anno di utilizzo
|
||||||
|
c[0, 0] = 10.0 # recente
|
||||||
|
cap = np.array([105.0])
|
||||||
|
PN.passo_fiscale(cap, np.array([100.0]), np.array([0.0]), c, 0.33, 0.0) # +5 di plusvalenza
|
||||||
|
# a fine passo le colonne SLITTANO di un anno: la recente e' in posizione 1, intatta
|
||||||
|
assert c[0, 1] == pytest.approx(10.0), "doveva restare intatta la piu' recente"
|
||||||
|
# la vecchia e' stata consumata per 5 e il residuo e' scaduto insieme a lei: e' corretto,
|
||||||
|
# era al suo ultimo anno — ma va detto, perche' un residuo che sparisce sembra una perdita.
|
||||||
|
assert c[0].sum() == pytest.approx(10.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_patrimoniale_colpisce_il_valore_non_il_guadagno():
|
||||||
|
cap = np.array([1_000.0])
|
||||||
|
imp = PN.passo_fiscale(cap, np.array([1_000.0]), np.array([0.0]), carry_vuoto(1), 0.0, 0.002)
|
||||||
|
assert imp[0] == pytest.approx(2.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_le_imposte_non_portano_il_capitale_sotto_zero():
|
||||||
|
cap = np.array([1.0])
|
||||||
|
PN.passo_fiscale(cap, np.array([0.0]), np.array([0.0]), carry_vuoto(1), 0.33, 0.9)
|
||||||
|
assert cap[0] >= 0.0
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# le due macchine, validate contro cio' che sostituiscono
|
||||||
|
# ===========================================================================
|
||||||
|
def test_la_sopravvivenza_a_fisco_spento_e_quella_gia_in_uso():
|
||||||
|
"""Controllo di replica: `sopravvivenza(...,0,0)` deve essere `CC.survival` allo stesso seed.
|
||||||
|
E' l'unica cosa che rende leggibile la riga col fisco."""
|
||||||
|
r = np.array([0.001, -0.002, 0.003, 0.0, -0.001, 0.002] * 200)
|
||||||
|
mia = PN.sopravvivenza(r, 1e6, 60_000.0, 20, 0.0, 0.0, n_paths=400)
|
||||||
|
sua = CC.survival(r, 1e6, 60_000.0, 20, 1.0, n_paths=400, block=PN.BLOCK, seed=PN.SEED_MURO)
|
||||||
|
assert mia["p_survive"] == pytest.approx(sua["p_survive"], abs=0.0)
|
||||||
|
assert mia["p10_end"] == pytest.approx(sua["p10_end"], rel=1e-12)
|
||||||
|
|
||||||
|
|
||||||
|
def test_l_accumulo_a_fisco_spento_e_capitalizzazione_semplice():
|
||||||
|
paths = np.full((3, 400), 0.001)
|
||||||
|
out = PN.accumula(paths, 0.0, 1e12, 0.0, 0.0, start=1_000.0)
|
||||||
|
assert out["cap"][0] == pytest.approx(1_000.0 * 1.001 ** 400)
|
||||||
|
assert out["versato"] == 1_000.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_fisco_non_puo_far_finire_con_PIU_soldi():
|
||||||
|
"""Direzione garantita: qualunque siano i path, tassare non arricchisce."""
|
||||||
|
rng = np.random.default_rng(0)
|
||||||
|
paths = rng.normal(0.0006, 0.006, (200, 365 * 3))
|
||||||
|
lordo = PN.accumula(paths, 250.0, 1e12, 0.0, 0.0)["cap"]
|
||||||
|
netto = PN.accumula(paths, 250.0, 1e12, 0.33, TX.PATRIMONIALE)["cap"]
|
||||||
|
assert (netto <= lordo + 1e-6).all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_primo_passaggio_al_bersaglio_e_il_PRIMO():
|
||||||
|
"""`colpito` e' un primo passaggio: se il capitale tocca il bersaglio e poi scende, il
|
||||||
|
traguardo resta raggiunto — e non va aggiornato a un passaggio successivo."""
|
||||||
|
paths = np.concatenate([np.full((1, 10), 0.5), np.full((1, 10), -0.5)], axis=1)
|
||||||
|
out = PN.accumula(paths, 0.0, 2_000.0, 0.0, 0.0, start=1_000.0)
|
||||||
|
assert out["colpito"][0] == 1 # 1000 -> 1500 (t=0) -> 2250 (t=1), poi crolla e resta 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_versamento_per_confidenza_e_monotono():
|
||||||
|
rng = np.random.default_rng(1)
|
||||||
|
paths = rng.normal(0.0006, 0.006, (300, 365 * 5))
|
||||||
|
v50 = PN.versamento_per_confidenza(paths, 60_000.0, 0.50, 0.33, TX.PATRIMONIALE)
|
||||||
|
v90 = PN.versamento_per_confidenza(paths, 60_000.0, 0.90, 0.33, TX.PATRIMONIALE)
|
||||||
|
assert v90 > v50 > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_bersaglio_irraggiungibile_ritorna_infinito_non_un_numero_grande():
|
||||||
|
"""Distinguere «serve tantissimo» da «non basta nemmeno il massimo cercato»: un numero
|
||||||
|
grande verrebbe letto come una risposta."""
|
||||||
|
paths = np.zeros((50, 365))
|
||||||
|
assert PN.versamento_per_confidenza(paths, 1e12, 0.9, 0.0, 0.0) == float("inf")
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# la costante pubblicata
|
||||||
|
# ===========================================================================
|
||||||
|
def test_il_bersaglio_citato_ovunque_e_quello_congelato():
|
||||||
|
"""Se qualcuno ricalcola il muro e aggiorna la costante, questo test dice che vanno rilette
|
||||||
|
tutte le tabelle che la usano — non e' un numero locale."""
|
||||||
|
assert LS.TARGET == 272_061.0
|
||||||
Reference in New Issue
Block a user