Files
Adriano Dal Pastro de83909db9 un flag sconosciuto non e' l'azione di default: guardia su 18 script, indice USDE orario, pulizia
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
2026-09-02 15:49:40 +00:00

159 lines
7.0 KiB
Python
Raw Permalink 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
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())