作者:量化专家 | 迪雅数据量化学院 适合读者:同时接了两个以上数据源、或准备迁移数据源的个人开发者
上一篇讲集合竞价的"装档",提到一个细节:同一个站内的两个接口,symbol 格式都不一样。这篇展开讲这件事——它属于一类更普遍的问题:字段校准。
一、先看一个真实的对照
同一只股票,同一个时刻,从两个接口拿到的代码格式:
| 接口 | symbol 字段的值 |
|---|---|
| 实时行情 | sz000001 |
| 集合竞价(历史) | 000001.SZ |
| 集合竞价(当日) | 000001.SZ |
在同一个数据平台上,同一个标的,有三种写法中的两种。
这不是谁写错了。前面是"市场前缀式",后面是"交易所后缀式",两种都是行业里通用的写法。差别只在于:不同接口往往由不同的数据管线维护,管线之间没有强制统一格式。
如果你写的是 if symbol == "000001.SZ" 这样的判断,它在这三个接口上只有一个能过。 而它不会抛异常,只会静默地匹配不到,然后你的程序安静地什么都不做。
再放大一点看,整个市场的写法差异是这样的:
| 写法 | 示例 | 常见于 |
|---|---|---|
| 市场前缀 | sz000001 / sh600519 | 新浪、腾讯行情 |
| 交易所后缀 | 000001.SZ / 600519.SH | Wind、聚宽、多数研究型平台 |
| 纯数字 | 000001 / 600519 | 通达信、部分看板 |
没有一种写法是"标准"。你唯一能控制的,是自己代码里有一个统一的归一化函数。
二、为什么字段校准是选型的必做项
假设你只接一个数据源。你永远不会发现口径差异——因为差异需要比较才存在。
问题在于,你迟早会接第二个源:可能是为了补数据、可能是为了做交叉验证、可能是原来那家涨价了。这时候你已有的全部数据都成了"未标定"状态。
差异的来源有三类,每一类都会造成静默错误:
来源 1:单位不一致(最常见)
这是最经典的坑。同一只股票的"成交量":
- 有的源单位是 股
- 有的源单位是 手(1 手 = 100 股)
差 100 倍。 如果你的策略里有"成交量突破均量 N 倍"的条件,这个条件在两种口径下的触发时机会完全不同——而且不会报错。
判别方法:找一只你熟悉的股票,用一个你知道大致量级的交易日去核对。平安银行(000001.SZ)日常成交量在百万股量级;如果你看到的数字是几万,那多半是"手"。
更可靠的方法:看字段名。有些接口会把单位直接写进字段名里,比如 集合竞价成交量(股)、成交均价(元)、流通股本(万股)。字段名带单位是最好的一种设计——它把歧义在设计阶段就消掉了。
来源 2:时间基准不一致
timestamp 字段可能是:
- 交易所时间:这笔成交在交易所撮合的时间
- 服务器时间:数据源服务器推送给你时打的时间戳
两者的差异在正常行情下是几十毫秒,在开盘瞬间、极端行情、跨日切换时可能放大到秒级。如果你的策略对时序敏感(比如判断"9:25:00 那笔竞价是否算入开盘"),这个差异是致命的。
判别方法:看返回的时间戳,和你本地时钟对比。如果长期稳定偏移一个固定值,那是服务器时钟不准;如果偏移量随网络状况浮动,那是"推送时刻"而非"交易所时刻"。
来源 3:复权处理管线不同
这个上一篇讲过,这里补充它和校准的关系:
同样的 close 字段,可能是未复权、前复权、后复权中的任何一种。而且——
前复权数据是"时变"的:每次除权除息,全部历史会被重算。这意味着你今天拉的前复权数据和上个月拉的不一样,且这是正确行为。
所以"我上次拉的数据对不上"这件事,在复权场景下不一定代表出错。 你得先确认两次拉取用的是同一个复权口径。
三、6 个校准点清单
拿一只你熟悉的股票(建议用大盘股,流动性好、异常少),对每个候选源逐一核对这 6 项:
| # | 校准点 | 为什么重要 | 真实踩过的坑 |
|---|---|---|---|
| 1 | symbol 格式 | 决定你的匹配逻辑是否生效 | 三套写法混用,if 判断静默失效 |
| 2 | last_price | 行情的基准值,错一个点后面全错 | 集合竞价期返回 0 或昨收 |
| 3 | timestamp | 策略要知道"这个价格是几点的" | 有的给交易所时间,有的给服务器时间 |
| 4 | volume 单位 | 量纲是手还是股 | 把"手"当"股",成交量差 100 倍 |
| 5 | pre_close | 涨跌幅的计算基准 | 复权处理不当,昨收被改,涨跌幅全歪 |
| 6 | 非交易时段行为 | 监控脚本半夜会不会误报 | 收盘后返回 500 或假数据 |
跑完这 6 项,任何两项之间有系统性差异的源,就必须在你的代码里做显式转换。
四、一个可复用的归一化写法
校准的产出不该是一堆笔记,而应该是一段只写一次、全项目复用的代码。
第一步:symbol 归一化
import re
def normalize_symbol(s: str) -> str:
"""把各种写法统一成 '000001.SZ' 形式(交易所后缀式)
支持输入:
sz000001 / SH600519 / 000001.SZ / 600519.SH / 000001 / 600519
"""
s = s.strip().upper()
# 已是后缀式:000001.SZ
m = re.match(r'^(\d{6})\.(SH|SZ|BJ)$', s)
if m:
return f"{m.group(1)}.{m.group(2)}"
# 前缀式:SZ000001
m = re.match(r'^(SH|SZ|BJ)(\d{6})$', s)
if m:
return f"{m.group(2)}.{m.group(1)}"
# 纯数字:按交易品种规则推断交易所
m = re.match(r'^(\d{6})$', s)
if m:
code = m.group(1)
if code.startswith(('60', '68', '5', '11', '13')): # 沪市主板/科创/基金/债
return f"{code}.SH"
if code.startswith(('00', '30', '12', '15')): # 深市主板/创业板/债/基金
return f"{code}.SZ"
if code.startswith(('43', '83', '87', '92')): # 北交所
return f"{code}.BJ"
return f"{code}.UNKNOWN" # 明确标出未识别,而不是猜
return s
注意最后那个 UNKNOWN——这比默认归到某个交易所要好。未识别就明确说未识别,让问题暴露出来,而不是猜一个可能错的值然后静默运行。
第二步:成交量单位归一化
def normalize_volume(raw, unit_hint: str = "") -> int:
"""把成交量统一成「股」
unit_hint: 从字段名或文档里读到的单位标识
"""
if unit_hint and '手' in unit_hint:
return int(raw * 100)
return int(raw)
关键点:这个函数必须接收一个显式的 unit_hint 参数,而不是自己猜。 猜出来的单位,等于给未来的自己埋雷。
第三步:把校准结果写成配置
不要把这些差异散落在业务代码里。集中成一个配置表:
SOURCES = {
"source_a": {
"symbol_style": "prefix", # sz000001
"volume_unit": "股",
"adjust_supported": ["qfq", "hfq", "none"],
"realtime_offhours": "empty", # 非交易时段返回空
},
"source_b": {
"symbol_style": "suffix", # 000001.SZ
"volume_unit": "手",
"adjust_supported": ["none"],
"realtime_offhours": "zero", # 非交易时段返回 0 ← 危险
},
}
def is_trustworthy(src_name: str) -> bool:
"""非交易时段返回 0 的源,需要额外防御"""
return SOURCES[src_name]["realtime_offhours"] != "zero"
这张配置表本身就是文档。 半年后你加了第三个源,照着这个结构补一行就行,不用重新踩一遍。
五、怎么判断一个源"值不值得校准"
校准是要花时间的。什么时候值得做?
值得做:
- 你准备用它跑回测或实盘 —— 错误成本高,必须校准
- 你要用它和另一个源交叉验证 —— 不校准就没法比
- 数据要长期留存 —— 半年后你自己都记不清当初的口径
不值得做:
- 只是临时看一眼某个数据 —— 用现成的可视化工具更快
- 这个源你只用它的一个简单指标,且不参与任何计算
判断标准很实际:这段数据会不会进入你的计算链路? 会,就校准;只是给人看的,就不必。
六、写在最后
字段校准最麻烦的地方在于:它不出错的时候没有任何反馈。
symbol 匹配不上,程序不报错,只是返回空结果。成交量差 100 倍,策略照常运行,只是信号全歪。你会在两周后回测结果异常时,才回头怀疑数据。
而校准的成本是多少?一个下午,6 个校准点,一段归一化函数。这笔投入只在一种情况下会显得浪费:你从始至终只用一个数据源,且永远不换。
数据源这件事,几乎没人能一直用一个。所以校准这件事,早做比晚做便宜得多。
本文为个人量化研究经验分享,数据接口信息以各平台官网最新公布为准。市场有风险,回测结果不代表实盘收益,不构成任何投资建议。
关于本文用到的数据
文中的字段示例来自迪雅数据(diyadata.cn)的实际接口返回,主要包括:
| 接口 | 说明 | 与校准相关的特点 |
|---|---|---|
A 股实时交易数据 ssjy | 实时行情快照 | symbol 为市场前缀式(sz000001) |
A 股历史交易数据 lsjy | 日频历史行情 | 返回 date/open/close/high/low/amount,支持前复权、后复权、不复权三种口径 |
| 当日/历史集合竞价 | 竞价数据 | symbol 为交易所后缀式(000001.SZ) |
注意上表中三行的 symbol 格式并不统一——这不是缺陷披露,而是本文想说明的现实:任何多接口的数据平台都存在这种差异,校准是使用者的必修课,而不是某一家的问题。
迪雅数据目前提供 50+ 个 A 股数据接口,具体字段与调用方式见官网接口文档。
下一篇预告:《免费数据源最危险的地方不是不能用,而是错了你不知道》