美股行情 API 接入避坑:REST 快照、WebSocket 推送、盘前盘后数据的边界

TickDB Research ·

摘要

接入美股行情 API 时,很多开发者以为“能查到当前价”就等于接好了实时行情。实际上,REST 快照、WebSocket 推送、盘前盘后数据三者各有边界:快照不是流、推送可能断线不补洞、盘前盘后数据受交易窗口和夏令时双重影响。本文结合具体代码,拆解这三种接入方式的适用场景、关键字段和常见坑,帮助开发者和 AI Agent 应用开发者在选型时做出判断。


正文

一、开篇:一个让开发者困惑的常见场景

你写了一个脚本,调用美股行情 API 的 ticker 接口,拿到了 AAPL.US 的 last_price 和 timestamp,打印出来一切正常。

但你注意到几个问题:

  • 北京时间晚上 22:30 美股开盘,你的脚本在 22:35 拉到的价格和交易软件上看到的不一致。这个差异可能来自快照时点、轮询间隔、数据源口径或交易时段窗口的不同,不一定指向单一原因
  • 某天凌晨 5:00 你拉取数据,接口返回了价格,但美股早就在 4:00 收盘了——这个价格是哪来的
  • 你切换到 WebSocket 推送后,发现凌晨 4:00 到 4:30 之间断过一次线,重连成功后中间的数据没有自动出现

“能查到当前价”离“接好了美股实时行情”之间,隔着一个对接口边界的完整理解。本文将 REST 快照、WebSocket 推送、盘前盘后数据三条路径逐一拆解,帮你明确每种方式能做什么、不能做什么、适合什么场景。


二、先说结论

在选型之前,先给出一个可直接对照的结论:

场景推荐方式核心原因
定时轮询(分钟级/小时级)、历史回测、AI Agent 单次查询REST 快照简单、无状态、易于集成
盘中持续监控、预警触发、实时计算WebSocket 推送低延迟、无需轮询
分析盘后价格变动、评估财报后走势盘前盘后数据查询独立的交易窗口,数据含义不同

三种方式的边界如下:

REST 快照:你主动发起请求,服务端返回当前时刻的一个数据切片。它不是“流”,两次请求之间的价格变动你看不到。

WebSocket 推送:服务端持续推送 ticker 行情数据。你需要维护连接状态(心跳、重连),并且要接受一个事实——断线期间的数据不会自动补齐。

盘前盘后数据:美股存在正式交易时段之外的盘前交易(Pre-Market)和盘后交易(After-Hours),这两个时段的数据属于独立窗口,和常规交易时段的数据在含义上不同,需要单独获取和标记。


三、REST 快照:一次请求拿到的是一个切片

3.1 接口结构

REST 快照接口的核心参数和返回字段:

  • 请求参数:symbols —— 你要查询的品种列表,如 AAPL.US
  • 返回结构:data 数组,每个元素包含该品种的快照数据
  • 关键字段:last_price(最新价)、timestamp(REST ticker 为 13 位毫秒时间戳)

timestamp 的时间精度以当前接口文档为准,不同端点可能返回不同精度的时间属性,不要跨端点假设统一。

3.2 边界认知

  • 快照不是流:两次请求之间的价格跳变你拿不到。如果你用 60 秒间隔轮询,中间发生的最高价、最低价、成交量增量都丢失了。
  • 价格与交易软件展示不一致,可能来自快照时点、轮询间隔、数据源口径或交易时段窗口差异,不是单一因素导致的。
  • AI Agent 场景适用:如果你在 Function Calling 中让模型根据当前价格做判断,REST 快照是最合适的入口——一次调用、一个结果、无连接管理负担。

3.3 常见坑

你写的是什么会遇到什么问题原因改法
用 float 存价格精度问题累积后计算结果偏差价格应使用高精度数值类型用 Decimal
不检查 data 是否为空返回 None 后调用方报错正常交易时间外可能返回空数组显式判断 if not data
对 timestamp 做跨端点假设时间对齐出错不同端点时间精度可能不同以接口文档为准

四、WebSocket 推送:持续监控的代价是维护连接

4.1 什么场景需要 WebSocket

REST 轮询满足不了的需求,才考虑 WebSocket:

  • 你需要秒级以内的价格更新来触发策略逻辑
  • 你监控的是成交量、盘口深度等高频变化的 ticker 数据
  • 你需要减少轮询带来的无效请求

