analista: manda l'analisi giornaliera su Telegram, con l'esito registrato

La notifica porta una testata coi numeri che si leggono senza aprire nulla
(equity, delta del giorno, cumulato, posizioni, leva, fill e round-trip) e
sotto l'analisi del modello.

Il trasporto Telegram e' pero' il punto singolo di guasto gia' misurato in
questo progetto: un tentativo, nessun retry, esito mai registrato, 6,9% di
invii persi (2/29). Su un messaggio al giorno sono ~25 messaggi persi
all'anno, quindi qui sono state fatte due delle tre riparazioni dichiarate
il 2026-08-23 e mai eseguite:

(a) `send(text, tentativi=1)` — retry OPZIONALE con backoff. Il default resta
    1, cosi' il comportamento e' invariato per tutti i chiamanti esistenti:
    alzarlo per tutti cambierebbe la latenza degli allarmi di venue_watch e
    book_execute su un percorso con soldi veri, e non e' una modifica da fare
    di straforo dentro un'altra funzionalita'. L'analista chiede 3.
(b) `ultimo_errore()` — il motivo si registra nel punto in cui l'eccezione
    veniva ingoiata, e NON sopravvive a un invio riuscito. Regola gia'
    codificata il 29/07 su un altro percorso e mai applicata al notifier.

La terza (marcare `alerted=True` solo a invio riuscito in venue_watch) cambia
il comportamento degli allarmi e resta una decisione dell'operatore.

L'esito finisce nel DB in tre stati: inviata / non configurato / FALLITA col
motivo. Un invio perso che non lascia traccia, il giorno dopo, non si
distingue da "non e' successo niente".

E se l'analisi manca, il messaggio parte lo stesso dicendo PERCHE': senza
quel ramo un guasto del modello si leggerebbe come una giornata senza nulla
da dire. Taglio a 4096 caratteri dichiarato, mai silenzioso; HTML del modello
neutralizzato.

708 test passano. Strategia, pesi, config INVARIATI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-08-23 19:40:56 +00:00
parent 7456be1ce5
commit 782c0ce4bc
6 changed files with 277 additions and 22 deletions
+38 -8
View File
@@ -8,6 +8,7 @@ Test della config: uv run python -m src.live.notifier "messaggio di prova"
from __future__ import annotations
import os
import time
import urllib.parse
import urllib.request
from pathlib import Path
@@ -33,17 +34,46 @@ def _cfg() -> tuple[str, str]:
return tok, chat
def send(text: str) -> bool:
# Ultimo esito d'invio, per chi vuole SAPERE se il messaggio e' partito. Fino al 2026-08-23
# `send()` faceva un tentativo e ingoiava l'eccezione senza scriverla da nessuna parte: quando
# serviva sapere se un allarme era arrivato, l'informazione non esisteva (tasso misurato di
# invii falliti: 6,9%, 2 su 29). Regola gia' codificata il 29/07 su un altro percorso e mai
# applicata qui: *se un errore si ingoia per non bloccare, si registra nel punto in cui lo si ingoia.*
_ULTIMO_ERRORE: str | None = None
def ultimo_errore() -> str | None:
"""Motivo dell'ultimo invio fallito, o None se l'ultimo e' riuscito. Non sopravvive a un
invio riuscito: una causa vecchia accanto a un invio nuovo manda sulla pista sbagliata."""
return _ULTIMO_ERRORE
def send(text: str, tentativi: int = 1, pausa_s: float = 2.0) -> bool:
"""`tentativi` DEFAULT 1 = comportamento invariato per tutti i chiamanti esistenti.
Chi vuole il retry lo chiede: alzarlo per tutti cambierebbe la latenza degli allarmi di
`venue_watch` e `book_execute` su un percorso con soldi veri, e non e' una modifica da fare
di straforo dentro un'altra funzionalita'.
"""
global _ULTIMO_ERRORE
tok, chat = _cfg()
if not tok or not chat:
_ULTIMO_ERRORE = "config assente (TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID)"
return False
try:
url = f"https://api.telegram.org/bot{tok}/sendMessage"
data = urllib.parse.urlencode({"chat_id": chat, "text": text, "parse_mode": "HTML"}).encode()
urllib.request.urlopen(url, data, timeout=10)
return True
except Exception:
return False
url = f"https://api.telegram.org/bot{tok}/sendMessage"
data = urllib.parse.urlencode({"chat_id": chat, "text": text, "parse_mode": "HTML"}).encode()
ultimo = ""
for i in range(max(1, tentativi)):
try:
urllib.request.urlopen(url, data, timeout=10)
_ULTIMO_ERRORE = None
return True
except Exception as e: # noqa: BLE001 — si registra, non si nasconde
ultimo = f"{type(e).__name__}: {str(e)[:120]}"
if i + 1 < max(1, tentativi):
time.sleep(pausa_s * (i + 1)) # backoff lineare
_ULTIMO_ERRORE = f"{max(1, tentativi)} tentativi falliti — {ultimo}"
return False
def notify(title: str, data: dict | None = None) -> bool: