Nuovo campo ChainAuditReport.ask_zero_count. La logica del loop parsifica prima l'ask: se ask<=0 è "missing ask side" (su Deribit significa nessun ordine in vendita), conta in ask_zero_count e non applica il check bid>ask. Solo con ask>0 si confronta con bid. Sul DB prod gli "bid>ask" del primo audit (27 ETH + 10 BTC negli ultimi 7d) erano tutti falsi positivi con ask=0 — concentrati sulla weekly 2026-05-29, OI alti ma volume basso. Dopo il fix: bid>ask=0, ask<=0=83 ETH + 13 BTC. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.2 KiB
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:
@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
ask_zero_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 (con ask > 0) | qualsiasi | dato corrotto, da indagare |
| option_chain_snapshots | ask ≤ 0 | qualsiasi | missing ask side: best ask vuoto al momento della query (≠ cross BBO) |
| option_chain_snapshots | IV null/non-parseable | conteggio + % | IV è chiave per BS skew calibration |
Nota: il check
book_depth_top3 = 0originariamente previsto è stato rimosso. La colonna è NULL by design sugli snapshot (il collectoroption_chain_snapshot_cyclenon chiama l'orderbook per ogni strike per non saturare l'API Deribit); viene popolata solo dal pathentry_cycleper 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
auditera già usato peraudit verify(hash chain dell'audit log). Il data-quality audit è stato inserito come siblingaudit dataper evitare collisione di namespace, anziché come comando top-levelcerbero-bite audit.
--db PATH(defaultdata/state.sqlite): path al DB SQLite.--since DAYS(default7): finestra di analisi, retro dalnow()corrente.--json(default off): stampa solo dump JSON serializzabile, niente tabelle umane.--asset(defaulttutti): 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
ask <= 0: 37 (missing ask side)
IV null: 12 quotes (0.03%)
=== BTC — ...
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 viarun_migrations. - Seed deterministico: insert manuali per riprodurre lo scenario.
- Test cases:
- market: copertura piena → 100%; un gap iniettato → conteggio gap=1;
streak
fetch_ok=0lunga 3 → flagged. - chain: snap mancante → expected − actual = 1; quote dimezzate in
un tick → quotes/snap p10 cala;
bid=10 ask=5→ bid>ask=1.
- market: copertura piena → 100%; un gap iniettato → conteggio gap=1;
streak
- 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.