全部文章
API 参考WebSocketPolymarket API交易机器人

Polymarket WebSocket:不掉线的实时数据

轮询订单簿是白白耗尽速率限制额度最常见的方式。下面介绍四个数据流、没人写进文档的订阅选项,以及那种让你的机器人收到一片沉默、却以为市场很平静的失效模式。

阅读约 10 分钟更新于 2026年9月24日

如果你的机器人用定时器轮询 GET /book,这一页能帮你省下大部分速率限制额度和一部分延迟。Polymarket 通过 WebSocket 推送订单簿更新,一个订阅只需一个连接,而不是成千上万个请求。

简要版本

连接到 wss://ws-subscriptions-clob.polymarket.com/ws/market,发送一个指明 assets_ids 的订阅,然后每 10 秒发送一次 PING,否则会被断开。如果你想要 best_bid_ask、new_market 或 market_resolved 事件,请设置 custom_feature_enabled: true:它们默认是关闭的,而它们的缺席看起来和一个平静的市场一模一样。

四个数据流

数据流URL内容认证
Marketwss://ws-subscriptions-clob.polymarket.com/ws/market公开的订单簿、价格和生命周期更新无
Userwss://ws-subscriptions-clob.polymarket.com/ws/user你的订单和成交更新需要
RTDS (旧版)wss://ws-live-data.polymarket.com评论:参考价格已迁移到 PolyBolt无
PolyBoltwss://ws-live-v2.polymarket.com/ws加密货币、股票和 TWAP 参考价格需要
Sportswss://sports-api.polymarket.com/ws实时比赛状态和比分无

User 频道是大家最容易忽略的

如果你的机器人轮询 /data/orders 来确认订单是否成交,你就是在为 User 频道会在第一时间推送给你的信息消耗速率限制额度,而且延迟还更高。任何会下单的机器人都应该订阅这个数据流。

参考价格已于 2026 年 9 月 15 日迁移

Polymarket 上线了 PolyBolt,并将 RTDS 的参考价格功能列为旧版。crypto_prices、crypto_prices_chainlink 和 equity_prices 现在位于 wss://ws-live-v2.polymarket.com/ws,来源从 Binance/Chainlink 改为 Pyth,交易对代码改为小写的 btcusd,而不是 btcusdt 或 btc/usd,并且现在需要认证。完整映射见 PolyBolt 迁移指南。本页的其他内容,即 CLOB 的 market 和 user 频道,不受影响。

订阅 market 频道

建立连接,然后发送一条 JSON 订阅消息。assets_ids 接收代币 ID,也就是你从市场元数据中拿到的那些,type 必须为 "market"。

{
  "assets_ids": ["65818619657568813474341868652308942079804919287380422192892211131408793125422"],
  "type": "market",
  "initial_dump": true,
  "level": 2,
  "custom_feature_enabled": true
}
字段必填默认值作用
assets_ids是—要订阅的代币 ID
type是—必须为 "market"
initial_dump否true订阅时发送一份完整的订单簿快照
level否2订阅级别:1、2 或 3
custom_feature_enabled否false启用 best_bid_ask、new_market 和 market_resolved 事件

custom_feature_enabled 默认为 false

这是让人白白浪费一个下午的订阅陷阱。三种事件类型(best_bid_ask、new_market 和 market_resolved)除非你主动开启,否则不会推送。没有错误,也没有警告;你只是永远收不到它们。如果你在等一个永远不来的 market_resolved 事件,原因就在这里。

除非有特别的理由,否则保持 initial_dump: true。没有它,你一开始就没有订单簿状态,必须等增量更新来重建,这意味着你的机器人一启动看到的就是不完整的画面。

消息类型

每条消息都带有一个 event_type。对它进行穷尽的分支处理:无法识别的类型应该记录到日志,而不是悄悄丢弃,因为这正是你发现协议发生变化的方式。

event_type含义需要开启
book完整订单簿快照否
price_change增量价格/档位更新否
last_trade_price发生了一笔成交否
tick_size_change市场的最小价格变动单位改变了否
best_bid_ask盘口最优价更新是
new_market创建了一个市场是
market_resolved一个市场已结算是

不要忽略 tick_size_change

