"""Test del piano versamenti (scripts/research/r0726_deposits.py). Il test piu' importante e' `test_la_rendita_al_muro_e_esattamente_il_bersaglio`: lega la tabella della "domanda inversa" al muro calcolato altrove. Se le due macchinerie divergono, una delle due tabelle sta mentendo e nessun altro controllo se ne accorgerebbe. """ 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 r0726_deposits as DP # noqa: E402 # =========================================================================== # calendario dei versamenti # =========================================================================== def test_versamento_mensile_conta_i_periodi_giusti(): s = DP.monthly_schedule(365, 100.0) assert (s > 0).sum() == 12, "12 versamenti in un anno di 365 giorni a passo 30" assert s[0] == 0.0, "il primo versamento non e' al giorno 0 (li' c'e' il capitale iniziale)" def test_stop_year_interrompe_davvero(): s = DP.monthly_schedule(365 * 10, 100.0, stop_year=3) ultimo = np.nonzero(s)[0].max() assert ultimo < 3 * 365 assert (s > 0).sum() == pytest.approx(36, abs=1) def test_la_crescita_aumenta_gli_importi_nel_tempo(): s = DP.monthly_schedule(365 * 10, 100.0, growth=0.05) nz = s[s > 0] assert nz[-1] > nz[0] * 1.5, "dopo ~10 anni al 5% l'importo deve essere >1.5x" piatto = DP.monthly_schedule(365 * 10, 100.0) assert s.sum() > piatto.sum() def test_crescita_zero_equivale_al_piatto(): a = DP.monthly_schedule(365 * 5, 200.0, growth=0.0) b = DP.monthly_schedule(365 * 5, 200.0) assert np.allclose(a, b) # =========================================================================== # simulazione # =========================================================================== def test_senza_versamenti_e_solo_capitalizzazione(): paths = np.full((3, 100), 0.001) cap, paid = DP.simulate(paths, np.zeros(100)) atteso = DP.START * (1.001 ** 100) assert cap[0] == pytest.approx(atteso, rel=1e-9) assert paid[0] == pytest.approx(DP.START) def test_a_rendimento_zero_il_capitale_e_la_somma_dei_versamenti(): paths = np.zeros((2, 365)) s = DP.monthly_schedule(365, 100.0) cap, paid = DP.simulate(paths, s) assert cap[0] == pytest.approx(DP.START + s.sum()) assert paid[0] == pytest.approx(DP.START + s.sum()) def test_il_totale_versato_include_il_capitale_iniziale(): """Confrontare un capitale finale con un 'versato' che dimentica i $600 iniziali gonfia il rapporto capitale/versato — errore gia' fatto una volta oggi nel contatore `paid`.""" s = DP.monthly_schedule(365, 50.0) _, paid = DP.simulate(np.zeros((2, 365)), s) assert paid[0] == pytest.approx(DP.START + s.sum()) assert paid[0] > s.sum() # =========================================================================== # il legame con il muro — il test che lega le due macchinerie # =========================================================================== def test_la_rendita_al_muro_e_esattamente_il_bersaglio(): """Un capitale pari al muro deve dare, per costruzione, esattamente TARGET_EUR_DAY. Se questa identita' si rompe, la tabella (D) e la tabella dei muri non parlano piu' della stessa cosa.""" import r0726_capwall_refresh as WR r = DP.deluck_returns() gross = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - CC.TAX_RATE) _, perp, wall = WR.perp_and_wall(r, 1.0, gross) rent = DP.rent_from_capital(np.array([wall]), perp)[0] assert rent == pytest.approx(CC.TARGET_EUR_DAY, rel=1e-6) def test_la_rendita_e_lineare_nel_capitale(): r = DP.rent_from_capital(np.array([100_000.0, 200_000.0]), 0.10) assert r[1] == pytest.approx(2 * r[0]) # =========================================================================== # i FINDING: se cambiano, e' una notizia # =========================================================================== def test_anticipare_batte_posticipare_a_pari_totale(): """Il finding di (C). Su ritorni a drift positivo deterministico e' un'identita' algebrica, e serve come controllo di direzione: se uscisse il contrario, la simulazione e' rotta.""" n = 365 * 20 paths = np.full((1, n), 0.0004) tot = 60_000.0 * CC.EURUSD presto, tardi = np.zeros(n), np.zeros(n) k = len(range(30, 365 * 5, 30)) for t in range(30, 365 * 5, 30): presto[t] = tot / k k2 = len(range(365 * 10, n, 30)) for t in range(365 * 10, n, 30): tardi[t] = tot / k2 assert presto.sum() == pytest.approx(tardi.sum(), rel=1e-9) cap_p, _ = DP.simulate(paths, presto) cap_t, _ = DP.simulate(paths, tardi) assert cap_p[0] > cap_t[0] * 1.5 def test_il_fattore_deluck_e_quello_misurato_non_quello_a_occhio(): """Il ×0.6 del 25/07 era scelto a occhio ed e' stato misurato ×0.87-0.91 il 26/07.""" assert DP.DELUCK == 0.89 def test_hazard_giornaliero_ricompone_alla_probabilita_annua(): """Il salto di venue: (1-haz)^365 deve dare (1-p). Sbagliare qui cambia tutte le colonne.""" for p in (0.005, 0.01, 0.02, 0.05): haz = 1.0 - (1.0 - p) ** (1.0 / 365.0) assert (1.0 - haz) ** 365 == pytest.approx(1.0 - p, rel=1e-9)