币安 L2 盘口数据完全指南:免费能拿到什么、自录要怎么写、怎么验证数据是真的
实测币安官方数据归档的十个数据类型与真实覆盖范围,说清 bookDepth 的"流动性分布"与逐档 L2 的区别,并给出自录 L2 的完整采集方案——含 U/u 序列号校验、断连恢复、数据自洽性验证。
做微观结构研究,第一件事永远是搞清数据的边界。这篇把币安公开的盘口数据拆开讲清楚: 哪些免费、哪些必须自己录、以及怎么确认你手上的数据是真的。
文里的目录结构、覆盖范围、文件体积、表头字段都是实测抓下来的,不是抄文档。
一、先明确你想要的是哪种”盘口数据”
市面上”L2 数据”这个词被用得很宽,但至少在三种不同粒度上被混用:
| 粒度 | 内容 | 典型来源 |
|---|---|---|
| 流动性分布 | 每分钟各价格带(±1%…±5%)的累计挂单量 | 币安官方归档 bookDepth |
| 最优价 | 每次变动时的 best bid/ask 及数量 | 币安 bookTicker |
| 逐档增量 | 每一个价位的每一笔增删(可完整重建订单簿) | 自录 / 商业数据商 |
这三者对研究能回答的问题完全不同。 拿第一种去做队列位置研究,会直接得出错误结论。
二、币安官方归档里到底有什么(实测)
数据在 data.binance.vision(S3 兼容),我实测列了 U 本位合约的目录:
日更目录(/data/futures/um/daily/)
aggTrades ← 聚合成交
bookDepth ← ★ 流动性分布(本文重点关注)
bookTicker ← 最优价变动
indexPriceKlines
klines
markPriceKlines
metrics ← 持仓量等指标
premiumIndexKlines
trades ← 逐笔成交
月更目录(/data/futures/um/monthly/)
aggTrades bookTicker fundingRate ← 月更多出资金费率
indexPriceKlines klines markPriceKlines
premiumIndexKlines trades
文件组织与体积(BTCUSDT 实测)
| 数据类型 | 单文件体积 | 备注 |
|---|---|---|
bookDepth | 约 452 KB / 天 | 一天一个 zip,含 .CHECKSUM |
aggTrades | 1.4 MB → 52.7 MB | 随时间增长,早期到近期差 37 倍 |
| 每个 zip 附带 | *.CHECKSUM | 可用于校验下载完整性 |
三、免费数据能回答什么、不能回答什么
这是本文最该记住的一张表:
| 问题 | 免费数据够吗 | 为什么 |
|---|---|---|
| 某时刻流动性在价格上如何分布 | ✅ | bookDepth 就是干这个的 |
| 大额订单的滑点估算 | ✅ 粗略 | 用 notional 按价格带累积 |
| 最优价变动频率与价差 | ✅ | bookTicker |
| 成交流的方向(主动买/卖) | ✅ | aggTrades |
| 持仓量 / 资金费率变化 | ✅ | metrics / fundingRate |
| 某一价位的挂单队列位置 | ❌ | 需要逐档数据 |
| 订单流失衡(OFI) | ❌ | 需要每一笔增删 |
| 挂单撤单行为 | ❌ | 免费数据里没有撤单事件 |
| 盘口重建与做市仿真 | ❌ | 需要 incremental_book_L2 |
四、自己录一份逐档 L2:完整方案
上一节表里那四个 ❌,只能靠自录解决。下面是我建议的架构,以及最容易出错的那个地方。
4.1 为什么必须”快照 + 增量”拼接
币安 WebSocket 的深度流给的是增量更新,不是完整盘口:
depth@100ms → 每个价位的数量变化(新增/修改/删除)
所以你无法”只订阅 WebSocket 就得到订单簿”。必须:
1. 先订阅 WebSocket 增量流,把收到的消息【缓存起来】
2. 再通过 REST 拉一份全量快照 /fapi/v1/depth?symbol=BTCUSDT&limit=1000
3. 用快照的 lastUpdateId 对齐缓存里的增量,丢弃快照之前的
4. 之后逐条应用增量,维护本地订单簿
顺序很重要:必须先订阅再拉快照。反过来做,中间那段增量就丢了。
4.2 ★ 唯一不能省的校验:序列号连续性
这是整个采集器里最关键、也最容易被跳过的一步。
币安的深度增量消息里带两个字段:
U = 本条消息的第一个更新 ID(first update id)
u = 本条消息的最后一个更新 ID(final update id)
正确的连续性判定是:
前一条的 u + 1 == 当前条的 U → 连续,可以应用
否则 → 中间丢包了,必须丢弃本地簿、重新拉快照
4.3 采集器骨架
import asyncio, json, gzip, time, aiohttp, websockets
from datetime import datetime, timezone
SYMBOL = "BTCUSDT"
WS = f"wss://fstream.binance.com/ws/{SYMBOL.lower()}@depth@100ms"
async def fetch_snapshot(session):
"""REST 全量快照 —— 必须返回 lastUpdateId"""
url = f"https://fapi.binance.com/fapi/v1/depth?symbol={SYMBOL}&limit=1000"
async with session.get(url) as r:
return await r.json()
class Book:
"""本地订单簿。只保留增量应用与连续性校验,不做撮合。"""
def __init__(self):
self.bids, self.asks = {}, {}
self.last_u = 0
self.resyncs = 0
def load(self, snap):
self.bids = {p: q for p, q in snap["bids"] if float(q) > 0}
self.asks = {p: q for p, q in snap["asks"] if float(q) > 0}
self.last_u = snap["lastUpdateId"]
def apply(self, msg) -> bool:
"""
返回 True 表示应用成功,False 表示检测到断档(调用方需重新同步)。
★ 这个函数是整个采集器的核心。
"""
U, u = msg["U"], msg["u"]
if self.last_u and U > self.last_u + 1:
# 中间缺了消息 —— 不能假装没事
self.resyncs += 1
return False
if u <= self.last_u:
return True # 过期消息,直接忽略
for side, key in ((self.bids, "b"), (self.asks, "a")):
for p, q in msg[key]:
if float(q) == 0:
side.pop(p, None)
else:
side[p] = q
self.last_u = u
return True
async def main():
book = Book()
out = gzip.open(f"{SYMBOL}-{datetime.now(timezone.utc):%Y%m%d}.jsonl.gz", "at")
async with aiohttp.ClientSession() as session:
while True:
try:
async with websockets.connect(WS, ping_interval=20) as ws:
# ★ 先订阅,再拉快照(顺序反了会丢增量)
pending = []
snap = await fetch_snapshot(session)
book.load(snap)
print(f"onboarded at lastUpdateId={book.last_u}")
async for raw in ws:
msg = json.loads(raw)
if not book.apply(msg):
# 断档:重新同步,而不是继续用失真的簿
snap = await fetch_snapshot(session)
book.load(snap)
continue
out.write(json.dumps({
"ts": int(time.time() * 1000),
"E": msg.get("E"), "U": msg["U"], "u": msg["u"],
"b": msg["b"], "a": msg["a"],
}) + "\n")
except Exception as e:
print(f"reconnect: {type(e).__name__}: {e}")
await asyncio.sleep(2)
asyncio.run(main())
代码里刻意保留的两点:
apply()返回False时必须重新拉快照,不能继续 —— 这是丢包恢复的唯一正确姿势- 每行都记
U/u,下游可以独立复核连续性,不用信任采集时的判断
4.4 体积估算与运维
| 项 | 估算 |
|---|---|
| 单标的深度流消息频率 | 币安 @depth@100ms,最多 10 条/秒 |
| 原始 JSONL | 约 100–300 MB/天/标的(视行情活跃度) |
| gzip 后 | 约 10–30 MB/天/标的(压缩比通常 8–15×) |
| 一年(2 标的,压缩后) | 约 7–22 GB |
运维要点(和你做其他采集一样):
- systemd 常驻 + 崩溃自动拉起
- 每日 gzip 轮转,保留期按磁盘算
- 监控
resyncs计数 —— 它突然变多说明网络或代码有问题,比看进程活着更有用 - 落盘前先写内存缓冲,避免高频 IO
五、怎么验证一份 L2 数据是真的
这一节与数据来源无关——不管你是自录还是买来的,都该做这三步。
5.1 内部自洽:增量重建 vs 定时快照
用增量流重建出 t 时刻的订单簿
再拉一份 t 时刻的 REST 快照
逐档比对
完全一致才能证明增量没丢。 有任何一档不同,先查是不是漏了消息。
5.2 跨数据源交叉:成交量的三方对齐
同一个时间段,用三种方式算成交量:
trades 逐笔成交求和
aggTrades 聚合成交求和
klines 1 分钟 K 线的 volume 字段
三者应当一致(aggTrades 与 trades 的差异仅在于聚合同一价格同一时刻的成交)。
对不上说明数据有问题。
5.3 合理性检验:不该出现的状态
| 检查 | 期望 |
|---|---|
| best bid ≥ best ask | 不应出现(出现了说明簿已损坏) |
| 任何价位数量 < 0 | 不应出现 |
| 时间戳单调不回退 | 应满足 |
| 单条消息跨越的价位数量 | 不应异常巨大(可能是快照混进了增量流) |
六、免费数据怎么拿
币安官方归档是公开的,可以直接 HTTP 下载,不需要 API key:
# 目录列表(S3 兼容,返回 XML)
https://s3-ap-northeast-1.amazonaws.com/data.binance.vision?prefix=data/futures/um/daily/bookDepth/BTCUSDT/
# 单文件直链
https://s3-ap-northeast-1.amazonaws.com/data.binance.vision/data/futures/um/daily/bookDepth/BTCUSDT/BTCUSDT-bookDepth-2024-05-01.zip
注意事项:
- 每个 zip 附带
.CHECKSUM,下载后务必校验(长跑批量下载时一定会遇到截断) - 归档有延迟(通常 T+1 或更长),当天数据拿不到
- 目录列表分页返回,脚本遍历时要处理分页,否则只会拿到前 1000 个 key
(我实测时就踩到这个:
aggTrades/BTCUSDT只列出 500 天,实际远不止)
七、这份数据能支撑哪些研究
有了它,下面这些文章里的方法才有数据基础(陆续补上):
做市容量评估 ← 用 bookDepth 的 notional 估算各价位可承载量
滑点与冲击成本 ← 用 bookDepth 按价格带累积,或用自录的逐档簿
订单流不平衡 ← 需要自录的逐档增量 + trades
高频 vs 分钟级边际 ← 不同采样粒度下同一信号的表现差异
一个诚实的提示:如果你要做的是分钟级以上的研究,免费的 bookDepth +
aggTrades + klines 通常就够了,不一定需要自录逐档 L2。
自录的成本(磁盘、运维、数据校验)只在做微观结构研究时才值得。
本文所有目录结构、覆盖范围与文件体积均为实测结果,采集时间 2026-09-17。 数据来源为币安公开数据归档。本文仅供技术交流,不构成投资建议。