美股实时行情接入:REST轮询与WebSocket推送的延迟

用户头像Fxdund
2026-10-19 发布

做美股行情工具或者量化策略的人,几乎都会纠结同一个问题:实时行情到底该用HTTP轮询还是WebSocket推送?网上的说法五花八门,有人说不轮询就是不懂行情,有人说WebSocket才是正经解法。与其听别人讲,不如自己写代码测一遍——把两种方式接到同一个数据源上,用统一的口径量一量延迟、请求量、资源占用,结论自然就出来了。
美股实时行情接入.jpg

这篇就是一次完整的测量过程记录:怎么设计测试、REST侧怎么测、WebSocket侧怎么测、结果怎么对比、最后什么场景该选哪个。

为什么延迟这事值得认真测

很多做美股的朋友一开始觉得"延迟差个一两秒无所谓"。这取决于你的策略:

  • 日频选股:收盘后拿数据,延迟完全不敏感
  • 分钟级监控:一两秒延迟可以接受,但要稳定
  • 盘口、事件驱动、套利类策略:延迟直接决定策略能不能成立

另外延迟不是只看"快不快",还要看抖不抖。一个接口平均100毫秒但偶尔跳到5秒,对策略的伤害比稳定200毫秒还大。所以测量的时候,别只记平均值,最大值和分布也要记。

测试设计:先定好口径再动手

延迟测试最容易出问题的地方是口径不统一。测REST和测WebSocket,如果"延迟"的定义不一样,对比就没有意义。

我建议统一用这个口径:

延迟 = 数据到达本地的时间 - 行情数据自身的生成时间戳

两种方式都对比这个差值。REST响应里有最新成交时间戳 t(毫秒),WebSocket推送的报价消息里也有 t。本地收到数据时记一个 time.time() * 1000,减去 t,就是这次数据的"端到端延迟"。

测量样本至少跑几百条,同时记录三组数:最小值、平均值、P95。只看平均会掩盖抖动,P95才能反映最差情况有多差。

还有一个公平性问题:两边要订阅同一批标的、在同一个时段测。美股盘中和盘后的推送频率完全不一样,跨时段对比出来的数字没有意义。

REST轮询怎么测

REST这边思路很简单:循环调用报价接口,每收到一次响应就计算一次延迟,连续跑N次,把延迟序列存下来。

import time
import requests

API_BASE = "//api.itick.org"
TOKEN = "your_token_here"
headers = {"accept": "application/json", "token": TOKEN}

def measure_rest_polling(symbol="AAPL", region="US", rounds=200):
    latencies = []
    for _ in range(rounds):
        # 记录请求发出前的时间(毫秒)
        t0 = time.time() * 1000
        resp = requests.get(
            f"{API_BASE}/stock/quote",
            headers=headers,
            params={"region": region, "code": symbol}
        )
        data = resp.json()["data"]
        # 行情时间戳 t 是毫秒
        latency = t0 - data["t"]
        latencies.append(latency)
        time.sleep(1)  # 模拟真实轮询间隔
    return latencies

lats = measure_rest_polling()
lats.sort()
n = len(lats)
print(f"样本数: {n}")
print(f"最小: {lats[0]:.0f}ms, 平均: {sum(lats)/n:.0f}ms, P95: {lats[int(n*0.95)]:.0f}ms")

这里有个细节值得注意:t0 记的是"请求发出前",但请求从发出到响应回来还有一个网络往返时间。严格说,REST轮询拿到的价格是上一次成交的价格,等你处理完这笔数据,行情可能已经又走了一段。所以REST的"有效延迟"天然比WebSocket大——它多了一个完整的请求-响应周期,哪怕你每秒刷一次也一样。

另外轮询频率是有上限的。一秒一刷,一小时就是3600个请求,一整天开着,请求量非常可观。对个人开发者来说,这既是成本问题,也是被限流的问题。

WebSocket推送怎么测

WebSocket这边,数据是服务端主动推过来的,测量逻辑变成:每收到一条报价消息,就用消息里的 t 和本地接收时间算延迟。

