Files
PythagorasGoal/docs/diary/2026-08-25-manutenzione-deribit-e-resilienza-venue.md
T
Adriano Dal Pastro 426735448e live: diagnosi del guasto venue, disaster-SL scoperto, isolamento per asset, cron :47
Nato da "stato trades" durante una manutenzione Deribit (system_maintenance 11051,
08:57-09:20 UTC). Contati i log invece che ricordarli: 1.499 giri dal 23/06, 3 con
manutenzione, 5 traceback duri — e 4 dei 5 sono il NOSTRO gateway, non il venue.
Le release Deribit escono il martedi' alle 09:00 UTC: il cron al :07 ci cadeva dentro
per costruzione (21/07, 11/08, 18/08, 25/08).

DIAGNOSI (src/live/venue_probe.py, nuovo)
Sonda l'API PUBBLICA Deribit senza gateway e senza credenziali, e classifica il guasto:
VENUE_MANUTENZIONE / VENUE_GIU / GATEWAY / IGNOTO. Prima ogni causa stampava la stessa
riga ("conto non leggibile (offline)") con nota di diagnosi CABLATA — P4 violata, e il
18/08 il codice 11051 era gia' dentro il processo senza arrivare a chi decideva la
gravita'. Parte solo dopo un guasto: sul percorso sano costa zero. P1 rispettata: la
firma 11051 si importa da venue_watch.is_maintenance, non si ridichiara.