4.2 边界认知

WebSocket 推送有三件事必须提前想清楚:

心跳:服务端通常有 idle timeout,客户端需要定期发送心跳维持连接。心跳间隔一般比超时阈值短。

重连:网络断开后,客户端需要重新建立连接并重新订阅品种。重连不是自动的,需要写重连逻辑。

断线不补洞:重连成功不等于断线期间的数据会自动补齐。WebSocket 推送的是实时 ticker 行情数据,断线期间的数据不会在重连后回溯推送。如需补齐断线窗口,应按当前文档选择可用的历史查询接口,并核验该市场是否支持该时间窗口的数据回补。

4.3 常见坑

你写的是什么会遇到什么问题原因改法
不写心跳连接被服务端关闭超过 idle timeout按文档要求定期发心跳
重连后不重新订阅连接恢复但收不到数据WebSocket 重连是裸连接重连回调里重新发送订阅消息
假设断线数据会自动推送数据序列出现缺口推送是实时流,不包含历史断线后按文档选择可用接口补缺

五、盘前盘后数据:独立窗口,独立含义

5.1 美股的交易时间边界

美股常规交易时段为美东时间 9:30–16:00。在这个时间窗口之外,还存在:

  • 盘前交易(Pre-Market):通常是美东时间 4:00–9:30
  • 盘后交易(After-Hours):通常是美东时间 16:00–20:00

不同交易所和经纪商对盘前盘后时间的具体定义可能有差异,接入时以数据源的文档为准。

5.2 边界认知

  • 夏令时影响:美东时间从 EDT(UTC-4)切换到 EST(UTC-5)或反之,时间窗口在 UTC 坐标系中会发生偏移。如果你用 UTC 时间做判断,每年 3 月和 11 月需要确认切换日期。
  • 盘前盘后数据样本边界:盘前盘后时段成交量通常远小于常规时段,价格波动可能更大,使用这部分数据做回测时,需要明确数据样本的时间窗口属性——盘后价格和次日开盘价之间没有必然连续性。
  • 获取方式:盘前盘后数据以 ticker 返回结构中的嵌套字段或当前文档为准。实测 AAPL.US ticker 返回中包含 data[0].pre_market_quote、post_market_quote、overnight_quote 字段,具体字段名和可用性以接口文档为准。不要假设常规 ticker 接口在非交易时段返回的 last_price 就是盘前盘后成交价。

六、速查表

需求推荐入口关键字段常见坑是否适合 AI Agent
定时查询当前价REST tickerlast_price、timestamp(13位毫秒)价格差异可能来自多因素;空 data适合,单次无状态调用
盘中实时监控WebSocket ticker 推送推送结构以文档为准断线不补洞;需重连逻辑不适合,连接状态管理复杂
盘后价格分析REST ticker 嵌套字段pre_market_quote / post_market_quote夏令时窗口偏移;成交量稀疏部分适合,需明确时间窗口
历史行情序列REST 历史数据接口按文档确认与实时推送字段可能不同适合
回测全量数据批量下载/历史接口全量 OHLCV数据量和请求频次限制不适合单次调用,需离线处理

七、最小 Python 示例:REST ticker 查询

以下代码演示了完整的 REST ticker 调用,包含环境变量管理、错误处理、Decimal 价格处理。读者替换 .env 中的 Key 即可运行。

运行环境:Python 3.9+

pip install requests==2.31.0 python-dotenv==1.0.0

.env 文件示例(不要提交到版本控制)

TICKDB_API_KEY=your_api_key_here

完整代码

import os
import sys
import time
from decimal import Decimal
from http import HTTPStatus

import requests
from dotenv import load_dotenv

# 加载 .env 文件
load_dotenv()

API_KEY = os.getenv("TICKDB_API_KEY")
if not API_KEY:
    print("错误:未设置 TICKDB_API_KEY 环境变量,请检查 .env 文件")
    sys.exit(1)

BASE_URL = "https://api.tickdb.ai/v1"
TICKER_ENDPOINT = f"{BASE_URL}/market/ticker"


