"""Test per r0726_venue_risk.py e per le aggiunte a r0726_capwall_refresh.py. Due invarianti che, se rotte, darebbero numeri plausibili e sbagliati: * l'hazard GIORNALIERO deve comporre esattamente alla probabilita' ANNUA dichiarata — un errore qui sposta l'intera tabella di sensibilita' senza che nulla sembri storto; * il contatore dei versamenti deve seguire il capitale (bug catturato in sessione: `paid` si congelava al traguardo mentre `cap` continuava a ricevere i depositi -> rapporto capitale/versato gonfiato del 70-80% a 25-30 anni). """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scripts" / "research")) sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) def _flat(vals, start="2020-01-01"): return pd.Series(np.asarray(vals, float), index=pd.date_range(start, periods=len(vals), freq="1D", tz="UTC")) # =========================================================================== # hazard: giornaliero -> annuo # =========================================================================== @pytest.mark.parametrize("p", [0.005, 0.01, 0.02, 0.05, 0.2]) def test_hazard_giornaliero_compone_alla_probabilita_annua(p): """(1-(1-p)^(1/365))^ -> su 365 giorni la sopravvivenza deve tornare (1-p).""" haz = 1.0 - (1.0 - p) ** (1.0 / 365.0) assert (1.0 - haz) ** 365 == pytest.approx(1.0 - p, rel=1e-9) def test_p_zero_non_uccide_nessun_venue(): import r0726_venue_risk as VR S = {"A": _flat([0.001] * 400), "B": _flat([0.001] * 400)} r = VR.simulate({"A": 0.5, "B": 0.5}, S, 0.0, n_paths=200, years=1) assert r["p_any_fail"] == 0.0 assert r["p_all_dead"] == 0.0 def test_p_certo_uccide_tutto_e_azzera_il_capitale(): """Controllo POSITIVO: se il gate non mordesse mai, il test a p=0 passerebbe comunque.""" import r0726_venue_risk as VR S = {"A": _flat([0.001] * 400), "B": _flat([0.001] * 400)} r = VR.simulate({"A": 0.5, "B": 0.5}, S, 1.0, n_paths=200, years=1) assert r["p_all_dead"] == 1.0 assert r["median_end"] == 0.0 def test_concentrato_perde_tutto_piu_spesso_del_diversificato(): """L'asimmetria che motiva l'intero studio: a parita' di p, 'perso TUTTO' e' molto piu' probabile con un solo venue, anche se 'almeno un fallimento' e' piu' probabile con tre.""" import r0726_venue_risk as VR S = {"A": _flat([0.0005] * 800), "B": _flat([0.0005] * 800), "C": _flat([0.0005] * 800)} conc = VR.simulate({"A": 1.0}, S, 0.05, n_paths=800, years=10) split = VR.simulate({"A": 0.34, "B": 0.33, "C": 0.33}, S, 0.05, n_paths=800, years=10) assert conc["p_all_dead"] > split["p_all_dead"] assert split["p_any_fail"] > conc["p_any_fail"] def test_venue_series_applica_il_deluck_sul_drift(): import r0726_venue_risk as VR S = VR.venue_series() assert set(S) == {"Deribit", "Hyperliquid", "IB"} for v in S.values(): assert isinstance(v.index, pd.DatetimeIndex) and len(v) > 100 # =========================================================================== # capwall: il contatore dei versamenti # =========================================================================== def test_focus_versato_segue_il_capitale(): """`paid` deve valere 600 + dep*n_versamenti, deterministico e indipendente dal path. Se si congelasse al traguardo (il bug), i path che arrivano presto mostrerebbero un versato piu' basso e il rapporto capitale/versato uscirebbe gonfiato.""" import r0725_capcurve as CC import r0726_capwall_refresh as CW r = np.full(2000, 0.0004) F = CW.focus(r, cap_needed=1e12, dep_eur=250.0, n_paths=50, years=5, seed=1) for y, paid in F["snap_paid"].items(): n_dep = (y * 365) // 30 atteso = 600.0 + 250.0 * CC.EURUSD * n_dep assert np.allclose(paid, atteso, rtol=0, atol=1e-6), f"anno {y}: {paid[0]} != {atteso}" def test_focus_bersaglio_irraggiungibile_non_produce_traguardi(): import r0726_capwall_refresh as CW F = CW.focus(np.full(2000, 0.0001), cap_needed=1e15, dep_eur=100.0, n_paths=50, years=3, seed=2) assert len(F["fin"]) == 0 assert len(F["paid_at_hit"]) == 0 def test_solve_deposit_e_monotono_nella_confidenza(): """Piu' certezza -> piu' soldi. Se la ricerca binaria riusasse campioni diversi a ogni passo, la funzione non sarebbe monotona e questo test lo direbbe.""" import r0726_capwall_refresh as CW r = np.random.default_rng(3).normal(0.0006, 0.006, 1500) d50, _ = CW.solve_deposit(r, 100_000.0, 10, 0.50, n_paths=400, seed=5) d90, _ = CW.solve_deposit(r, 100_000.0, 10, 0.90, n_paths=400, seed=5) assert d90 >= d50 def test_solve_deposit_ritorna_nan_se_irraggiungibile(): import r0726_capwall_refresh as CW r = np.full(1500, -0.01) # book che perde l'1% al giorno dep, versato = CW.solve_deposit(r, 1e12, 5, 0.5, n_paths=200, seed=6) assert np.isnan(dep) and np.isnan(versato)