最小价格变动单位(tick size)是按市场设置的,并且可能随着市场成熟而收紧。如果你在启动时缓存了 tick size,而市场改变了它,之后的每一笔限价单都会因违反最小 tick size 而被拒绝:一个看起来像是你定价逻辑出 bug 的 400。处理这个事件并更新你的缓存。

保持连接:每 10 秒 PING 一次

这是每个人都会踩到的要求。客户端必须每 10 秒发送一次 PING;服务器回复 PONG。错过心跳,连接就会被断开。

注意方向:这是客户端心跳。你不是在等服务器来 ping 你。库自带的、以其他间隔运行的 WebSocket 保活机制不能替代它,你需要自己发送应用层的 PING。

import asyncio, json, time
import websockets

URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
STALE_AFTER = 30          # seconds without any message = treat as dead


async def stream(token_ids, on_event):
    backoff = 1
    while True:
        try:
            async with websockets.connect(URL) as ws:
                await ws.send(json.dumps({
                    "assets_ids": token_ids,
                    "type": "market",
                    "initial_dump": True,
                    "custom_feature_enabled": True,   # or lose 3 event types
                }))
                backoff = 1                            # reconnected cleanly
                last_msg = time.monotonic()

                async def heartbeat():
                    while True:
                        await asyncio.sleep(10)        # required cadence
                        await ws.send("PING")

                hb = asyncio.create_task(heartbeat())
                try:
                    while True:
                        # Silence is a failure mode, not a quiet market.
                        raw = await asyncio.wait_for(ws.recv(), timeout=STALE_AFTER)
                        last_msg = time.monotonic()
                        if raw == "PONG":
                            continue
                        for msg in json.loads(raw) if raw.startswith("[") else [json.loads(raw)]:
                            on_event(msg)
                finally:
                    hb.cancel()

        except Exception as exc:
            # Reconnect with capped exponential backoff + jitter.
            wait = min(backoff, 30) * (0.5 + 0.5 * asyncio.get_event_loop().time() % 1)
            print(f"ws down ({exc}); reconnecting in {wait:.1f}s")
            await asyncio.sleep(wait)
            backoff = min(backoff * 2, 30)

除非有理由,否则使用 SDK

polymarket-client(Python)和 @polymarket/client(TypeScript)实现了心跳、重连和消息帧处理,并把多个数据流合并成一个类型化的事件流。只有在你需要 SDK 未暴露的控制能力时,自己手写才值得。注意订阅仅支持异步:同步客户端没有实现 subscribe()。

无需重连即可更改订阅

五分钟市场不断到期,所以一个跟踪“当前 BTC 5m 市场”的机器人需要持续更改它的订阅集合。你不需要为此断开连接:market 频道支持在活动连接上订阅和取消订阅。

这比听起来更重要。每次市场轮换都重连,意味着反复付出建立连接的成本、重新请求初始快照,并在新市场开盘的那一刻恰好出现一个什么都收不到的空窗。

会让你亏钱的失效模式

掉线的 WebSocket 不会抛出异常。它只是不再推送消息。对一个简单的机器人来说,“没有更新”和“市场没有变动”无法区分,所以一个持有仓位的机器人会无限期地守着一个过时的价格,还坚信什么都没变。

  • 跟踪最后一条消息的时间。如果在一个活跃市场上约 30 秒没有收到任何东西,那你是掉线了,而不是市场平静。上面的 asyncio.wait_for 超时做的正是这件事。
  • 把数据过时当作错误状态处理。停止报价,如果风控规则要求就平仓,然后重连,不要在冻结的订单簿上继续交易。
  • 重连时退避,并加入随机抖动。在故障期间紧密循环重连,会让你在掉线之外再被限流;而一批机器人同步重连会让情况更糟。
  • 重连后重新同步状态。你离开期间错过了更新。以新的 initial_dump 为准,丢弃之前的订单簿,而不是把增量更新合并到一个过时的快照上。
  • 任何中断之后都要核对持仓。如果你从 User 频道掉线了,你可能错过了一笔成交。在基于假设的状态采取行动之前,重新读取持仓。

WebSocket 与速率限制

这是一个常见问题,而它的答案正是这一切的意义所在:订阅不会像轮询那样消耗按请求计算的速率限制额度。Polymarket 文档中的限制是按端点计算的 HTTP 限制,流式推送几乎完全绕开了它们。

