a2f28153d8
Il giro delle 21:25 (primo con bite gia' cancellato): 576 chiamate, 0 risposte 429, 0 errori. E' il controllo che conta, perche' il guasto del 29/07 era la raffica di bite che saturava il rate limit per-IP. Aggiunta la tabella dei 5 giri della giornata, che mostra invarianza prima/dopo. ⚠️ E una precisazione sul nostro stesso strumento: 'ok' significa ALMENO UN LATO del book, non quota completa. Con ok=572 le righe a due lati sono 432 (75.5%). Che sia strutturale (opzioni molto OTM) e non un degrado si vede dall'invarianza fra i giri, non dal fatto che il numero sembri alto. E' 'una riga presente non e' un dato presente' un livello piu' in giu', applicata allo strumento costruito per quella lezione: la battuta di cuore vedrebbe il guasto del 29/07 (quote vuote) ma non una deriva verso book a un lato solo. Nessuna soglia cablata: con 5 giri di storia sarebbe inventata, e una soglia inventata e' peggio di nessuna soglia. La colonna da guardare e' 'due lati %'. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
180 lines
12 KiB
Markdown
180 lines
12 KiB
Markdown
# 2026-07-30 (2º filone) — cerbero-bite viene eliminato: assorbita la raccolta, non il motore
|
|
|
|
**Decisione dell'operatore:** `/opt/docker/cerbero-bite` viene smantellato e il suo lavoro passa a
|
|
PythagorasGoal. **Esito:** book, pesi, config e strategia INVARIATI. Cambia solo *chi* raccoglie
|
|
la catena opzioni — e cambia in meglio su tre assi misurati.
|
|
|
|
## Cosa di bite valeva la pena, e cosa no
|
|
|
|
L'unica parte **irreversibile** è il dato: una catena opzioni non si ricostruisce a posteriori
|
|
(Deribit non serve book storici, non esiste un secondo venue). Tutto il resto è codice, e il codice
|
|
si riscrive.
|
|
|
|
| parte | assorbita? | perché |
|
|
|---|---|---|
|
|
| `option_chain_snapshots` (1.23M righe) | **sì** | ha appena falsificato il *f* di VRP01; irrecuperabile |
|
|
| `market_snapshots` (17.402 righe, 26/03+) | **sì** | dealer net gamma, gamma flip, rischio liquidazioni, funding cross: dati che il progetto non ha altrove |
|
|
| raccolta continua | **sì, riscritta** | senza, l'archivio si congela e "aspettare il crash" muore |
|
|
| `dvol_history` | no | `fetch_dvol.py` ha storia **più lunga** (2020+ vs 2026-05): copiarla sarebbe una seconda copia peggiore |
|
|
| motore credit-spread ETH | no | il progetto ha già la regola *niente short-vol da modello in deploy*; e il conto era a $52 contro un minimo di $720 |
|
|
| GUI, kill switch, dead-man, audit chain | no | PythagorasGoal ha già `venue_watch`, `edge_watch`, `monitor_health`, `fee_watch` |
|
|
| `decisions` / `positions` | no | 59 valutazioni d'ingresso, 0 posizioni |
|
|
|
|
Prima di toccare qualsiasi cosa: snapshot `VACUUM INTO` del DB (461 MB) + `audit.log` + config in
|
|
`/opt/docker/backups/manual/cerbero-bite-20260730/`, con SHA256.
|
|
|
|
## Il collettore nuovo — tre difetti di bite non replicati
|
|
|
|
`scripts/live/collect_chain.py`, cron **`25 * * * *`**.
|
|
|
|
**1. Una chiamata per strumento invece di due.** `public/get_order_book?depth=3` restituisce già
|
|
quote, greche, IV, open interest, volume, book **e** `underlying_price`. Bite chiamava ticker e
|
|
orderbook separatamente: doppio costo, e i due potevano riferirsi a istanti diversi.
|
|
Un `get_book_summary_by_currency` iniziale dà l'OI di tutta la catena in **una** chiamata e
|
|
prefiltra sotto soglia: 551 → **~290 chiamate per asset**.
|
|
|
|
**2. Pacing invece di raffica.** ⚠️ **Il carico non è mai stato il problema.** ~570 chiamate/ora
|
|
sono **0.16/s** se distribuite; bite le sparava in ~26 secondi (**~44/s**) e si auto-saturava il
|
|
rate limit per-IP — 12.186 risposte 429 in 26 ore, il 96% nel minuto `:00`. Token bucket a 4/s +
|
|
backoff: primo giro reale **574 chiamate, 0 risposte 429**.
|
|
|
|
Il minuto `:25` è una scelta, non un default: `:00` era la raffica di bite, `:07` è `cron_book`
|
|
(da lì passa il feed 5m di SKH01, la cosa che non deve trovare l'IP occupato).
|
|
|
|
**3. Stato esplicito della quota.** `quote_status` ∈ {`ok`, `no_quote`, `error`}:
|
|
- `no_quote` = il venue ha risposto, il book è vuoto → **fatto di mercato**
|
|
- `error` = la chiamata è fallita → **fatto di infrastruttura**
|
|
|
|
Bite li faceva collassare entrambi su "riga con bid/ask NULL", ed è per questo che il guasto del
|
|
29/07 (50% di quote perse per 38 ore) non ha prodotto **nessun** segnale: il conteggio righe non
|
|
cambiava. Per lo stesso motivo `book_depth_top3` ora è **NULL su errore, mai 0** — bite scriveva 0,
|
|
e "chiamata fallita" diventava indistinguibile da "book vuoto".
|
|
|
|
⚠️ Le righe ereditate portano `quote_status='unknown'`. Bite non registrava il perché e **a
|
|
posteriori non è ricostruibile**: si dichiara l'ignoranza invece di inventare uno stato.
|
|
|
|
## Il silenzio, di nuovo
|
|
|
|
Un collettore fermo non produce niente, e il niente si legge come "nessun dato quel giorno" invece
|
|
che come "raccolta rotta" — su una serie irrecuperabile è il modo più caro di sbagliare. Ogni giro
|
|
scrive una riga in `data/chain_collect/runs.jsonl`, **anche quando fallisce**, e
|
|
`monitor_health` la sorveglia con cadenza 1h e `max_age_h=3` (due giri persi). È la stessa lezione
|
|
di `paper_dvolspread` e di `fresh_5m`, applicata prima che serva invece che dopo.
|
|
|
|
## Un difetto trovato per strada: due formati ISO nella stessa colonna
|
|
|
|
L'import del contesto restituiva **17.310 righe dal 2026-05-01** contro le 17.402 dal **2026-03-26**
|
|
del database. 92 righe di backfill sono scritte senza microsecondi
|
|
(`2026-03-26T12:00:00+00:00`), le altre con (`2026-05-01T15:45:00.062918+00:00`).
|
|
`pd.to_datetime` senza `format` **inferisce un formato solo** dal primo elemento e manda gli altri
|
|
a `NaT`, che il `dropna` a valle rimuoveva **in silenzio**: la serie perdeva i suoi 5 settimane più
|
|
vecchi e la data d'inizio slittava di un mese senza un messaggio.
|
|
|
|
Corretto con `format="ISO8601"` **e** con uno scarto rumoroso (`_drop_unparsed` stampa quante righe
|
|
cadono e perché). **REGOLA: `dropna` dopo un parsing è un rilevatore di difetti travestito da
|
|
pulizia — se toglie righe deve dirlo.** Un import silenzioso non è un import pulito, è un import
|
|
che non sai se ha funzionato.
|
|
|
|
## Eliminazione (stessa sera, 21:20 UTC)
|
|
|
|
Container rimossi, volume `cerbero-bite_bite-data` rimosso, immagine `cerbero-bite:dev` rimossa,
|
|
`/opt/docker/cerbero-bite` cancellata. **12 GB liberati** (73G → 61G di disco usato).
|
|
|
|
Prima di cancellare, quattro verifiche — nessuna delle quali era "il backup esiste":
|
|
|
|
1. **Lo snapshot è *completo*, non solo integro.** SHA256 `OK` su entrambi i file, ma soprattutto
|
|
conteggi confrontati tabella per tabella fra il volume vivo e lo snapshot: `option_chain_snapshots`
|
|
1.232.212, `market_snapshots` 17.406, `decisions` 59, `positions` 0 — **4 su 4 identici**, e
|
|
identici anche ai parquet importati. *Un hash prova che il file non è corrotto, non che contenga
|
|
tutto.*
|
|
2. **I 10,6 GB di backup interni al volume non contenevano dati unici.** 32 snapshot sqlite storici:
|
|
la domanda giusta non era la loro dimensione ma *se bite potasse lo storico*. Tutti e tre i
|
|
campioni controllati (09/06, 01/07, finale) hanno **la stessa riga più vecchia**
|
|
(`2026-05-01T20:53:49.294928`) e conteggi monotoni crescenti (218.500 → 665.949 → 946.990 →
|
|
1.232.212) → nessuna potatura, sottoinsiemi stretti del DB finale.
|
|
3. **Nessuna dipendenza a runtime.** Né cron, né systemd, né route traefik, né altri progetti: i
|
|
soli riferimenti rimasti sono documentazione (questo diario, CLAUDE.md, un commento in
|
|
`backup.sh` e uno in `health-watch.py`) — cioè memoria storica, che è corretto lasciare.
|
|
4. ⚠️ **`cerbero-mcp` è un progetto DIVERSO e serve a PythagorasGoal** (universo Hyperliquid, e il
|
|
percorso del conto): progetto compose separato, e la rete `traefik` che condividono è dichiarata
|
|
`external:` nel compose di bite → `down -v` non la tocca. Verificato dopo: `Up 41 hours
|
|
(healthy)`. *Due servizi con lo stesso prefisso nel nome sono un incidente che aspetta.*
|
|
|
|
Il codice non è stato cancellato: era già su Gitea (`Adriano/Cerbero-Bite`), e l'unica modifica
|
|
locale pendente — il ripiego del collector da `:00` a `:20`, superato dal successore — è stata
|
|
committata e pushata come commit di dismissione. **Il dato era l'unica parte irreversibile.**
|
|
|
|
**REGOLA: prima di cancellare una sorgente si verifica che la copia sia completa, non che
|
|
esista** — e "quanto è grande" non è la domanda da fare a un backup, "cosa contiene che l'originale
|
|
non ha più" sì.
|
|
|
|
### Il primo giro dopo l'eliminazione, e un modo di leggere male il nostro stesso indicatore
|
|
|
|
Verificato invece che dedotto (21:25 UTC, il primo giro con bite già cancellato):
|
|
**576 chiamate, 0 risposte 429, 0 errori.** È il controllo che conta, perché il guasto del 29/07
|
|
era esattamente la raffica di bite che saturava il rate limit per-IP.
|
|
|
|
| giro | righe | ok | no_quote | error | **due lati** |
|
|
|---|---|---|---|---|---|
|
|
| 20:00 | 570 | 552 | 17 | 1 | 71.6% |
|
|
| 20:06 | 570 | 570 | 0 | 0 | 75.8% |
|
|
| 20:22 | 570 | 570 | 0 | 0 | 75.6% |
|
|
| 20:25 | 570 | 570 | 0 | 0 | 75.6% |
|
|
| **21:25** | **572** | **572** | **0** | **0** | **75.5%** |
|
|
|
|
(Il `1 error / 17 no_quote` delle 20:00 è il primo giro in assoluto, con bite ancora acceso a
|
|
sparare al minuto `:00`: la firma del problema, catturata nel suo ultimo momento di vita.)
|
|
|
|
⚠️ **`ok = 572` non significa "572 quote complete".** Nel collettore `ok` vuol dire *almeno un lato
|
|
del book* (`bid is not None or ask is not None`); le righe con **entrambi** i lati sono **432 su
|
|
572 (75.5%)**. Il resto sono book a un lato solo, normali sulle opzioni molto OTM. Che sia
|
|
strutturale e non un degrado si vede dal fatto che la percentuale è **identica a tutti i giri**,
|
|
prima e dopo l'eliminazione — non dal fatto che sembri alta.
|
|
|
|
È la lezione «*una riga presente non è un dato presente*» un livello più in giù, applicata allo
|
|
strumento costruito **per** quella lezione: la battuta di cuore distingue `ok`/`no_quote`/`error`,
|
|
quindi vedrebbe il guasto del 29/07 (quote vuote), ma **non** vedrebbe una deriva più sottile —
|
|
book che diventano progressivamente a un lato solo. Non è cablata una soglia: con cinque giri di
|
|
storia sarebbe inventata, e una soglia inventata è peggio di nessuna soglia. **La colonna da
|
|
guardare quando ci saranno settimane di serie è "due lati %".**
|
|
|
|
## File
|
|
|
|
- `scripts/live/collect_chain.py` + `scripts/cron_chain.sh` — la raccolta (cron `25 * * * *`)
|
|
- `scripts/analysis/import_cb_archive.py` — import una-tantum dell'archivio
|
|
- `scripts/analysis/certify_cb_chain.py` — certificazione dello store (era `fetch_cb_chain.py`)
|
|
- `scripts/research/cblib.py` — `load_chain()` ora unisce archivio + raccolta
|
|
- `src/live/monitor_health.py` — nuovo spec `collect_chain`
|
|
- `tests/test_collect_chain.py` (11 casi)
|
|
|
|
## Addendum (stessa sera) — spegnimento, backup, ripresa dopo un riavvio
|
|
|
|
**Spegnimento.** Prima di fermare i container ho preso uno snapshot **finale** (bite aveva raccolto
|
|
fino alle 20:15, oltre il backup delle 19:55) e ho reimportato: archivio a **1.232.212 righe**.
|
|
Consegna verificata: ultima riga di bite 20:15:00, prima nostra 20:00:18 → **sovrapposizione, nessun
|
|
buco**. `docker compose stop` (non `down -v`: i volumi restano). Effetto misurato: **0 risposte 429**
|
|
su cerbero-mcp nei minuti successivi, contro ~650 per ogni giro orario.
|
|
|
|
**Backup.** `data/raw/` è gitignored e il backup rotativo della VPS non copriva PythagorasGoal: dopo
|
|
lo spegnimento di bite l'archivio esisteva in due copie **sullo stesso disco**. Aggiunta
|
|
`do_pythagoras` a `/opt/docker/scripts/backup.sh` con un criterio dichiarato: **si salva ciò che non
|
|
si può riscaricare**. Dentro: catena + contesto, `data/paper_*` e `data/chain_collect` (serie
|
|
forward-only che alimentano i gate pre-registrati — non ricalcolabili per costruzione),
|
|
`options_daily`, `live`, `venue_watch`, `fee_watch`, `config/live.json` → **28 MB**. Fuori: i ~110 MB
|
|
che si riscaricano dai venue.
|
|
|
|
Due guardie, entrambe provate **nei due versi**: la funzione fallisce se la catena è assente o
|
|
vuota (invece di produrre un archivio che sembra a posto), e verifica che il tar prodotto contenga
|
|
davvero la catena. Il caso negativo produce **0 file**. Per poterlo provare la radice è
|
|
sovrascrivibile via `PYG_ROOT` — *una guardia che non si riesce a far scattare non è distinguibile
|
|
da una rotta.*
|
|
|
|
⚠️ `/opt/docker/scripts` **non è un repo git**: quella modifica vive solo su disco, e questo diario
|
|
è l'unico posto in cui è scritta.
|
|
|
|
**Riavvio.** La raccolta riprende da sola: `cron` è `enabled` e `active`, e il collettore non
|
|
dipende da docker né da cerbero-mcp (API pubblica Deribit diretta) — il contrario di bite, che era un
|
|
container. Verificato girando `cron_chain.sh` con `env -i PATH=/usr/bin:/bin`: 574 chiamate, 0
|
|
errori, dato e battuta di cuore scritti. ⚠️ **Finestra scoperta: fino al `:25` successivo, senza
|
|
recupero.** Il collettore non fa catch-up e non potrebbe: le quote passate non sono richiedibili.
|