feat: EDGE WATCH — criteri di kill pre-registrati per il book LIVE

"Come faccio a capire se l'edge e' morto?" scopre un buco: il progetto ha gate di kill
pre-registrati per i CANDIDATI (DVOLSPREAD 24/10, XSR01 23/10, STATARB 27/09) e NESSUNO
per il book che gira con soldi veri. La risposta implicita era "si vedra'", cioe' quello
che il progetto non accetta dai candidati.

LA RISPOSTA SCOMODA: non si puo' sapere in fretta. Sharpe rolling a 12 mesi: il 56% dei
casi "edge morto" e' indistinguibile da uno vivo. Un anno brutto e' rumore, non
informazione.

CRITERIO A (ritorno, book intero): Sharpe rolling 36 mesi sotto -0.5. Tarato sul nullo
(edge intatto) -> falso kill 1.8% in 10 anni; controllo positivo (edge morto) -> lo
riconosce nel 91% dei casi, rilevamento mediano 3.8 anni. La lentezza non e' un difetto
della regola ma statistica: a 12 mesi la stessa regola ucciderebbe un edge VIVO nell'80%
dei casi. Non esiste una versione veloce e onesta.

CRITERIO B (TP01, che e' DIFENSIVO): serve un criterio diverso perche' il LOO del 26/07 ha
misurato il contributo hold-out di TP01 negativo nel 99.1% delle configurazioni d'ancora —
firma dell'assicurazione, che paga premio negli anni senza incendio. "Non ha guadagnato"
NON e' evidenza di morte. Criterio: in un anno con DD buy&hold > 10%, il DD di TP01 deve
restare sotto il 75%. Storico 8 anni di sinistro, 8/8 superati (protezione 1.8x-34.4x).
Negli anni SENZA sinistro il criterio non si valuta: non c'e' informazione. Questo criterio
e' VELOCE dove l'altro e' lento.

COSA SUCCEDE SE SCATTANO (dichiarato ora per non deciderlo nel momento sbagliato):
(A) il book NON si spegne da solo -> revisione con weights_tilt_null + deflated-Sharpe sui
dati nuovi; spegnere e' decisione dell'operatore. (B) fallito in DUE anni di sinistro
consecutivi -> TP01 non assicura piu' e il peso 75% va rimesso in discussione.

CABLATO: scripts/live/edge_watch.py in cron_daily.sh, allerta Telegram, non tocca
l'esecuzione. Stato oggi: Sharpe 36m +1.51, protezione 8/8.

IL LIMITE, DETTO: il criterio A rileva la morte ~4 anni dopo, e non e' riparabile con una
regola migliore (e' il contenuto informativo dei dati). La difesa vera e' che il piano
regge a un edge dimezzato (11.6 -> 15.8 anni, P(20a) ancora 77%) e che i rischi VELOCI
(venue, esecuzione, feed) hanno sorveglianze che scattano in ore.

404 test verdi (+11). Book, pesi, config INVARIATI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-26 21:14:53 +00:00
parent 1296fa2d96
commit a1b4416ac0
5 changed files with 597 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
"""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