Skip to content

feat(data): add report-only data quality checks (D3) - #45

Merged
StackOverFlow11 merged 2 commits into
mainfrom
feat/data-layer-d3-quality-report
Jun 19, 2026
Merged

feat(data): add report-only data quality checks (D3)#45
StackOverFlow11 merged 2 commits into
mainfrom
feat/data-layer-d3-quality-report

Conversation

@StackOverFlow11

Copy link
Copy Markdown
Owner

Summary

Data Layer D3 — a small, pure, report-only data-quality layer that flags
suspicious upstream daily-market / adj_factor / 1min-intraday data near ingestion.
It never mutates data, filters rows, repairs values, alters cache coverage,
changes qfq/front-adjust, or touches factor/alpha/portfolio/runtime — it only
surfaces findings. No cache / feed / backtest semantics changed.

This PR is the library + tests only; the optional data-update integration is
deferred to D3b (the goal sanctions deferral) — no config change, no extra
Tushare calls.

Changed files (8, all new)

  • data/quality/__init__.py, data/quality/report.py, data/quality/market.py,
    data/quality/intraday.py, data/quality/_frames.py
  • tests/test_data_quality_market.py, tests/test_data_quality_intraday.py,
    tests/test_data_quality_report.py

No other file is touched.

Quality checks implemented

Daily market / adj_factor (market.py): duplicate (date,symbol),
non-positive OHLC, high<low, close outside [low,high], negative
volume/amount, invalid adj_factorhard; extreme close day-over-day
move (|pct_change|>threshold, default 0.5) and missing dates vs an explicit
calendar → warning.

1min intraday (intraday.py): duplicate bars (bar_end/time,symbol),
non-monotonic timestamps, non-positive OHLC, high<low, close outside range,
negative volume/amounthard; missing minutes vs an explicit calendar →
warning.

Reporting (report.py): immutable QualityFinding; make_finding bounds
examples to 5 and cleans values; findings_to_frame (stable, deterministically
ordered, hard-first); render_report (deterministic Markdown, explicit "no
findings" on clean input); has_hard. Findings carry only dataset/check metadata

  • bounded {date/time, symbol, value} examples.

Report-only confirmation

  • Pure functions: input DataFrame → findings; inputs never mutated.
  • Clean synthetic daily and intraday panels produce zero hard findings.
  • "hard" is a report severity, not a default exception; nothing fails a
    backtest. No integration hook in this PR (deferred to D3b, default-off when added).

Verification

  • targeted: pytest tests/test_data_quality_market.py tests/test_data_quality_intraday.py tests/test_data_quality_report.py → all pass (34 tests).
  • pytest -p no:cacheprovider618 passed (584 prior + 34 new).
  • ruff check .All checks passed!
  • validate-config over all 17 config/*.yaml → all OK (no config added).
  • run-phase0 --config config/example.yamlic_mean=0.9600, annual_return=0.8408 (no behavior drift).
  • git diff --check main...HEAD → clean; merge-marker scan → 0.
  • Scope-smell check (rg "TushareCache\(|CoverageLedger\(|PanelStore|simulate_fills|RollingICWeightAlpha|EqualWeightAlpha" data/quality tests) → 0 hits — the quality layer references no runtime/alpha/portfolio/cache internals.

Secret scan

  • real .config.json token value over changed files → 0 hits.
  • The only .config.json / tushare.token literals in the diff are the
    negative-assertion scan targets in
    test_data_quality_report.py::test_render_has_no_secret_looking_paths (the
    goal-required test that asserts the renderer does not emit secret-looking
    paths) — not leaked data, not in any rendered report/log.

Please review before merge.

A small, pure, report-only data-quality layer that flags suspicious upstream
daily-market / adj_factor / 1min-intraday data near ingestion. It NEVER mutates
data, filters rows, repairs values, alters cache coverage, or touches qfq /
factor / alpha / portfolio / runtime logic — it only surfaces findings.

New package data/quality/:
- report.py: QualityFinding (immutable) + make_finding (bounds examples to 5,
  cleans values), findings_to_frame (stable, deterministically ordered),
  render_report (deterministic Markdown; "no findings" on clean), has_hard,
  clean_value. Findings carry only dataset/check metadata + bounded
  {date/time, symbol, value} examples — no token, no secret path, no unbounded dump.
- market.py: daily checks — duplicate (date,symbol), non-positive OHLC, high<low,
  close outside [low,high], negative volume/amount, invalid adj_factor (hard);
  extreme close moves (|pct_change|>threshold) and missing dates vs an explicit
  calendar (warning). run_market_checks / run_adj_factor_checks orchestrators.
- intraday.py: 1min checks — duplicate bars, non-monotonic timestamps,
  non-positive OHLC, high<low, close outside range, negative volume/amount (hard);
  missing minutes vs an explicit calendar (warning). run_intraday_checks.
- _frames.py: pure index-normalization + bounded example extraction helpers.

Pure functions: input DataFrame (raw cache-shaped columns or MultiIndex panel) ->
findings; inputs are never mutated. Clean input yields zero hard findings.

Tests (tests/test_data_quality_{market,intraday,report}.py): each daily/intraday
issue is caught; clean panels produce zero hard findings; examples are bounded
(<=5) and deterministic; rendering is deterministic and asserts no secret-looking
paths in output.

Integration deferred to D3b: this is the library + tests only; no data-update
hook, no config change, no extra Tushare calls. Non-goals (unchanged): cache
semantics, CoverageLedger storage, concurrency, schema registry, PanelStore,
factor/alpha/portfolio/runtime/fills/OOS/analytics math.
…ew fix)

Codex review found a contract gap: render_report() (and stored findings) could
emit a secret-looking config path or token key if a caller passed one in a
finding note or example value — the checks never do, but the public report API
did not defend against it.

Add a small, deterministic report-safety guard in data/quality/report.py:
- sanitize_text() redacts -> "[REDACTED]": any path ending in .config.json, the
  literal tushare.token, and obvious token=.../token: ... values. Idempotent;
  benign values (symbols, ISO dates, numbers) are untouched.
- applied at the three string entry points: clean_value (string example values),
  make_finding (note), and _format_examples (rendered output, so a secret-looking
  example KEY is covered too).

Update tests/test_data_quality_report.py to pass secret-looking inputs (a full
.config.json path + tushare.token in example values AND in note) and assert they
are redacted from both the stored finding and the rendered report, while benign
symbols/ISO dates still render and examples stay bounded to 5.

Report-only and scope-tight: no data-update, config, cache/ledger, feed, factor/
alpha/portfolio/runtime, or PanelStore change; D3 stays library + tests only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant