Files
PythagorasGoal/docs/diary/2026-08-25-manutenzione-deribit-e-resilienza-venue.md
T
Adriano Dal Pastro ee5c6ed539 ricerca: il piano a 10 anni dal conto vero, l'ETF-quando-flat SCARTATO, slippage rifatto
Giornata partita da "stato trades" e finita sul disegno del sistema. Versamento di
1.400 USDC atterrato alle 11:03:35Z (equity $667,88 -> $2.066,88); il giro delle 11:47
ha ribilanciato correttamente e ha ripiazzato i disaster-SL alla taglia nuova -- prima
volta che il ramo riparato stamattina gira sul serio.

(1) PIANO A 10 ANNI DAL CONTO VERO -- r0825_piano_10a_500.py (nuovo)
Le tabelle pubblicate partono da $600/$635: rifatte su $2.067, lente L3 CONGIUNTA.
Replica superata: chiede EUR 1.718/m dove la tabella da $635 chiedeva EUR 1.733.
EUR 500/mese per 10 anni -> mediana $114.934, rendita 18,35 EUR/g, P(>=50 EUR/g) = 0,0%
su 3.000 traiettorie. Il bersaglio con EUR 500/m arriva al 17o anno.

  L'EQUIVALENZA CHE ORDINA IL PIANO: EUR 100/mese in piu' == +4,07%/anno di drift,
  cioe' +27% su TUTTO il drift del libro. A 10 anni i bonifici fanno il 59%.
  Tutta la leva autorizzabile vale quanto EUR 100-150/mese: k=1,25 (+15,6%) vale MENO
  di EUR 100/mese in piu' (+19,1%), e si porta dietro il peggior giorno al 21,48%.

  E il libro a k=1 rende MENO dell'S&P (15,19% vs 17,40%): il vantaggio sta nello
  Sharpe (1,35 vs 0,89) e senza leva NON si converte in rendimento. Senza leva il libro
  non si giustifica come veicolo di ACCUMULO -- si giustifica come veicolo di RENDITA,
  dove serve 2,5x meno capitale ($254k contro $646k) perche' la perpetua vive sul DD.

