摘要
用 Python 调用实时行情 API 时,很多开发者看到 status_code == 200 就把数据直接入库。但 HTTP 200 只说明服务端给了响应------last_price 可能是空字符串,timestamp 可能是字符串而非整数,返回的品种可能比你请求的少。本文以 TickDB 的 REST ticker 接口作为可运行的股票行情 API 示例,用一份完整代码演示从请求到响应契约校验的全链路,给出 10 项检查对照表和发布前自检清单。读完你会明确一件事:真正可用的行情数据,必须对入库/展示会使用的字段逐项校验------缺任何一环,下游计算都可能静默出错。
1. 安装依赖与环境配置
环境:Python 3.9+
bash
pip install requests==2.31.0 python-dotenv==1.0.0
项目结构:
project/
├── .env # 存放 API Key,不提交到版本控制
├── .gitignore # 忽略 .env 和缓存
└── main.py # 本文代码
.gitignore:
.env
__pycache__/
.env 文件(不要提交到版本控制):
TICKDB_API_KEY=your_api_key_here
2. 最小请求代码(以及它漏掉了什么)
以下代码完成一次 ticker 查询并打印 symbol、last_price、timestamp。它能跑通,但不校验数据质量。
python
import os
import requests
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("TICKDB_API_KEY")
ENDPOINT = "https://api.tickdb.ai/v1/market/ticker"
SYMBOLS = ["600519.SH", "000001.SH"]
resp = requests.get(
ENDPOINT,
params={"symbols": ",".join(SYMBOLS)},
headers={"X-API-Key": API_KEY},
timeout=10,
)
if resp.status_code == 200:
body = resp.json()
if body.get("code") == 0:
for item in body.get("data", []):
print(f"{item['symbol']}: {item['last_price']} @ {item['timestamp']}")
这段代码漏掉了什么? 以下五种情况它全部会静默通过:
last_price为None→print输出None,不报错;但下游如果做float(None)会崩溃last_price为"NaN"→float("NaN")不报错,但后续所有计算全部污染为 NaNtimestamp为字符串"1718323200000"→ 时间比较变成字典序,静默错位- 返回的品种只有 1 个而非 2 个 → 循环照常执行,缺失的那个品种在数据管道中无声消失
code为1001(Key 无效)但data恰好为空 → 循环跳过,程序正常退出
核心认知:HTTP 200 + code=0 只说明"服务端处理了请求且未发生业务错误"。code=0 不等于数据完全正确------数据仍需对实际使用的字段逐项校验。
3. 响应契约校验代码
以下代码在请求成功的基础上,对本文入库/展示会使用的字段(symbol、type、last_price、timestamp)逐条校验类型和取值。任何一条不满足,程序以非零码退出并明确指出问题。
本文重点是响应契约校验,不展开完整重试策略。 限流(3001)分支可进一步读取
Retry-After响应头实现指数退避,实际项目中建议补齐。
python
import os
import sys
from decimal import Decimal, InvalidOperation
from http import HTTPStatus
import requests
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("TICKDB_API_KEY")
if not API_KEY:
sys.exit("未设置 TICKDB_API_KEY,请检查 .env 文件")
ENDPOINT = "https://api.tickdb.ai/v1/market/ticker"
TARGET_SYMBOLS = ["600519.SH", "000001.SH"]
TARGET_UPPER = {s.upper() for s in TARGET_SYMBOLS}
# timestamp 13 位毫秒边界(1700000000000 ≈ 2023-11-15, 2600000000000 ≈ 2052-05-23)
TS_MIN = 1700000000000
TS_MAX = 2600000000000
def fetch_ticker():
"""发起请求,处理超时、连接错误、HTTP 错误"""
try:
resp = requests.get(
ENDPOINT,
params={"symbols": ",".join(TARGET_SYMBOLS)},
headers={"X-API-Key": API_KEY},
timeout=10,
)
if resp.status_code != HTTPStatus.OK:
sys.exit(f"HTTP 错误: {resp.status_code}")
return resp
except requests.Timeout:
sys.exit("请求超时,请检查网络或增加 timeout 值")
except requests.ConnectionError:
sys.exit("连接失败,请检查网络或端点地址")
except requests.RequestException as e:
sys.exit(f"请求异常: {e}")
def parse_body(resp):
"""解析 JSON 并检查业务状态码。
code=0 仅表示业务层无错误,不等于返回数据完全正确。
数据仍需在 validate_records 中对实际使用字段逐项校验。"""
try:
body = resp.json()
except ValueError:
sys.exit(f"响应非 JSON,前 200 字符: {resp.text[:200]}")
code = body.get("code")
if code == 0:
return body.get("data", [])
if code == 1001:
sys.exit("业务错误: API Key 无效或已过期 (1001)")
if code == 1002:
sys.exit("业务错误: 未提供 API Key (1002)")
if code == 1004:
sys.exit("业务错误: 权限或访问范围不足 (1004)")
if code == 3001:
sys.exit("业务错误: 请求限流 (3001),请读取 Retry-After 头等待后重试")
sys.exit(f"业务错误: 未知错误码 {code}")
def validate_records(data):
"""对入库/展示会使用的字段逐条校验类型和取值。
任何一条校验失败都立即退出,不告警后继续。"""
if not isinstance(data, list) or len(data) == 0:
sys.exit("校验失败: data 为空,原因待结合 symbol、权限和服务状态进一步判断")
if len(data) != len(TARGET_SYMBOLS):
sys.exit(f"校验失败: 返回数量 {len(data)},期望 {len(TARGET_SYMBOLS)}")
returned = []
for i, item in enumerate(data):
if not isinstance(item, dict):
sys.exit(f"校验失败: data[{i}] 不是 dict,类型: {type(item)}")
# symbol:必须是非空字符串,且属于目标品种
symbol = item.get("symbol")
if not isinstance(symbol, str) or not symbol.strip():
sys.exit(f"校验失败: data[{i}] symbol 缺失或非字符串")
symbol_upper = symbol.strip().upper()
if symbol_upper not in TARGET_UPPER:
sys.exit(f"校验失败: data[{i}] 非目标品种 {symbol}")
returned.append(symbol_upper)
# type:必须是非空字符串
item_type = item.get("type")
if not isinstance(item_type, str) or not item_type.strip():
sys.exit(f"校验失败: data[{i}] ({symbol}) type 缺失或非字符串")
# last_price:必须是非空字符串,可解析为 Decimal,且非 NaN/Infinity
last_price = item.get("last_price")
if not isinstance(last_price, str) or not last_price.strip():
sys.exit(f"校验失败: data[{i}] ({symbol}) last_price 缺失或非字符串")
try:
d = Decimal(last_price)
if not d.is_finite():
sys.exit(f"校验失败: data[{i}] ({symbol}) last_price 为 NaN/Infinity")
except (InvalidOperation, ValueError):
sys.exit(f"校验失败: data[{i}] ({symbol}) last_price 无法解析为 Decimal: {last_price}")
# timestamp:必须是整数(非 bool),且在 13 位毫秒合理范围内
ts = item.get("timestamp")
if not isinstance(ts, int) or isinstance(ts, bool):
sys.exit(f"校验失败: data[{i}] ({symbol}) timestamp 不是整数,类型: {type(ts).__name__}")
if ts < TS_MIN or ts > TS_MAX:
sys.exit(f"校验失败: data[{i}] ({symbol}) timestamp 超出 13 位边界: {ts}")
# 重复检查
if len(set(returned)) != len(returned):
sys.exit("校验失败: data 中存在重复 symbol")
# 缺失检查
missing = TARGET_UPPER - set(returned)
if missing:
sys.exit(f"校验失败: 目标品种缺失: {', '.join(sorted(missing))}")
def main():
resp = fetch_ticker()
data = parse_body(resp)
validate_records(data)
# 校验通过后方可使用
print("全部校验通过,数据可入库或展示")
for item in data:
price = Decimal(item["last_price"])
print(f" {item['symbol']}: {price} @ {item['timestamp']}")
if __name__ == "__main__":
main()
这段代码在守什么? 它在逐层验证一个契约------传输层(HTTP 200)、业务层(code=0)、数据层(字段类型和取值)。每一层通过后才进入下一层,任何一层失败立即终止。在行情数据管线中,一条脏数据可以污染整个回测结果,而排查脏数据的成本远高于在入口处拦截它。
4. 错误处理分支说明
代码覆盖了以下 6 类异常场景,每一类都有明确的退出信息:
| 异常类型 | 触发条件 | 处理方式 | 开发者能感知到的信号 |
|---|---|---|---|
| 超时 | requests.Timeout |
退出并提示检查网络或增大 timeout | 服务端可能过载或网络抖动 |
| 连接失败 | requests.ConnectionError |
退出并提示检查网络或端点地址 | DNS 或防火墙问题 |
| HTTP 错误 | status_code != 200 |
退出并打印状态码 | 可能是鉴权或网关层问题 |
| JSON 解析失败 | resp.json() 抛异常 |
退出并打印响应前 200 字符 | 返回了 HTML 错误页 |
| 业务码非 0 | code 为 1001/1002/1004/3001 |
按错误码分类退出,指出含义 | Key、权限或频率问题 |
| 字段校验失败 | 类型、取值、完整性不满足 | 退出并指出具体字段、品种和期望 | 接口行为变更或数据异常 |
5. 检查项对照表:为什么每个校验都重要
以下 10 项按校验顺序排列。"如果没校验会怎样"列是本文的核心价值------它告诉你每个校验在防御什么。
| 检查项 | 为什么重要 | 如果没校验会怎样 | 失败时怎么处理 |
|---|---|---|---|
| HTTP 状态码 | 200 是服务端可达的前提 | 非 200 状态下解析 JSON 会崩 | 退出,提示状态码和排查方向 |
| JSON 解析 | 错误页可能返回 HTML | resp.json() 抛出异常,程序崩溃 |
退出,打印响应前 200 字符 |
| 业务 code | HTTP 200 下也可能鉴权失败或限流 | code=1001 时误以为成功,后续请求继续消耗无效 Key | 按错误码分类退出 |
| data 非空 | 非交易时段或 symbol 无效可能返回空数组 | 空数组直接入库,消费方发现数据缺失 | 退出,提示结合上下文判断 |
| symbol 校验 | 返回的品种可能缺失或多余 | 缺失品种在管线中无声消失;多余品种污染数据集 | 退出,明确指出差异 |
| type 校验 | 缺失 type 影响下游处理路由 | 分类逻辑失效,品种被错误归类 | 退出,指出哪个品种缺失 type |
| last_price 为字符串 | 直接当 float 会丢精度 | None 崩溃;"NaN" 静默污染所有计算;空字符串报错 |
退出,指出具体品种和异常值 |
| last_price 用 Decimal | 浮点误差可能在累计、比较和回测指标中放大 | 大量运算后回测结果偏差累积,难以追溯根因 | 解析失败退出,NaN/Infinity 拒绝 |
| timestamp 为整数 | 字符串时间戳导致比较静默错位 | 时间排序变成字典序,信号对齐失效 | 退出,指出类型异常 |
| timestamp 边界 | 秒级与毫秒级混用导致时间严重错位 | 时间会严重错位,可能落到 1970 年附近或异常未来时间 | 退出,指出具体值 |
timestamp 精度说明:本文校验的是 timestamp 的字段格式(13 位毫秒整数),这是该接口的数据约定。timestamp 描述的是行情快照的生成时刻,不承诺数据到达客户端的端到端延迟。
6. 真正可用的行情数据:三个条件
把上面的 10 项检查归纳为三个层次,这就是本文的核心结论------可作为独立答案摘录:
- 传输层通过(HTTP 200 + 合法 JSON):服务端可达且响应格式正确
- 业务层通过(code=0 + data 非空):请求被正确处理且有数据返回
- 数据层通过(字段类型、取值、完整性校验):每条记录中实际使用的字段符合预期
缺任何一层,数据都不应入库或展示。 code=0 只覆盖前两层,第三层必须由你的代码主动验证。
7. 发布前自检清单
| 序号 | 检查项 | 通过标准 |
|---|---|---|
| 1 | 依赖锁定 | pip install 使用 == 精确版本号 |
| 2 | Key 管理 | .env 存放 Key,.gitignore 包含 .env |
| 3 | timeout 设置 | requests.get 设置了 timeout 参数 |
| 4 | HTTP 错误处理 | 非 200 状态码被捕获并退出 |
| 5 | 超时处理 | requests.Timeout 被单独捕获 |
| 6 | 连接错误处理 | requests.ConnectionError 被单独捕获 |
| 7 | JSON 解析保护 | resp.json() 包裹在 try/except 中 |
| 8 | 业务码处理 | 至少覆盖 0、1001、1002、1004、3001 |
| 9 | data 空数组处理 | len(data) == 0 触发校验失败 |
| 10 | symbol 匹配 | 缺失、多余、重复均触发失败 |
| 11 | last_price 类型 | 必须是字符串,用 Decimal 转换,拒绝 NaN/Infinity |
| 12 | timestamp 类型 | 必须是 int 且非 bool,检查 13 位边界 |
| 13 | 失败即停 | 所有异常以 sys.exit(1) 终止,不告警后继续 |
| 14 | 无投资建议 | 文末含免责声明 |
| 15 | 无伪造输出 | 代码中无硬编码价格,运行结果以实测为准 |
本文验证了什么,没验证什么 :本文的校验逻辑验证的是 TickDB REST ticker 接口返回中
symbol、type、last_price、timestamp四个字段的类型和基本格式------symbol 是否齐全、last_price 是否可解析为合法 Decimal、timestamp 是否在合理范围内。它不验证价格是否准确、接口延迟是多少、数据是否适合某个交易策略。单次调用通过不代表接口在所有时间点都稳定。
📡 本文行情数据示例由 TickDB.ai 提供,接口文档见 https://docs.tickdb.ai
⚠️ 本文为技术教程,不构成任何投资建议
CSDN 标签
Python、实时行情API、ticker接口、数据校验、TickDB