16d73c309a
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>
202 lines
7.2 KiB
Markdown
202 lines
7.2 KiB
Markdown
# 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
|
||
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 = 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
|
||
ask <= 0: 37 (missing ask side)
|
||
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.
|