▌tickfoundry
实时采集中EN登录获取数据 →
§ 订单簿

Polymarket 订单簿数据
每一次订单簿更新,每侧 25 档。

L2 · parquet / csv免费样本TickFoundry · 数据来自免费样本,无需注册

Polymarket 上的每个结果都在各自的中央限价订单簿上交易:同一代币的挂单买价与卖价,以概率的美分计价。L2 表会在平台广播的每一次变化时记录这本订单簿,作为每侧最优 25 个价位的完整快照,包含每一档的数量和纳秒级接收时间戳。下文介绍这张表包含什么、一本订单簿一天下来是什么样子,以及它实际有多深:全部在免费样本上,用一个你可以自己运行的脚本测得。

样本:BTC Up or Down 4h · 2026-08-19 · 完整 UTC 日 · 26 个结果代币共 640,735 行订单簿数据。
↓ parquet 数据包(23.0 MB)↓ pm_depth.py

Polymarket 公开 API 能提供什么

Polymarket 的 CLOB API 回答的是关于当下的问题。/book 返回某个结果代币当前的挂单买卖盘,并附带一个哈希值,方便你判断两次读取之间订单簿是否变化;它不接受时间参数。/prices-history 只返回时间戳与价格对,没有买价、卖价、数量或深度;我们请求一个上线两周的市场在整个存续期内的一分钟精度数据时,它返回的数据点间隔十分钟。市场 websocket 确实会实时推送每一次订单簿变化,但只推给当时在线连接的人。

所以,一本订单簿的历史只有在有人录制时才存在。我们自 2026-05-11 起一直在持续录制,订单簿历史可追溯至 2026 年 2 月。这就是本页介绍的数据。

L2 订单簿文件里有什么

每个结果代币的每次订单簿更新对应一行。每一行都是更新之后的完整订单簿,而不是增量:最优 25 个买价和最优 25 个卖价,含价格和数量,超出现有最后一档的位置以 NaN 填充。样本当天,平台重发整本订单簿 9,819 次,发送增量变化 630,916 次;两者都以完整快照存储,通过 event_kind 区分。数量单位为份额(合约),因此某一档的美元深度等于价格乘以数量。

l2.parquet · 字段text
l2.parquet — one row per book update per outcome token
ts_recv_ns int64 our receive time, ns since epoch (authoritative order)
ts_src_ms int64 the venue's own timestamp, ms
condition_id str the market
token_id str the outcome (YES or NO leg) — read as a string
event_kind str "snapshot" (venue resent the whole book) | "update"
source_event_type str upstream message: book | price_change
msg_seq int64 capture sequence number
bid_levels int16 price levels in the WHOLE bid book (the file keeps 25)
ask_levels int16 price levels in the whole ask book
best_bid, best_ask float64 top of book, rebuilt from the levels
payload_best_bid float64 top of book as the venue's message declared it
payload_best_ask float64
bid_ask_reconciles bool rebuilt top of book == declared top of book
bid_px_1 … bid_px_25 float64 price per level, best first, NaN past the book
bid_sz_1 … bid_sz_25 float64 size per level, in shares (contracts)
ask_px_1 … ask_px_25 float64
ask_sz_1 … ask_sz_25 float64

Premium 和 Custom 套餐包含全部 25 档;Explorer 和免费市场·日包含最优 10 档。数据包还包含 L1(每次变化时的盘口最优价),以及自 2026-04-13 起的成交记录。完整参考见 schema 文档(英文)。

一本订单簿的一天:深度热力图

“Bitcoin Up or Down - August 19, 4:00PM-8:00PM ET”的 YES 一侧,时间为该问题覆盖的四个小时。每个格子代表一分钟、一美分的价格区间;亮度表示挂在该处的美元数量,按秒取平均。买单为绿色,位于中间价下方;卖单为红色,位于中间价上方;圆圈表示成交。

Polymarket 订单簿深度热力图,Bitcoin Up or Down(8 月 19 日 4PM 至 8PM ET,即 20:00 至 24:00 UTC)的 YES 结果:挂单买盘以绿色显示在中间价下方,卖盘以红色显示在上方。中间价从 52¢ 升至 99.8¢,并在 20:50 从 68¢ 跳升至 93¢。
2026-08-19 20:00–24:00 UTC · 每 1¢ 区间的挂单 USDC · 文件保留的每侧 25 档 · 空白表示在这些档位内该处没有挂单。

看 21:00 前的那次跳升。20:49:44 UTC 时中间价为 68¢,六十秒后变为 93¢。在这一分钟里,YES 订单簿变化了 2,049 次,产生了 15 笔成交,合计约 $1,400。成交记录只能告诉你这些,剩下的要看订单簿:这一分钟开始时,69¢ 到 93¢ 之间挂着 $5,680 的卖单;结束时最优卖价为 94¢。价格图能显示这次跳升;只有订单簿能告诉你当时能否成交穿过这段价格,以及能成交多大规模。

