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>
6.9 KiB
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():
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:
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/rawora sonoadriano:adriano(tmp.replaceli rigenera con l'owner del processo) → l'ownership si auto-sana, il problema non si ripresenta anche concopy2. book_execute.pyin dry-run leggeultima barra 2026-07-15e ricalcola → BTC flat / ETH long $75 → HOLD. Pipeline sbloccata.
Lezioni
- 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 intry/except, mai propagato allo scrittore del feed live. shutil.copy2è insidioso in ambienti multi-utente:copystatrichiede 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.baknon servono).- 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
onlinedel book protegge dai problemi di conto, non da un feed stale. Follow-up raccomandato: alert seultima_barra < today − 2ginbook_execute(staleness-gate esplicito). - 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)
- Rilevare:
stat -c '%y %n' data/raw/btc_1h.parquet→ se mtime > 1-2g fa, il feed è fermo. Conferma incrociata:ultima barranellogs/cron_book.log. - Diagnosi:
grep -A3 "REBUILD STORICO" logs/cron_daily.log | tail→ cercare Traceback nello step rebuild. - Sbloccare:
uv run python scripts/analysis/rebuild_history.py --asset BTC ETH→ deve stamparescritto ..._5m/15m/1h.parquete l'audit cross-venue (atteso < ~10 bps mediana). - Verificare il book:
uv run python scripts/live/book_execute.py(senza--execute) →ultima barradeve essere la data odierna.
Nessun file di trading (strategie/pesi/config) toccato. Solo rebuild_history.py.