Files
PythagorasGoal/docs/diary/2026-07-29-feed-skh-causa.md
T
Adriano Dal Pastro 7d64dd4c2b live(feed): l'allerta funzionava, la sua CAUSA era una riga cablata
Il 29/07 il feed 5m di SKH01 e' ricaduto sul certificato in 6 giri orari su 8
(eta' 265->685 min, +60 a ogni giro = firma esatta del fallback): latenza
d'uscita da ~1h a ~11h, book flat, nessuna posizione esposta. L'allerta del
26/07 ha segnalato 6/6, poi ha stampato un perche' che non aveva misurato —
la nota "fetch pubblico KO" era cablata, identica in ogni caso, compreso
quello in cui la coda fresca E' attaccata e il vecchio e' il certificato.

E la causa vera non era recuperabile per costruzione: _fetch_recent_5m ingoia
l'eccezione di pagina con un break e a prima pagina fallita ritorna un frame
vuoto, indistinguibile da "il venue non ha barre".

Cablato: livefeed.last_fetch_error() (registra E logga nel punto in cui
l'errore viene ingoiato) -> book_report.skh_feed_errors -> allerta con la
causa. Stesso buco chiuso sul ramo gemello "conto offline", che la ragione
l'aveva gia' in mark_src e non la stampava mai.

La causa del 29/07 resta IGNOTA e va citata cosi': una prima stesura la
attribuiva a un rate limit per-IP come se fosse un fatto -> rimossa, sarebbe
stato lo stesso difetto che stavo correggendo scritto meglio. Cron spostato
al minuto :07 come ripiego da UNA osservazione, dichiarato tale.

Test 11 -> 16 (incluso il caso a meta' paginazione: coda parziale attaccata,
mancano le barre PIU' recenti). Book/pesi/config/strategia INVARIATI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 12:20:42 +00:00

8.1 KiB

2026-07-29 — L'allerta funzionava, la sua CAUSA era una riga cablata

Fatto operativo del giorno. Il 29/07 il feed 5m che alimenta il segnale SKH01 e' ricaduto sul feed certificato in 6 giri orari su 8 fra le 05:00 e le 12:00 UTC. L'allerta cablata il 26/07 ha segnalato ogni volta — ha fatto esattamente il suo lavoro. Poi ha stampato una causa che non aveva misurato.

Codice: src/live/livefeed.py (last_fetch_error), src/live/book.py (skh_feed_errors nel report), scripts/live/book_execute.py (allerta con la causa), scripts/cron_book.sh (nota sul minuto). Test: tests/test_skh_feed_freshness.py (11 → 16). Book, pesi, config, strategia: INVARIATI. Nessuna posizione aperta durante l'incidente (book flat: TP01 risk-off, SKH01 flat).


1. Cosa e' successo, misurato

Dal log logs/cron_book.log (la VPS si e' riavviata alle 04:11 UTC, kernel 6.8.0-134 → 6.8.0-136; il giro delle 04:00 non e' partito perche' la macchina era gia' giu' dalle 03:55):

giro (UTC)   eta' ultima barra 5m     durata del giro
05:00              265 min                  46 s
06:00              325                      24
07:00              385                      29
08:00              445                      26
09:00              505                      34
10:00              565                      25
11:00                0 (fresco)             16      <- ma qui e' fallita la lettura del CONTO
12:00              685                      25
12:07                0 (fresco)             10

L'eta' cresce di 60 minuti a ogni giro: e' la firma esatta del fallback, cioe' l'ultima barra resta quella del feed certificato (rebuild giornaliero delle 00:30) mentre l'orologio avanza. Al picco la latenza d'uscita di SKH01 e' passata da ~1 ora a ~11 ore.

2. Cosa ha funzionato e cosa no

Ha funzionato: l'instrumentazione del 26/07. feed_age_minutesskh_feed_age_min → allerta Telegram, 6 volte su 6, con la scelta dichiarata allora (allerta, non blocca — bloccare fermerebbe anche il ribilancio di TP01, nettato sullo stesso strumento, per un guasto di rete).

Non ha funzionato: la riga che diceva perche'. Era questa, cablata:

"nota": "fresh_5m e' ricaduto sul feed certificato (fetch pubblico KO)"

Non veniva da nessuna misura: veniva stampata identica in ogni caso, compreso quello in cui la coda fresca era attaccata e il vecchio era il certificato stesso. E' una presunzione con l'aspetto di un dato — la forma di difetto che questo progetto ha gia' pagato tre volte in docstring di produzione (26/07, tre correzioni in due giorni).

Peggio: la causa vera non era recuperabile a posteriori, per costruzione. In _fetch_recent_5m l'eccezione di pagina viene ingoiata con un break e la funzione ritorna quel che ha raccolto; se a fallire e' la prima pagina il risultato e' un DataFrame vuoto, che a valle e' indistinguibile da "il venue non ha barre". Quando il guasto e' rientrato, non resta niente da interrogare — e infatti alle 12:14, provando a mano, fresh_5m rispondeva in 1.7 s senza errori.

3. La correzione: registrare la causa, non indovinarla

livefeed._LAST_ERROR + last_fetch_error(). Il fallback resta silenzioso come comportamento (mai operare a cieco > mai operare, scelta del 26/07 confermata): quello che smette di essere silenzioso e' il perche'. La causa risale fino al report (skh_feed_errors, per asset) e fino all'allerta, che ora dice cosa e' successo invece di ipotizzarlo.

Tre guasti che prima erano la stessa riga, e ora sono tre righe diverse:

caso cosa si vede oggi
eccezione al fetch (rete, rate limit, venue) RuntimeError: 429 ... (pagina 1, BTC/USD:BTC)
risposta senza barre, nessuna eccezione nessuna barra restituita da BTC/USD:BTC (risposta vuota...)
coda attaccata, ma il certificato e' vecchio coda fresca attaccata: il feed certificato stesso e' vecchio

La terza riga e' quella che la nota cablata negava: e' il caso in cui il colpevole non e' la rete ma il rebuild giornaliero, cioe' il feed-freeze del 14/07 in un'altra veste.

⚠️ Il caso intermedio, che un test pigro salta: l'errore a meta' paginazione. La coda parziale viene attaccata comunque — quindi il feed non e' "caduto", e' solo piu' vecchio del dovuto — e siccome la paginazione va in avanti, cio' che manca sono le barre piu' recenti. E' proprio il caso in cui l'eta' da sola direbbe poco. Blindato in test_causa_registrata_anche_con_coda_TRONCATA.

Blindato anche che la causa non sopravviva a una chiamata riuscita: un errore appiccicato farebbe allertare su un guasto gia' rientrato, e un'allerta che grida senza motivo e' un'allerta che si smette di leggere.

4. Sull'onestà della diagnosi: la causa del 29/07 resta IGNOTA

Ed e' importante scriverlo, perche' la tentazione era chiudere il cerchio con una spiegazione plausibile. Cio' che si sa e' solo circostanziale:

  • i giri falliti durano quanto quelli riusciti (16-46 s in entrambi gli stati) → errore immediato, non timeout;
  • la finestra si apre subito dopo il riavvio delle 04:11, ma non si chiude con esso (alle 11:00 funziona, alle 12:00 no);
  • lo stesso giorno il percorso del conto — rete diversa, via cerbero-mcp, stesso venue a valle — rispondeva ReadTimeout(15s) / 404 / 502;
  • i due percorsi falliscono in alternanza, non insieme (11:00 feed ok + conto ko; 12:00 conto ok
    • feed ko).

Ipotesi compatibili e non distinguibili a posteriori: rate limit per-IP del venue, contesa di rete/CPU sulla VPS al minuto tondo, degrado post-riavvio. Una prima stesura di questa modifica scriveva nei commenti "la causa vera era il rate limit Deribit per-IP saturato da un altro progetto sulla stessa VPS" come se fosse un fatto: rimossa. Sarebbe stato lo stesso difetto della riga che stavo correggendo, scritto meglio.

5. Il ripiego sul cron, dichiarato come tale

Il job e' stato spostato da 0 * * * * a 7 * * * * durante l'incidente (ultimo giro al minuto tondo: 12:00; primo al :07: 12:07, riuscito). Motivo: l'ipotesi della contesa al minuto tondo, che e' quando parte tutto il resto della macchina.

⚠️ Non e' un fix verificato: e' un ripiego da UNA osservazione, preso perche' costa zero. Un giro riuscito al :07 contro sei falliti al :00 non e' un esperimento — e' un punto. Se il feed torna stantio anche al :07, l'ipotesi e' morta; e in entrambi i casi la risposta la dara' skh_feed_errors alla prossima occorrenza, non un altro ragionamento. La nota sta in testa a scripts/cron_book.sh, perche' la riga di crontab vive fuori dal repo e una mitigazione invisibile e' una mitigazione che verra' rimossa per sbaglio.

6. Sottoprodotto: anche "conto offline" diceva solo che era offline

Stesso buco, altro ramo. Quando online e' falso la ragione c'e' gia' — sta in mark_src ("fallback close (<Eccezione>)") — ma non veniva mai stampata: l'allerta diceva "conto offline, salto l'esecuzione", che e' vero e inutile. Ora l'allerta porta mark_src per asset. Il comportamento (fermarsi, non operare a cieco) e' invariato.


Regole

  1. Una nota di diagnosi cablata e' peggio di nessuna nota. Nessuna nota manda a guardare i dati; una nota sbagliata manda a guardare la pista sbagliata, e sembra una misura.
  2. Se un errore viene ingoiato per non bloccare, va registrato nello stesso punto in cui lo si ingoia. Fra il break e la fine dell'incidente non c'e' un secondo momento buono: quando la diagnosi diventa urgente, il guasto e' gia' rientrato.
  3. Un'allerta risponde a due domande diverse — cosa e perche'. La prima decide (si opera o no), la seconda ripara. Misurare solo la prima costa un'intera occorrenza del guasto.
  4. Una mitigazione basata su un'osservazione sola si applica pure, ma si scrive che lo e' — altrimenti fra tre mesi e' una scelta di progetto di cui nessuno ricorda la fragilita'.
  5. Distinguere i guasti anche quando l'azione e' la stessa. "Rete ko", "venue senza barre" e "certificato vecchio" portano tutti a "non mi fido del segnale", ma a tre riparazioni diverse.