"""Test della macchineria RENDITA / COST-FLOOR (ondata 2026-07-25). Blocca tre cose che, se si rompono in silenzio, producono numeri credibili e sbagliati: 1. il simulatore di prelievo (survival) — validato contro casi con risposta NOTA; 2. il modello di costo IB a pavimento fisso — deve penalizzare il capitale piccolo e deve essere salvato dalla banda (altrimenti GTAA01 sembra eseguibile quando non lo e'); 3. la regressione del bug HWM nel modello funded (base di prelievo aggiornata ogni giorno -> payout ~0): e' l'errore realmente commesso in sessione. """ from __future__ import annotations import importlib.util import sys from pathlib import Path import numpy as np import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) def _load(name: str, rel: str): spec = importlib.util.spec_from_file_location(name, ROOT / rel) mod = importlib.util.module_from_spec(spec) sys.modules[name] = mod spec.loader.exec_module(mod) return mod @pytest.fixture(scope="module") def cc(): return _load("r0725_capcurve", "scripts/research/r0725_capcurve.py") # --------------------------------------------------------------- 1) survival def test_survival_asset_deterministico(cc): """Un asset che rende esattamente 10%/anno senza vol sostiene un prelievo perpetuo appena sotto il 10% e NON sostiene il 15%.""" r = np.full(2000, 1.10 ** (1 / 365) - 1.0) assert cc.survival(r, 1e6, 1e6 * 0.05, 20, 1.0, n_paths=100)["p10_end"] > 1e6 assert cc.survival(r, 1e6, 1e6 * 0.09, 20, 1.0, n_paths=100)["p10_end"] > 1e6 # sopra il rendimento dell'asset il capitale si erode, e molto sopra si va in rovina assert cc.survival(r, 1e6, 1e6 * 0.11, 20, 1.0, n_paths=100)["p10_end"] < 1e6 assert cc.survival(r, 1e6, 1e6 * 0.15, 20, 1.0, n_paths=100)["p_survive"] < 0.05 def test_survival_drift_zero_non_sostiene_rendita(cc): """Random walk a drift ZERO: nessun prelievo perpetuo e' sostenibile (il criterio p10 >= capitale iniziale deve fallire gia' a prelievo ~0).""" rng = np.random.default_rng(0) r = rng.normal(0.0, 0.15 / np.sqrt(365), 4000) assert cc.survival(r, 1e6, 0.0, 20, 1.0, n_paths=500)["p10_end"] < 1e6 assert cc.survival(r, 1e6, 1e6 * 0.03, 20, 1.0, n_paths=500)["p_survive"] < 0.95 def test_survival_prelievo_nullo_non_fallisce_mai(cc): """Senza prelievi un book a rendimenti limitati non puo' andare in rovina.""" rng = np.random.default_rng(1) r = rng.normal(0.0005, 0.01, 3000) assert cc.survival(r, 1e6, 0.0, 20, 1.0, n_paths=300)["p_survive"] == 1.0 def test_leva_aumenta_la_vol_dei_path(cc): """La leva k scala i ritorni: e' l'assunzione dichiarata del confronto iso-rischio.""" rng = np.random.default_rng(2) r = rng.normal(0.0003, 0.01, 2000) p1 = cc._boot_paths(r, 200, 365, 20, np.random.default_rng(3)) p2 = cc._boot_paths(r, 200, 365, 20, np.random.default_rng(3)) * 2.0 assert np.isclose(p2.std(), 2.0 * p1.std(), rtol=1e-9) # ------------------------------------------------------ 2) pavimento IB / banda def test_ib_pavimento_penalizza_il_capitale_piccolo(cc): """Il costo IB e' FISSO per ordine: a capitale piccolo il drag esplode. Se questo test fallisce, il modello e' tornato proporzionale e GTAA01 sembrera' eseguibile a $600.""" small = cc.metrics(cc.gtaa_realistic(600.0)) big = cc.metrics(cc.gtaa_realistic(50_000.0)) assert small["cagr"] < big["cagr"], "il pavimento fisso deve penalizzare il piccolo" assert small["cagr"] < 0.0, "a $600 il ribilanciamento continuo su IB deve essere negativo" def test_la_banda_salva_gtaa_a_capitale_piccolo(cc): """Con banda + cadenza settimanale, GTAA01 a $600 torna positivo. E' la condizione che rende il 3o sleeve eseguibile a basso capitale.""" nudo = cc.metrics(cc.gtaa_realistic(600.0)) banda = cc.metrics(cc.gtaa_banded(600.0, cc.GTAA_BAND_USD, cc.GTAA_EVERY)) assert banda["sharpe"] > nudo["sharpe"] + 0.5 assert banda["cagr"] > 0.0 def test_tp01_deribit_insensibile_al_capitale_sopra_soglia(cc): """Su Deribit la fee e' proporzionale e non c'e' pavimento: sopra ~$500 lo Sharpe realistico coincide col modellato (conferma della lezione 'min-order = banda ottimale').""" a = cc.metrics(cc.tp01_realistic(1_000.0))["sharpe"] b = cc.metrics(cc.tp01_realistic(50_000.0))["sharpe"] assert abs(a - b) < 0.02 # --------------------------------------------------- 3) regressione bug HWM funded @pytest.fixture(scope="module") def pc(): return _load("r0725_prop_config", "scripts/research/r0725_prop_config.py") def test_funded_payout_non_e_zero_regressione_hwm(pc): """REGRESSIONE: aggiornare la base di prelievo ogni giorno (come un HWM mobile) azzera per costruzione il guadagno misurato al checkpoint mensile -> payout ~0. Con un book che rende ~20%/anno e split 80% il payout atteso su $100k e' dell'ordine di $10k, NON di $200. Questo test e' il guardiano di quell'errore.""" r = np.full(3000, 1.20 ** (1 / 365) - 1.0) # +20%/anno deterministico, mai in perdita f = pc.sim_funded(r, max_loss=0.10, daily_loss=0.05, k=1.0, years=1, n_paths=200) assert f["p_alive"] == 1.0, "un asset che sale ogni giorno non puo' bustare" assert f["e_payout"] > 8_000.0, f"payout implausibilmente basso: {f['e_payout']:.0f}" assert f["e_payout"] < 20_000.0 def test_funded_bust_su_max_loss_statico(pc): """Il max-loss e' STATICO dal saldo iniziale: un book che perde costantemente deve bustare, e il payout deve essere zero.""" r = np.full(3000, -0.002) f = pc.sim_funded(r, max_loss=0.06, daily_loss=0.05, k=1.0, years=1, n_paths=200) assert f["p_alive"] == 0.0 assert f["e_payout"] == 0.0 def test_eval_target_irraggiungibile_non_passa(pc): """Sanity dell'eval: un book piatto non raggiunge mai il target di profitto.""" r = np.zeros(3000) e = pc.sim_eval(r, target=0.10, max_dd=0.06, daily_loss=0.04, k=1.0, n_paths=200) assert e["p_pass"] == 0.0 # ------------------------------------------------ 4) scala di conti funded (ladder) @pytest.fixture(scope="module") def pl(): return _load("r0725_prop_ladder", "scripts/research/r0725_prop_ladder.py") def test_wick_calibrato_sui_percentili_del_recon(pl): """Il gap intraday deve riprodurre l'ordine di grandezza misurato dal recon MTM 1h del 24/07 (p50 ~-0.17pp, p90 ~-1pp, p99 ~-3pp). Se la calibrazione scivola, i breach diventano finzione in una direzione o nell'altra.""" q = pl.wick_percentiles() assert 0.0010 < q[0.50] < 0.0025 assert 0.005 < q[0.90] < 0.015 assert 0.015 < q[0.99] < 0.050 def test_lente_intraday_e_sempre_piu_severa_del_close_only(pl): """I wick possono solo AGGIUNGERE breach: la lente intraday non puo' produrre piu' reddito di quella close-only. Se questo si inverte, il gap sta entrando col segno sbagliato.""" M, names = pl.sleeve_panel() slots = list(pl.policies(names).values())[1] close = pl.simulate(M, slots, n_paths=400, lev=1.0, seed=1, intraday=False) intra = pl.simulate(M, slots, n_paths=400, lev=1.0, seed=1, intraday=True) assert intra["p_target"] <= close["p_target"] assert intra["p_zero"] >= close["p_zero"] def test_conti_sullo_stesso_book_sono_perfettamente_correlati(pl): """Il punto centrale dello studio: N conti che girano lo STESSO book non diversificano. Verifica strutturale sui pesi, non sull'output del MC.""" M, names = pl.sleeve_panel() pols = pl.policies(names) conc = pols["CONC-DIV ogni conto = book diversificato"] assert all(np.allclose(s, conc[0]) for s in conc), "CONC deve avere slot identici" sparso = pols["SPARSO 1 sleeve per conto (a rotazione)"] assert not np.allclose(sparso[0], sparso[1]), "SPARSO deve avere slot diversi" def test_senza_cassa_nessun_conto_e_nessun_reddito(pl): """Controllo di sanita' del meccanismo cassa->biglietto: a cassa zero non si compra nulla.""" M, names = pl.sleeve_panel() slots = list(pl.policies(names).values())[0] old = pl.START_EUR try: pl.START_EUR = 0.0 r = pl.simulate(M, slots, n_paths=200, lev=1.0, seed=2) assert r["p_zero"] == 1.0 finally: pl.START_EUR = old # ------------------------------------------- 5) regola di CONSISTENCY HyroTrader @pytest.fixture(scope="module") def hy(): return _load("r0725_hyro", "scripts/research/r0725_hyro.py") def test_consistency_MORDE_su_un_book_grumoso(hy): """PROVA CHE IL CONTROLLO FUNZIONA. Un UNICO giorno da +12% porta da solo tutto il profitto e non arriva mai altro: con la regola 40% quel path non deve passare MAI (0.12 > 0.40*0.12), senza regola passa subito. Se questo test non discrimina, il 'costo ~0pp' misurato sul book vero sarebbe un bug, non un risultato. NB sul probe: sim_eval fa BLOCK BOOTSTRAP (blocchi di 20g), non replay -> uno strappo isolato finirebbe solo in alcuni path. Qui gli strappi sono spaziati di 20 giorni = esattamente uno per blocco, cosi' OGNI path ne vede uno ogni 20 giorni in modo deterministico. In 40 giorni: senza regola si passa al 1o strappo (+12% >= target 10%); con la regola servono 3 strappi perche' il giorno migliore scenda sotto il 40% del profitto cumulato -> oltre la finestra. """ r = np.zeros(200) r[::20] = 0.12 senza = hy.sim_eval(r, 1.0, consistency=False, intraday=False, max_days=40, n_paths=200) con = hy.sim_eval(r, 1.0, consistency=True, intraday=False, max_days=40, n_paths=200) assert senza["p_pass"] > 0.9, "senza regola un +12% secco deve passare il target 10%" assert con["p_pass"] < 0.05, "la regola 40% deve bloccare un profitto concentrato in 1-2 giorni" def test_consistency_non_morde_su_un_book_liscio(hy): """Controprova: un book che accumula il target in tanti giorni piccoli non e' toccato dalla regola. E' il motivo per cui sul book vero il costo misurato e' ~0.""" r = np.full(400, 0.0008) # ~+0.08%/g: il 10% arriva in ~120 giorni senza = hy.sim_eval(r, 1.0, consistency=False, intraday=False, n_paths=200) con = hy.sim_eval(r, 1.0, consistency=True, intraday=False, n_paths=200) assert abs(con["p_pass"] - senza["p_pass"]) < 0.02 def test_dd_statico_6pct_e_binding(hy): """Il maxDD 6% STATICO di HYRO e' il vincolo vero: un book con DD storico sopra il 6% non puo' girare a leva piena. Sanity: alzando la leva la sopravvivenza funded scende.""" cfg = hy.configs() r = list(cfg.values())[0].values.astype(float) lo = hy.sim_funded(r, 0.5, False, 100_000.0, n_paths=400)["p_alive"] hi = hy.sim_funded(r, 1.0, False, 100_000.0, n_paths=400)["p_alive"] assert lo > hi