de83909db9
MISURATO oggi durante la revisione: `trades_db.py --help` non stampava l'uso, cadeva in sync() e riscriveva meta.ultimo_sync. Nessuno dei 18 script di scripts/live/ usava argparse: un flag sbagliato era il ramo else. Su journal.py avrebbe scritto pagina e riga di DB, su analista.py avrebbe speso una chiamata al modello e mandato un Telegram. - src/live/cli.valida: prima istruzione di ogni __main__, prima di connect()/sync/rete. --help -> 0 con l'uso; flag ignoto, valore mancante o posizionale -> 2 con l'elenco dei previsti (P4). NIENTE argparse: cambierebbe messaggi, codici d'uscita e --help di script che il cron gia' chiama. - 18 script cablati (i 3 che scrivono + 14 + cc01), flag invariati. - tests/test_cli_flag.py (30): elenco DERIVATO dalla cartella (P1), valida come prima istruzione, uso che documenta i flag, e i flag che il CRON usa davvero restano accettati (P15/P16); end-to-end su --help e flag ignoto con trades.db non toccato (M15). Verificato a mano: monitor_health --quiet, trades_db --sync --quiet, book_execute dry-run. Debito §5.15, primo passo: balance_watch (orario) registra `usde_usdc` a ogni campione — None con la ragione se illeggibile, mai 1,0. Il cablaggio nel bound quando la serie ha storia. Pulizia dalla revisione: tests/helpers.carica_script al posto della 15a copia del loader importlib (5 file del libro live); il fill di prova via upsert_fills invece di un INSERT che lasciava verified NULL; asserzioni non ancorate al padding; movimenti_capitale accetta le righe gia' lette (una SELECT invece di due ai due lati di una scrittura del cron). Test 910 verdi (+52). Diario 2026-09-02c. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XDJsH3iDSaBns3ccpPBwiu
159 lines
7.0 KiB
Python
159 lines
7.0 KiB
Python
#!/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
|
||
|
||
|
||
USO = """uso: edge_watch.py [--quiet]
|
||
|
||
edge_watch.py — sorveglianza dell'EDGE del book live. Riporta e allerta; non tocca nulla.
|
||
uv run python scripts/live/edge_watch.py # report
|
||
uv run python scripts/live/edge_watch.py --quiet # solo se qualcosa scatta (cron)
|
||
|
||
Dettaglio nel docstring in testa al file."""
|
||
|
||
if __name__ == "__main__":
|
||
from src.live.cli import valida
|
||
valida("edge_watch.py", USO, flag=("--quiet",), con_valore=())
|
||
raise SystemExit(main())
|