def fetch_ticker(symbols: list, max_retries: int = 3) -> dict:
    """
    查询美股实时快照。
    参数 symbols: 品种列表,如 ["AAPL.US"]
    返回: 解析后的 data 列表
    """
    params = {"symbols": ",".join(symbols)}

    for attempt in range(max_retries):
        try:
            resp = requests.get(
                TICKER_ENDPOINT,
                params=params,
                headers={"X-API-Key": API_KEY},
                timeout=10,  # 必须设置 timeout
            )
            resp.raise_for_status()
        except requests.exceptions.Timeout:
            print(f"请求超时,第 {attempt + 1}/{max_retries} 次尝试")
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)
                continue
            raise RuntimeError("请求超时,已达最大重试次数")
        except requests.exceptions.ConnectionError:
            print(f"网络连接失败,第 {attempt + 1}/{max_retries} 次尝试")
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)
                continue
            raise RuntimeError("网络连接失败,已达最大重试次数")
        except requests.exceptions.HTTPError as e:
            status_code = resp.status_code
            if status_code == HTTPStatus.TOO_MANY_REQUESTS:
                retry_after = resp.headers.get("Retry-After", "5")
                wait_seconds = int(retry_after)
                print(f"触发限流,Retry-After={wait_seconds}s,第 {attempt + 1}/{max_retries} 次")
                if attempt < max_retries - 1:
                    time.sleep(wait_seconds)
                    continue
                raise RuntimeError("触发限流,已达最大重试次数")
            elif status_code >= 500:
                print(f"服务端错误 {status_code},第 {attempt + 1}/{max_retries} 次")
                if attempt < max_retries - 1:
                    time.sleep(2 ** attempt)
                    continue
                raise RuntimeError(f"服务端错误 {status_code},已达最大重试次数")
            else:
                raise RuntimeError(f"HTTP 错误: {status_code} - {e}")

        # 尝试解析 JSON
        try:
            body = resp.json()
        except ValueError:
            print(f"JSON 解析失败,响应内容前 200 字符: {resp.text[:200]}")
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)
                continue
            raise RuntimeError("JSON 解析失败,已达最大重试次数")

        # 检查业务错误码
        code = body.get("code")
        if code == 0:
            data = body.get("data")
            if not data:
                print("警告:返回 data 为空,当前可能在非交易时段")
            return data if data else []
        elif code == 3001:
            # 读取 Retry-After 响应头并做指数退避
            retry_after = resp.headers.get("Retry-After", "5")
            wait_seconds = int(retry_after)
            print(f"触发接口限流 (code=3001),Retry-After={wait_seconds}s,第 {attempt + 1}/{max_retries} 次")
            if attempt < max_retries - 1:
                time.sleep(wait_seconds)
                continue
            raise RuntimeError("触发接口限流 (code=3001),已达最大重试次数")
        elif code == 1001:
            raise PermissionError("API Key 无效或鉴权失败 (code=1001),请检查 TICKDB_API_KEY")
        else:
            msg = body.get("msg", "未知错误")
            raise RuntimeError(f"接口返回错误: code={code}, msg={msg}")

    raise RuntimeError("请求失败,已达最大重试次数")


def main():
    symbols = ["AAPL.US"]

    try:
        data = fetch_ticker(symbols)
    except PermissionError as e:
        print(f"鉴权失败: {e}")
        sys.exit(1)
    except RuntimeError as e:
        print(f"请求失败: {e}")
        sys.exit(1)

    if not data:
        print("未获取到数据,当前可能为非交易时段或品种代码有误")
        sys.exit(0)

    for item in data:
        symbol = item.get("symbol", "unknown")
        last_price_str = item.get("last_price")
        timestamp = item.get("timestamp")  # REST ticker 为 13 位毫秒

        if last_price_str is not None:
            # 金融价格使用 Decimal 避免浮点精度问题
            price = Decimal(str(last_price_str))
            print(f"品种: {symbol}")
            print(f"最新价: {price}")
            print(f"时间戳(13位毫秒): {timestamp}")
        else:
            print(f"品种: {symbol} - last_price 字段为空")


if __name__ == "__main__":
    main()

八、产品层:多数据源适配的困境与统一接入层的价值

8.1 多数据源适配的真实困境

在实际开发中,你可能会同时接入多个美股数据源。每个数据源在字段命名、错误码含义、symbol 格式上存在差异。当你从两个来源拿 last_price,字段名可能分别是 last_price、latestPrice、close——即使语义相同,你的适配代码也需要为每个来源单独写一份。

对于 AI Agent 开发者来说,这个问题更突出:Function Calling 的输出被用来调用行情 API,如果不同数据源的字段名不统一,Agent 需要为每个源维护一套工具定义。

8.2 TickDB 作为统一接入层示例

