做行情看板、做策略回测、做盯盘提醒,干这几件事的时候,绕不开同一个问题:手上得有一条随时能拉数据的通道。我最早接触实时股票数据API接口,是为了给一个内部监控面板做行情刷新,当时觉得“不就是个HTTP请求吗”,结果从选接口到跑通,前后磨了快两周。后来做股票历史行情查询API接口的批量回补,又在复权因子和停牌处理上栽了跟头。市面上那些“大全”“汇总”型的清单文章,标题写得热闹,真点进去一半链接打不开,能打开的字段口径还各不相同,最要命的是没有任何一篇告诉你——这些接口到底该怎么验证、哪些坑是必须提前知道的。
这篇东西想解决的就是这件事。我不打算再列一遍谁家的接口地址是什么,那种清单你搜十篇有八篇重复,而且三个月后大概率失效。我更想聊的是:当你拿到一堆候选接口之后,怎么用一套可复跑的流程把它们筛出来,实时接口和历史接口在对接上分别要注意什么,参数怎么定,脏数据怎么揪,以及那些只有真跑过一遍才会遇到的问题。适合刚接手行情模块的后端、做量化研究的数据同学,也适合想自己搭个小工具看看盘面的人。代码示例我用的是通用写法,域名都是占位符,你把地址换成自己申请到的那条就能跑。
1. 先把“股票数据”这四个字拆开,需求分层决定接口形态
很多人一开始就把“我要股票数据”当成一个整体需求去搜,结果搜出来一堆东西,看哪个都像自己要的,看哪个又都不太对。问题出在“股票数据”这四个字太笼统了。它底下其实至少分了四五个层级,每个层级对应的接口形态、更新节奏、数据量级完全不一样。你如果没先把自己的需求落到某一层,选型阶段基本就是瞎撞。
1.1 实时快照、逐笔成交、分钟线、日线,是四种完全不同的东西
实时快照是最常见的一类,通常就是某个标的在某一刻的最新价、涨跌幅、成交量、买卖五档(或者十档)。它的特点是字段少、更新频率高、单次请求返回体小,适合做行情面板、价格提醒。这类接口一般有轮询和推送两种形态,轮询就是你每隔几秒问一次,推送是服务端主动把变化发给你。
逐笔成交是另外一个量级。它记录的是每一笔成交的时间、价格、数量、买卖方向,一天下来单只标的可能有几万到几十万条。这个数据一般只有做微观结构研究或者高频回测才用得上,普通看板完全不需要。
分钟K线介于两者之间,常见的是1分钟、5分钟、15分钟、30分钟、60分钟几档。它是对逐笔或者快照做聚合之后的产物,一条记录包含开、高、低、收、成交量、成交额。做日内策略、做分时图,基本都是拿这个。
日线及以上就是大家最熟的那套:每天一条,包含开高低收、成交量、成交额、换手率、复权因子等等。历史行情接口主要就是提供这一层,以及更粗的周线、月线。
我见过有人拿逐笔接口去拼日线,理由是“逐笔最原始最准”——技术上没错,但成本高得离谱,而且聚合逻辑要自己写、要处理各种边界情况,纯属给自己找事。反过来的错误更常见:拿日线接口的“最新价”去做实时刷新,那个价格可能是几小时前的,盯盘会盯出心脏病。所以第一步,先把你到底要哪一层写清楚,写成一句话,比如“我需要全市场标的的实时快照,刷新频率10秒一次”,这句话写出来了,接口选型范围立刻缩小一大半。
1.2 历史行情的三个隐形维度:复权、停牌、退市
历史行情接口看起来最简单——给个代码、给个日期区间,返回一堆K线。但真正决定这份数据能不能用的,往往不是价格本身,而是三个容易被忽略的维度。
第一个是复权。不做复权的原始价格,遇到分红送股就会出现一个巨大的跳空缺口,回测的时候你的策略会以为那天跌了30%,其实是除权。复权分前复权和后复权:前复权是以当前价格为基准往前调整,最新价和真实成交价一致,适合看图;后复权是以某个历史基准往后调整,历史价格不变,适合做收益率计算和回测。关键在于,不同接口给出的“复权因子”定义可能正好相反——有的是乘,有的是除,有的因子是累计值,有的是当期值。这个必须实测验证,不能看文档想当然。
第二个是停牌。一只标的停牌期间,有的接口会返回空,有的会返回一条成交量为0的记录,有的干脆用前一日收盘价补一条。这三种处理方式落到你的数据表里,会导致完全不同的结果。做回测的时候,如果停牌日被补了价格,你的持仓市值看起来是平的,但实际是没法交易的,这会系统性地高估策略表现。
第三个是退市。如果你要做全市场历史回测,必然会踩到“幸存者偏差”——只拉当前还在上市的标的,你拉到的样本本身就是经过筛选的,那些退市的、表现差的都被剔掉了。合格的接口应该能提供已退市标的的历史数据,或者至少提供一个包含退市标记的标的列表。
1.3 需求与接口能力对照表
下面这张表是我自己用的,每次接新项目先填一遍,填完基本就知道该往哪个方向找接口了。
| 需求场景 | 必需数据层 | 更新节奏 | 关键字段要求 | 容易踩的坑 |
|---|---|---|---|---|
| 行情面板刷新 | 实时快照 | 3到10秒 | 最新价、涨跌幅、成交量 | 时间戳时区不统一 |
| 价格提醒推送 | 实时快照 | 10到60秒 | 最新价、昨收 | 停牌时价格冻结,误触发 |
| 日内策略回测 | 分钟K线 | 盘后批量 | 开高低收、成交量、成交额 | 分钟线缺失时如何补 |
| 日频策略回测 | 日线 | 盘后批量 | 开高低收、复权因子、换手率 | 复权口径不明确 |
| 全市场横截面研究 | 日线 + 标的列表 | 每日一次 | 行业分类、上市日期、退市标记 | 幸存者偏差 |
| 微观结构分析 | 逐笔成交 | 实时推送 | 时间、价格、数量、方向 | 数据量极大、存储成本高 |
这张表还有一个用途:跟数据提供方沟通的时候,你拿着表去问,对方就能明白你要什么,比你说“我要行情数据”有效得多。尤其“容易被忽略的维度”那一列,你主动问出来,对方一般会认真回答,因为这说明你不是随便试试的人。
2. 接口选型的六个硬指标,别被“免费”两个字带偏
我早期选接口有个毛病,先看价格,免费的直接收藏,收费的先放一边。这个习惯让我浪费了很多时间——免费接口往往在频率、字段完整性、历史深度上有各种限制,等你把代码写完、数据灌进去,才发现根本不够用,返工成本远高于那点费用。现在我选接口,顺序是反过来的:先看能不能满足需求,再看成本。
2.1 延迟与更新节奏:先搞清楚“实时”到底有多实
所有号称实时的接口,都要问一句:数据从交易所出来到你手上,中间隔了多久?这个延迟由几段构成——交易所到数据商的链路、数据商内部处理、网络传输、你的请求排队。普通HTTP轮询接口,端到端延迟在一秒到几秒之间是正常的;推送型接口会好一些,但也别指望毫秒级。
关键是要跟你自己的刷新频率对齐。如果你的面板是10秒刷一次,那接口延迟3秒还是5秒,用户根本感知不到;但如果你要做分钟级的信号触发,延迟2秒和延迟8秒就是两回事了。我的做法是先定刷新频率,再倒推可接受的延迟上限,然后拿这个上限去筛接口,而不是反过来被接口的“实时”宣传牵着走。
另外一个容易忽略的点是更新节奏的规律性。有些接口在开盘瞬间和收盘前几分钟会有明显的响应变慢,因为那会儿所有人都在拉数据。你如果只测了盘中某个平静时段,上线后遇到尾盘高峰就会很难看。测的时候一定要覆盖开盘后五分钟、午间休市前后、收盘前三十分钟这几个时间点。
2.2 频率限制与配额算法:算一遍就知道够不够用
频率限制一般有三种表述:每秒N次、每分钟N次、每天N次。看着简单,但实际影响差别很大。每分钟60次的限制,你以为可以每秒钟一次,实际上不行——它可能是在一分钟的窗口内做滑动统计,你连续发60次之后,接下来那一分钟就完全不能发了。
更关键的是把配额换算成实际耗时。举个真实算过的例子:假设你要回补全市场5000只标的、10年日线,单标的约2400个交易日,总量约1200万行。如果接口单次最多返回500行、每分钟允许60次请求,那么需要 1200万 ÷ 500 = 2.4万次请求,2.4万 ÷ 60 ≈ 400分钟,也就是将近7个小时。这还只是理论值,没有算重试、没有算失败重拉。
这个计算的意义在于:它告诉你“能不能在收盘后到第二天开盘前跑完”。如果算出来要7小时,那就是可行的,晚上跑、早上用;如果算出来要30小时,那这条接口就不适合做全量回补,只能做增量更新。我把这个计算做成了一个小脚本,把标的数、年数、单次返回上限、频率限制四个参数填进去就直接出结果,省得每次凭感觉。
2.3 字段口径与数据完整性:文档和实际返回经常不是一回事
字段口径这件事,文档写得再详细也不如实际拉一条数据看一眼。我遇到过文档写“成交量单位为手”,实际返回是股的情况;也遇到过“成交额”字段在某些标的上是空值、某些标的上是0、某些标的上正常的情况。这些如果不提前发现,等数据灌了几百万行再回头改,就是灾难。
我的检查清单是这样的:先拉一只大盘标的、一只小盘标的、一只当天停牌的标的、一只刚上市不久的标的,四条数据摆在一起对比。看数值量级合不合理(比如成交量是不是比其他标的小好几个数量级)、看空值分布(哪些字段是普遍空、哪些是偶发空)、看类型(价格是字符串还是数字、有没有带单位后缀)。这四类标的能覆盖绝大多数边界情况,比随机拉一百条有用。
数据完整性还有一层是历史深度。有的接口只提供近三年,有的提供十年,有的宣称全历史但实际早期数据缺失严重。验证方法很简单:拉一只上市超过十五年的标的,看最早那条记录是哪天,然后跟公开的上市日期对一下。如果差了好几年,说明早期数据不全。
2.4 可用性验证:用一周时间判断一条接口靠不靠谱
接口这东西,测一次两次都挺好,用一周就开始出问题。所以我现在的做法是做一个为期一周的“陪跑测试”——写个小脚本,每隔固定时间请求一次,把响应时间、状态码、返回条数、关键字段是否为空都记下来,跑满一周再回头统计。这个成本很低,但信息量极大。
统计的时候重点看三个数:成功率(成功请求 ÷ 总请求)、P95响应时间(把所有响应时间排序,第95%位置那个值,比平均值有意义得多)、异常分布(失败是集中在某几个时间点,还是随机分布)。如果失败集中在每天开盘前那几分钟,那可能是对方系统在做日切;如果随机分布,可能是网络抖动或者限流。
我一般要求成功率在99%以上、P95响应时间在自己设定的超时阈值的一半以内,才认为可以进生产环境。达不到就先降级使用——比如只做盘后批量,不做实时。
2.5 授权边界与合规使用:这一步别省
数据接口的授权范围,很多人直接跳过用户协议点同意。但实际使用中,能不能对外展示、能不能二次分发、能不能用于商业产品,这些条款差别很大。个人研究和小范围内部使用,通常限制较少;一旦涉及对外提供服务,就必须看清楚授权范围。
我的建议是:把用途写清楚再去申请。比如“用于内部数据可视化面板”“用于学术研究中的历史数据回测”,说清楚用途,对方一般会告诉你哪一档授权合适。反过来,如果你拿个人研究授权去做对外服务,后面出问题是自己的责任。这一块不要图省事,也不要听信“大家都这么用”的说法。
2.6 成本测算:把隐性成本也算进去
成本不只是接口费用。真正的大头往往在三个地方:存储(1200万行日线,按每行150字节算约1.8GB,加上索引和冗余备份,实际占用可能翻倍,如果还要存分钟线,量级直接上两个数量级)、计算(复权计算、聚合、清洗都要跑批)、维护(接口变更、字段调整、异常处理的人力)。
我做过一个粗略的对比:自建一套完整的行情数据管道,从接入、清洗、存储到监控,一个人全职做大概要一到两个月能跑顺,之后的维护成本大约是每周几个小时。这个投入值不值,取决于你的使用频率——如果只是偶尔看看数据,直接用现成的查询工具更划算;如果是长期、高频、需要定制字段,那自建就是必须的。
3. 实时行情接口对接:从鉴权到落库的完整链路
假设你现在已经选定了一条实时快照接口,接下来就是把它接进来。这一段我按实际动手的顺序讲:先解决鉴权,再写拉取逻辑,最后落库。
3.1 三种常见鉴权方式与各自的坑
第一种是固定Token。申请之后拿到一串字符,每次请求放在Header或者Query里。这种方式最简单,坑也最明显——Token泄露就等于账号泄露,而且很多服务的Token是长期有效的,一旦写进了前端代码或者提交到了公开仓库,基本等于公开。我的做法是Token只放在服务端环境变量里,绝不写进代码,更不进前端。
第二种是时间戳加签名。每次请求带上当前时间戳和一个用密钥算出来的签名,服务端校验签名和时间戳的有效窗口。这种方式安全性好很多,但有两个细节要注意:一是时间戳的时区和精度必须和服务端约定一致,差几分钟就可能验签失败;二是签名串的拼接顺序、参数排序规则、大小写,必须严格按文档来,少一个字符都不行。我调试签名的时候,一般会先用一个最简单的请求(只有一个必填参数)跑通,再逐步加参数,这样出问题能快速定位是哪个参数影响了签名。
第三种是IP白名单。把你的服务器出口地址报给对方,只有这些地址能访问。这种方式对服务端调用很友好,但如果你的服务器是弹性伸缩的、出口地址会变,就要提前说明。
实际生产中常见的是组合使用,比如Token加IP白名单。这里有个小细节:如果你的服务部署在多台机器上,每台都要能访问,记得把整批地址都报上去,别只报了测试机。
3.2 轮询式拉取的最小可运行实现
下面这段是我常用的骨架,做了频率控制、超时、重试和异常记录。域名和参数名都是占位符,替换成你实际的即可。
import time import random import requests from datetime import datetime API_BASE = "https://api.example-provider.test/v1/quote" TOKEN = "YOUR_TOKEN_HERE" # 频率控制:两次请求之间至少间隔多少秒 MIN_INTERVAL = 1.2 _last_call = 0.0 def fetch_snapshot(codes, timeout=5, max_retry=3): """批量拉取快照,codes 是代码列表,建议单次不超过 50 个""" global _last_call url = f"{API_BASE}/snapshot" headers = {"Authorization": f"Bearer {TOKEN}"} params = {"codes": ",".join(codes)} for attempt in range(max_retry): # 主动限速,避免触发对方的频率限制 wait = MIN_INTERVAL - (time.time() - _last_call) if wait > 0: time.sleep(wait) try: _last_call = time.time() resp = requests.get(url, headers=headers, params=params, timeout=timeout) if resp.status_code == 200: payload = resp.json() return payload.get("data", []) elif resp.status_code == 429: # 被限流了,退避后重试,退避时间带随机抖动 backoff = (2 ** attempt) + random.uniform(0, 0.5) print(f"[{datetime.now()}] 触发限流,{backoff:.1f}s 后重试") time.sleep(backoff) elif 500 <= resp.status_code < 600: backoff = (2 ** attempt) + random.uniform(0, 0.5) print(f"[{datetime.now()}] 服务端错误 {resp.status_code},{backoff:.1f}s 后重试") time.sleep(backoff) else: # 4xx 一般是参数或鉴权问题,重试没意义 print(f"[{datetime.now()}] 请求失败 {resp.status_code}: {resp.text[:200]}") return [] except requests.exceptions.Timeout: print(f"[{datetime.now()}] 请求超时,第 {attempt + 1} 次") except requests.exceptions.RequestException as e: print(f"[{datetime.now()}] 网络异常: {e}") time.sleep(1) return [] if __name__ == "__main__": result = fetch_snapshot(["XXXXXX", "XXXXXX"]) for row in result: print(row)这段代码里有几个地方是刻意的。退避时间加了随机抖动,因为如果一批客户端同时被限流、又同时按固定间隔重试,会在同一秒再次撞上去,抖动能把这个峰值打散。4xx 不重试,因为参数错、鉴权错重试一万次还是错,只会浪费配额。单次批量不超过50个代码,这个数字不是随便定的,很多接口对URL长度或者参数个数有限制,50是一个比较安全的值,实际可以按你自己那条接口的上限调整。
3.3 推送式接入的要点与断线处理
推送型接口(通常是长连接)比轮询省配额、延迟也更低,但工程上复杂不少。核心要处理三件事:断线重连、心跳保活、消息乱序或重复。
断线重连要设计成指数退避,不能一断就疯狂重连,会把自己打成攻击源。心跳一般按服务端要求的间隔发,如果对方没要求,30秒左右是一个常见值。消息重复是很容易被忽略的——网络抖动导致的重发、服务端补发,都可能让你收到同一条数据两次,所以落库的时候必须有幂等处理,一般用“标的代码 + 数据时间戳”做唯一键。
还有一个实际经验:推送接口在本地开发环境跑得好好的,上了服务器就断,八成是中间的网络设备对长连接有超时回收策略。解决方式无非两种,要么把心跳间隔调到比那个超时时间更短,要么让应用层自己检测并主动重建连接。
3.4 表结构设计与去重策略
实时快照不需要存全量历史,一般只保留最新一条加上一段时间的滚动窗口。我的表结构大致是这样:
| 字段 | 类型 | 说明 |
|---|---|---|
| code | varchar(16) | 标的代码,与数据源口径一致 |
| trade_time | datetime | 数据时间,注意统一到同一时区 |
| last_price | decimal(12,4) | 最新价 |
| pre_close | decimal(12,4) | 昨收,用于算涨跌幅 |
| volume | bigint | 成交量,单位需与数据源确认 |
| amount | decimal(20,4) | 成交额 |
| bid1 / ask1 | decimal(12,4) | 买一/卖一价 |
| src | varchar(16) | 数据来源标识,方便多源对比 |
| updated_at | datetime | 入库时间,用于排查延迟 |
唯一索引建在(code, trade_time, src)上,写入用“存在则更新、不存在则插入”的方式。加src字段是我吃过亏之后加的——同时接两条数据源做对比的时候,没有来源标识根本分不清哪条是哪条。
4. 历史行情批量回补:分页、增量与复权因子
历史数据的活儿比实时枯燥,但对数据质量的要求高得多。实时数据错一条,下一秒就刷过去了;历史数据错一条,可能让你几个月的回测结论全废。
4.1 分页游标怎么设计才不会漏数据
大部分历史接口都是分页返回,参数无非是页码加页大小,或者起始时间加结束时间。看起来简单,但漏数据的情况非常普遍。
按页码分页的问题在于,如果数据在拉取过程中发生了变化(比如当天收盘数据的补录),页边界会漂移,导致某些记录被跳过或者重复。按时间游标分页更可靠:每次请求带上“上次拉到的最后一条数据的时间”,下次从那个时间往后继续。但这里有个细节——如果多条记录的时间戳完全相同(比如同一天的分钟线在某些数据源里时间戳精度只到日),用时间做游标会卡住。
我的处理办法是用“时间 + 主键”的复合游标,时间相同的时候用主键做次级排序。如果接口不支持这种游标,那就退一步:每次拉取的时间区间做一点点重叠(比如多拉一天),然后在本地按唯一键去重。重叠的代价是多拉一点数据,但能保证不漏。
4.2 复权因子的正确用法与换算
这是历史数据处理里最容易出错的地方,我单独拎出来讲。
不复权价格遇到除权除息会跳空,直接拿来回测,收益率计算是错的。复权的本质是把历史价格按某个比例调整,让价格序列连续。数学上就一个因子的事:
- 后复权价 = 不复权价 × 复权因子
- 前复权价 = 不复权价 × 复权因子 ÷ 最新复权因子
这里的关键是“复权因子”这个字段在不同数据源里的定义可能完全不同。有的因子是累计的(每次除权后累乘),有的是当期的(只反映最近一次),有的是乘数,有的是除数。文档不一定写清楚,必须实测。
我的验证方法很土但有效:找一只发生过明显分红送股的标的,拉它的不复权日线、后复权日线、复权因子三份数据,手算一遍。取某个历史日期,用不复权价乘以当日因子,看等不等于后复权价。如果差一个数量级,多半是单位问题;如果完全对不上,可能是因子定义不同(比如需要除而不是乘);如果只有最近的日期对得上、早期对不上,那就是累计值和当期值的区别。
这一步花半小时,能省掉后面几天的返工。
4.3 断点续传与幂等写入
全量回补动辄几小时,中途断掉是常态——网络抖动、服务端重启、你自己机器被踢。所以任务必须能断点续传。
我的做法是维护一张进度表,记录“某标的、某数据层、已拉到的最后日期”。任务启动时先查进度,从断点继续。写入用批量插入加唯一键冲突忽略的方式,保证同一个(标的天数)只写一次。这样即使某一段重复拉取,也不会产生重复数据。
还有一个细节是分批提交。不要等全部拉完再一次性写库,每拉一批就写一批、更新一次进度。这样断了只丢最后一批,不会全丢。
4.4 自洽性校验:四条能揪出脏数据的规则
数据灌进去之后,必须跑一遍校验。我固定用这四条:
第一条,高低价关系。任何一条K线,最高价必须大于等于开收高低中的其他三个,最低价必须小于等于其他三个。这条能揪出字段错位——有些接口返回的顺序和文档不一致,一校验就露馅。
第二条,价格区间合理性。单日涨跌幅超过某个阈值的记录单独抽出来人工看。不是说过滤掉,而是抽出来核对,因为可能是真实的大幅波动,也可能是数据错误(比如小数点错位)。
第三条,时间连续性。相邻两条记录的日期差,如果超过正常间隔(比如日线出现七天以上间隔),就要检查是不是有停牌没处理,或者数据缺失。
第四条,成交量与成交额的关系。成交额应该大致等于成交量乘以均价,如果差出一个数量级,说明单位有问题——这条规则帮我发现过一次“成交量单位是股、文档写的是手”的错误,差了100倍。
这四条规则写成一个校验脚本,每次回补完自动跑,输出异常清单。跑久了你会发现,绝大多数问题都能被这四条抓住。
5. 把“可直接点击验证”做成一套可复跑的体检流程
回到标题里那个“可直接点击验证测试”。我的看法是:别人给你的链接,点一次能通,不代表明天能通,更不代表数据是对的。真正有价值的不是那个链接,而是你自己的一套验证流程——换任何一条接口,都能在半小时内判断它能不能用。
5.1 体检清单:八个必查项
每次拿到新接口,我按这个顺序走一遍:
| 序号 | 检查项 | 具体做法 | 不合格的表现 |
|---|---|---|---|
| 1 | 鉴权是否通过 | 用最小参数发一次请求 | 401/403,或返回业务错误码 |
| 2 | 返回结构是否与文档一致 | 对比字段名、类型、层级 | 字段缺失、类型不符 |
| 3 | 时间戳时区 | 用当前时间对比返回的最新时间 | 差8小时或差若干分钟 |
| 4 | 数值量级 | 对比大盘标的和小盘标的的成交量 | 相差几个数量级 |
| 5 | 空值分布 | 统计关键字段的空值比例 | 关键字段大面积为空 |
| 6 | 边界数据 | 拉停牌标的、次新标的、长期停牌标的 | 报错或返回异常结构 |
| 7 | 频率限制实测 | 短时间连续请求,观察第几次开始报限流 | 实际限制远严于文档 |
| 8 | 历史深度 | 拉一只老标的,看最早记录日期 | 比上市日期晚很多 |
这八项里,第3、4、6项是最容易出问题的,也是文档最不会写的。尤其是时区,我遇到过返回时间比实际早8小时的,如果没发现,所有基于时间戳的去重和排序都会错乱。
5.2 自动化冒烟测试脚本
把上面的清单写成脚本,每次接入新接口跑一遍,输出一份报告。核心逻辑大概是这样:
import json from datetime import datetime, timedelta def run_smoke_test(client, sample_codes): report = {} # 1. 基础连通性 try: rows = client.snapshot(sample_codes[:1]) report["连通性"] = "通过" if rows else "返回空" except Exception as e: report["连通性"] = f"失败: {e}" return report # 2. 字段完整性 expected = {"code", "last_price", "pre_close", "volume", "trade_time"} actual = set(rows[0].keys()) missing = expected - actual report["字段完整性"] = "通过" if not missing else f"缺失: {missing}" # 3. 时区校验:返回的最新时间不应早于当前时间太多 try: ts = datetime.strptime(rows[0]["trade_time"], "%Y-%m-%d %H:%M:%S") diff = abs((datetime.now() - ts).total_seconds()) report["时区"] = "通过" if diff < 3600 else f"偏差 {diff/3600:.1f} 小时" except Exception as e: report["时区"] = f"解析失败: {e}" # 4. 量级对比:不同规模标的的成交量应存在明显差异 multi = client.snapshot(sample_codes[:5]) vols = [r.get("volume", 0) for r in multi] if len(vols) >= 2 and min(vols) > 0: ratio = max(vols) / min(vols) report["量级合理性"] = "通过" if ratio > 1.5 else f"比值仅 {ratio:.2f},疑似异常" else: report["量级合理性"] = "样本不足或存在零值" # 5. 历史深度 try: hist = client.daily(sample_codes[0], "20000101", "20001231") report["历史深度"] = "有早期数据" if hist else "无早期数据" except Exception as e: report["历史深度"] = f"查询失败: {e}" return report跑完把报告打印出来或者写进文件,两条接口一对比,高下立判。这个脚本本身没什么技术含量,价值在于它能保证你每次都按同一套标准检查,不会因为某天心情好就少看两项。
5.3 结果沉淀成矩阵表,选型不靠感觉
单次测试结果没什么意义,攒够三五条接口之后,做成一张矩阵表:
| 接口 | 成功率 | P95延迟 | 字段完整 | 历史深度 | 频率限制 | 成本 | 综合判断 |
|---|---|---|---|---|---|---|---|
| A | 99.8% | 320ms | 完整 | 15年 | 60次/分 | 中 | 可用于生产 |
| B | 97.2% | 780ms | 缺换手率 | 5年 | 200次/分 | 低 | 适合原型 |
| C | 99.9% | 150ms | 完整 | 10年 | 30次/分 | 高 | 回补太慢 |
这张表的用法不是选出“最好的那一条”,而是按场景选。比如 C 接口质量最好但限额低,就用来做实时快照,不做全量回补;B 接口限额高但历史浅,就用来做盘中高频刷新。现实中往往需要两三条接口组合使用,用src字段区分来源,互为备份。
6. 踩坑实录:那些文档里不会写的问题
这一段是我这些年攒下来的,都是当时花了不少时间才搞明白的。
6.1 报错与异常速查表
| 现象 | 最常见原因 | 排查方向 |
|---|---|---|
| 返回 200 但数据为空 | 参数格式不对(日期格式、代码前缀) | 用接口文档的示例参数原样跑一遍 |
| 403 或鉴权失败 | Token 过期、签名时间戳偏差、IP 未白名单 | 先确认服务器时间是否准,再查签名串拼接顺序 |
| 429 频繁出现 | 实际限额严于文档,或存在多进程共用配额 | 全局加锁限速,别让多个进程各自计数 |
| 数据明显滞后 | 缓存未刷新、请求打到了备用节点 | 加一个随机参数绕过缓存,观察是否改善 |
| 偶发超时 | 盘中高峰、长连接被中间设备回收 | 覆盖高峰时段测试,心跳间隔调短 |
| 数值差100倍 | 成交量单位“手”与“股”混淆 | 用成交额除以成交量反推均价,验证单位 |
| 时间差8小时 | UTC 与本地时间未统一 | 全链路统一存 UTC,展示层再转 |
| 缺失某些标的 | 代码前缀规则不同(如是否带交易所标识) | 用接口的标的列表接口先同步一次全量代码 |
这张表我贴在工位上过,因为高频出问题的就那么几个。
6.2 数据对不上的五类原因
做多源比对的时候经常会发现两条接口的数据不一致,第一反应是“有一条是错的”,但实际上大部分情况两者都没错,只是口径不同。
第一类,成交量单位不同。一手等于100股是常识,但不是所有数据源都用“手”,有的直接用“股”,有的用“万手”。差100倍、差10000倍都是常见的。
第二类,成交额是否含大宗交易。有的数据源把大宗交易单独列出,有的合并进总额,导致成交额对不上而成交量对得上。
第三类,复权口径不同。一条是不复权,一条是前复权,价格自然不一样。比对之前必须先确认两边都是不复权。
第四类,停牌日的处理不同。一方补了前收盘价,一方留空,条数就差了。
第五类,数据更新时间不同。盘中实时数据,两条源刷新节奏不一样,同一时刻拉到的价格本来就该不同。这种情况要把时间戳对齐了再比。
搞明白这五类之后,我看到数据不一致就不慌了,先分类,再判断是不是真错误。
6.3 几条用血换来的经验
别在开盘瞬间和大盘标的上同时压测。我早期做压测,专挑大盘标的、专挑开盘那一分钟,结果测出来一堆超时,误判接口不行。后来发现是自己的请求太集中,把配额瞬间打满了。压测要分散,要模拟真实的使用节奏。
日志里一定要记录原始返回体。有一次数据对不上,查了半天代码没找到问题,最后翻日志发现是对方某天调整了一个字段的含义,返回体还是那个结构,但数值口径变了。如果当时没留原始返回,根本无从查起。日志保留期至少要覆盖一个完整的对账周期。
接口文档里的“示例”比“说明”更可信。说明文字经常是复制粘贴改漏的,示例参数和示例返回一般是实际跑过的。两者冲突的时候,信示例。
先跑通再优化,别一上来就设计完美架构。我见过不少人(包括我自己)一上来就搞分库分表、消息队列、多级缓存,结果连一条数据都没拉通。先用最朴素的脚本拉通一条数据、看一眼数据结构,再决定怎么设计,效率高得多。
留一条备用通道。不管主接口多可靠,都要有一条能顶上的备选。备用通道平时不用,但每周跑一次简单的心跳,确保它还能用。真到主接口出问题的时候,你会庆幸做过这件事。
最后分享一个我自己在用的笨办法。我建了一个文档,每次接入新接口就把“踩过的坑”记一条进去,不写长篇,就一句话加一个现象描述。两年下来攒了六十多条,现在接任何新接口,先翻一遍,基本能避开八成的问题。这比任何一篇“大全”都有用,因为那里面每一条都是我自己验证过的。数据接口这行变化快,链接会失效、字段会改、限额会调,唯一能沉淀下来的,就是你自己那套验证流程和这份踩坑清单。