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:
@@ -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
|
||||
Reference in New Issue
Block a user