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
10 KiB
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.