方式跟踪 20 个市场一小时的成本
每秒轮询 /book72,000 次请求,额度为每 10 秒 1,500 次
每秒轮询 /books(批量)3,600 次请求,额度为每 10 秒 500 次
WebSocket 订阅一个连接,外加 360 次心跳
轮询给你的答案还更差:你看到的是你上次轮询时的订单簿,而不是最近一次变化时的订单簿。

你仍然需要发起 HTTP 调用,比如市场发现、tick size、下单、持仓核对,这些仍受文档中的速率限制约束。流式推送只是去掉了请求量中最大、也最没用的那部分来源。

延迟是伪装起来的限流

由于 Cloudflare 层是通过延迟而不是拒绝来限流的,一个超出限制的轮询机器人看不到 429,它看到的是响应时间悄悄地上升。如果你的订单簿数据延迟从 40ms 漂移到了 3 秒,你遇到的不是故障,而是你自己的请求速率。

数据流给不了你的东西

有一条边界值得直说,因为这是人们接下来会尝试的事。WebSocket 给你的是从你订阅那一刻起的订单簿。它给不了你上周二的订单簿。

Polymarket 不保留历史订单簿。市场一旦结算,它的订单簿就返回 404,prices-history 只保留大约每分钟一个点。如果你想用市场的真实交易情况来测试策略,就需要当时有人记录下来的快照。PolyTest 为加密货币 Up/Down 市场做这件事,每边 8 档深度,已结算的市场也会保留。或者今天就启动你自己的采集程序,接受从零历史开始的事实,然后等待。

常见问题

Polymarket 的 WebSocket URL 是什么?
公开的 market 频道是 wss://ws-subscriptions-clob.polymarket.com/ws/market,需要认证的 user 频道是 wss://ws-subscriptions-clob.polymarket.com/ws/user。另外还有两个数据流:位于 wss://ws-live-data.polymarket.com 的 RTDS,用于参考价格、评论和交易动态;以及位于 wss://sports-api.polymarket.com/ws 的体育数据源。
Polymarket WebSocket 有速率限制吗?
WebSocket 订阅不会像 HTTP 轮询那样消耗按请求计算的速率限制额度:Polymarket 文档中的限制是按端点计算的 HTTP 限制。流式推送正是推荐用来避开这些限制的方式。连接上真正的约束是心跳:每 10 秒发送一次 PING,否则连接会被断开。你仍需为市场发现、tick size 和下单发起的 HTTP 调用,依然受常规限制约束。
如何保持 Polymarket WebSocket 连接不断开?
每 10 秒发送一条 PING 消息;服务器会回复 PONG。这是客户端的应用层心跳,所以 WebSocket 库自带的、以不同间隔运行的保活机制不能替代它。还要跟踪最后收到消息的时间:掉线的连接会停止推送数据而不报错,所以在活跃市场上约 30 秒的沉默应被视为断线,而不是市场平静。
为什么我收不到 best_bid_ask 或 market_resolved 事件?
这些事件默认是关闭的。在订阅消息中把 custom_feature_enabled 设为 true,即可启用 best_bid_ask、new_market 和 market_resolved。缺少这个标志时既没有错误也没有警告,事件只是永远不会到来,很容易被误认为市场不活跃。
我的 Polymarket 机器人应该用 WebSocket 还是轮询 REST API?
实时订单簿和价格数据用 WebSocket。每秒轮询一次 /book 来跟踪 20 个市场,每小时要 72,000 次请求,而额度是每 10 秒 1,500 次;订阅则只需一个连接加上心跳。而且轮询返回的结果更陈旧,因为你看到的是上次轮询时的订单簿,而不是最近一次变化时的订单簿。REST 留给市场发现、tick size、下单和持仓核对。
能在不重连的情况下更改订阅的市场吗?
能。market 频道支持在活动连接上订阅和取消订阅资产,这对 5 分钟 Up/Down 合约这类不断轮换的短期市场很重要。每次轮换都重连,会反复付出建立连接的成本,并在新市场开盘的那一刻恰好留下覆盖空窗。

获取 Polymarket 不保留的历史数据

PolyTest 在 Polymarket 加密货币 Up/Down 市场运行期间实时记录:240M+ 条快照,包含 8 档订单簿深度、亚秒级时间戳,并保留已结算的市场。从 3 天免费试用开始。

继续阅读