"""Test del filone lump-sum + split di venue (scripts/research/r0727_lumpsum_split.py). Il test piu' importante e' `test_riproduce_esattamente_i_numeri_del_26_07`: la macchineria e' una GENERALIZZAZIONE di `r0726_venue_risk.simulate` (capitale iniziale e deposito diventano parametri). Se la generalizzazione avesse cambiato anche solo l'ordine di consumo dell'RNG, i numeri pubblicati e quelli nuovi non sarebbero piu' confrontabili — e la tabella nuova sembrerebbe comunque plausibile. Il secondo che conta e' `test_sotto_la_soglia_lo_split_non_esiste`: uno 'split' in cui la seconda gamba e' sotto il minimo eseguibile non e' uno split, e' cash su un secondo conto — che protegge dal fallimento del venue ma NON e' lo sleeve che i numeri assumono. """ 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")) import r0727_lumpsum_split as LS # noqa: E402 @pytest.fixture(scope="module") def series(): """Serie sintetiche a drift noto: i test sulla struttura non devono dipendere dal feed.""" idx = pd.date_range("2020-01-01", periods=1200, freq="D", tz="UTC") rng = np.random.default_rng(7) return { "Deribit": pd.Series(rng.normal(0.0005, 0.008, len(idx)), index=idx), "IB": pd.Series(rng.normal(0.0002, 0.004, len(idx)), index=idx), "Hyperliquid": pd.Series(rng.normal(0.0003, 0.010, len(idx)), index=idx), } # =========================================================================== # la replica — senza questa, la tabella nuova non e' confrontabile con quella vecchia # =========================================================================== def test_riproduce_esattamente_i_numeri_del_26_07(series): import r0726_venue_risk as VR cfg = {"Deribit": 1.0} kw = dict(n_paths=200, years=3, seed=LS.SEED) mine = LS.simulate(cfg, series, 0.01, lump_usd=0.0, dep_eur=VR.DEP_EUR, **kw) theirs = VR.simulate(cfg, series, 0.01, **kw) assert mine["p_reach"] == theirs["p_reach"] assert mine["p_all_dead"] == theirs["p_all_dead"] assert mine["median_end"] == theirs["median_end"] def test_il_deposito_di_default_e_quello_delle_traiettorie_pubblicate(series): """€250/mese: la colonna con cui si confronta tutto il piano.""" import inspect import r0726_venue_risk as VR assert inspect.signature(LS.simulate).parameters["dep_eur"].default == VR.DEP_EUR # =========================================================================== # il lump-sum # =========================================================================== def test_un_lump_sum_non_puo_peggiorare_il_traguardo(series): kw = dict(n_paths=300, years=5, seed=1) senza = LS.simulate({"Deribit": 1.0}, series, 0.0, lump_usd=0.0, **kw) con = LS.simulate({"Deribit": 1.0}, series, 0.0, lump_usd=10_000.0, **kw) assert con["p_reach"] >= senza["p_reach"] assert con["median_end"] > senza["median_end"] def test_con_un_conto_solo_la_rovina_non_dipende_dal_capitale(series): """Fatto strutturale, non aneddoto: con UN venue 'almeno un fallimento' COINCIDE con 'perso tutto', e quella probabilita' e' una proprieta' del TEMPO di esposizione. Se un giorno questa uguaglianza si rompesse, il modello di rovina e' cambiato.""" kw = dict(n_paths=400, years=5, seed=3) a = LS.simulate({"Deribit": 1.0}, series, 0.02, lump_usd=0.0, **kw) b = LS.simulate({"Deribit": 1.0}, series, 0.02, lump_usd=20_000.0, **kw) assert a["p_all_dead"] == b["p_all_dead"] def test_senza_deposito_e_senza_lump_non_si_arriva(series): r = LS.simulate({"Deribit": 1.0}, series, 0.0, lump_usd=0.0, dep_eur=0.0, n_paths=200, years=5, seed=2) assert r["p_reach"] == 0.0 # =========================================================================== # lo split, e la soglia che lo rende reale # =========================================================================== def test_sotto_la_soglia_lo_split_non_esiste(): """A $6.050 servirebbe il 50% su IB per fare $3.000: oltre il tetto, quindi None. Dichiararlo 'possibile' vorrebbe dire modellare uno sleeve che non e' eseguibile.""" assert LS.split_config(600.0) is None assert LS.split_config(6_050.0) is None def test_sopra_la_soglia_e_il_capitale_a_scegliere_la_quota(): cfg = LS.split_config(11_500.0) assert cfg is not None assert cfg["IB"] == pytest.approx(3_000.0 / 11_500.0, rel=1e-9) # ~26%, non il 25% preferito assert cfg["Deribit"] + cfg["IB"] == pytest.approx(1.0) def test_a_capitale_ampio_si_torna_alla_quota_raccomandata(): cfg = LS.split_config(50_000.0) assert cfg["IB"] == LS.IB_SHARE def test_la_quota_ib_non_supera_mai_il_tetto(): for tot in (8_600.0, 10_000.0, 12_000.0, 100_000.0): cfg = LS.split_config(tot) if cfg: assert cfg["IB"] <= LS.IB_SHARE_MAX + 1e-12 def test_due_conti_riducono_la_rovina(series): kw = dict(n_paths=600, years=6, seed=5, lump_usd=10_900.0) conc = LS.simulate({"Deribit": 1.0}, series, 0.05, **kw) split = LS.simulate(LS.split_config(11_500.0), series, 0.05, **kw) assert split["p_all_dead"] < conc["p_all_dead"] def test_la_rovina_non_dipende_da_cosa_sta_sulla_seconda_gamba(series): """SPLIT-GTAA e SPLIT-CASSA devono avere la stessa P(perso tutto): la protezione viene dal numero di conti, non dai loro ritorni. E' la lettura centrale della sezione 2-bis.""" kw = dict(n_paths=500, years=6, seed=11, lump_usd=10_900.0) cfg = LS.split_config(11_500.0) zero = {**series, "IB": series["IB"] * 0.0} assert (LS.simulate(cfg, series, 0.03, **kw)["p_all_dead"] == LS.simulate(cfg, zero, 0.03, **kw)["p_all_dead"]) # =========================================================================== # costi dichiarati # =========================================================================== def test_haircut_toglie_il_drift_dichiarato(series): s = LS.haircut(series["IB"], 0.009) drift = (series["IB"].mean() - s.mean()) * 365 assert drift == pytest.approx(0.009, rel=1e-9) def test_il_haircut_ib_somma_taglia_piccola_e_veicolo_ucits(): """0.8pp (CAGR 3.8%->3.0% a $3k, misurato 25/07) + 0.1pp (drag UCITS, misurato 26/07).""" assert LS.IB_HAIRCUT_YR == pytest.approx(0.009) def test_il_bersaglio_e_lo_stesso_per_entrambi_i_bracci(): """Conservativo CONTRO lo split (che avrebbe un muro leggermente diverso): se un giorno si usassero due bersagli, il confronto smetterebbe di essere leggibile.""" import r0726_venue_risk as VR assert LS.TARGET == VR.TARGET