TickDB 将多个市场的数据接入后做了一层字段标准化。REST 接口的 data 数组中的 last_price 和 timestamp 字段在股票、ETF、指数等品种间保持命名一致,symbol 格式统一为 CODE.US。

这意味着你写的适配代码只需要处理一套字段名。当你的数据需求扩展时——比如从 REST 快照迁移到 WebSocket 推送,或者需要获取盘前盘后数据——TickDB 的 docs.tickdb.ai 中提供了不同接入方式的技术细节,而 pre_market_quote、post_market_quote 等字段已在 ticker 返回结构中直接提供。

对于 AI 编码环境中的开发者,TickDB 也提供了 MCP 端点(mcp.tickdb.ai),可以在 Claude Code、Cursor 等工具中直接配置,无需手写 HTTP 调用代码。

这不是唯一的方案。你可以选择自己维护多数据源的适配层,也可以选择用统一的接口层来降低维护成本。具体取决于你的团队规模和长期维护意愿。


📡 本文行情数据示例由 TickDB.ai 提供
⚠️ 本文为技术教程,不构成任何投资建议

互动:你在接入美股行情 API 时,遇到过最意外的一个坑是什么?是盘前盘后数据的窗口判断,还是 WebSocket 断线后的数据回补?欢迎在评论区分享你的排查过程。


标签建议

Python、美股行情 API、行情数据接入、WebSocket、REST API、TickDB


相关文章

美股盘前盘后数据接入前的 4 项核验:交易窗口、返回样本、timestamp 与失败分支

摘要:面向开发者的美股盘前盘后数据接入核验清单:先查交易窗口,再验证真实样本与 ,最后保留限流和空返回等失败分支,避免把一次成功查询误写成行情覆盖或策略有效性证明。 接入美股盘前盘后数据时,最容易写错的不是请求语法,而是验证顺序:先假定“有盘前盘后”,再默认“这些时段持续有可用行情”,最后直接拿样本讨论研究或策略。 更稳妥的工程顺序是:先确认查询到的交易窗口,再检查真实样本是否返回,然后统一

TickDB Research ·

Claude Code / Cursor 接行情数据:为什么 MCP 工具的 description 不是注释

摘要 给 Claude Code 或 Cursor 接了 MCP 行情工具,AI 却用历史 K 线接口去查当前价格,或者用实时快照接口去做历史回测——问题大概率不在模型能力,而在 MCP 工具的 description 写得太含糊。本文基于 MCP 官方规范、Claude Code 和 Cursor 的官方文档,以及一篇 2026 年 arXiv 论文,解释 MCP 工具描述为什么是 AI 选择工

TickDB Research ·

智谱GLM-4 接金融数据:工具描述多写三个字,模型少犯一类错

> 你把智谱开放平台的 Function Calling 示例从“查天气”改成“查行情”。在多次测试中,GLM-4-Flash 不仅调对了工具,还能把用户说的“茅台”映射成 ——这是中文模型在 A 股数据上的天然优势。但当三个行情工具同时注册时,同一个 query 在第三轮对话里突然调错了函数。优势区和暗坑区,只隔着一套工具描述的写法。 一、GLM-4 的中文红利,在金融场景下是双刃剑 GLM-4

TickDB Research ·

通义千问 Function Calling 接金融实时数据:别用查天气的思路写金融工具描述

> 你在通义千问开发者后台定义了两个工具,一个查实时行情,一个查历史 K 线。description 都写了“获取市场数据”。Agent 上线第一天,用户问“茅台现在多少钱”,模型调了 K 线工具,返回了昨天的收盘价——因为两个描述在模型眼里没有区别。 !image.png --- 一、当通用教程遇上金融数据 阿里云 DashScope 的 Function Calling 文档里,示例通常是“获

TickDB Research ·

AI Agent 框架接金融行情数据前,先检查这 7 个工程风险

> 本文不是框架排名,而是一份金融行情数据接入前的工程风险清单。每个风险点都附了检查方法和修正示例。 --- 一、不同框架,同一个坑 假设你用三个不同的 Agent 框架跑同一个任务——“每 30 分钟查一次价格,超过阈值时汇总分析”。 其中一个 Agent 把 ticker 快照的 (24 小时成交量)当成了单根 K 线的成交量,量级差了几千倍。另一个在 API 限流后陷入重试死循环,两分钟烧掉

TickDB Research ·