订单簿深度与价差:实际有多深

同一市场、同一时间窗口,每秒采样一次,使一阵密集更新按其持续的时间计入,而不是按它产生了多少条消息计入。价差取最优卖价减最优买价。

价差≤ 1¢1–2¢2–5¢> 5¢中位数
时间占比63.9%20.6%15.5%0.0%1.0¢

14,274 个双边均有报价的秒 · 2026-08-19 20:00–24:00 UTC

挂单范围买单 · 中位数买单 · p10卖单 · 中位数卖单 · p10
中间价 ±1¢ 以内$256$0$141$0
中间价 ±2¢ 以内$944$102$294$76
中间价 ±5¢ 以内$2,260$719$2,918$419

USDC(价格 × 数量)· p10 = 深度最薄的 10% 秒数

档位该档买单累计买单该档卖单累计卖单该卖档存在
1$110$110$77$77100%
2$130$305$95$19693%
3$99$568$88$30493%
5$98$936$98$60685%
10$66$2,428$74$2,37877%
25$40$4,736$53$10,48025%

该档位的 USDC 中位数,以及从最优价累计到该档的总额 · 买方在 99.9% 的秒数里 25 档齐全

有三点值得注意。盘口很薄:最优买价挂单的中位数为 $110,最优卖价为 $77,所以稍大一点的订单都会逐档吃穿价格档位。中间价附近的深度时有时无:至少有十分之一的秒数里,某一侧在中间价 1¢ 以内没有任何挂单。而且两侧并不对称:在三分之二的时间窗口里价格高于 90¢,中间价与 100¢ 之间空间很小,订单簿的中位数是 111 个买价档位对 17 个卖价档位。这些都无法从价格序列中看出来。

用 pandas 复现

以上三张表背后的脚本,原样呈现。解压样本,在数据包目录中运行;也可以用在你下载的任何其他市场·日上:它会自动选出文件中最活跃的市场及其对应的时间窗口。

pm_depth.pypython
# pm_depth.py — how deep was a Polymarket order book, level by level and near the mid?
# Runs on the free sample bundle: https://tickfoundry.com/samples
import numpy as np
import pandas as pd
l2 = pd.read_parquet("l2.parquet") # one full 25-level snapshot per book update
markets = pd.read_csv("reference/markets.csv")
# The YES leg of the day's busiest market, over the four hours its question covers.
busiest = l2.groupby("condition_id").size().idxmax()
m = markets.set_index("condition_id").loc[busiest]
book = l2[l2.token_id == str(m.yes_token_id)].sort_values("ts_recv_ns").reset_index(drop=True)
end = pd.Timestamp(m.end_time)
start = end - pd.Timedelta(hours=4)
# Sample the book once a second so every statistic is time-weighted:
# a burst of 50 updates inside one second counts as one second, not as 50.
grid = np.arange(start.value, end.value, 1_000_000_000)
at = np.searchsorted(book.ts_recv_ns.to_numpy(), grid, side="right") - 1
snap = book.iloc[at[at >= 0]]
snap = snap[snap.best_bid.notna() & snap.best_ask.notna()] # drop one-sided seconds
px = {s: snap[[f"{s}_px_{i}" for i in range(1, 26)]].to_numpy() for s in ("bid", "ask")}
usd = {s: px[s] * snap[[f"{s}_sz_{i}" for i in range(1, 26)]].to_numpy() for s in ("bid", "ask")}
mid = ((snap.best_bid + snap.best_ask) / 2).to_numpy()[:, None]
spread = (snap.best_ask - snap.best_bid).to_numpy() * 100 # cents; never the rounded spread column
print(m.question, f"· {start:%Y-%m-%d %H:%M} to {end:%H:%M} UTC")
print(f"{len(book):,} book updates for this token on the day, "
f"{((book.ts_recv_ns >= start.value) & (book.ts_recv_ns < end.value)).sum():,} inside the window; "
f"{len(snap):,} two-sided seconds sampled\n")
print("spread (¢) median", round(np.median(spread), 2),
"| ≤1¢", f"{np.mean(spread <= 1.0001):.1%}",
"| 1–2¢", f"{np.mean((spread > 1.0001) & (spread <= 2.0001)):.1%}",
"| 2–5¢", f"{np.mean((spread > 2.0001) & (spread <= 5.0001)):.1%}",
"| >5¢", f"{np.mean(spread > 5.0001):.1%}\n")
rows = []
for c in (1, 2, 5): # USDC resting within c cents of the mid
bid = np.nansum(np.where(px["bid"] >= mid - c / 100 - 1e-9, usd["bid"], 0), axis=1)
ask = np.nansum(np.where(px["ask"] <= mid + c / 100 + 1e-9, usd["ask"], 0), axis=1)
rows.append({"within": f"±{c}¢", "bid median $": np.median(bid), "bid p10 $": np.percentile(bid, 10),
"ask median $": np.median(ask), "ask p10 $": np.percentile(ask, 10)})
print(pd.DataFrame(rows).round(0).to_string(index=False), "\n")
rows = []
for lvl in (1, 2, 3, 5, 10, 25): # size at each level, and through it
rows.append({"level": lvl,
"bid $ at level": np.nanmedian(usd["bid"][:, lvl - 1]),
"bid $ through": np.median(np.nansum(usd["bid"][:, :lvl], axis=1)),
"ask $ at level": np.nanmedian(usd["ask"][:, lvl - 1]),
"ask $ through": np.median(np.nansum(usd["ask"][:, :lvl], axis=1)),
"ask levels present": f"{np.mean(~np.isnan(px['ask'][:, lvl - 1])):.0%}"})
print(pd.DataFrame(rows).round(0).to_string(index=False), "\n")
print("price levels in the whole book (the file keeps the best 25 a side): median bid",
int(np.median(snap.bid_levels)), "· ask", int(np.median(snap.ask_levels)))

