From 07a8bbf5c856e4e4b26b6ab35750af7164831ecc Mon Sep 17 00:00:00 2001 From: root Date: Tue, 12 May 2026 22:01:44 +0000 Subject: [PATCH] feat(analysis): skeleton modulo data_audit (dataclass + soglie) --- src/cerbero_bite/analysis/__init__.py | 5 ++ src/cerbero_bite/analysis/data_audit.py | 95 +++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/cerbero_bite/analysis/__init__.py create mode 100644 src/cerbero_bite/analysis/data_audit.py diff --git a/src/cerbero_bite/analysis/__init__.py b/src/cerbero_bite/analysis/__init__.py new file mode 100644 index 0000000..20c9d6f --- /dev/null +++ b/src/cerbero_bite/analysis/__init__.py @@ -0,0 +1,5 @@ +"""Analysis utilities — pure functions over the state DB. + +Modules here read SQLite, never write. They are ergonomic to call +from CLI commands, notebooks, or one-off scripts. +""" diff --git a/src/cerbero_bite/analysis/data_audit.py b/src/cerbero_bite/analysis/data_audit.py new file mode 100644 index 0000000..69a98b5 --- /dev/null +++ b/src/cerbero_bite/analysis/data_audit.py @@ -0,0 +1,95 @@ +"""Data quality audit over market_snapshots + option_chain_snapshots. + +Pure functions: each takes a ``sqlite3.Connection`` and a UTC time +window, returns a frozen dataclass. No side effects, no MCP, no +writes. The CLI layer (``cli.audit``) is responsible for I/O and +formatting. + +Thresholds are module-level constants by design: the audit and the +runtime live in different contexts and must not share operational +parameters. To tune a threshold, edit this file. +""" + +from __future__ import annotations + +import sqlite3 +import statistics +from dataclasses import dataclass, field +from datetime import UTC, datetime, timedelta +from decimal import Decimal + +__all__ = [ + "ChainAuditReport", + "GapRecord", + "MarketAuditReport", + "audit_market_snapshots", + "audit_option_chain", +] + +# Tick cadence + gap tolerance. Cron is */15; +5 min tolerance covers +# late-arriving MCP responses. +_TICK_INTERVAL_MIN: int = 15 +_GAP_THRESHOLD_MIN: int = 20 + +# fetch_ok=0 streak threshold: 1-2 are transient MCP failures, 3+ is a +# pattern worth flagging. +_FETCH_OK_STREAK_THRESHOLD: int = 3 + +# A numeric column with >10% NULL in the window is too unreliable for +# backtesting that metric. +_NULL_RATE_FLAG: Decimal = Decimal("0.10") + +# Columns to NULL-audit on market_snapshots. fetch_ok / fetch_errors_json +# are excluded (they are status fields, not metrics). +_MARKET_NUMERIC_COLUMNS: tuple[str, ...] = ( + "spot", + "dvol", + "realized_vol_30d", + "iv_minus_rv", + "funding_perp_annualized", + "funding_cross_annualized", + "dealer_net_gamma", + "gamma_flip_level", + "oi_delta_pct_4h", + "macro_days_to_event", +) + + +@dataclass(frozen=True) +class GapRecord: + """One gap between consecutive market_snapshots ticks.""" + + prev_timestamp: datetime + next_timestamp: datetime + gap_minutes: int + + +@dataclass(frozen=True) +class MarketAuditReport: + asset: str + since: datetime + until: datetime + expected_ticks: int + actual_ticks: int + coverage_pct: Decimal + gaps: tuple[GapRecord, ...] = field(default_factory=tuple) + fetch_ok_zero_count: int = 0 + max_fetch_ok_zero_streak: int = 0 + null_rate_by_column: dict[str, Decimal] = field(default_factory=dict) + + +@dataclass(frozen=True) +class ChainAuditReport: + asset: str + since: datetime + until: datetime + expected_snapshots: int + actual_snapshots: int + coverage_pct: Decimal + quotes_per_snap_median: int = 0 + quotes_per_snap_p10: int = 0 + quotes_per_snap_p90: int = 0 + bid_gt_ask_count: int = 0 + iv_null_count: int = 0 + iv_null_pct: Decimal = Decimal("0") + depth_zero_pct: Decimal = Decimal("0")