摘要:给程序或 Agent 接美股行情,第一个坑往往不是 API Key 配错,而是没分清 REST 快照、WebSocket 推送和交易时段这三个边界。本文以一次盘后告警失效的踩坑经历切入,拆解三种接入方式的适用场景、关键字段差异和常见排错路径,附 REST ticker 最小验证代码和一张可收藏的排错清单。关键词:美股行情 API、REST ticker、WebSocket 推送、交易时段、盘前盘后、工程验证。
一、一个盘后告警失效的踩坑经历
同事在周四晚上十点发来消息:告警脚本跑了一整天没问题,偏偏在收盘后触发了一次误报------last_price 还是下午四点的值,但脚本当成当前价发了告警。
排查过程很短:他用的是 REST ticker 快照,定时五分钟轮询一次。交易时段内一切正常,但收盘后 GET /v1/market/ticker 返回的仍是当日最后一笔成交价。脚本没有检查 timestamp,也没有判断当前是否在交易时段,直接把旧数据当成了实时信号。
修起来也不复杂------加了时间戳新鲜度检查和交易时段判断。但这个问题本身暴露了一个更深层的认知缺口:能拿到数据,和能拿到"对当前决策有效的数据",不是一回事。
美股行情的三种数据获取方式------REST 快照、WebSocket 推送、历史 K 线------对应的是三种完全不同的工程边界。换一个场景,边界就变了,代码也得跟着变。
二、三个边界:一张表说清楚
别急着选接口,先看清你的需求和接口之间的边界在哪。
| 你的需求 | 接入方式 | 关键字段 | 最常见三个坑 |
|---|---|---|---|
| 页面加载时显示一次最新价 | REST ticker | last_price、timestamp |
① 没检查 timestamp 新鲜度,收盘后拿旧数据当实时价;② 盘前盘后 last_price 可能是上一交易日收盘价,此时应关注 pre_market_quote 或 post_market_quote 等嵌套字段;③ symbol 格式写错(AAPL 而非 AAPL.US) |
| 盘中持续监控价格变化 | WebSocket 推送 | 推送中的 last_price、timestamp |
① 连接断开后恢复时订阅丢失;② 心跳只发 ping 不检查 pong;③ 推送 symbol 格式可能与 REST 不同 |
| 回测或趋势分析 | REST kline | close、time、volume |
① 用 ticker 的 volume_24h 替代 kline 的 volume;② time 和 timestamp 字段名混淆;③ 周期参数写错(不同接口对 1w 的定义可能不同) |
核心区别一句话:
- REST ticker 是一次查询,返回请求时刻的快照------你不知道这个价格是 1 秒前还是 20 分钟前的。
- WebSocket 推送是持续连接,服务端在价格变化时主动推送------你能知道价格变动的时间点。
- 交易时段定义了快照是否还在更新 。美股盘后(16:00-20:00 ET)和盘前(04:00-09:30 ET)虽然有交易,但流动性稀疏,
last_price的参考价值不同于盘中。此时 ticker 响应中若有pre_market_quote或post_market_quote嵌套字段,应优先用这些字段获取盘前盘后的报价信息,而不是只盯着主字段的last_price。
三、REST ticker 最小验证
下面这段代码只做一件事:验证你能拿到 AAPL.US 的最新快照,并把时间戳转换成可读时间,便于判断数据新鲜度。
参数说明:/v1/market/ticker 接受 symbols(复数),多个品种用逗号分隔,如 symbols=AAPL.US,TSLA.US。
环境准备
bash
pip install requests python-dotenv
.env 文件(不要提交到版本控制):
TICKDB_API_KEY=你的API Key
代码
python
import os
from datetime import datetime, timezone
import requests
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("TICKDB_API_KEY")
BASE_URL = "https://api.tickdb.ai"
def get_ticker(symbols: str):
"""
获取品种最新行情快照。
参数:
symbols: 逗号分隔的品种代码,如 "AAPL.US,TSLA.US"
返回:
dict: API 原始响应
注意:
这是快照,不是实时推送。timestamp 表示快照生成时间,
不代表当前仍在交易。调用方需自行判断数据新鲜度。
"""
resp = requests.get(
f"{BASE_URL}/v1/market/ticker",
headers={"X-API-Key": API_KEY},
params={"symbols": symbols},
timeout=10
)
resp.raise_for_status()
data = resp.json()
if data.get("code") != 0:
raise Exception(f"API 错误: code={data.get('code')}, message={data.get('message')}")
return data
def check_timestamp_freshness(timestamp_ms: int, max_age_seconds: int = 60):
"""
检查时间戳新鲜度。
参数:
timestamp_ms: 毫秒 UTC 时间戳
max_age_seconds: 最大允许数据年龄(秒),即快照生成至今的时间上限
返回:
tuple: (是否新鲜, 时间差秒数, 可读UTC时间字符串)
"""
now_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
age_seconds = (now_ms - timestamp_ms) / 1000
ts_readable = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc)
return age_seconds <= max_age_seconds, age_seconds, str(ts_readable)
if __name__ == "__main__":
try:
result = get_ticker("AAPL.US")
for item in result.get("data", []):
symbol = item["symbol"]
price = item["last_price"]
ts = item["timestamp"]
fresh, age, ts_str = check_timestamp_freshness(ts)
status = "✓ 新鲜" if fresh else f"✗ 已过期 ({age:.0f} 秒前)"
print(f"{symbol}: price={price}, timestamp={ts_str} [{status}]")
if not fresh:
print(f" ⚠ 数据年龄已超过新鲜度阈值,如需持续更新请评估 WebSocket 推送")
except Exception as e:
print(f"调用失败: {e}")
这段代码验证了什么
运行后你会看到类似输出:
AAPL.US: price=308.33, timestamp=2026-05-28 20:00:05+00:00 [✗ 已过期 (452 秒前)]
⚠ 数据年龄已超过新鲜度阈值,如需持续更新请评估 WebSocket 推送
这里 timestamp 显示的是 UTC 时间 20:00:05,转换到美东夏令时(ET)是下午 16:00:05,正是当日美股收盘附近。如果当前时间是晚上十点,那这个 last_price 就是收盘价------它可以用于展示上一交易日的收盘参考,但不能用于触发盘中交易决策。
至此,你已经完成了接入美股行情的第一道验证:不是"能不能拿到数据",而是"拿到的数据对当前决策有没有用"。
四、排错清单:快照查到了,然后呢?
上面代码跑通后,别急着往生产环境推。按下面四步逐项核对:
| 检查项 | 怎么验证 | 不通过时意味着什么 |
|---|---|---|
| symbol 格式 | 确认使用了含后缀格式,如 AAPL.US 而非 AAPL;多个品种用逗号分隔,参数名为 symbols(复数) |
短格式可能返回错误,或与其他市场同号品种混淆 |
| timestamp 新鲜度 | 对比返回的 timestamp(毫秒 UTC)和 datetime.now(UTC),计算数据年龄 |
年龄过大说明拿到的可能是收盘快照,不能当实时价用 |
| 交易时段判断 | 确认 timestamp 转换到美东时间后处于哪个时段(盘前/盘中/盘后/休市) |
盘后拿到的 last_price 可能是收盘价或稀疏成交价;此时应检查 ticker 中是否有 post_market_quote 等盘后字段 |
| 字段语义 | last_price 是快照时刻最新成交价,volume_24h 是 24 小时累计成交量 |
不要把 volume_24h 当单日成交量或 K 线周期成交量 |
排错优先级:先查 symbol 格式(一票否决),再查 timestamp 新鲜度(决定数据能否用),最后查字段语义(决定用得对不对)。
五、什么时候该从 REST 切到 WebSocket
REST ticker 适合的场景有边界。一旦越过这些边界,就该评估 WebSocket:
| REST ticker 适合 | 应该评估 WebSocket 的场景 |
|---|---|
| 页面加载时查一次价格 | 需要持续监控价格变化 |
| 定时轮询(间隔 ≥1 分钟) | 需要更频繁的行情推送需求 |
| 非交易时段查询收盘参考价 | 盘中需要知道价格变动的时间点 |
| 首次接入的连通性验证 | 生产环境对断线恢复有要求 |
切到 WebSocket 后,需要额外处理心跳、断线重连、订阅恢复和推送格式兼容------这些是连接生命周期管理的问题,另文展开。
六、本文不能保证什么
- 不能保证 REST 快照的
last_price一定反映当前最新成交。数据新鲜度取决于请求时刻与最近一笔成交的时间差,非交易时段可能长达数小时。 - 不能保证所有 timestamp 字段统一为毫秒。不同接口、不同品种下的时间戳单位可能存在差异,接入前需逐接口核验。
- 不能保证美股盘前盘后交易的完整覆盖 。盘前盘后时段流动性稀疏,某些品种可能长时间无成交,快照返回的是当日最后一笔价。盘前盘后报价信息可能出现在
pre_market_quote或post_market_quote嵌套字段中,具体以接口响应结构为准。 - 不能从"能拿到数据"推出"可以做交易决策"。行情数据接入和交易执行是两个独立问题,本文仅涉及前者。
你用 REST ticker 做价格监控时,有没有被收盘后的旧数据坑过?你是怎么判断数据新鲜度的------检查 timestamp,还是额外拉取交易时段日历?评论区聊聊你的做法。
| 你接下来卡住的问题 | 值得继续看的方向 |
|---|---|
| REST 数据不够实时 | WebSocket 订阅、心跳和重连的工程实现 |
| 想用历史数据做回测 | K 线接口的字段差异和复权概念 |
| 数据拿到了但不敢用 | symbol 格式、时间戳单位、字段语义的系统梳理 |
参考文献:TickDB API 文档,可搜索查阅 https://docs.tickdb.ai。
📡 数据由 TickDB.ai 提供
本文不构成任何投资建议。
工程笔记
标签:美股行情 API / REST ticker / 交易时段 / 盘前盘后 / 工程验证 / TickDB