# 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.
