Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6.3 KiB
2026-06-12 — Reconcile resting + guard feed↔book + epoca report (v1.1.27)
Contesto: la notte ha validato (di nuovo) il tema osservabilità
Due eventi nelle 24h hanno motivato la tornata:
- MR02_BTC (06:00): il TP resting LIMIT ha fillato sul book reale a 60481
nella notte (+6.55 USD reali) mentre il feed sim non è mai sceso sotto 63285
(−443 bps di divergenza, sim chiuso a time_limit con −1.25). Il disaster-SL è
risultato
order_not_foundalla cancel (probabile auto-cancel a posizione flat). Caso SPECULARE del TP_PHANTOM: lì il feed stampa wick che il book non ha; qui il book si muove e il feed non lo vede. Real-truth ha bookato il reale (corretto), ma il sistema se n'è accorto solo ORE dopo, al close sim. - Incidente venue (dalle ~09:47): il conto Deribit testnet ha iniziato a
rifiutare ordini (
locked_by_admin,invalid_reduce_only_order) e le posizioni sul conto sono cambiate da sole (ETH LONG 0.141 con avg 1849.5 — prezzo vecchio di settimane): rollback/reset admin del testnet, non un nostro bug. Difese OK:REAL_CLOSE_PARTIAL, leg-fail con unwind, orfani registrati (ETH −0.026, SOL +0.7),sim_fallbackdichiarato. MA: il reconciler delle 11:40 è morto in silenzio su un 502 — il guardiano che non suona è indistinguibile dal tutto-ok.
Fatto
1. Reconcile degli ordini RESTING (cron :40, read-only)
- cerbero-mcp 9a74052: nuovo endpoint Deribit
get_open_orders(private/get_open_orders_by_currency; per i trigger untriggered interrogare anchetype='trigger_all'e merge per order_id). Pattern già presente per Hyperliquid/IBKR. Rebuild + smoke su testnet (book vuoto == stato atteso). books.expected_resting(): TP/DSL attesi dai libri dei worker single-leg in posizione reale (i pairs non hanno resting).reconcile_account.compute_resting_drift()— tre classi:FILLED_UNBOOKED(atteso non in book + fill nel trade history col worker ancora in posizione = il caso MR02),MISSING(atteso, non in book, zero fill — per il DSL triggered il fill ha order_id NUOVO → appare qui e il drift posizioni completa),STALE(in book con label di un nostro worker ma nessun libro: fillerebbe a sorpresa). Anti-race come per le posizioni; alertRESTING_DRIFT.RECONCILE_FAIL: il main è ora guardato — su eccezione alert Telegram + exit 2 (fix del crash silenzioso delle 11:40).
2. Guard FEED_BOOK_GAP nel runner
_check_feed_book_gap: ad ogni poll confronta il close della candela in corso col
mark dello strumento d'ESECUZIONE (USDC, get_ticker_batch, 1 chiamata);
oltre soglia (overrides.feed_book_gap_bps, default 150) → alert per episodio,
recovery con isteresi a soglia/2, fail-open su errori di rete. Le decisioni
restano sul feed (il sim è la verità che guida): questo dice solo QUANDO i fill
reali possono divergere dal sim. Avrebbe segnalato il caso MR02 in tempo reale.
3. Epoca v1.1.26 nell'hourly_report
I conteggi CHIUSI cumulativi erano inquinati dal churn TP-fantasma dell'11-06
17:32–17:58 (~24 giri pre-fix). Nuova riga epoca v1.1.26+ (TP_PHANTOM attivo)
da EPOCH_V1126 = 2026-06-11T21:40 (deploy del gate): oggi legge 7/1 contro
il 18/17 storico — è il numero da confrontare col backtest da qui in poi.
Test e deploy
pytest 121 passed (114 + 7 nuovi: test_reconcile_resting.py,
test_feed_book_gap.py). Deploy v1.1.27 (runner nel container; reconciler e
hourly_report girano da host → già attivi). cerbero-mcp rebuildato (endpoint).
Aperto / lezioni
- Drift ETH +0.167 non spiegato dal rollback testnet: quando il conto si
sblocca serve
reset_flatten+ riallineamento ledger (decisione utente). - I residui dei close single-leg falliti (
REAL_CLOSE_PARTIAL, es. MR07 0.102) NON finiscono inorphan_legs→ il reconciler li vede come drift NON spiegato. Estendere la registrazione orfani anche al single-leg è il naturale passo successivo (punto in analisi sviluppi). - Il testnet può riscrivere il conto sotto i piedi (lock + rollback): nessuna invariante "il conto cambia solo per nostri ordini" è affidabile. Il reconciler orario è l'unico rilevatore; valutare cadenza più fitta in presenza di posizioni reali aperte.
Addendum (pomeriggio, v1.1.28): orfani single-leg + circuit-breaker venue-lock
I due punti promossi dall'analisi sviluppi, entrambi figli dell'incidente di oggi:
- Orfani anche per i close single-leg.
REAL_CLOSE_PARTIALora registra la quota residua inStrategyWorker.orphan_legs(stessa semantica dei pairs, persistita in status.json, letta dabooks.real_books) → il reconciler vede il drift come SPIEGATO invece che anomalo (il caso MR07 0.102 ETH delle 09:47 restava invisibile ai libri). Inoltre_close_positionora persiste lo stato a fine booking (prima solo al save del tick: un crash li' in mezzo perdeva capital e orfano). NB: nessun save a meta' chiusura — conreal_in_positionancora true l'orfano conterebbe doppio nei libri. - Circuit-breaker venue-lock in
ExecutionClient. Dopolock_trip=3reject 'locked' consecutivi (es.locked_by_admindi oggi) le APERTURE sono sospese senza toccare l'API (Fill failed → path REAL_OPEN_FAIL/sim_fallback esistente; per i pairs entrambe le gambe rifiutate localmente → niente leg parziali da unwindare, niente fee sprecate). Le CHIUSURE si tentano SEMPRE (path gia' sicuro: partial/orphan/netting) e un loro reject 'locked' rinfresca il cooldown. Riarmo: dopolock_cooldown_s=900la prossima apertura fa da probe. AlertVENUE_LOCKal trip + RIENTRATO al primo ordine accettato.
Test: test_single_leg_orphan.py, test_venue_lock_breaker.py (126 passed).
Chiusura incidente (13:05): testnet sbloccato, conto riallineato
Sequenza eseguita a venue sbloccato: stop runner → reset_flatten (chiusi i due
artefatti del rollback: ETH long 0.141 @1664.25, SOL long 0.7 @66.651, conto FLAT)
→ svuotati gli orphan_legs di ETH_SOL (le quote orfane non esistono piu' sul
conto post-flatten; edit a container fermo, altrimenti il worker li riscrive
dalla memoria) → restart. Reconciler: OK — conto allineato ai libri (posizioni
e resting), runner healthy, log puliti. Il ledger NON e' stato toccato: i booking
dell'incidente erano gia' dichiarati (sim_fallback/real) e l'equity (€2020.82)
resta la verita' contabile del paper.