from __future__ import annotations from fastapi import APIRouter, Depends, Query from findata_core.db import connection from findata_api import auth as A router = APIRouter(prefix="/quality", tags=["quality"]) @router.get("") def quality(dataset_id: str = "market_bars.stock.1m.raw", p: A.Principal = Depends(A.require("read:market"))): with connection() as conn: report = conn.execute("SELECT * FROM quality_reports WHERE dataset_id=%s ORDER BY created_at DESC LIMIT 1", (dataset_id,)).fetchone() by_code = conn.execute("SELECT code, severity, count(*) n, sum((detail->>'rows')::int) rows FROM quality_anomalies WHERE dataset_id=%s AND created_at > now() - interval '30 days' GROUP BY 1,2 ORDER BY n DESC", (dataset_id,)).fetchall() reports = conn.execute("SELECT id, created_at, rows_checked, anomalies, summary FROM quality_reports WHERE dataset_id=%s ORDER BY created_at DESC LIMIT 30", (dataset_id,)).fetchall() return {"latest": report, "anomalies_by_code_30d": by_code, "reports": reports, "codes": {"ohlc_inconsistent": "high < max(open,close) or low > min(open,close)", "nonpositive_price": "price <= 0", "negative_volume": "volume < 0", "zero_volume": "volume == 0 (vendor normally omits these)", "fractional_volume": "non-integral volume", "off_interval": "timestamp not on the interval grid", "duplicate": "duplicate (symbol, timestamp)", "outside_session": "outside 04:00–20:00 ET", "non_trading_day": "bars on a non-session date", "price_jump": ">25 % move between consecutive bars"}} @router.get("/anomalies") def anomalies(dataset_id: str = "market_bars.stock.1m.raw", code: str | None = None, severity: str | None = None, symbol: str | None = None, limit: int = Query(200, le=5000), p: A.Principal = Depends(A.require("read:market"))): with connection() as conn: rows = conn.execute("SELECT id, symbol, session_date, code, severity, detail, created_at FROM quality_anomalies WHERE dataset_id=%s " "AND (%s::text IS NULL OR code=%s) AND (%s::text IS NULL OR severity=%s) AND (%s::text IS NULL OR symbol=%s) ORDER BY created_at DESC, id DESC LIMIT %s", (dataset_id, code, code, severity, severity, symbol, symbol, limit)).fetchall() return {"data": rows}