摘要
接入宣称“覆盖多市场”的行情 API 时,很多开发者的验收标准止于“请求返回 HTTP 200”。HTTP 200 只证明服务端可达——就像收到回信只说明信封完好,不证明对方答应了你的请求。本文用一份 contract test 骨架,演示通过 5 个 symbol 对 TickDB REST ticker 接口做最小验证的正确姿势:校验 symbol、type、last_price、timestamp 的格式与类型,用 Decimal 拒绝非法数值,以及明确区分已验证与未验证边界。
⚠️ 本文 REST 脚本基于官方接口契约编写,截至本文发布尚未有本轮运行输出。文中“本轮实测”结论来自 TickDB MCP
get_ticker,不可跨命名空间自动等同于 REST 行为。REST 真实运行后请以实测结果为准。
正文
1. HTTP 200 只是一张完好的信封
你寄出一封信,几天后收到回信。信封完好,邮票清晰。拆开一看——信纸上只有一行字:“您的来信已收到,但无法处理。”
HTTP 200 就是这样一张信封。它告诉你服务端收到了请求,也给出了响应。但响应内容可能是鉴权失败、参数错误,或是静默丢弃了你一半的查询。
更隐蔽的问题藏在返回结构里:last_price可能是null,timestamp可能是字符串,返回的品种比你请求的多或少。这些问题都不会影响 HTTP 状态码,但会在下游计算中引发静默错误。
2. 测试输入:五类行情,五个代表品种
| 代表类别 | symbol | 资产类型 |
|---|---|---|
| A 股 | 600519.SH | 股票 |
| 港股 | 700.HK | 股票 |
| 美股 | AAPL.US | 股票 |
| 加密 | BTCUSDT | 加密货币 |
| 外汇 | EURUSD | 外汇 |
选择原则:覆盖三类股票市场 + 加密 + 外汇共五类行情。只有候选 API 明确宣称覆盖且当前 Key 具备对应权限时,五个代表品种才都应返回。
本轮实测 PoC 卡(2026-06-06,TickDB MCPget_ticker):
| 验证项 | 结果 |
|---|---|
| 五个目标品种全部返回 | ✅ |
均含symbol、type、last_price、timestamp | ✅ |
last_price为字符串 | ✅ |
timestamp为整数,本轮均为 13 位 | ✅ |
| 无效 symbol 混合查询只返回有效品种 | ✅ |
纯无效 symbol 返回空data,code仍为 0 | ✅ |
3. 校验步骤与失败分支(合并表)
以下五步是递进的:任何一步失败,后续步骤即无意义。所有校验异常都应使程序以非零码退出,而非告警后继续。
| 步骤 | 校验项 | 通过条件 | 失败现象 | 排查方向 |
|---|---|---|---|---|
| ① | HTTP 状态 | status_code == 200 | 非 200,或超时 | 网络、防火墙、DNS、端点地址 |
| ② | JSON 解析 | resp.json()不抛异常 | 非 JSON 响应 | 端点路径是否正确 |
| ③ | 业务状态码 | body["code"] == 0 | 1001/1002/1004/3001 | 见下方错误码表 |
| ④ | data 非空 | data为列表且len > 0 | data为空数组 | 原因待结合 symbol、权限和服务状态进一步判断 |
| ⑤ | 品种与字段 | 见 §4 完整校验规则 | 缺失、多余或字段畸形 | 见 §4 |
步骤③错误码对照:
| code | 含义 | 动作 |
|---|---|---|
1001 | API Key 无效或已过期 | 阻断,检查 Key |
1002 | 请求参数错误 | 阻断,检查参数格式 |
1004 | 权限或访问范围不足 | 阻断,确认 Key 的授权范围 |
3001 | 请求限流 | 等待 Retry-After 后重试 |
4. 必须校验的字段与规则
步骤⑤不仅检查品种是否齐全,还必须逐条校验每个返回项的字段格式。以下任何一条不满足,都应判定校验失败并终止程序。
逐条校验规则:
| 字段 | 规则 | 失败处理 |
|---|---|---|
symbol | 非空字符串,大小写不敏感匹配目标集合 | 终止 |
type | 非空字符串 | 终止 |
last_price | 非空字符串;Decimal(str(...))成功;is_finite()为 True(拒绝 NaN/Infinity) | 终止 |
timestamp | int类型且不是bool;满足当前 ticker 的 13 位边界(1700000000000 <= ts <= 2600000000000) | 终止 |
集合校验规则:
| 检查项 | 规则 | 失败处理 |
|---|---|---|
| 返回数量 | len(data) == len(TARGET_SYMBOLS) | 终止 |
| 重复 symbol | data中无重复的symbol | 终止 |
| 意外 symbol | 返回的symbol全部属于目标集合 | 终止 |
| 畸形元素 | data每个元素均为dict | 终止 |
为什么这些校验重要:
last_price为null或"NaN"→ 下游计算不会报错,但结果全部污染timestamp为字符串 → 时间比较静默失败,信号对齐错位- 返回额外品种 → 可能 Key 权限泄露或接口行为变更
- 返回数量不等于请求数量 → 静默丢弃已在数据层发生
5. 教学代码(基于官方契约,未经本轮运行)
以下代码基于 TickDB REST ticker 接口契约编写。替换.env中的 API Key 即可运行。代码不依赖任何本轮运行输出——所有异常路径均以sys.exit(1)终止。
环境:Python 3.9+
pipinstallrequests==2.31.0 python-dotenv==1.0.0.env 文件(不要提交到版本控制;务必加入 .gitignore)
TICKDB_API_KEY=your_api_key_here.gitignore 补充
.envcontract_test.py
""" 多市场行情 API 最小 contract test 基于 TickDB REST ticker 接口契约编写。 任何校验失败均以 sys.exit(1) 终止,不告警后继续。 截至本文发布尚无本轮运行输出。 """importosimportsysfromdecimalimportDecimal,InvalidOperationfromhttpimportHTTPStatusimportrequestsfromdotenvimportload_dotenv load_dotenv()API_KEY=os.getenv("TICKDB_API_KEY")ifnotAPI_KEY:sys.exit("未设置 TICKDB_API_KEY,请检查 .env 文件")ENDPOINT="https://api.tickdb.ai/v1/market/ticker"TARGET_SYMBOLS=["600519.SH","700.HK","AAPL.US","BTCUSDT","EURUSD",]TARGET_UPPER={s.upper()forsinTARGET_SYMBOLS}# timestamp 13 位边界(1700000000000 ≈ 2023-11-15, 2600000000000 ≈ 2052-05-23)TS_MIN=1700000000000TS_MAX=2600000000000PASS="PASS"FAIL="FAIL"defexit_fail(step:str,detail:str=""):print(f"{FAIL}({step})")ifdetail:print(f"{detail}")sys.exit(1)defstep1_check_http():print("步骤① HTTP 状态码...",end=" ")try:resp=requests.get(ENDPOINT,params={"symbols":",".join(TARGET_SYMBOLS)},headers={"X-API-Key":API_KEY},timeout=10,)ifresp.status_code==HTTPStatus.OK:print(PASS)returnresp exit_fail(f"状态码:{resp.status_code}")exceptrequests.Timeout:exit_fail("请求超时")exceptrequests.ConnectionError:exit_fail("连接失败")exceptrequests.RequestExceptionase:exit_fail(f"请求异常:{e}")defstep2_check_json(resp):print("步骤② JSON 解析...",end=" ")try:body=resp.json()print(PASS)returnbodyexceptValueError:print(f"{FAIL}(响应非 JSON)")print(f" 响应前 200 字符:{resp.text[:200]}")sys.exit(1)defstep3_check_code(body):print("步骤③ 业务状态码...",end=" ")code=body.get("code")ifcode==0:print(PASS)returnbody.get("data",[])messages={1001:"API Key 无效或已过期",1002:"请求参数错误",1004:"权限或访问范围不足",3001:"请求限流",}detail=messages.get(code,f"未知错误码:{code}")exit_fail(f"code={code}",detail)defstep4_check_data_not_empty(data):print("步骤④ data 非空...",end=" ")ifisinstance(data,list)andlen(data)>0:print(f"{PASS}(返回{len(data)}条)")returndata exit_fail("data 为空","原因待结合 symbol、权限和服务状态进一步判断")defstep5_validate_records(data):print("步骤⑤ 品种与字段校验...")iflen(data)!=len(TARGET_SYMBOLS):exit_fail("返回数量不匹配",f"期望{len(TARGET_SYMBOLS)}条,实际{len(data)}条")returned_symbols=[]fori,iteminenumerate(data):ifnotisinstance(item,dict):exit_fail(f"data[{i}] 不是 dict",f"类型:{type(item)}")symbol=item.get("symbol")ifnotisinstance(symbol,str)ornotsymbol.strip():exit_fail(f"data[{i}] symbol 缺失或非字符串",f"symbol:{symbol}")symbol_upper=symbol.strip().upper()ifsymbol_uppernotinTARGET_UPPER:exit_fail(f"data[{i}] 返回了非目标品种",f"symbol:{symbol}")returned_symbols.append(symbol_upper)item_type=item.get("type")ifnotisinstance(item_type,str)ornotitem_type.strip():exit_fail(f"data[{i}] type 缺失或非字符串",f"symbol:{symbol}, type:{item_type}")last_price=item.get("last_price")ifnotisinstance(last_price,str)ornotlast_price.strip():exit_fail(f"data[{i}] last_price 缺失或非字符串",f"symbol:{symbol}, last_price:{last_price}")try:d=Decimal(last_price)ifnotd.is_finite():exit_fail(f"data[{i}] last_price 为 NaN 或 Infinity",f"symbol:{symbol}, value:{last_price}")except(InvalidOperation,ValueError):exit_fail(f"data[{i}] last_price 无法解析为 Decimal",f"symbol:{symbol}, value:{last_price}")ts=item.get("timestamp")ifnotisinstance(ts,int)orisinstance(ts,bool):exit_fail(f"data[{i}] timestamp 不是整数",f"symbol:{symbol}, timestamp:{ts}, type:{type(ts).__name__}")ifts<TS_MINorts>TS_MAX:exit_fail(f"data[{i}] timestamp 超出 13 位边界",f"symbol:{symbol}, timestamp:{ts}")iflen(set(returned_symbols))!=len(returned_symbols):exit_fail("data 中存在重复 symbol")missing=TARGET_UPPER-set(returned_symbols)ifmissing:exit_fail("目标品种缺失",f"缺失:{', '.join(sorted(missing))}")print(f"{PASS}五个品种全部通过字段校验")defmain():print("多市场行情 API contract test")print(f"目标品种:{', '.join(TARGET_SYMBOLS)}")print()resp=step1_check_http()body=step2_check_json(resp)data=step3_check_code(body)data=step4_check_data_not_empty(data)step5_validate_records(data)print()print("全部校验通过")print()print("已验证(仅限本轮 MCP 实测):")print(" - 五个目标品种 ticker 快照查询成立")print(" - 返回结构含 symbol/type/last_price/timestamp")print(" - last_price 为字符串,timestamp 本轮为 13 位")print("未验证(REST 待运行,不得外推):")print(" - K 线、逐笔成交、WebSocket 推送、订单簿")print(" - 其他品种、所有市场、所有时间点")print(" - 任何商业用途的适用性")print(f"数据来源: TickDB (https://api.tickdb.ai)")if__name__=="__main__":main()6. 已验证与未验证边界
本轮实测事实(2026-06-06,TickDB MCPget_ticker):
- 单次输入五个目标 symbol,返回
code=0,data包含五个目标品种 - 五项均包含
symbol、type、last_price、timestamp last_price为字符串;timestamp为整数,本轮均为 13 位- 无效 symbol 与有效 symbol 混合查询只返回有效品种;纯无效 symbol 返回空
data,二者code均为 0
限制条件:
本轮测试存在四个结构性限制:
- 单次查询不能证明稳定性:网络波动、服务端重启、配额周期切换都可能改变结果。
- 五个品种是采样:不代表该接口覆盖的所有资产类别和品种。
- ticker 快照端点不可外推:不能等同于 K 线、逐笔成交、WebSocket 推送或订单簿端点的行为。
- MCP 实测不可跨命名空间继承:MCP 路径的返回结构和字段路径可能与 REST 存在差异,REST 需独立验证。
未验证(不得外推):
- 所有品种、所有市场、所有时间点的查询可用性
- K 线、逐笔成交、订单簿、WebSocket 推送等端点
- 任何商业用途的适用性
- 价格数据、延迟数据、SLA、覆盖率或其他动态指标的承诺
7. REST、WebSocket、MCP 独立验证要求
统一 API Key 体系不等于各协议鉴权承载相同,统一接口降低了接入协调和迁移成本,但不能抹平市场制度、字段语义及授权差异。三个入口必须分别验证,不可互相继承结论。
| 入口 | 验证方式 | 本轮状态 |
|---|---|---|
| REST | 运行 §5 contract test,逐条校验字段 | 基于契约编写,待运行 |
| MCP | 在 Claude Code/Cursor 中调用get_ticker,检查返回结构 | ✅ 已实测,结论见 §2、§6 |
| WebSocket | 建立连接、订阅、检查推送结构与字段路径 | 未进行 |
8. 常见问题
Q1:为什么选这 5 个品种?
A 股、港股、美股代表三类股票市场,加密和外汇代表两类非股票行情。只有候选 API 明确宣称覆盖且当前 Key 具备对应权限时,五个代表类别才都应返回。
Q2:如果某个品种没返回,一定是接口的问题吗?
不一定。先检查 symbol 格式是否与接口要求一致,再确认 Key 权限是否覆盖该品种。如果格式和权限均确认无误但返回中仍缺失,可向数据源反馈。
Q3:这个 test 能替代完整的接口测试吗?
不能。它只是一个最小 smoke test。完整测试还应覆盖 K 线接口、WebSocket 连通性、错误码处理、限流恢复、长时间运行稳定性等。
Q4:代码中为什么不打印last_price?
本文验证的是通路和字段结构,动态价格随时间变化,不应出现在以验证结构为目的的文章中。
Q5:如果我的项目只需要 A 股,还需要这样测吗?
可以精简品种数量,但校验步骤(HTTP → JSON → code → data 非空 → 字段类型与值校验)建议全部覆盖。
Q6:REST 和 MCP 用同一套 API Key,结论能通用吗?
不能。统一 API Key 体系简化了接入流程,但两个入口的返回结构、字段路径可能存在差异,必须分别验证,不可互相继承。
📡 本文验证示例由 TickDB.ai 提供,接口文档见https://docs.tickdb.ai
⚠️ 本文为工程验证教程,不构成任何投资建议
CSDN 标签
Python、行情API、contract test、TickDB、多市场