"""Test della sorveglianza dell'edge (scripts/live/edge_watch.py). Due criteri, tarati in scripts/research/r0726_edge_death.py: A) Sharpe rolling 36m < -0.5 (falso kill 1.8% in 10 anni, rilevamento mediano 3.8 anni) B) TP01 deve tenere il DD sotto il 75% del buy&hold negli anni con sinistro (DD b&h > 10%) Il test piu' importante e' `test_un_anno_senza_sinistro_non_viene_valutato`: e' la differenza fra giudicare un'assicurazione sul premio e giudicarla sul sinistro, ed e' l'errore che il criterio B esiste per non fare. """ 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" / "live")) import edge_watch as EW # noqa: E402 def _serie(vals, start="2020-01-01"): return pd.Series(vals, index=pd.date_range(start, periods=len(vals), freq="D", tz="UTC")) # =========================================================================== # criterio A — Sharpe rolling # =========================================================================== def test_storia_insufficiente_non_e_un_via_libera(): """None significa 'non misurabile', e il chiamante deve poterlo distinguere da 'va bene'.""" assert EW.trailing_sharpe(_serie([0.001] * 100)) is None def test_sharpe_rolling_usa_solo_la_finestra_recente(): """Un passato ottimo non deve mascherare un presente pessimo.""" n = int(EW.WIN_MONTHS * 30.44) rng = np.random.default_rng(1) buono = list(rng.normal(0.004, 0.005, n)) # tratto ottimo, poi scartato cattivo = list(rng.normal(-0.002, 0.005, n)) s = EW.trailing_sharpe(_serie(buono + cattivo)) assert s is not None and s < 0, f"la finestra guarda troppo indietro: {s}" def test_la_soglia_congelata_non_cambia_per_sbaglio(): """Le soglie valgono la taratura (falso kill 1.8%/10a): cambiarle la invalida.""" assert EW.WIN_MONTHS == 36 assert EW.SHARPE_KILL == -0.5 assert EW.SINISTRO_DD == 0.10 assert EW.PROTECT_MAX == 0.75 # =========================================================================== # criterio B — protezione, ed e' qui che vive la lezione # =========================================================================== def test_un_anno_senza_sinistro_non_viene_valutato(): """IL test. In un anno in cui il buy&hold non perde, TP01 non ha niente da dimostrare: l'anno non deve entrare nella statistica ne' come successo ne' come fallimento.""" calmo = _serie([0.0005] * 365, start="2021-01-01") # b&h sale sempre: nessun sinistro tp = _serie([-0.001] * 365, start="2021-01-01") # TP01 perde: irrilevante qui assert EW.protection_by_year(tp, calmo) == [] def test_un_anno_con_sinistro_protetto_passa(): n = 365 bh = np.full(n, 0.001) bh[100:160] = -0.02 # crash: DD b&h grande tp = np.full(n, 0.0) tp[100:105] = -0.01 # TP01 esce quasi subito out = EW.protection_by_year(_serie(tp, "2021-01-01"), _serie(bh, "2021-01-01")) assert len(out) == 1 and out[0]["ok"] is True assert out[0]["ratio"] < EW.PROTECT_MAX def test_un_anno_con_sinistro_non_protetto_fallisce(): n = 365 bh = np.full(n, 0.001) bh[100:160] = -0.02 tp = bh.copy() # TP01 subisce tutto il crash out = EW.protection_by_year(_serie(tp, "2021-01-01"), _serie(bh, "2021-01-01")) assert len(out) == 1 and out[0]["ok"] is False assert out[0]["ratio"] == pytest.approx(1.0, rel=0.05) def test_il_conteggio_dei_fallimenti_e_consecutivo_non_cumulativo(): """Un fallimento isolato dieci anni fa non deve sommarsi a uno di oggi.""" prot = [dict(ok=False), dict(ok=True), dict(ok=False)] consec = 0 for p in prot: consec = consec + 1 if not p["ok"] else 0 assert consec == 1 # =========================================================================== # controlli positivi: la regola deve SCATTARE quando deve # =========================================================================== def test_controllo_positivo_edge_morto_fa_scattare_A(): n = int(EW.WIN_MONTHS * 30.44) rng = np.random.default_rng(3) s = EW.trailing_sharpe(_serie(list(rng.normal(-0.0015, 0.004, n)))) assert s is not None and s < EW.SHARPE_KILL def test_controllo_negativo_edge_vivo_non_fa_scattare_A(): n = int(EW.WIN_MONTHS * 30.44) rng = np.random.default_rng(4) s = EW.trailing_sharpe(_serie(list(rng.normal(0.0007, 0.004, n)))) assert s is not None and s > EW.SHARPE_KILL # =========================================================================== # stato reale + cablaggio # =========================================================================== def test_il_book_reale_supera_oggi_entrambi_i_criteri(): """Se questo test fallisce non e' il test a essere rotto: e' il book.""" r = EW.run() assert r["sharpe"] is None or r["sharpe"] >= EW.SHARPE_KILL assert r["consec_fail"] < 2 assert all(p["ok"] for p in r["protection"]), \ f"anni di sinistro falliti: {[p['year'] for p in r['protection'] if not p['ok']]}" def test_edge_watch_e_cablato_nel_cron_giornaliero(): sh = (ROOT / "scripts" / "cron_daily.sh").read_text() assert "edge_watch.py" in sh