摘要

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


正文

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

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

但你注意到几个问题:

  • 北京时间晚上 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_quotepost_market_quoteovernight_quote 字段,具体字段名和可用性以接口文档为准。不要假设常规 ticker 接口在非交易时段返回的 last_price 就是盘前盘后成交价。

六、速查表

需求 推荐入口 关键字段 常见坑 是否适合 AI Agent
定时查询当前价 REST ticker last_pricetimestamp(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_pricelatestPriceclose——即使语义相同,你的适配代码也需要为每个来源单独写一份。

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

8.2 TickDB 作为统一接入层示例

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

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

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

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


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

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


标签建议

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


Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