(2) "ETF QUANDO IL LIBRO E' FLAT" -- SCARTATO, r0825_capitale_fermo.py (nuovo)
Il libro e' flat il 28,0% dei giorni (2,7% nel 2021, 77,0% nel 2022, 75,6% nel 2026);
live, esposto 14 giorni su 64, leva lorda mediana 0,00x e max 0,52x su un tetto di 1,0x
-- il cap non ha mai morso, il vincolo e' il segnale.
A ISO-RISCHIO il dinamico PERDE: Sharpe 1,01 contro 1,40 del 50/50 e 1,35 del libro. E
il null a maschera casuale (400 estrazioni, stessa quota, blocchi 20g) lo mette al 30o
percentile: fa PEGGIO di commutare a caso.
A iso-nozionale sembrava vincere (drift 17,09%, il piu' alto) perche' aveva la vol piu'
alta: e' la trappola di M6, il de-levering e' il PRIMO test.

  E IL MECCANISMO CHE SEMBRAVA OVVIO NON ESISTE. Aggregato: SPY 4,91% nei giorni flat
  contro 21,48% negli altri (-16,57%), e la storia si scrive da sola. FALSA: scomposta
  per anno il segno ALTERNA (3 su, 4 giu') e il 2022 -- l'anno che doveva reggerla --
  ha il segno OPPOSTO. Artefatto di composizione: i giorni flat stanno negli ANNI brutti
  per l'azionario, non nei GIORNI brutti. M9 ha fatto il suo lavoro su di me.

(3) SLIPPAGE RIFATTO ALLA TAGLIA VERA -- r0822_slip_audit.py riparato
26 fill (erano 18), $6-$319. I due piu' grandi mai eseguiti sono di oggi e hanno preso
1,01% e 0,54% della loro barra 5m, con il print a meta' del range (q=0,50). Il caso
peggiore resta un fill da $74 del 18/07 al 21,9%: un sabato, nastro inesistente.
L'attrito non e' funzione della TAGLIA ma di taglia/volume-della-barra -- l'estrapolazione
lineare che prevedeva ~71% a questo capitale e' refutata dalla misura diretta.
NB non e' "assente", e' "non ancora testato": manca un fill grande in una barra sottile,
e il weekend e' dove TP01 fa il 38% del proprio gross.

  DIFETTO P1 RIPARATO: l'equity era CABLATA a 636.0 con un commento che diceva di
  leggerla dal watermark. Il giorno del versamento avrebbe stampato $636 sbagliando di
  3,3x proprio la sezione che esiste per dire QUANDO la misura scade. Riparata leggendo
  il watermark -- e poi riparata di nuovo, perche' i fill del campione sono stati
  eseguiti a conti diversi ($597-$2.067) e una base sola e' sbagliata comunque la si
  scelga. Ora la normalizzazione e' PER FILL, e a $600 riproduce il 22,0% originale.

DUE ERRORI MIEI, CATTURATI PRIMA DI PUBBLICARLI
- maschera VUOTA vestita da risultato: la prima stesura di r0825_capitale_fermo prendeva
  i giorni flat dalla serie DE-LUCKATA, ma `deluck` sottrae una costante e lo zero esatto
  sparisce -> maschera vuota, dinamico identico al book, e lo script ha stampato tabelle
  piene e un p-value. Lo ha rivelato solo la riga "0 = 0.0%".
  REGOLA: stampare la CARDINALITA' di una maschera prima di usarla.
- il meccanismo falso della (2), demolito dalla scomposizione per anno.

Suite: 730 passati, 1 fallito -- quello gia' noto di 5.9 (deriva dati, non codice).
Watermark del libro live sopravvissuto intatto alla suite (fixture autouse di stamattina).

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

272 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.**
---
## Seconda parte della giornata: il versamento, e le tre misure che ne sono uscite
Alle **11:03:35Z** sono atterrati **1.400 USDC**: equity **$667,88 → $2.066,88**. Il giro delle
11:47 ha ribilanciato correttamente (cap/asset $1.033, BTC +$238, ETH +$319, disaster-SL `placed`
su entrambi alla taglia nuova) — la prima volta che il ramo riparato al mattino gira sul serio.
Poi l'operatore ha chiesto tre cose in fila, e ognuna ha rotto qualcosa.
### (1) «€500/mese per 10 anni»
Rifatto sul conto vero invece che citare la tabella da $635. **Non centra**: capitale mediano
$114.934, rendita 18,35 €/g, **P(≥50 €/g) = 0,0%** su 3.000 traiettorie. Il bersaglio con €500/m
arriva al 17° anno; per averlo a 10 servono €1.718/mese.
Il numero che ordina tutto: **€100/mese in più valgono +4,07%/anno di drift**, cioè +27% su tutto
il drift del libro. Dettaglio in `30-piano-capitale-fisco.md`.
### (2) «A cosa serve XSR01, allora?» → e poi: «l'abbiamo fatto per la leva»
Due osservazioni dell'operatore, entrambe centrate, che hanno spostato la conversazione dal
candidato al **disegno**.
XSR01 ha già fallito `weights_tilt_null` (peso ottimo ~0). Ma la risposta vera è che **anche un
uplift di strategia riuscito vale meno di un bonifico modesto**, e questo si applica a qualunque
sleeve, non solo a XSR01.
Poi la misura che ha ribaltato la discussione sulla leva: **il libro non è poco levato, è FERMO**.
Esposizione lorda realizzata in 64 giorni live: mediana **0,00x**, media 0,05x, **max 0,52x** su un
tetto strutturale di 1,0x, esposto in **14 giorni su 64**. Il cap non ha mai morso: il vincolo è il
segnale. *Il basso drawdown di TP01 non viene dall'essere piccolo nel mercato — viene dallo stare
fuori dal mercato.*
⚠️ Correzione che mi sono fatto da solo: quel 78% è il **regime attuale**, non la media. Sull'intera
storia i giorni flat sono il **28,0%** — 2,7% nel 2021, 77,0% nel 2022, 75,6% nel 2026.
### (3) «Allora usiamo l'ETF quando il libro è flat» → SCARTATO
Test in `r0825_capitale_fermo.py`. **Perde** a iso-rischio (Sharpe 1,01 contro 1,40 del 50/50 e
1,35 del libro), e il null a maschera casuale lo mette al **30° percentile**: fa peggio di
commutare a caso.
E qui la parte che vale più del risultato. Stavo per scrivere un meccanismo pulito — *«il libro va
flat quando anche l'azionario soffre»* — sostenuto da un aggregato netto (SPY 4,91% nei giorni
flat contro 21,48% negli altri, 16,57%). **La scomposizione per anno lo ha demolito**: il segno
alterna, 3 anni su e 4 giù, e il **2022** — l'anno che doveva reggerlo — ha il segno **opposto**.
Artefatto di composizione: i giorni flat stanno negli **anni** brutti per l'azionario, non nei
**giorni** brutti. M9 ha fatto esattamente il suo lavoro, su di me.
### I due difetti di misurazione trovati addosso
1. **Maschera vuota vestita da risultato.** La prima versione prendeva i giorni flat dalla serie
**de-luckata**; ma `deluck` sottrae una costante, quindi lo zero esatto sparisce. Maschera
vuota → dinamico identico al book → e lo script ha stampato tabelle piene e un p-value. Lo ha
rivelato solo la riga che stampava `0 = 0.0%`.
**Regola: stampare la CARDINALITÀ di una maschera prima di usarla.**
2. **`r0822_slip_audit.py` aveva l'equity CABLATA a `636.0`** con un commento che diceva di leggerla
dal watermark — P1 in piena regola. Il giorno del versamento avrebbe continuato a stampare $636,
sbagliando di 3,3x proprio la sezione che esiste per dire *quando la misura scade*. Riparato
leggendo il watermark — e poi riparato **di nuovo**, perché la prima riparazione divideva tutti i
fill per l'equity di oggi mentre i fill del campione sono stati eseguiti a conti diversi
($597$2.067). Ora la normalizzazione è **per fill**, e a $600 riproduce il 22,0% originale.
### La misura di slippage, rifatta alla taglia vera
26 fill (erano 18), taglia $6$319. I due più grandi mai eseguiti sono di oggi e hanno preso
**1,01%** e **0,54%** della loro barra 5m, con il print **a metà del range** (q=0,50). Il caso
peggiore resta un fill da **$74 del 18/07 al 21,9%** — un sabato, nastro inesistente.
📌 **L'attrito non è funzione della taglia: è funzione di `taglia / volume della barra`.** Un fill
4,3× più grande in un'ora liquida ha fatto 20× meno partecipazione di uno piccolo in un'ora morta.
⚠️ Ma non è «assente», è **«non ancora testato»**: nel campione non c'è ancora un fill grande in una
barra sottile, e il weekend è dove TP01 fa il 38% del proprio gross.