币安 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
aggTrades1.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())

代码里刻意保留的两点

  1. apply() 返回 False必须重新拉快照,不能继续 —— 这是丢包恢复的唯一正确姿势
  2. 每行都记 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 字段

三者应当一致aggTradestrades 的差异仅在于聚合同一价格同一时刻的成交)。 对不上说明数据有问题。

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。 数据来源为币安公开数据归档。本文仅供技术交流,不构成投资建议。