import json
import threading
import time
import websocket

WS_URL = "wss://api.itick.org/stock"
TOKEN = "your_token_here"
authenticated = False
latencies = []

def on_message(ws, message):
    global authenticated
    payload = json.loads(message)

    if payload.get("resAc") == "auth" and payload.get("code") == 1 and not authenticated:
        authenticated = True
        ws.send(json.dumps({
            "ac": "subscribe",
            "params": "AAPL$US",
            "types": "quote"
        }))
        threading.Thread(target=heartbeat, args=(ws,), daemon=True).start()
        return

    if payload.get("resAc") == "pong":
        return

    data = payload.get("data") or {}
    if data.get("type") == "quote":
        now = time.time() * 1000
        latency = now - data["t"]
        latencies.append(latency)

def heartbeat(ws):
    while True:
        time.sleep(30)
        ws.send(json.dumps({"ac": "ping", "params": str(int(time.time() * 1000))}))

ws = websocket.WebSocketApp(
    WS_URL,
    header=[f"token: {TOKEN}"],
    on_message=on_message
)
ws.run_forever()

跑一段时间,比如等几百条消息进来,然后离线分析 latencies:

lats = sorted(latencies)
n = len(lats)
print(f"样本数: {n}")
print(f"最小: {lats[0]:.0f}ms, 平均: {sum(lats)/n:.0f}ms, P95: {lats[int(n*0.95)]:.0f}ms")

WebSocket的延迟有两个特点:一是推送频率跟着行情走——盘口活跃的时候消息多,清淡的时候消息少,不像轮询那样固定节奏;二是连接建立后只占一个长连接,不管订阅多少个标的,请求量都是恒定的,这是它跟轮询最本质的区别。

对比维度,不只是延迟

把两边都测完,你会得到两组数字。但选择方案不能只看延迟,我建议用四个维度一起看:

维度 REST轮询 WebSocket推送
延迟 多一个请求-响应周期,端到端延迟偏高 服务端主动推送,延迟低且稳定
请求量 与轮询频率成正比,长时间运行成本高 连接建立后恒定,不随标的数量增长
资源占用 每次请求都要建连、HTTP开销 一个长连接常驻,需要维护心跳
复杂度 简单,一个requests循环就够 需要处理鉴权、心跳、断线重连

结论其实很清晰:低频、短时、脚本化场景用REST,高频、长时、多标场景用WebSocket。日频任务、一次性拉快照、测试脚本,REST足够;盘中盯盘、事件驱动、跨标的实时监控,走WebSocket。

实测之外的几个坑

测量过程中顺便把几个老坑也验证了一遍,写下来免得大家再踩:

鉴权时序。 WebSocket连接建立不等于可以订阅,必须等 resAc:"auth" 且 code:1 再发subscribe。我见过直接连上就订阅的代码,数据一条都收不到,程序还不报错。

心跳不能省。 文档要求每30秒发一次心跳,不发的话长连接会被服务端断开。跑测量脚本一挂几小时,心跳漏了,后半段数据就全没了。

时间戳单位。 报价和K线的时间戳都是毫秒,直接用 time.time()(秒)去减会得到一个巨大的负数。统一用毫秒口径,或者写个工具函数转换,别在业务代码里随手除。

交易状态要判断。 美股盘前、盘中、盘后的行情特征差别很大,ts 字段标识交易状态。做延迟测试最好选正常交易时段,不然测出来的分布毫无参考价值。

小结

"轮询还是推送"这个问题,答案不在网上,在你的测量脚本里。把口径定好、两边各跑一遍、四维度对比,你的场景该用哪个自然就清楚了。对大多数做美股行情的个人开发者来说,最终方案通常是:REST拿快照和历史数据,WebSocket做盘中实时监控,两者各司其职。

数据源的具体参数和返回字段说明,写代码的时候边写边翻官方文档对照,比自己猜参数靠谱多了。

GitHub:https://github.com/itick-org/

评论