feat(chain): assorbita la raccolta catena opzioni, cerbero-bite dismesso
cerbero-bite viene eliminato. L'unica sua parte irreversibile e' il DATO:
una catena opzioni non si ricostruisce a posteriori (Deribit non serve book
storici, non c'e' un secondo venue). Il codice si riscrive; le ore non
raccolte no.
ASSORBITO
- scripts/live/collect_chain.py + scripts/cron_chain.sh (cron 25 * * * *):
raccolta propria, ~570 strumenti/giro, ~3 min.
- scripts/analysis/import_cb_archive.py: archivio 1.23M righe (2026-05-01+)
+ market_snapshots 17.402 righe (2026-03-26+: dealer gamma, gamma flip,
rischio liquidazioni, funding cross — dati che non abbiamo altrove).
- snapshot sqlite integrale in /opt/docker/backups/manual/ (SHA256).
NON ASSORBITO, con motivo: motore credit-spread ETH (regola "niente
short-vol da modello in deploy", conto a $52 contro minimo $720), GUI, kill
switch/dead-man/audit (abbiamo venue_watch/edge_watch/monitor_health/
fee_watch), dvol_history (fetch_dvol.py ha storia PIU' LUNGA: 2020+ contro
2026-05), decisions/positions (0 posizioni).
TRE DIFETTI DI BITE NON REPLICATI, tutti misurati il 30/07:
1. una chiamata per strumento invece di due (get_order_book?depth=3 da' gia'
quote+greche+IV+OI+book+underlying) + prefiltro OI in una chiamata sola:
551 -> ~290 chiamate per asset;
2. pacing invece di raffica. Il carico non e' mai stato il problema: 570
chiamate/ora = 0.16/s DISTRIBUITE; bite le sparava in 26s (~44/s) e si
auto-saturava il rate limit per-IP (12.186 risposte 429 in 26h, 96% al
minuto :00). Primo giro reale: 574 chiamate, 0 risposte 429. Il minuto :25
e' scelto: :00 era la raffica, :07 e' cron_book (feed 5m di SKH01).
3. quote_status esplicito {ok, no_quote, error} e book_depth NULL su errore
mai 0. "Book vuoto" e "chiamata fallita" sono cose diverse: e' per questo
che il guasto del 29/07 (50% di quote perse, 38 ore) non produsse alcun
segnale. Le righe ereditate restano 'unknown': bite non lo registrava e a
posteriori non e' ricostruibile.
Battuta di cuore in data/chain_collect/runs.jsonl anche a giro fallito,
sorvegliata da monitor_health (1h, max_age 3h): un collettore fermo non
produce niente, e il niente si legge come "nessun dato quel giorno".
Difetto trovato per strada: due formati ISO nella stessa colonna (92 righe
di backfill senza microsecondi). pd.to_datetime senza `format` ne inferisce
uno solo e manda gli altri a NaT -> il dropna a valle li toglieva in
silenzio, e la serie di contesto perdeva 5 settimane slittando dal 26/03 al
01/05. Corretto con format="ISO8601" e scarto RUMOROSO.
Book, pesi, config, strategia INVARIATI. 537 test verdi.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""IMPORT dell'archivio cerbero-bite dentro PythagorasGoal — una volta sola, prima della sua
|
||||
eliminazione (2026-07-30).
|
||||
|
||||
cerbero-bite viene smantellato; la sua raccolta continua in `scripts/live/collect_chain.py`. Questo
|
||||
script prende cio' che NON e' ricostruibile e lo porta dentro:
|
||||
|
||||
* `option_chain_snapshots` -> data/raw/cb_chain/bite_archive.parquet (1.23M righe, 2026-05-01+)
|
||||
* `market_snapshots` -> data/raw/cb_market_snapshots.parquet (17k righe, 2026-03-26+:
|
||||
spot, DVOL, RV30, IV-RV, funding perp e cross-exchange, dealer net gamma, gamma flip level,
|
||||
OI delta 4h, rischio liquidazioni long/short, giorni all'evento macro)
|
||||
|
||||
NON importato e perche':
|
||||
* `dvol_history` — il progetto ha gia' `fetch_dvol.py` con storia PIU' LUNGA (2020+ contro
|
||||
2026-05): reimportarlo aggiungerebbe una seconda copia peggiore della prima.
|
||||
* `decisions` / `positions` — 59 righe di valutazioni d'ingresso a capitale $52, 0 posizioni:
|
||||
e' il log di una strategia che il progetto ha gia' deciso di non deployare (regola
|
||||
"niente short-vol da modello in deploy").
|
||||
* `audit.log` — conservato come file nel backup, non e' una serie storica.
|
||||
|
||||
⚠️ LO STATO DELLA QUOTA NELL'ARCHIVIO E' `unknown`, NON `ok`. Bite scriveva la riga anche quando
|
||||
la chiamata falliva, con bid/ask NULL, senza registrare il perche': "book vuoto" e "chiamata
|
||||
fallita" sono indistinguibili a posteriori. Il collettore nuovo li separa (`no_quote` / `error`);
|
||||
per lo storico si dichiara l'ignoranza invece di inventare uno stato. Chi misura sull'archivio
|
||||
deve sapere che le righe senza quota sono ~1.5% fino al 28/07 e ~50% dal 29/07 (guasto di
|
||||
rate-limit, vedi diario 2026-07-30).
|
||||
|
||||
uv run python scripts/analysis/import_cb_archive.py --db /opt/docker/backups/manual/cerbero-bite-20260730/state.sqlite
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
RAW = PROJECT_ROOT / "data" / "raw"
|
||||
STORE = RAW / "cb_chain"
|
||||
ARCHIVE = STORE / "bite_archive.parquet"
|
||||
MARKET = RAW / "cb_market_snapshots.parquet"
|
||||
DEFAULT_DB = Path("/opt/docker/backups/manual/cerbero-bite-20260730/state.sqlite")
|
||||
|
||||
|
||||
def parse_ts(s: pd.Series) -> pd.Series:
|
||||
"""Timestamp ISO con formati MISTI nella stessa colonna.
|
||||
|
||||
bite scrive sia '2026-03-26T12:00:00+00:00' (backfill) sia
|
||||
'2026-05-01T15:45:00.062918+00:00' (runtime). `pd.to_datetime` senza `format` inferisce UN
|
||||
formato dal primo elemento e manda gli altri a NaT: con un `dropna` a valle spariscono in
|
||||
silenzio (qui: 92 righe, e la data d'inizio della serie slittava dal 26/03 al 01/05).
|
||||
`format="ISO8601"` accetta entrambe le varianti.
|
||||
"""
|
||||
return pd.to_datetime(s, utc=True, format="ISO8601", errors="coerce")
|
||||
|
||||
|
||||
def _drop_unparsed(df: pd.DataFrame, cols: list[str], etichetta: str) -> pd.DataFrame:
|
||||
"""Scarta le righe con timestamp illeggibile DICHIARANDOLO. Uno scarto silenzioso in un
|
||||
import e' indistinguibile da un dato che non e' mai esistito."""
|
||||
before = len(df)
|
||||
out = df.dropna(subset=cols)
|
||||
if len(out) != before:
|
||||
print(f" ATTENZIONE [{etichetta}]: {before - len(out)} righe scartate "
|
||||
f"(timestamp illeggibile) su {before}")
|
||||
return out
|
||||
|
||||
|
||||
def import_chain(db: Path) -> pd.DataFrame:
|
||||
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
|
||||
try:
|
||||
df = pd.read_sql_query(
|
||||
"""select timestamp, asset, instrument_name, strike, expiry, option_type,
|
||||
bid, ask, mid, iv, delta, gamma, theta, vega,
|
||||
open_interest, volume_24h, book_depth_top3, source
|
||||
from option_chain_snapshots""", con)
|
||||
finally:
|
||||
con.close()
|
||||
for c in ("strike", "bid", "ask", "mid", "iv", "delta", "gamma", "theta", "vega",
|
||||
"open_interest", "volume_24h", "book_depth_top3"):
|
||||
df[c] = pd.to_numeric(df[c], errors="coerce")
|
||||
df["ts"] = parse_ts(df["timestamp"])
|
||||
df["exp"] = parse_ts(df["expiry"])
|
||||
df = _drop_unparsed(df.drop(columns=["timestamp", "expiry"]), ["ts", "exp", "strike"], "catena")
|
||||
# bite non distingue "book vuoto" da "chiamata fallita": si dichiara, non si indovina.
|
||||
df["quote_status"] = df["bid"].notna().map({True: "ok", False: "unknown"})
|
||||
df["underlying_price"] = pd.NA
|
||||
df["index_price"] = pd.NA
|
||||
df["source"] = "bite:" + df["source"].astype(str)
|
||||
return df
|
||||
|
||||
|
||||
def import_market(db: Path) -> pd.DataFrame:
|
||||
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
|
||||
try:
|
||||
df = pd.read_sql_query("select * from market_snapshots", con)
|
||||
finally:
|
||||
con.close()
|
||||
df["ts"] = parse_ts(df["timestamp"])
|
||||
return _drop_unparsed(df.drop(columns=["timestamp"]), ["ts"], "contesto")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--db", type=Path, default=DEFAULT_DB)
|
||||
args = ap.parse_args()
|
||||
if not args.db.exists():
|
||||
print(f" DB assente: {args.db}")
|
||||
return 1
|
||||
|
||||
STORE.mkdir(parents=True, exist_ok=True)
|
||||
ch = import_chain(args.db)
|
||||
ch.to_parquet(ARCHIVE, index=False)
|
||||
st = ch["quote_status"].value_counts().to_dict()
|
||||
print(f" catena -> {ARCHIVE}")
|
||||
print(f" {len(ch):,} righe {ch['ts'].min():%Y-%m-%d} -> {ch['ts'].max():%Y-%m-%d} "
|
||||
f"| ok {st.get('ok', 0):,} unknown {st.get('unknown', 0):,} "
|
||||
f"({100*st.get('unknown', 0)/len(ch):.1f}%)")
|
||||
|
||||
mk = import_market(args.db)
|
||||
mk.to_parquet(MARKET, index=False)
|
||||
print(f" contesto -> {MARKET}")
|
||||
print(f" {len(mk):,} righe {mk['ts'].min():%Y-%m-%d} -> {mk['ts'].max():%Y-%m-%d} "
|
||||
f"| colonne: {', '.join(c for c in mk.columns if c not in ('ts', 'asset'))[:110]}...")
|
||||
print("\n NON importati (motivo nel docstring): dvol_history, decisions, positions, audit.log")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user