From 9fa3e6a0beee2620c7c1ddb88411fb80ba3f8198 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Wed, 15 Jul 2026 18:37:42 +0000 Subject: [PATCH] =?UTF-8?q?fix(feed):=20backup=20rebuild=20non-fatale=20?= =?UTF-8?q?=E2=80=94=20copy2=E2=86=92copyfile,=20il=20.bak=20non=20abortis?= =?UTF-8?q?ce=20piu'=20il=20feed=20live?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ...2026-07-15-feed-freeze-rebuild-copystat.md | 126 ++++++++++++++++++ scripts/analysis/rebuild_history.py | 10 +- 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 docs/diary/2026-07-15-feed-freeze-rebuild-copystat.md diff --git a/docs/diary/2026-07-15-feed-freeze-rebuild-copystat.md b/docs/diary/2026-07-15-feed-freeze-rebuild-copystat.md new file mode 100644 index 0000000..02c2149 --- /dev/null +++ b/docs/diary/2026-07-15-feed-freeze-rebuild-copystat.md @@ -0,0 +1,126 @@ +# 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`. diff --git a/scripts/analysis/rebuild_history.py b/scripts/analysis/rebuild_history.py index 9dc60a8..a0e9328 100644 --- a/scripts/analysis/rebuild_history.py +++ b/scripts/analysis/rebuild_history.py @@ -186,7 +186,15 @@ def build(asset: str, write: bool, smoke: bool) -> None: for tf, d in built.items(): path = _parquet_path(asset, tf) if path.exists(): - shutil.copy2(path, BACKUP / f"{asset.lower()}_{tf}.parquet.prebuild.bak") + # Backup best-effort: copyfile (solo contenuto, NO copystat). + # copy2 copiava anche i metadati -> os.utime/chmod su un .bak di proprieta' + # altrui (es. root) fallisce con EPERM per un membro-gruppo e ABORTIVA il + # rebuild del feed live (incidente 2026-07-15: feed fermo 7g). Un backup e' + # difensivo: un suo errore non deve MAI bloccare la scrittura del feed. + 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}") tmp = path.with_suffix(".parquet.tmp") d.to_parquet(tmp, index=False) tmp.replace(path)