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

6.9 KiB
Raw Blame History

2026-07-15 — Incident ops: feed BTC/ETH del book congelato 7 giorni (copy2copystat 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) copy2copyfile — 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.