Files
PythagorasGoal/scripts/live/edge_watch.py
T
Adriano Dal Pastro a1b4416ac0 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>
2026-07-26 21:14:53 +00:00

149 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
"""edge_watch.py — sorveglianza dell'EDGE del book live. Riporta e allerta; non tocca nulla.
IL BUCO CHE CHIUDE. Il progetto ha gate di kill pre-registrati per i CANDIDATI (DVOLSPREAD 24/10,
XSR01 23/10, STATARB 27/09) e non ne aveva **nessuno per il book che gira con soldi veri**.
DUE CRITERI, PERCHE' I DUE SLEEVE NON SI GIUDICANO ALLO STESSO MODO
(tarati in `scripts/research/r0726_edge_death.py`, 4.000 percorsi bootstrap):
A) RITORNO (book nel suo insieme) — **Sharpe rolling a 36 mesi sotto 0.5**.
Tarato sul nullo: falso kill **1.8% in 10 anni** con edge intatto. Controllo positivo: se
l'edge e' davvero morto lo riconosce nel **91%** dei casi, in **3.8 anni** mediani.
⚠️ La lentezza NON e' un difetto della regola, e' statistica: a 12 mesi la stessa regola
ucciderebbe un edge VIVO nell'80% dei casi. Non esiste una versione veloce e onesta.
B) PROTEZIONE (TP01, che e' DIFENSIVO) — in un anno con **drawdown buy&hold > 10%**, il
drawdown di TP01 deve restare **sotto il 75%** di quello. Storico: 8 anni con sinistro,
**8/8 superati** (protezione da 1.8x a 34.4x).
⚠️ Serve un criterio separato perche' il leave-one-out del 26/07 ha misurato il contributo
hold-out di TP01 **negativo nel 99.1%** delle configurazioni d'ancora: e' la firma
dell'assicurazione, che paga premio negli anni senza incendio. **"Non ha guadagnato" non e'
evidenza di morte per uno sleeve difensivo** — la sua morte e' non proteggere nel sinistro.
Negli anni SENZA sinistro il criterio non si valuta: non c'e' informazione.
Regola di kill dichiarata ORA per non doverla decidere nel momento sbagliato:
* (A) scatta -> il book NON si spegne da solo: si apre una revisione con `weights_tilt_null`
e si ricalcola il deflated-Sharpe sui dati nuovi. Spegnere e' una decisione dell'operatore.
* (B) fallito in **due anni di sinistro consecutivi** -> TP01 non assicura piu', e il suo peso
va rimesso in discussione (e' l'unico motivo per cui e' nel book al 75%).
uv run python scripts/live/edge_watch.py # report
uv run python scripts/live/edge_watch.py --quiet # solo se qualcosa scatta (cron)
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live.notifier import notify # noqa: E402
from src.portfolio.portfolio import combine_outer # noqa: E402
from src.portfolio.sleeves import deribit_book_sleeves, tp01_sleeve # noqa: E402
# --- soglie CONGELATE (cambiarle invalida la taratura di r0726_edge_death.py) ---
WIN_MONTHS = 36
SHARPE_KILL = -0.5
SINISTRO_DD = 0.10 # un anno "conta" come sinistro se il buy&hold perde piu' di questo
PROTECT_MAX = 0.75 # DD di TP01 ammesso, in frazione del DD buy&hold
def book_daily() -> pd.Series:
sl = deribit_book_sleeves()
return combine_outer({s.name: s.daily() for s in sl}, {s.name: s.weight for s in sl})
def buyhold_daily() -> pd.Series:
from src.data.downloader import load_data
from src.strategies.trend_portfolio import resample_1d, simple_returns
cols = {}
for a in ("BTC", "ETH"):
df = resample_1d(load_data(a, "1h"))
cols[a] = pd.Series(simple_returns(df["close"].values.astype(float)),
index=pd.to_datetime(df["datetime"]))
J = pd.concat(cols, axis=1, join="inner").fillna(0.0)
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
def maxdd(s: pd.Series) -> float:
e = (1.0 + s).cumprod()
return float((1.0 - e / e.cummax()).max())
def trailing_sharpe(s: pd.Series, months: int = WIN_MONTHS) -> float | None:
"""Sharpe annualizzato sulla finestra piu' recente. None se la storia non basta —
e 'non basta' NON e' 'va bene': si riporta a parte."""
w = int(months * 30.44)
v = s.dropna().values[-w:]
if len(v) < w or v.std() <= 0:
return None
return float(v.mean() / v.std() * np.sqrt(365))
def protection_by_year(tp: pd.Series, bh: pd.Series) -> list[dict]:
"""Per ogni anno con SINISTRO, il rapporto di protezione. Gli anni senza sinistro sono
esclusi: non contengono informazione sull'assicurazione."""
common = tp.index.intersection(bh.index)
tp, bh = tp.loc[common], bh.loc[common]
out = []
for y, g in bh.groupby(bh.index.year):
d_bh = maxdd(g)
if d_bh <= SINISTRO_DD:
continue
d_tp = maxdd(tp.loc[g.index])
out.append(dict(year=int(y), dd_bh=d_bh, dd_tp=d_tp,
ratio=d_tp / d_bh, ok=(d_tp / d_bh) <= PROTECT_MAX))
return out
def run() -> dict:
book = book_daily()
sh = trailing_sharpe(book)
prot = protection_by_year(tp01_sleeve(weight=1.0).daily(), buyhold_daily())
# due anni di SINISTRO CONSECUTIVI falliti (consecutivi fra gli anni con sinistro)
consec = 0
for p in prot:
consec = consec + 1 if not p["ok"] else 0
alerts = []
if sh is not None and sh < SHARPE_KILL:
alerts.append(f"criterio A: Sharpe {WIN_MONTHS}m = {sh:+.2f} < {SHARPE_KILL:+.1f}")
if consec >= 2:
alerts.append(f"criterio B: TP01 non ha protetto in {consec} anni di sinistro consecutivi")
return dict(sharpe=sh, protection=prot, consec_fail=consec, alerts=alerts)
def main() -> int:
r = run()
if "--quiet" not in sys.argv:
print("=" * 78)
print(" EDGE WATCH — il book live sta ancora funzionando?")
print("=" * 78)
s = r["sharpe"]
print(f"\n A) RITORNO — Sharpe rolling {WIN_MONTHS}m: "
f"{f'{s:+.2f}' if s is not None else 'storia insufficiente'}"
f" (kill sotto {SHARPE_KILL:+.1f}; falso kill 1.8%/10a, rilevamento ~3.8a)")
print(f"\n B) PROTEZIONE — TP01 negli anni con sinistro (DD buy&hold > {SINISTRO_DD:.0%}):")
print(f"\n {'anno':>6}{'DD b&h':>10}{'DD TP01':>10}{'protez.':>10}{'esito':>8}")
for p in r["protection"]:
print(f" {p['year']:>6}{p['dd_bh']:>9.1%}{p['dd_tp']:>9.1%}"
f"{1/p['ratio']:>9.1f}x{' OK' if p['ok'] else ' FAIL':>8}")
n_ok = sum(p["ok"] for p in r["protection"])
print(f"\n {n_ok}/{len(r['protection'])} anni di sinistro superati · "
f"fallimenti consecutivi in corso: {r['consec_fail']}")
if r["alerts"]:
notify("🚨 EDGE WATCH — criterio di kill scattato sul book LIVE",
{f"alert {i+1}": a for i, a in enumerate(r["alerts"])})
for a in r["alerts"]:
print(f" ALERT: {a}")
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())