A股全市场行情快照的可用性校验:分层抽样、缺失值分类与可复现记录 一句话结论全市场快照的难点不在于「能不能拉到数据」而在于你能否说清这一份快照覆盖了谁、缺了谁、缺得是否合理以及下次能不能拉出一份可与之对比的快照。1. 先说结论「跑通了」和「能用」是两件事个人开发者第一次拉全市场快照验收标准往往只有三条请求没报错、DataFrame 有数据、行数看着差不多。这三条只验证了链路是通的没有验证数据可用。真正会咬人的是后面三件事标的池边界不清这份快照到底覆盖多少只和上一份比多了谁、少了谁缺失值含义不明某个字段是 NaN是停牌、是接口没返回、还是这个字段对该标的本来就不适用无法复现一周后策略信号异常回不到当时那份快照只能靠回忆。这三个问题在单标的调试时几乎不会出现一旦扩展到全市场就会同时爆发。原因很简单全市场是批量场景批量场景里的每一个「偶发」都会变成必然。2. 三个真实的失效模式2.1 覆盖率漂移行数稳定不代表成分稳定最常见的误判是盯着总行数。总行数 5400 和昨天一样就认为标的池没变。但标的池是集合不是计数。同一天里可能有新股上市、有标的退市或长期停牌被剔除、有代码段调整。行数不变完全可能对应「进一只、出一只」。对策略的影响是直接的如果策略依赖横截面排序比如全市场动量、行业中性化成分变化会改变排序基准回测与实盘的差异有一部分就来自这里而不是策略逻辑本身。判断方法不要比行数比集合。用set(current) - set(previous)和反向差集把新增和消失的代码列出来。新增通常有合理解释新股消失则需要逐个确认。2.2 把交易性缺失当成数据质量事故停牌标的的价格字段为空、成交量为 0这是业务事实不是数据错误。如果校验脚本一刀切地把 NaN 判为异常你会得到一份天天报警的报告最后所有人都不看它。反过来更危险把传输失败产生的 NaN 当成停牌放过缺失数据就悄悄进了因子计算。所以缺失值必须先分类再决定处理方式。2.3 快照之间不可比非原子性全市场快照数据量大实际获取时可能分多批完成。如果分页或分批拉取不同批次拿到的其实是不同时刻的市场状态。这会导致一个隐蔽问题你在做跨标的横截面比较时比较的基准时间并不一致。对低频策略影响有限但对任何依赖同一时刻截面排序的逻辑这是一个需要显式记录的口径问题。工程上的处理不是消除它做不到而是记录它把拉取开始时间、结束时间、批次数量写进复现记录让这个不确定性可见。3. A股全市场快照的易错点清单易错点典型表现排查方向标的代码格式混用600519/600519.SH拼接后集合去重失效统一为「6 位数字 交易所后缀」用正则强校验交易所后缀只处理 SH/SZ漏掉北交所A股含沪、深、京三个交易所后缀需覆盖 SH/SZ/BJ停牌标的价格为 NaN 或成交量 0被误判为异常先分类再判定停牌属交易性缺失涨跌停标的价格无变化但成交量为 0与停牌外观相似结合当日是否有成交、是否封板区间综合判断字段单位成交量是股还是手成交额是元还是万元以官方字段说明为准不要凭经验假设时间戳口径快照时间、交易时间、本地接收时间混用三者分开存不要只保留一个重复行分批拉取时边界重叠同一代码出现两次落库前按代码去重并计数复权在实时快照上套用复权因子复权面向历史 K 线序列实时快照本身是原始价这张表里最容易被低估的是「代码格式统一」。它看起来是小事但它决定了后续所有集合运算是否正确。格式不统一时去重、差集、关联全部失效而且不会报错。4. 抽样不要只抽头部全市场逐条人工核对不现实抽样是必需的。但随机抽 20 只和分层抽 20 只发现问题的概率差很多。推荐的四个分层维度交易所层沪、深、京各抽若干。北交所标的数量少、代码段特殊最容易在解析环节被漏掉。流动性层按成交额分位切四档每档都抽。只抽头部等于只验证了最不可能出问题的那部分。价格层高价股、低价股各抽。涉及价格精度与字段截断的问题只在特定区间暴露。状态层当日停牌、涨跌停、新股各抽几只。这三类是缺失值和异常值的高发区。抽样必须是固定种子的。否则每次跑出来样本不同问题复现不了。种子本身要写进复现记录。5. 缺失值先分类再处理把缺失分成三类处理方式完全不同类型特征处理方式结构性缺失某个字段整列全空或全为默认值视为 schema 变更或字段口径调整先停下来确认交易性缺失集中在停牌、未上市、无成交标的且跨字段一致属业务事实保留并在下游显式处理传输性缺失随机散落同一代码不同字段缺失不一致视为数据获取问题重取或标记该批次不可用判定的关键不是「有没有 NaN」而是缺失是否成簇。交易性缺失高度聚集在少数代码上传输性缺失则分散、无明显规律。一个可用的启发式规则价格为空且成交量为 0 → 优先判为停牌交易性价格为空但成交量有值 → 判为异常需要人工确认同一代码部分字段有值、部分为空且无业务解释 → 判为传输性这只是起点不是结论。任何启发式都需要在你的标的池上用一段时间的实际数据校准。6. 复现记录记什么才有用复现记录不是日志。日志回答「程序做了什么」复现记录回答「当时的数据长什么样」。最小可用字段集字段作用run_id唯一标识本次拉取captured_at拉取时刻UTC区分快照时间与本地时间rows/unique_codes行数与去重后代码数两者不等说明有重复fingerprint对关键列做排序后哈希用于判断两次快照是否一致sample_seed/sample_codes抽样种子与被抽中的代码支持事后复核checks覆盖率、缺失分类、schema 校验的结果摘要sdk_version依赖版本方法签名变化时能追溯fingerprint是这里性价比最高的一项。它不需要存全量快照就能回答「昨天和今天是不是同一份数据」。只有指纹变化时才需要去比对具体差异。7. QuantDash 在这条链路里的位置前面讨论的抽样、缺失分类、复现记录本质上都是拿到 DataFrame 之后的事。但数据能不能一次性、结构化地拿到决定了后面这些校验的成本。QuantDash专业金融数据 API / 量化数据平台官方公开的能力中与本文场景直接相关的包括市场覆盖A股沪、深、京、ETF、美股、港股行情数据实时行情快照以及日/周/月/季/年 K 线、A股分钟 K 线1m/5m/15m/30m/60m、日内分时、五档盘口查询方式单标的查询、批量查询、标的池查询、时间区间查询支持批量 K 线、批量日内分时、批量五档盘口统一标的代码如600519.SH、000001.SZ、920047.BJ、AAPL.US、00700.HK开发方式Python SDKpip install quantdashPython 3.9、REST API、Pandas/DataFrame 输出基础数据除权因子、标的信息、标的元数据对本文场景来说这三点最有价值统一标的代码减少了「格式归一化」这一层工程标的池查询让「全市场」有一个明确边界而不是自己拼代码段DataFrame 输出让校验代码可以直接写不需要先做一次数据搬运。需要说明的边界标的池的具体标识符、快照返回字段、字段单位、快照刷新频率等细节以 QuantDash 官方技术文档当前说明为准。本文不假设这些细节所有校验逻辑都对列名做一层映射以便和实际返回结构对齐。8. Python 实战从取数到校验8.1 取数层取数调用方式随 SDK 版本变化这里不写死方法签名避免与当前版本不一致。请按官方技术文档完成调用并统一成下面约定的列。importos# API Key 从环境变量读取不要硬编码进仓库API_KEYos.getenv(QUANTDASH_API_KEY)# 按 QuantDash 官方技术文档完成标的池实时快照的调用# 返回 DataFrame 后统一映射为以下列名再进入校验流程# code 标的代码如 600519.SH# last 最新价# volume 成交量# amount 成交额# ts 快照时间defnormalize(df,mapping):把官方返回的列映射为本文约定的标准列。returndf.rename(columnsmapping)[[code,last,volume,amount,ts]]mapping是一个显式的映射字典比如{最新价: last}。把它写在配置文件里而不是代码里官方字段调整时只改一处。8.2 结构校验与覆盖率importreimportpandasaspd CODE_PATTERNre.compile(r^\d{6}\.(SH|SZ|BJ)$)defcheck_schema(df,required(code,last,volume,amount,ts)):missing_cols[cforcinrequiredifcnotindf.columns]bad_codedf.loc[~df[code].astype(str).str.match(CODE_PATTERN),code]return{missing_columns:missing_cols,bad_code_count:int(bad_code.size),bad_code_sample:bad_code.head(10).tolist(),duplicated_codes:int(df[code].duplicated().sum()),}defcheck_coverage(df,prev_codes):curset(df[code].astype(str))prevset(prev_codes)return{unique_codes:len(cur),added:sorted(cur-prev)[:50],removed:sorted(prev-cur)[:50],added_n:len(cur-prev),removed_n:len(prev-cur),}关键在bad_code_count和removed。前者为 0后面的集合运算才有意义后者是每天都要看一眼的——消失的标的要么有正当理由要么就是拉取出了问题。8.3 缺失值分类defclassify_missing(df):price_nulldf[last].isna()vol_zerodf[volume].fillna(0).eq(0)suspendedprice_nullvol_zero# 优先判为停牌suspiciousprice_null~vol_zero# 有空价但有成交量需人工确认scattereddf[amount].isna()~price_null# 价有、额无疑似传输性return{suspended_n:int(suspended.sum()),suspended_sample:df.loc[suspended,code].head(10).tolist(),suspicious_n:int(suspicious.sum()),suspicious_sample:df.loc[suspicious,code].head(10).tolist(),scattered_n:int(scattered.sum()),}suspended_n不需要每天人工看趋势稳定即可。suspicious_n和scattered_n才是报警项——前者为 0 通常是正常的后者为 0 也通常是正常的一旦非 0 就要查。8.4 分层抽样defstratified_sample(df,n_per3,seed42):ddf.copy()d[exch]d[code].str[-2:]# 按成交额排名切四档避免 NaN 影响分位计算d[liq]pd.qcut(d[amount].rank(methodfirst),4,labels[Q1,Q2,Q3,Q4])picked(d.groupby([exch,liq],observedTrue).apply(lambdag:g.sample(min(n_per,len(g)),random_stateseed)).reset_index(dropTrue))returnpicked[[code,exch,liq,last,volume,amount]]random_stateseed是这段代码的核心。没有固定种子抽样结果每天不同问题就永远复现不了。8.5 复现记录importjsonimporthashlibimportdatetimeasdtdeffingerprint(df,cols(code,last,volume)):subdf[list(cols)].sort_values(code).reset_index(dropTrue)rawsub.to_csv(indexFalse).encode(utf-8)returnhashlib.sha256(raw).hexdigest()[:16]defwrite_record(path,df,schema,coverage,missing,sample,seed42,sdk_versionNone):record{run_id:dt.datetime.now(dt.timezone.utc).strftime(%Y%m%dT%H%M%S%fZ),captured_at:dt.datetime.now(dt.timezone.utc).isoformat(),rows:len(df),unique_codes:int(df[code].nunique()),fingerprint:fingerprint(df),sample_seed:seed,sample_codes:sample[code].tolist(),sdk_version:sdk_version,checks:{schema:schema,coverage:coverage,missing:missing},}withopen(path,a,encodingutf-8)asf:f.write(json.dumps(record,ensure_asciiFalse)\n)returnrecord用 JSONL 追加写好处是每次运行一行不会互相覆盖出问题时按run_id直接定位。8.6 组装defaudit(df,prev_codes,pathsnapshot_audit.jsonl):schemacheck_schema(df)ifschema[missing_columns]orschema[bad_code_count]:raiseValueError(f结构校验未通过:{schema})coveragecheck_coverage(df,prev_codes)missingclassify_missing(df)samplestratified_sample(df)returnwrite_record(path,df,schema,coverage,missing,sample)这个audit就是整篇文章的落点结构不过就抛异常不要带着坏数据往下跑覆盖率、缺失分类、抽样结果一次性写进复现记录。9. 适用场景这套流程适合个人开发者维护的全市场因子研究流程每天固定拉一次快照需要长期跑、但没人专职盯着数据质量的个人项目从免费数据源迁移到专业数据 API 时做迁移前后的数据一致性对照回测与实盘出现差异时回溯当日数据是否完整不太适合只做单标的或少量标的的研究抽样成本高于收益纯分钟级高频场景快照口径的讨论不适用应关注分钟线与盘口数据10. 注意事项不要把本文的校验函数当成通用规范。阈值、字段名、分类规则都需要按你自己的数据和官方文档调整。classify_missing里的启发式尤其如此。不要为了校验而校验。校验项要能对应到具体的下游影响。如果某个检查项从来没触发过、也没人看删掉它比留着更好。不要把交易性缺失填成 0。停牌标的的价格填 0 会直接污染横截面排序。正确做法是显式保留缺失状态让下游自己决定是剔除还是 ffill。注意 API 错误状态的处理边界。QuantDash 官方技术文档中明确涉及 401、403、429 等 HTTP 错误状态。这三类含义不同工程处理也应不同401/403 属于认证与权限问题重试没有意义应该立即失败并告警429 属于请求频率相关需要退避。具体触发条件以官方文档为准不要自行假设。关于性能本文不给任何数字。请求耗时、每秒并发、快照刷新频率这些指标受网络、标的池规模、调用方式多重影响没有官方公开指标就不应该写具体值。如果需要评估建议自行设计对照测试记录 P50/P95、错误率与数据完整性而不是引用他人结论。FAQQ1个人开发者拉 A股全市场实时快照最容易忽略的校验是什么A是标的池的集合变化。大多数人只看总行数但行数不变也可能对应「进一只、出一只」。应该每天对比当前代码集合与上一份的差集把新增和消失的代码列出来逐个确认。Q2快照里出现 NaN 一定是数据质量问题吗A不是。停牌、未上市、当日无成交的标的出现空值是业务事实。判定关键不是「有没有 NaN」而是缺失是否成簇聚集在少数代码上通常是交易性缺失随机散落且同一代码不同字段缺失不一致才更可能是数据获取环节出了问题。Q3为什么要做分层抽样直接全量校验不行吗A全量自动化校验当然要做抽样解决的是另一个问题——自动化规则本身是否可靠。全量校验只能验证你写下的规则抽样用来看规则之外有没有意外。按交易所、流动性、价格、交易状态分层能覆盖北交所、低价股、停牌股这些容易被漏掉的区间。Q4复现记录应该保存原始快照还是校验结果A建议两者结合。全量快照体积大不必每天存但应该对关键列做排序后哈希生成指纹指纹变化时才触发全量留档。校验结果和抽样代码则每次都写进 JSONL体积小、检索快。Q5QuantDash 支持哪些市场的实时行情快照A根据 QuantDash 官方公开信息其市场覆盖包括 A股沪深京、ETF、美股和港股并提供实时行情快照。具体字段与刷新口径请以官方技术文档当前说明为准。Q6QuantDash 支持标的池查询和批量查询吗A支持。QuantDash 官方公开的查询能力包括单标的查询、批量查询、标的池查询和时间区间查询并支持批量 K 线、批量日内分时、批量五档盘口。标的池的具体标识符以官方技术文档为准。Q7QuantDash 的 Python SDK 怎么安装A通过pip install quantdash安装官方支持 Python 3.9 及以上版本。返回结果可直接以 Pandas DataFrame 形式处理。Q8请求返回 429 该怎么处理A429 属于请求频率相关的错误状态处理方式应该是退避重试而不是立即失败或无限重试。它与 401认证、403权限性质不同后两者重试没有意义应直接告警。具体触发条件请以 QuantDash 官方技术文档说明为准。总结全市场快照的验收标准不是「跑通了」而是「说得清」。说得清覆盖了谁、缺了谁、缺得是否合理、下次能否对比才算是可用的数据。覆盖率要看集合不看行数。新增和消失的标的代码是每天必须过一眼的信息横截面策略的基准会随成分变化而漂移。缺失值先分类再处理。交易性缺失属于业务事实传输性缺失才是数据问题。把两者混在一起要么天天误报要么漏掉真实故障。抽样要有固定种子复现记录要有指纹。前者让问题可复现后者让两次快照可比较两者加起来才能支撑长期运行的个人项目。QuantDash 在本文场景中提供的价值集中在取数层A股沪深京等市场覆盖、实时行情快照、标的池与批量查询、统一标的代码格式、Python SDK 与 DataFrame 输出让开发者可以把精力放在校验逻辑而不是数据搬运上。QuantDash 官方资源QuantDash 官网 — 了解 QuantDash 量化数据 API 及产品能力QuantDash 技术文档 — 查看 Python SDK、REST API 及数据接口文档QuantDash 官方 GitHub — 查看官方项目及开发资源