quality 模块

August 22, 2026 · View on GitHub

路径:src/cnequity/quality/

数据质量保障:run 级审计、湖级健康、跨数据集对账、主备源 diff、failover 快照写入。


文件一览

文件职责
audit.pyrun_audit(), lake_health()
historical_validity.py历史研究窗口的机器可读严格合同(默认 all-A,也支持 all_a_sh_sz
dataset_checks.pyPK 重复、mock、空集、行数突变
cross_checks.pybars×calendar、valuation×bars、adj 对账、ST 标签对照
macro_checks.py宏观月度序列的陈旧检测与修订留痕
authority_checks.py与发布方(统计局 / 交易所)对照
source_diff.py主源 vs snapshot 字段 diff
failover.py主源失败时写 source_snapshots

质量报告(包括权威交叉校验结果)通过同目录临时文件原子发布;外部校验失败只记录 状态,不用一份截断或半写入的报告覆盖上一次可读证据。


audit.py

run_audit(cfg, run_id, trade_date) → int

写入 meta/quality/findings/{run_id}.json

  • 各数据集 dataset_checks
  • cross_checks(若依赖数据集已 compact)
  • source_diff(若配置了 failover)
  • 上下文 findings(compact 跳过、derive 警告)

返回 findings 条数。

lake_health(cfg, anchor_date) → dict

cne audit --full 使用。检查:

说明
empty_datasets无 parquet 的注册数据集
stale_datasets水位落后超过 max_staleness_days
findings_by_severityerror / warning / info 计数
info_findings已持久化的非阻断质量证据,例如已核实的源端历史缺口;不会降低 healthy,但不会被丢弃
historical_universe_validity历史研究门禁(默认 all-A,也支持 all_a_sh_sz);通过 /api/health 暴露为 universe、ready、窗口和 blockers
historical_all_a_st_evidence与选定研究口径并列保存的全 A ST 证据基线;用于防止 scoped READY 隐藏 BJ 来源限制
index_bars_calendar_coverage指数序列与交易日历对账;已核实的 399001.SZ 18 个共同源缺口为 info,未知缺口仍为 warning
adj_factor_reconciliation复权收益极值 + 缺 corporate_actions;已记录的缩股/减资等股本重组由 share_structure 解释
healthy无 error 级 finding

与普通 run 级审计只检查当前活跃分区不同,--full 会对每个已有数据集的全部 Parquet 文件逐文件执行 schema contract 检查,并对全历史范围执行 PK/必填字段 检查。扫描按文件有界,不会把整个数据集一次性读入内存;历史文件可以缺少后来 新增的可空列,但不能缺少主键、来源、版本、抓取时间或行情核心字段,也不能含 NaN/Inf、非法 OHLC 关系等值。这样历史脏数据会进入 error_findings,而不是只在 最新分区被发现。

此外写入 meta/quality/historical-validity-latest.json。它把以下三项组合为独立的 historical_<universe>_universe_validity 合同:

  • daily_bars 是否完整包住请求窗口
  • 历史 ST 标签覆盖是否早于窗口起点
  • 退市目录发现、末次有效成交与 instruments 身份是否通过

这不会改变运维 healthy 的含义;日更可以健康,但长窗口研究仍被标记为 universe_ready=false。需要让命令对研究缺口返回非零时,显式运行:

cne audit --full --research-start 2020-01-01 --research-end 2024-12-31
# 若使用沪深子集,显式记录研究口径:
cne audit --full --research-universe all_a_sh_sz \
  --research-start 2020-01-01 --research-end 2024-12-31

该合同不替下游证明复权精确性、特征覆盖或财报 PIT 语义,这些由研究工作台继续组合门禁。

dense 数据集与 watermark

daily_barsindex_bars、分钟线、分笔和 adj_factors 注册为 coverage_mode=session_dense:它们承诺覆盖范围内每个交易日至少有数据。compact 更新这类数据集的 watermark 时,会用交易日历检查从覆盖起点到最新落盘日的连续前缀, 遇到内部缺口就停在缺口前一天。这样后续增量窗口仍会重试缺失日;仅靠最大 trade_date 会把缺口永久留在 watermark 之后。稀疏事件/快照数据不使用这个闸门, 因为没有记录不等于抓取失败。


dataset_checks.py

检查严重度
schema contracterror(全量审计按文件检查历史必填字段、非有限数值与行情语义)
PK 重复error
mixed_partition_granularityerror(盘上分区粒度与注册表不一致;跨粒度会让同一 PK 出现两次)
source="mock" 且非测试error
分区行数相对上次 run 突变warning
partition_fragmentationwarning(分区过细,几乎全是 footer)
空数据集(预期非空)warning

cross_checks.py

检查说明
daily_bars vs trading_calendar交易日无真实成交 bar(volume=0 的停牌占位行不算覆盖)
valuation vs daily_bars估值有、真实成交行情无
daily_bars_amount_completeness按 source 检查成交额完整性;Sina 不发布 amount,记录为 info,其他源缺失记录为 warning
adj_factor_coverage按标的核对 daily_barsadj_factors 的起止跨度;因子缺失或只覆盖部分历史时记录为 warning
adj_factor_reconciliation真实成交 bar-to-bar 复权收益 > 阈值;除权日缺 corporate_actions(缩股/减资等已在 share_structure 记录的股本重组除外)
st_label_crosschecktrading_status 的 ST 标签 vs instruments 简称里的 ST 前缀

st_label_crosscheck

两侧都已在 curated 里,不产生任何请求

这是一个真正独立的对照:ST 简称由交易所指定,经 TDX 二进制协议进入 instruments;风险警示板名单经 东财 HTTP 进入 trading_status。不同厂商、 不同协议、同一个交易所事实。已移除的 AkShare ST 并集只是看起来独立——它查的是 和东财适配器完全相同的 push2 端点与 fs 过滤条件,永远不可能给出不同答案 (issue #3)。

容差 ST_CROSSCHECK_MAX_DISAGREEMENT = 3:改名当天两个 step 分别抓取, 个位数的边界名单属于正常抖动。

2026-08-01 实测:两侧各 205 个,对称差 0


macro_checks.py

检查严重度说明
macro_indicator_stalewarning月度指标最新观测距运行日超过阈值
macro_value_revisedwarning / info已入湖的 (indicator_id, obs_date) 数值被改写

为什么需要 macro_indicator_stale

月度序列每次运行都重抓全量、按 (indicator_id, obs_date) 去重,所以 一个停止发布的源和一个健康的源在 curated 里长得一模一样——旧行都还在, 没有任何 step 会失败。只有「最新观测距今多远」能暴露它。

阈值按各指标实测发布节奏 + 约 1.5 个月余量设定(MONTHLY_STALE_DAYS), 即错过大约一个发布周期才告警。2026-08-01 实测余量:

indicator最新观测滞后阈值余量
pmi_manufacturing2026-07-311d45d44d
m2_yoy2026-06-3032d75d43d
social_financing2026-06-3032d75d43d

m2_yoysocial_financing 同为央行月中发布,所以阈值相同。

这条检查的动机就来自一次实测:社融原先读商务部转载,落后两个发布周期且带着 修订前的旧值,而湖里看不出任何异常——旧行都在,没有 step 失败。换直连央行 之后滞后回到 32 天,但检查保留:下一次某个源静默停更时,只有它会说话。 见 pboc 适配器

为什么修订是「留痕」而不是「阻止」

compact 按主键保留最新 fetched_at,所以发布方修订某个月时旧值会被覆盖且不可恢复。

这个覆盖行为是要保留的——正是它让 #3 里错误的 m2_yoy 历史在下次运行时自愈, 不需要迁移脚本。所以检查放在 step 里、写入之前:比对增量与 curated,把变化记进 findings,然后照常写入。curated 仍然只持有最新发布值,findings 是旧值存在过的唯一记录。

判定:相对变化 > REVISION_MATERIAL_RELATIVE(5%)记 warning,否则 info。


authority_checks.py

其他检查看的都是湖内自洽:值是不是陈旧、有没有被改写、已有的两个源是否一致。 它们都看不见「厂商按时发布、格式正确、但数字是错的」——而这正是 #3 里 m2_yoy 的形态(整段历史存的是 M0 环比,按时、字段类型也对)。

要抓这种,必须有一个来自外部的读数

检查对照对象严重度
macro_pmi_vs_nbs国家统计局采购经理指数发布稿error
st_labels_vs_exchange上交所 / 深交所上市列表的 ST 简称error

两者都要发网络请求,因此都按 [sources.*] 开关控制,且缺省为关 (与 daily_bars_close_crosscheck_findings 一致)——没有配置就意味着离线湖, cne audit 不该悄悄开始发请求。源不可达时静默跳过。

为什么用发布稿而不是统计局的查询接口

data.stats.gov.cn/easyquery.htm 在非大陆出口返回 403(WAF UrlACL), 而站点根路径与 www.stats.gov.cn 下的发布稿正常。把检查建在被封的那条路上, 等于让它对最难自查的那批用户静默失效——和 ths 适配器记录的东财 push2his 是同一个坑。所以读发布稿里那句多年未变的话:

7月份,制造业采购经理指数(PMI)为49.2%,比上月下降1.1个百分点

只读最新一期。这个检查是为了发现「厂商开始偏离发布方」, 一个此刻正确、两年前错过的厂商不是它要防的东西。

为什么只比对「共同宇宙」

交易所把一家公司挂到正式退市,而行情源在它停止交易时就删掉了。 2026-08-01 实测:上交所仍将 600355、603388 标为 ST,而东财与 TDX 都已不再收录。 两个方向都只在双方都有的标的上比对——只限制一侧不够,否则这 2 个会变成 永久性缺口,把容差吃掉大半,真正的分歧反而报不出来。

上交所的 stockType=1(主板)与 stockType=8(科创板)下载不含风险警示板, 实测漏掉 5 个 ST。用 stockType=10(全部 A 股)才完整。

为什么没有 M2

央行的货币供应量表只发余额,且自 2025-01 修订了 M1 口径。 从余额跨口径变更去推同比,等于自己造一个发布方刻意用可比口径另行计算的数, 报出来的「偏离」会是我们自己算法的产物。东财的 M2 已人工核对过 (2026-06:同比 8%、余额 356.71 万亿,M1/M0 亦同),常驻检查等央行直接发布该比率。

留痕

结果同时写入 meta/quality/source_diffs/authority-{date}.json, 即使没有分歧也写。checks 会明确记录 agreeddisagreedskipped_disabledskipped_no_curatedskipped_not_dueunavailableerror; 返回给普通 audit 流的 findings 仍只包含实际分歧。


source_diff.py

读取 meta/source_snapshots/ 与 curated 抽样比对;除了字段漂移,也会双向检查主备 source 的 primary-key 覆盖,避免只发现“备源少了主源的行”,却漏掉“主源少了备源的行”:

  • 价格类:price_tolerance_bps(默认 10bps)
  • 先比较主备源 PK 覆盖,再比较字段值;backup_missing_for_dateprimary_missing_for_datebackup_coverage_gapno_pk_overlap 都是 warning,不会把“没有可比数据”误读成“一致”
  • 输出 meta/quality/source_diffs/{run_id}.json

同 PK 不自动换源(ADR-0003 switching)。不相交 key 的路由(BJ→sina、tip TDX 缺口→东财 clist)见 ADR-0005


failover.py

配置驱动([failover.datasets]):

  1. 主源 batch 失败(或 tip 缺 key)
  2. tip 日:一次 push2 clist → 只把缺失 key 写入 staging(source=eastmoney),并写 snapshot
  3. 多日窗口:对失败 symbol 走 per-symbol kline → staging + snapshot
  4. audit 阶段 source_diff 仍比对 primary vs snapshot

相关文档