P9: la manutenzione dentro lo slot declassa il titolo a info, ma solo su EVIDENZA della
sonda (mai sull'orologio) e solo dentro la durata annunciata — oltre, RIALZA. Un gateway
rotto di martedi' mattina resta al massimo.

DISASTER-SL SCOPERTO (src/live/execution.py)
ensure_disaster_sl cancella i bracket incoerenti PRIMA di ripiazzarne uno: fra le due
chiamate la posizione e' senza stop. Se il ripiazzamento sollevava, l'eccezione risaliva
a main() e il guasto peggiore aveva la faccia di un errore qualunque. Ora: due tentativi,
poi stato `naked`, distinto da `place-failed` (P5). Non e' "non sono riuscito a
proteggere": e' "ho tolto la protezione e non sono riuscito a rimetterla" -> allarme
massimo, mai declassato. La SEQUENZA non e' stata invertita: piazza-poi-cancella sembra
piu' sicuro ma "sembra" non basta con soldi veri senza misurarlo.

ISOLAMENTO PER ASSET (scripts/live/book_execute.py)
Il 21/07 un 502 dentro ensure_disaster_sl su BTC ha ucciso il giro intero: nel log ETH
non compare — ne' ribilanciato ne' verificato nella protezione. Ora un asset che esplode
non ferma il ciclo, e il giro degradato esce con codice 2.

CRON :07 -> :47
Il vincolo vero non era ":07" ma "fuori dai ~26s del minuto tondo" (rate-limit per-IP
auto-saturato dal collettore catena, misura del 30/07). Il :47 lo soddisfa e in piu' sta
fuori dallo slot di release. PREVISIONE DICHIARATA (M12): 3 delle 4 finestre osservate
sono rientrate entro l'ora -> il :47 ne avrebbe scavalcate 3 su 4; se martedi' prossimo
becca comunque la manutenzione, la previsione e' sbagliata.

WATERMARK AVVELENATO DAI TEST (tests/conftest.py, nuovo)
Trovato addosso: lanciando la suite, data/live/equity_seen.json passava a $5.000 e il giro
successivo mandava un allarme Telegram FALSO ("USCITA DI FONDI -86,6%"). Non cosmetico:
cap_fallback = min(cap_config, watermark x frac) sarebbe passato da $334 a $2.500/asset,
~7,5x di leva su un conto da $668, sul ramo eq_fallback che allerta e NON blocca — cioe'
i test potevano armare il pericolo che il watermark esiste per impedire. Riparato con una
fixture AUTOUSE, non per-test: chiedere a ogni autore di ricordarsene ha gia' perso il
26/07 e il 21/08. Resta esposto lo stesso errore su trades.db e book_executions.jsonl.

BLOCCATO, NON RINVIATO
Il fallback diretto ai privati Deribit richiede chiavi API create dall'operatore: in
locale esiste solo CERBERO_TOKEN. Il gateway resta un punto singolo di guasto non
aggirabile (CLAUDE.md 5.11). La sonda dice di chi e' il guasto, non lo aggira.

Test: 20 nuovi, nessuno tocca la rete; controllo positivo fatto (5 su 7 falliscono contro
il codice vecchio). Suite 730 passati, 1 fallito — quello gia' noto di 5.9 (deriva dati).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B9UyJLHzR7EJzxR3iQ3RN1
2026-08-25 10:27:36 +00:00

10 KiB
Raw Blame History

2026-08-25 — La manutenzione del martedì, e i due difetti che ha scoperchiato

Scritto da Claude (agente), su richiesta dell'operatore. Firmato come vuole P13: l'analisi in prosa è opinione di un lettore fallibile, i numeri qui sotto vengono dai log e sono riproducibili.

Come è cominciata

Domanda dell'operatore: «stato trades». Report normale del libro di bordo — 24 fill, equity $598,06 → $667,88 (+11,67%), 16 round-trip di cui 15 in utile, netto +39,78. Poi il --reconcile ha stampato la riga che ha aperto la giornata:

venue: NON LETTO (HTTPError) — non e' 'zero trade', e' 'non misurato'

Non era il gateway. Era Deribit in manutenzione: system_maintenance, codice 11051, HTTP 503 sia sul pubblico che sul privato. Iniziata fra le 08:57:40Z (ultimo 200 OK nei log di cerbero-mcp) e le 09:01:46Z (primo 503). Rientrata verso le 09:20 — ~20 minuti, coerenti con i 15-30 annunciati da Deribit per le sue release.

Cosa dicono i log, contati invece che ricordati

1.499 giri di cron_book fra il 2026-06-23 e il 2026-08-25:

esito n %
manutenzione Deribit 3 0,20%
traceback duro 5 0,33%
giri senza esito utile 26 1,7%

E gli episodi di venue non sono sparsi:

2026-07-21T09:00  Tue  502 su get_positions (Deribit giù, vista dal gateway)
2026-08-11T09:07  Tue  system_maintenance 11051
2026-08-18T09:07  Tue  system_maintenance 11051   (giù anche alle 10:07)
2026-08-25T09:07  Tue  system_maintenance 11051

Quattro martedì su dieci, tutti fra le 09:00 e le 09:07 UTC. Il supporto Deribit conferma la meccanica: le release escono il martedì alle 09:00 UTC. Il minuto :07 del cron cadeva dentro quella finestra — e ci cadeva per costruzione, non per sfortuna.

Il punto singolo di guasto non è Deribit: siamo noi. Dei 5 traceback, 4 sono il nostro gateway cerbero-mcp.tielogic.xyz (un 404 su get_positions, un 502, due ReadTimeout).

I due difetti veri

1. Una riga sola per due guasti che vogliono azioni opposte

Fino a oggi qualunque guasto sul percorso Deribit stampava conto non leggibile (offline), con la nota di diagnosi cablata. Ma "Deribit in manutenzione" (aspetta, rientra da sola) e "il nostro gateway è rotto" (ripara) non sono la stessa notizia. È P4 violata, con l'aggravante di P4 seconda metà: una nota di diagnosi cablata è peggio di nessuna nota, perché si legge come una misura.

Peggio ancora: il 18/08 il codice 11051 era già dentro il processo, nello stesso minuto, raccolto da livefeed. Semplicemente non arrivava a chi decideva la gravità dell'allarme.

2. La finestra scoperta del disaster-SL — e l'asset che sparisce

ensure_disaster_sl ricostruisce un bracket incoerente cancellando prima e ripiazzando dopo. Fra le due chiamate la posizione è senza alcuno stop on-book. Finché il ripiazzamento sollevava, quell'eccezione risaliva fino a main(): il guasto peggiore (posizione scoperta) aveva la stessa faccia di un errore qualunque.

E c'è il corollario che è successo davvero. Il 2026-07-21 alle 09:00 UTC il 502 è arrivato dentro ensure_disaster_sl su BTC. Nel log di quel giro ETH non compare: non è stato ribilanciato e — quel che conta — la sua protezione non è stata verificata. Un guasto su un asset toglieva la rete di sicurezza all'altro.

Cosa è stato fatto

src/live/venue_probe.py (nuovo). Interroga l'API pubblica Deribit in diretta — niente gateway, niente credenziali — e classifica: VENUE_MANUTENZIONE / VENUE_GIU / GATEWAY / IGNOTO. La sonda parte solo dopo un guasto: sul percorso sano costa zero. Rispetta P1: non ridichiara la firma 11051, la importa da venue_watch.is_maintenance.

Su P9 (un allarme massimo speso per un evento atteso è un allarme che non verrà letto il giorno che è vero): la manutenzione dentro lo slot declassa il titolo a . Ma con due paletti, perché il rischio qui è costruire il silenzio proprio nell'ora in cui serve:

  • declassa solo su evidenza della sonda, mai sull'orologio da solo → un gateway rotto di martedì mattina resta 🛑;
  • declassa solo dentro la durata annunciata (30 min) → il 18/08 alle 10:07 la manutenzione aveva sforato, e torna una notizia. Stessa logica di MAINT_GRACE_HOURS, altra domanda.

Isolamento per asset in book_execute: un asset che esplode non ferma il ciclo, e il giro esce con codice 2 per essere contabile.

Stato naked in ensure_disaster_sl: due tentativi di ripiazzamento, e se falliscono entrambi lo stato è distinto da place-failed (P5: guasti diversi si distinguono anche quando l'azione è la stessa). Non è "non sono riuscito a proteggere": è "ho tolto la protezione e non sono riuscito a rimetterla" → 🚨 sempre, mai declassato da P9.

Non è stata invertita la sequenza in piazza-poi-cancella: due STOP reduce_only contemporanei sono probabilmente innocui, ma "probabilmente" non basta per cambiare il ciclo di vita dei bracket su un percorso con soldi veri senza misurarlo. Riparato il silenzio, non toccata la sequenza.

Cron :07:47. I due vincoli sono entrambi misurati e compatibili: fuori dai ~26s del minuto tondo (il collettore catena si auto-satura il rate-limit per-IP: 12.186 risposte 429 in 26 ore, 96% nel minuto :00, misura del 30/07) e fuori dallo slot di release. Il commento in cron_book.sh è stato riscritto: lasciarlo dire :07 sarebbe stato il difetto §5.7 in versione nuova.

Previsione dichiarata (M12). Sulle 4 finestre osservate, 3 sono rientrate entro l'ora. Il :47 ne avrebbe scavalcate 3 su 4. Se martedì prossimo il :47 becca comunque la manutenzione, la previsione è sbagliata e lo slot non è quello che credo.

Cosa NON è stato fatto, e perché

Il fallback diretto ai privati Deribit è bloccato, non rinviato. Le credenziali Deribit esistono solo dentro il gateway: in locale c'è CERBERO_TOKEN e basta. Leggere conto e posizioni scavalcando cerbero-mcp richiede chiavi API create dall'operatore sul conto Deribit. È una decisione con una superficie di rischio propria (una chiave in più che può trapelare), non un refactor.

È stato fatto il pezzo che non le richiede — la sonda pubblica — e resta a debito in §5.11 il resto. Nota onesta: la sonda pubblica dice di chi è il guasto, non lo aggira. Con il gateway giù il libro continua ad astenersi; sa solo dire perché.

Test

19 nuovi (12 in test_venue_probe.py, 7 in test_book_resilienza_venue.py), nessuno tocca la rete. Suite: 730 passati, 1 fallito — il fallito è quello già noto di §5.9 (SKH01 canonical 1,9223 contro banda cablata <1,9, deriva dei dati, non del codice).

Controllo positivo fatto, perché un test mai visto fallire non dimostra niente: contro il codice vecchio 5 dei 7 test di resilienza falliscono. Con una riserva da dichiarare — il test di isolamento, sul codice vecchio, fallisce perché il modulo di diagnosi non esiste, non perché dimostri l'isolamento rotto. La prova di quel difetto è il log del 21/07, dove ETH non compare.


Coda: la suite di test ha mandato un allarme falso sul telefono dell'operatore

Il primo giro al nuovo minuto :47 è andato bene — entrambi gli asset elaborati, entrambi i disaster-SL verificati ok, exit 0 — ma nel log c'era una riga che non poteva essere vera:

💰 USCITA DI FONDI: $5,000.00 -> $667.68 (-86.6%) · cap/asset ora $333.84

Il conto non ha mai visto $5.000. Ha visto $598-668 da sempre.

L'ho causato io, lanciando uv run pytest per verificare le riparazioni di oggi. test_book_live.py::test_la_formula_del_report_ha_potenza_anche_a_libro_flat prende solo monkeypatch (niente tmp_path), sostituisce shadow_report con uno che dichiara real_equity=5000.0, e chiama book.book_report() — che come effetto collaterale scrive data/live/equity_seen.json. Riprodotto isolando il singolo test: watermark $667,68 → $5.000.

L'helper _write_cfg esisteva già proprio per questo — il difetto gemello è del 2026-07-26 — ma quel test, aggiunto il 21/08, non lo usa. È la terza volta che la stessa scommessa perde.

Non era cosmetico

Il watermark alimenta il cap di fallback:

cap_fallback = min(cap_fisso_di_config, watermark × frac)

Con $5.000 dentro: min($3.000, $2.500) = $2.500 per asset invece di $334. Su un conto da $668 sono fino a $5.000 di nozionale lordo, ~7,5x di leva. E il ramo che ci arriva è raggiungibile: eq_fallback in book_execute allerta e NON blocca, per scelta dichiarata.

Cioè: lanciare la suite di test poteva armare esattamente il pericolo che il watermark esiste per impedire (nota del 26/07 in book.py sul cap fisso e la leva 3,35x).

Danno reale oggi: nessuno. Alle 09:47 l'equity era leggibile, quindi il cap è stato calcolato sull'equity vera e il watermark è stato riscritto col valore giusto. La finestra scoperta è stata ~09:25 → 09:47, e in quella finestra non c'è stato nessun giro con equity illeggibile. È andata bene per la direzione del caso, non perché ci fosse una protezione.

Riparazione, e perché è strutturale

tests/conftest.py con una fixture autouse che devia EQUITY_WATERMARK in tmp_path per ogni test. Chiedere a ogni autore di ricordarsi il monkeypatch è la scommessa che ha già perso due volte: la protezione deve valere anche per il test che qualcuno scriverà domani senza aver letto niente. Un test che vuole davvero pilotare il watermark continua a funzionare — il suo monkeypatch esplicito gira dopo e vince.

Più una guardia in test_cap_watermark.py che si accende se qualcuno rimuove la fixture: controllo positivo, perché una protezione mai vista fallire non è una protezione.

Resta aperto il principio più largo (§5.12): un test non dovrebbe poter scrivere in data/live/ affatto. Oggi è deviato solo il watermark — trades.db e book_executions.jsonl sono ancora esposti allo stesso errore.

La lezione

Un effetto collaterale su file trasforma un test puro in un attore sul sistema vivo. Qui il test non menzionava il watermark, non lo importava, non lo asseriva: lo scriveva passando per una funzione di produzione tre livelli più in basso. Il perimetro di un test non è quello che il test dice di toccare: è quello che tocca il codice che chiama.