如果想用这本订单簿为订单定价,而不只是描述它,请看如何回测 Polymarket,其中用 $100、$500 和 $2,000 的市价买单逐档走过同一个价格档位。

Polymarket 订单簿数据能用来做什么

考虑真实成交的回测
每一笔模拟订单都按其接收时刻实际挂着的价格档位撮合成交,而不是按中间价或最新成交价。
做市与排队研究
价差状态、盘口被扫后多快补回、挂单量在各档位间如何分布,以及临近结算时这些如何变化。
新闻前后的流动性
价格变动的那几秒里订单簿发生了什么:哪一侧撤了单、价差扩大了多少、实际能成交多少。
微观结构研究
覆盖数千个市场的 tick 级订单簿,从体育、选举到加密货币,成交记录使用同一时钟。

归档中的两个实例:西班牙对比利时的四分之一决赛(英文),价格波动 64 个点期间价差扩大到 31¢;以及从订单簿看世界杯决赛(英文)。

常见问题

Polymarket 有获取历史订单簿数据的 API 吗?
没有获取过去订单簿的 API。CLOB 的 /book 端点返回某个结果代币当前的订单簿,并附带一个哈希值,方便你判断两次读取之间订单簿是否变化;它不接受时间参数。/prices-history 返回时间戳与价格对的列表,没有买价、卖价、数量或深度;请求一个市场整个存续期的一分钟精度数据时,它返回的数据点间隔十分钟。市场 websocket 会实时推送每一次订单簿变化,但只推给当时在线连接的人。一本订单簿的历史,只有在有人录制时才存在。
数据包含订单簿的多少档?
每次更新时每侧最优的 25 个价位,每档包含价格和数量。另有 bid_levels 和 ask_levels 两列,统计整本订单簿的价位数,让你知道订单簿何时超出了文件保留的范围:在样本中最活跃的市场上,订单簿中位数为 111 个买价档位和 17 个卖价档位。Premium 和 Custom 套餐包含全部 25 档;Explorer 和免费市场·日包含最优 10 档。
订单簿多久更新一次?
平台每广播一次变化就更新一次,每次都以变化后的完整 25 档快照存储。在样本最活跃的四个小时里,一个结果代币共更新 92,325 次:平均每秒 6.4 次,单秒最多 180 次,三分之二的秒数里至少有一次更新。
YES 和 NO 两本订单簿都需要吗?
对于二元市场,NO 订单簿就是 YES 订单簿以 1 减价格镜像而来:在样本市场时间窗口的全部 14,400 秒中,YES 最优买价每一秒都等于 1 减 NO 最优卖价,且数量完全相同。两条腿都在文件里,用哪个方便就用哪个;成交会同时出现在两个代币上。
bid_ask_reconciles 是什么意思?
每一行都同时包含根据各档位重建的盘口最优价,以及平台消息中声明的最优买价和最优卖价。两者不一致时,该行会被标记而不是丢弃:样本当天占 1.3% 的行,大多发生在订单簿某一侧为空、消息给出 0¢ 或 100¢ 占位价的时刻。
数据最早到什么时候?是什么格式?
订单簿历史可追溯至 2026 年 2 月;自 2026-05-11 起,我们自己的采集器持续采集整个平台,包括原始 websocket 消息。数据按市场、按 UTC 日交付,格式为 parquet 并附带同内容的 CSV,可通过签名下载链接、REST API 或 SFTP(Premium)获取。
获取你所交易市场的订单簿。

免费账户可在 2026 年 7 月的任意日期中领取 5 个市场·日,schema 完全相同,pm_depth.py 无需改动即可在每一个上运行。无需绑卡。

选择你的市场 →包含完整 25 档的套餐