# Data Quality Audit — Design Spec **Date:** 2026-05-12 **Status:** Approved (design phase) **Author:** session-driven (operator + agent) ## Motivation Prima di costruire il backtest non-stilizzato su `option_chain_snapshots` (prossimo macro-step del progetto), serve confermare che i dati raccolti negli ultimi 11 giorni (ETH) e 8 giorni (BTC) siano usabili: copertura temporale piena, niente buchi sistemici, niente quote malformate (bid > ask, IV mancante, depth book a zero). Lo stesso audit dev'essere ri-eseguibile periodicamente come check operativo. `market_snapshots` rientra nello scope per simmetria (entrambe le tabelle alimentano la decisione di entrata e il monitoring), mentre `dvol_history` è escluso: appena migrato a multi-asset (commit `19695e4`), serie troppo corta per BTC (29 righe al momento del design) e copertura ETH già implicita in `market_snapshots`. ## Scope **In scope:** - `market_snapshots`: continuità temporale, fetch_ok streaks, NULL rate per colonna numerica, parità ETH/BTC. - `option_chain_snapshots`: snapshot mancanti, distribuzione quote per snap, bid/ask sanity, IV null rate, book depth. - CLI subcommand `cerbero-bite audit data`, output stdout + opzionale `--json`. **Out of scope:** - `dvol_history`, `decisions`, `positions`, `instructions`, `manual_actions` (non rilevanti per il backtest non-stilizzato). - Audit di consistenza cross-tabella (es: per ogni snapshot chain esiste uno snapshot market) — interessante ma rinviato. - Persistenza dei risultati audit nello stesso DB. ## Architecture ``` src/cerbero_bite/analysis/ __init__.py data_audit.py # logica pura, no I/O lato MCP src/cerbero_bite/cli.py # nuovo subcommand `audit data` tests/unit/ test_data_audit.py # DB temporaneo + seed deterministico ``` `data_audit.py` espone funzioni pure che prendono una `sqlite3.Connection` e una finestra temporale, ritornano `dataclass` di risultati. Il CLI apre la connection in read-only, chiama le funzioni, formatta l'output. Funzioni principali: ```python @dataclass(frozen=True) class MarketAuditReport: asset: str expected_ticks: int actual_ticks: int coverage_pct: Decimal gaps_over_threshold: list[GapRecord] fetch_ok_zero_count: int max_fetch_ok_zero_streak: int null_rate_by_column: dict[str, Decimal] @dataclass(frozen=True) class ChainAuditReport: asset: str expected_snapshots: int actual_snapshots: int coverage_pct: Decimal quotes_per_snap_median: int quotes_per_snap_p10: int quotes_per_snap_p90: int bid_gt_ask_count: int iv_null_count: int iv_null_pct: Decimal def audit_market_snapshots(conn, *, asset, since, now) -> MarketAuditReport: ... def audit_option_chain(conn, *, asset, since, now) -> ChainAuditReport: ... ``` ## Checks & Thresholds | Tabella | Check | Soglia "bad" | Rationale | |---|---|---|---| | market_snapshots | gap tra tick consecutivi | > 20 min | cron è `*/15`; +5 min tolleranza | | market_snapshots | streak `fetch_ok=0` | ≥ 3 consecutivi | 1-2 = transient MCP, 3+ = pattern | | market_snapshots | NULL rate per colonna | > 10% nella finestra | una metrica con >10% NULL non è affidabile per backtest | | option_chain_snapshots | snap mancanti | qualsiasi (count visibile) | cron `*/15`, ogni miss è significativo | | option_chain_snapshots | quote/snap < 50% mediana 24h | qualsiasi | rilevatore di chain truncate (mismatch with width filter) | | option_chain_snapshots | bid > ask | qualsiasi | dato corrotto, da indagare | | option_chain_snapshots | IV null/non-parseable | conteggio + % | IV è chiave per BS skew calibration | > **Nota:** il check `book_depth_top3 = 0` originariamente previsto è > stato rimosso. La colonna è NULL by design sugli snapshot (il collector > `option_chain_snapshot_cycle` non chiama l'orderbook per ogni strike > per non saturare l'API Deribit); viene popolata solo dal path > `entry_cycle` per gli strike candidati al picker. Il check di liquidità > ha senso solo lì, non in questo audit. Le soglie sono costanti modulo (non config YAML) per ridurre il blast radius dei cambi: il backtest e l'audit girano in contesti diversi, non condividono parametri operativi. ## CLI ``` cerbero-bite audit data [--db PATH] [--since DAYS] [--json] [--asset ETH|BTC] ``` > **Deviazione dal design originale**: il group `audit` era già usato per > `audit verify` (hash chain dell'audit log). Il data-quality audit è > stato inserito come sibling `audit data` per evitare collisione di > namespace, anziché come comando top-level `cerbero-bite audit`. - `--db PATH` (default `data/state.sqlite`): path al DB SQLite. - `--since DAYS` (default `7`): finestra di analisi, retro dal `now()` corrente. - `--json` (default off): stampa solo dump JSON serializzabile, niente tabelle umane. - `--asset` (default `tutti`): filtra ad un singolo asset. Exit code: - `0`: audit completato (a prescindere dai problemi trovati). - `2`: errori di connessione/DB (DB inesistente, schema mancante). Niente exit code per "found issues": l'audit è informativo, decide l'umano. Far diventare l'audit un gate CI è out of scope. ## Output **Stdout (default):** ``` === ETH — market_snapshots (last 7d, 2026-05-05 → 2026-05-12) === ticks: 672 expected: 672 coverage: 100.0% gaps > 20min: 0 fetch_ok=0: 4 rows (max streak: 1) null rate: dealer_net_gamma 2.1% oi_delta_pct_4h 0.3% === ETH — option_chain_snapshots (last 7d) === snapshots: 672 expected: 672 coverage: 100.0% quotes/snap: median 55 p10 50 p90 60 bid > ask: 0 IV null: 12 quotes (0.03%) === BTC — ... ``` **JSON (`--json`):** ```json { "since": "2026-05-05T20:46:00+00:00", "until": "2026-05-12T20:46:00+00:00", "assets": { "ETH": { "market": {"expected_ticks": 672, "actual_ticks": 672, ...}, "chain": {"expected_snapshots": 672, ...} }, "BTC": {...} } } ``` ## Testing `tests/unit/test_data_audit.py`. Per ogni funzione: - DB temporaneo (`tmp_path`), schema migrato via `run_migrations`. - Seed deterministico: insert manuali per riprodurre lo scenario. - Test cases: - market: copertura piena → 100%; un gap iniettato → conteggio gap=1; streak `fetch_ok=0` lunga 3 → flagged. - chain: snap mancante → expected − actual = 1; quote dimezzate in un tick → quotes/snap p10 cala; `bid=10 ask=5` → bid>ask=1. - 0 dipendenze nuove (sqlite + pytest standard). ## Performance Tabelle attuali: ~57k quote chain. Le query usano gli index `idx_option_chain_asset_ts` e `(asset, timestamp)` di `market_snapshots`. L'audit deve girare in < 2s su 7gg. ## Anti-goals (esplicito) - Nessun salvataggio dei risultati nello stato del DB. - Nessun trigger automatico (no cron job, no APScheduler). - Nessun alert/notifica: stdout + JSON sono lo strumento, l'operatore decide cosa farne. - Nessun ML / detection di anomalie sofisticate. Soglie costanti. ## Open Questions Nessuna al momento della scrittura. Eventuali punti emergeranno durante l'implementazione e andranno annotati qui.