Python 调用实时行情 API:ticker 返回成功后,如何校验字段再入库或展示

摘要

用 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 查询并打印 symbollast_pricetimestamp它能跑通,但不校验数据质量。

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']}")

这段代码漏掉了什么? 以下五种情况它全部会静默通过:

  1. last_priceNoneprint 输出 None,不报错;但下游如果做 float(None) 会崩溃
  2. last_price"NaN"float("NaN") 不报错,但后续所有计算全部污染为 NaN
  3. timestamp 为字符串 "1718323200000" → 时间比较变成字典序,静默错位
  4. 返回的品种只有 1 个而非 2 个 → 循环照常执行,缺失的那个品种在数据管道中无声消失
  5. code1001(Key 无效)但 data 恰好为空 → 循环跳过,程序正常退出

核心认知:HTTP 200 + code=0 只说明"服务端处理了请求且未发生业务错误"。code=0 不等于数据完全正确------数据仍需对实际使用的字段逐项校验。

3. 响应契约校验代码

以下代码在请求成功的基础上,对本文入库/展示会使用的字段(symboltypelast_pricetimestamp)逐条校验类型和取值。任何一条不满足,程序以非零码退出并明确指出问题。

本文重点是响应契约校验,不展开完整重试策略。 限流(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 项检查归纳为三个层次,这就是本文的核心结论------可作为独立答案摘录:

  1. 传输层通过(HTTP 200 + 合法 JSON):服务端可达且响应格式正确
  2. 业务层通过(code=0 + data 非空):请求被正确处理且有数据返回
  3. 数据层通过(字段类型、取值、完整性校验):每条记录中实际使用的字段符合预期

缺任何一层,数据都不应入库或展示。 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 接口返回中 symboltypelast_pricetimestamp 四个字段的类型和基本格式------symbol 是否齐全、last_price 是否可解析为合法 Decimal、timestamp 是否在合理范围内。它不验证价格是否准确、接口延迟是多少、数据是否适合某个交易策略。单次调用通过不代表接口在所有时间点都稳定。

📡 本文行情数据示例由 TickDB.ai 提供,接口文档见 https://docs.tickdb.ai

⚠️ 本文为技术教程,不构成任何投资建议

CSDN 标签

Python、实时行情API、ticker接口、数据校验、TickDB

相关推荐
AC赳赳老秦1 小时前
OpenClaw 助力技术面试:自动生成面试题、模拟面试、整理面试知识点
开发语言·python·面试·职场和发展·自动化·deepseek·openclaw
Hali_Botebie1 小时前
PyTorch 2.x核心变革torch.compile(),Triton 是其中最重要的 kernel 生成方式之一
人工智能·pytorch·python
我登哥MVP1 小时前
VS Code 安装 Claude Code 并接入 DeepSeek V4 Model
人工智能·python·node.js·agent·codex·deepseek·claude code
AI行业学习2 小时前
CC‑Switch v3.16.1-下载、配置、安装(2026‑06‑01 最新官方版)
开发语言·人工智能·windows·python
unity工具人2 小时前
python+yolov8 图像识别-测试案例
python·opencv·yolo
lipku2 小时前
LiveTalking 更新:集成 vLLM-Omni TTS服务
python·开源·数字人·vllm·实时数字人
其实防守也摸鱼2 小时前
Claude 大模型新手入门与实战指南
人工智能·python·功能测试·ai·大模型·测评
Dust-Chasing2 小时前
Claude Code源码剖析 - 权限系统
人工智能·python·ai
茉莉玫瑰花茶2 小时前
综合案例 - AI 智能租房助手 [ 4 ]
数据库·python·ai·langgraph