Files
PythagorasGoal/docs/diary/2026-07-15-feed-freeze-rebuild-copystat.md
T
Adriano Dal Pastro 9fa3e6a0be fix(feed): backup rebuild non-fatale — copy2→copyfile, il .bak non abortisce piu' il feed live
Il feed BTC/ETH del book era congelato 7 giorni (2026-07-08 -> 07-15): rebuild_history.py
usava shutil.copy2 per il .prebuild.bak; copy2=copyfile+copystat e copystat (os.utime/chmod
con valori espliciti) richiede la PROPRIETA' del file, non basta il group-write. I .bak sono
root:adriano, il cron gira come adriano -> EPERM ogni notte, abortendo PRIMA della scrittura
del feed (data/raw/*.parquet fermo a Jul 8). Il book ha ri-girato ogni ora su barra vecchia
(TP01 1d non ri-valutato); nessuna perdita (segnale fresco == tenuto) ma cieco 7g.

Fix: backup best-effort = shutil.copyfile (solo contenuto, no copystat) in try/except OSError.
Un .bak difensivo non deve mai poter bloccare la scrittura del feed live.

Verificato: rebuild rigirato -> feed a 2026-07-15, audit cross-venue BTC 1.9/ETH 2.0 bps,
file ora adriano:adriano (auto-sana), book dry-run legge ultima barra 2026-07-15 -> HOLD.

Diario: docs/diary/2026-07-15-feed-freeze-rebuild-copystat.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:37:42 +00:00

127 lines
6.9 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-07-15 — Incident ops: feed BTC/ETH del book congelato 7 giorni (`copy2`→`copystat` EPERM)
**Tipo:** incident operativo (pipeline dati), non ricerca. Nessun edge, nessun cambio a
strategie/pesi/config di trading. Documentato qui perché ha **congelato il segnale del book live**
per una settimana: il book ha ri-girato ogni ora ma su una barra vecchia (`ultima barra 2026-07-08`),
senza ri-valutare TP01. Trovato durante un check di `stato`, non da un alert (il fallimento era
silenzioso — vedi Lezioni).
## Sintomo
Nel log del `cron_book` orario, ogni run dal 2026-07-08 in poi mostrava:
```
ultima barra : 2026-07-08 # <-- oggi e' 2026-07-15
...
ETH TP +0.000 · SKH +1(LONG@1868.9) -> net $+75 | pos $+77 -> HOLD (a target)
=> Nessuna azione: conto gia' al target netto del book.
```
Conto **online** e sano ($600.5), esecuzione armata, cron regolari — ma il segnale TP01 (1d) fermo
al 8 luglio. SKH01 meno colpito: prende il 5m fresco **in memoria** via `livefeed.fresh_5m`, non da
disco. Il congelamento riguardava il **feed certificato su disco** (`data/raw/{btc,eth}_*.parquet`),
mtime fermo a **Jul 8 00:32**.
## Diagnosi (catena completa)
Il `cron_daily` (00:30) ricostruisce il feed Deribit con `rebuild_history.py --asset BTC ETH`. Nel
`logs/cron_daily.log`, **ogni notte dal 9 luglio**:
```
REBUILD STORICO da DERIBIT MAINNET — FULL (scrive data/raw, backup)
Traceback (most recent call last):
File "scripts/analysis/rebuild_history.py", line 189, in build
shutil.copy2(path, BACKUP / f"{asset.lower()}_{tf}.parquet.prebuild.bak")
PermissionError: [Errno 1] Operation not permitted
```
Il resto del daily (Hyperliquid, DVOL, ETF, paper trader) proseguiva regolare — solo il **primo**
step, il rebuild BTC/ETH, moriva. Sequenza dentro `build()`:
```python
if path.exists():
shutil.copy2(path, BACKUP / f"...prebuild.bak") # <-- crash QUI
tmp = path.with_suffix(".parquet.tmp")
d.to_parquet(tmp, index=False) # <-- mai raggiunto
tmp.replace(path) # <-- il feed non viene mai scritto
```
**Causa radice:** `shutil.copy2` = `copyfile` **+ `copystat`**. `copystat` fa `os.utime`/`chmod`/
`chflags` sulla destinazione con valori espliciti → richiede la **proprietà** del file (o `CAP_FOWNER`),
**non basta il group-write**. I file erano `root:adriano 660`; il cron gira come **adriano** (uid 1001,
membro del gruppo `adriano`). adriano può *scrivere* nel `.bak` (group-write) ma non `utime`-arlo →
`EPERM`.
Firma diagnostica coerente: `btc_5m.parquet.prebuild.bak` aveva mtime **odierno** mentre gli altri
`.bak` erano fermi al 7 luglio. Ovvero il `copyfile` interno riusciva (scriveva contenuto + mtime),
poi `copystat` lanciava → l'eccezione abortiva il loop **prima** di scrivere `data/raw/btc_5m.parquet`.
Perché ha iniziato il 9 luglio e non prima: l'ownership `root:*` dei file di `data/raw` è residuo di
run/manutenzione eseguiti come root (l'incidente Traefik del 9 luglio è nella stessa finestra). Finché
il processo era root, `copystat` sui file root riusciva; passato a run come adriano, EPERM. La causa
prossima non è *chi* ha creato i file, ma che **un backup difensivo poteva abortire il feed live**.
## Impatto
- **7 giorni (2026-07-08 → 07-15)** di feed Deribit BTC/ETH fermo → TP01 (1d) del book non ha
ri-valutato. Il book ha tenuto un ETH long SKH01 (@1868.9) impostato il 8 luglio.
- **Nessuna perdita né ordine errato:** il segnale ricalcolato sul feed fresco **coincide** con quello
tenuto (BTC flat, ETH long $75, già a target → 0 ordini di divergenza). Rischio evitato per fortuna
di regime, non per design — il book è comunque stato **cieco** a una settimana di segnale TP01.
- Paper trader e dashboard **non impattati**: usano feed diversi (HL/DVOL/ETF, aggiornati regolarmente).
## Fix
`scripts/analysis/rebuild_history.py` — backup reso **best-effort e non fatale**:
```python
if path.exists():
# copyfile (solo contenuto, NO copystat) dentro try/except: un .bak difensivo
# non deve MAI poter bloccare la scrittura del feed live.
try:
shutil.copyfile(path, BACKUP / f"{asset.lower()}_{tf}.parquet.prebuild.bak")
except OSError as e:
print(f" WARN backup {path.name} saltato (non fatale): {e}")
```
Due cambi: (1) `copy2``copyfile` — un `.bak` non ha bisogno dei metadati originali, e `copystat`
era l'unica fonte dell'EPERM; (2) `try/except OSError` con warning — anche un futuro errore di backup
(disco pieno, permessi) non blocca più il feed.
**Verifica end-to-end:**
- rebuild rigirato a mano → `scritto {btc,eth}_{5m,15m,1h}.parquet`, feed fresco a **2026-07-15**,
audit cross-venue **BTC 1.9 bps / ETH 2.0 bps** (pulito).
- i file di `data/raw` ora sono **`adriano:adriano`** (`tmp.replace` li rigenera con l'owner del
processo) → l'ownership si auto-sana, il problema non si ripresenta anche con `copy2`.
- `book_execute.py` in dry-run legge **`ultima barra 2026-07-15`** e ricalcola → BTC flat / ETH long
$75 → HOLD. Pipeline sbloccata.
## Lezioni
1. **Un backup difensivo non deve mai poter uccidere l'operazione che protegge.** Il `.bak` è una
rete di sicurezza; il suo fallimento (metadati, permessi, disco) va isolato in `try/except`, mai
propagato allo scrittore del feed live.
2. **`shutil.copy2` è insidioso in ambienti multi-utente:** `copystat` richiede *ownership*, non
group-write. In una dir a proprietà mista (`root:adriano`) un cron non-root ci sbatte. Per un
backup, `copyfile` è la scelta giusta (i metadati del `.bak` non servono).
3. **Fallimento silenzioso = il peggiore.** Il conto era online, i cron giravano, gli alert tacevano:
solo la barra vecchia nel log tradiva il congelamento. Il gate `online` del book protegge dai
problemi di *conto*, non da un feed *stale*. **Follow-up raccomandato:** alert se
`ultima_barra < today 2g` in `book_execute` (staleness-gate esplicito).
4. **La sicurezza del book ha retto a metà:** non ha operato a cieco su un conto irraggiungibile, ma
*ha* operato (HOLD) su un segnale stantio. La coincidenza segnale-vecchio == segnale-nuovo è
fortuna di regime; un mercato in movimento avrebbe lasciato il book fuori posizione.
## Runbook (feed BTC/ETH stantio)
1. **Rilevare:** `stat -c '%y %n' data/raw/btc_1h.parquet` → se mtime > 1-2g fa, il feed è fermo.
Conferma incrociata: `ultima barra` nel `logs/cron_book.log`.
2. **Diagnosi:** `grep -A3 "REBUILD STORICO" logs/cron_daily.log | tail` → cercare Traceback nello
step rebuild.
3. **Sbloccare:** `uv run python scripts/analysis/rebuild_history.py --asset BTC ETH` → deve stampare
`scritto ..._5m/15m/1h.parquet` e l'audit cross-venue (atteso < ~10 bps mediana).
4. **Verificare il book:** `uv run python scripts/live/book_execute.py` (senza `--execute`) → `ultima
barra` deve essere la data odierna.
Nessun file di trading (strategie/pesi/config) toccato. Solo `rebuild_history.py`.