"""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