# Orderbook Sanity

> 行情数据质量"照妖镜"——用户把订单簿/K线/tick 行情数据或数据采集/解析/落库/读取代码丢过来,逐项审查"会让数据悄悄变脏、进而毒害策略和回测"的工程坑(盘口交叉、深度缺口、checksum 失配、snapshot/增量衔接错误、序列缺口、K线缺失/重复/乱序、未闭合 bar 泄漏、时间戳单位混淆/漂移/时区、未来时间戳、symbol 归一化、幸存者偏差),定位到 文件:行,按严重度(致命/高危/中/低)输出一张"数据体检报告"。 触发词(中):订单簿审查、行情数据质量、数据照妖镜、交叉盘口、深度缺口、checksum 校验、增量衔接、K线缺失、时间戳乱序、重复 bar、未闭合 bar、未来 bar、时间戳单位、时区错误、时钟漂移、symbol 归一化、幸存者偏差。 触发词(英):orderbook sanity、market data quality、crossed book、checksum mismatch、snapshot delta splice、sequence gap、kline gap、duplicate bar、out-of-order timestamps、unit confusion、clock skew、survivorship bias、orderbook-sanity。 不适用:策略 alpha/信号逻辑/调参(那是策略层)、非数据问题的通用代码审查、实盘下单/风控逻辑本身的 bug、要你承诺数据"绝对干净/零脏数据"(工程审查只能降低风险,不能背书数据完美)。

- Skill: `paidaxing1234/orderbook-sanity` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add paidaxing1234/orderbook-sanity`
- Raw SKILL.md: https://api.skillmd.com/api/skills/paidaxing1234/orderbook-sanity/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: paidaxing1234 (https://skillmd.com/u/paidaxing1234)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/paidaxing1234/orderbook-sanity

---


# orderbook-sanity / 数据照妖镜

## 核心理念

脏数据是**静默杀手**。盘口交叉、checksum 失配、K线缺口、时间戳漂移这类问题**不会抛异常、不会让程序崩溃**——它们只会让你的因子、信号和回测悄悄建在沙子上:回测曲线漂亮得不像话,实盘却怎么也对不上。

最危险的不是"明显坏掉"的数据(那种一眼能看出来),而是**数值仍然合法、类型仍然正确、却语义错误**的数据:一个落在 1970 年的毫秒时间戳、一根还在形成中的未闭合 bar、一个序列号连续却被错误应用、checksum 已经对不上的本地订单簿。它们能静默通过所有"非空校验",一路污染到 PnL。

本 skill 不写策略、不调 alpha。它只做一件事:**像照妖镜一样,把会让数据悄悄变脏的工程坑逐个照出来,定位到 文件:行,告诉你为什么会脏、怎么自测确认、怎么修。**

更高一层的铁律:**命中可疑模式只是"线索"不是"判决"。** 行情里大量"看起来脏"的现象其实是**真实市场状态**——停牌的真缺口、冷门 symbol 的零量桶、极速行情里一瞬的锁价、跨所聚合时 A 所买价高于 B 所卖价的真实套利。每条陷阱都标注了"**合法例外**",审查时务必读上下文区分**真坑**与**真实行情**,否则报告会被假阳性淹没、失去可信度。

## 何时启用

- 用户丢来**行情数据样本**(订单簿快照/增量、K线、逐笔成交)让你看"这数据干不干净"。
- 用户丢来**数据采集/解析/落库/读取代码**(WS 订阅、REST 拉取、重采样、入库、DataFrame 加载)让你审质量坑。
- 用户问:"为什么回测和实盘对不上 / 这数据能信吗 / 帮我查 K 线有没有缺口 / 时间戳是不是错了 / 盘口怎么会交叉 / 本地订单簿对不上交易所"。
- 在数据进入因子库、回测引擎、实盘信号**之前**做一道体检。

**不适用**:求策略 alpha、信号调参、实盘下单/风控逻辑 bug、非数据的通用代码审查、要你保证数据"绝对干净"。

## 审查协议

1. **定位数据流**。先问清楚/读清楚数据走过的全链路:**采集**(WS/REST)→ **解析**(字段映射/单位换算)→ **落库**(去重/排序/主键)→ **读取**(reindex/resample/join)。脏数据可能在任一环节注入,但常在解析和落库环节静默成型。
2. **逐类过三遍**。按下面三大类——**① 订单簿/盘口、② K线/tick、③ 通用时间戳/对齐**——逐条比对代码与数据,挑出命中的陷阱。
3. **定位到 文件:行**。每条发现必须指向具体 `path/to/file.py:行号`,并贴出触发的代码片段(code smell),不要泛泛说"可能有问题"。找不到证据的项标"未发现/需人工确认",绝不脑补。
4. **过一遍"合法例外"再定罪**。命中可疑模式后,必须回答:这是工程坑,还是真实市场状态(停牌/休市/无量/极速行情/跨所聚合)?判为合法用法的不写进问题清单,或写入"已排除"附注。
5. **给出实测校验**。每条都附**可立即运行的自测断言/统计**(如 `df['open_time'].is_monotonic_increasing`、`df.duplicated(subset=['symbol','open_time']).sum()`),让用户自己在真实数据上确认,而非只听你口述。
6. **按严重度排序输出**。用下面的"数据体检报告"模板,致命在前。

每一类的完整陷阱清单、检测代码与修复方案见 `references/orderbook-pitfalls.md`,下面只列每类最致命的几条作为审查锚点。

## 三大类陷阱(审查锚点)

详细的检测代码、code smell、修复方案见 `references/orderbook-pitfalls.md`,这里只给每类最该先抓的几条。每条格式:`陷阱名` — 怎么发现(含**合法例外**) — 修复方向。

### ① 订单簿 / 盘口

- **checksum / CRC32 失配 (checksum mismatch)**:**致命**。OKX、Kraken、Bitstamp、Coinbase 等会对 top-N 档位下发 CRC32/checksum,**这是交易所给你的、最强的本地簿一致性原语**——专门用来抓"序列号连续、却被错误应用/丢档/精度截断"这类纯靠 sequence 检测**发现不了**的失同步。代码里订阅了带 checksum 的频道却**不校验**,或校验算法(拼接顺序、价格/数量格式化、参与档数)与交易所文档不一致 → 本地簿已脏却毫不知情。**合法例外**:Binance 现货增量本身不下发 checksum,此时靠 sequence(`U`/`u`/`pu`)衔接即可,不算漏校验——只对**确实下发 checksum 的交易所**判该坑。修:按交易所文档逐字复现 checksum(档数、价×量拼接顺序、定点格式化),每次应用增量后比对,**失配即丢弃本地簿重拉 snapshot**;价格用定点整数当 key 避免浮点拼接误差。
- **snapshot ↔ 增量衔接错误 (snapshot/delta splice)**:**致命**。盘口最常见的"出生即脏"——snapshot 与增量的拼接没按交易所铁律来。Binance:**先订增量入 buffer → 再拉 REST snapshot → 丢弃 `u <= lastUpdateId` 的增量 → 首条应用的增量必须满足 `U <= lastUpdateId+1 <= u`**,否则中间漏/重一段,本地簿从第一根就偏。**合法例外**:不同交易所衔接规则不同(OKX/Bybit 的 seq 字段语义、首帧是否含 snapshot 都不一样),按该所文档核对,别用 Binance 规则硬套别家。修:严格按交易所衔接规则,首条增量做 `U/u/lastUpdateId` 区间断言,断言失败即重拉。
- **盘口交叉 (crossed book)**:`best_bid >= best_ask`。增量更新丢包/乱序后本地簿失同步的典型症状,任何基于盘口的报价、midprice、价差信号全错。**合法例外**:① snapshot 拼接窗口内、极速行情一瞬的**锁价(locked, bid==ask)** 在个别撮合/聚合口径下可短暂出现,需看是否瞬时自愈;② **跨交易所聚合簿**里 A 所 best_bid > B 所 best_ask 是**真实跨所套利机会**,不是脏数据——只对**单一交易所同一本地簿**判交叉为坑。修:每次应用 update 后断言 `best_bid < best_ask`;单簿持续交叉即丢弃本地簿重拉 snapshot。
- **qty==0 删档语义错误 (zero-qty delete)**:增量里 `qty==0` / `size==0` / 空数组表示**删除该价格档**,不是"把价格置 0"、也不是"保留一个零数量档"。处理错会在簿里留下**幽灵档 / 深度缺口**,midprice、深度加权价全错。修:`qty==0` 一律 `del book[price]`(档不存在则忽略),绝不写入零档。
- **深度缺口 / 档位错乱 (depth gap)**:价格档位不连续、买卖档跳变。本地订单簿与交易所失同步的表征。**合法例外**:稀薄市场本就档位稀疏、价格档之间天然有"空隙",这是**真实流动性形态**不是缺口——别把"档少"当"档丢"。修:校验是增量应用错误(配合 checksum/seq)还是真实稀薄;前者重拉 snapshot,后者保留。
- **序列缺口 (sequence gap)**:增量推送的 `U`/`u`/`pu` 序列号跳号,意味着漏掉了 update,本地簿必然偏离真实状态。修:逐条断言 `next == prev+1`(或该所衔接规则),缺口即丢弃本地态 + 重拉 snapshot;**重快照本身也要退避+限频**防死循环。

> 盘口类陷阱以**本地簿失同步**为核心,检测优先级:checksum 失配 > 衔接错误 > seq 缺口 > 交叉 > 深度缺口。有 checksum 的交易所务必优先校 checksum——它能抓到前几项都漏掉的"静默脏簿"。详见 references。

### ② K线 / tick

- **使用未闭合 bar (unclosed/forming bar leakage)**:**致命**。把仍在形成中的 bar(`is_closed=false` / Binance WS 的 `k.x=false`)当终值用——回测里是典型未来函数,实盘会用一个"close 还会变"的 bar 触发交易,回测虚高、实盘对不上。**合法例外**:做**实时盘中展示/监控**(画当前正在走的那根)时用 forming bar 合法——只对"forming bar 的值进入信号/落库为终值/喂回测"判致命。修:仅 `k.x==true` 才采纳为终值;forming bar 标 provisional,永不入信号、永不当终值落库。
- **K线缺口 / 缺失 bar (kline gap)**:连续周期里某些时间桶没有 bar。下游 MA/ATR/动量会把缺口两端当相邻 bar,信号错位。**合法例外(重点)**:**停牌、交易所维护、休市(股票隔夜/周末/节假日)、冷门 symbol 在该桶内真没成交**——这些是**数据真实地不存在**,不是采集丢数据,处理方式与"采集丢包"**完全相反**。绝不能盲目 `ffill`/回补把真空洞填成假数据。修:用期望时间网格 reindex 找缺口,**先分类**:真缺口(交易所有数据但你没采到)走 REST 回补;停牌/休市/无量(交易所也没有)标 `halted`/`NaN` 并在下游显式跳过,不参与 rolling 相邻关系。
- **重复 bar / 重复成交 (duplicate kline / trade)**:同 `open_time` 多条、同 `tradeId` 多次。成交量被重复累加,VWAP 虚高。**合法例外**:**REST 翻页边界重叠、WS 重连后重推**本就是预期行为,正确姿势是**幂等去重**而非报警——别把"收到重复帧"本身当 bug,只对"重复数据被**累加/重复计量**"判坑。修:以 `(symbol,interval,open_time)` / `tradeId` 做幂等去重 + DB 唯一约束,入库 upsert 而非 append。
- **时间戳乱序 / tradeId 缺口 (out-of-order / sequence gap)**:bar 或逐笔未按时间单调到达、`tradeId` 跳号。所有 rolling/shift/diff 假设有序,乱序则相邻关系全错;tradeId 缺口意味着成交永久丢失。**合法例外**:同一毫秒内多笔成交,`transact_time` 相等但 `tradeId` 递增属正常,需用 `(transact_time, tradeId)` 复合键判序而非单看时间戳。修:入库前强制按复合键排序,tradeId 缺口走 REST 回补。
- **OHLC 不变式破坏 (OHLC invariant violation)**:`low <= min(open,close)` 且 `high >= max(open,close)` 被破坏,通常是字段映射 o/h/l/c 错位或聚合 bug。下游突破/ATR/止损全错。修:向量化断言硬性 reject 并定位映射代码。
- **价格离群尖刺 (outlier spike / bad print)**:乌龙指、坏报价、小数点错位。**合法例外**:**真实涨跌停、闪崩闪涨、巨型市价单 print、开盘跳空**在纯阈值下与坏报价长得一模一样——别误删真实极端行情。修:用 MAD/Hampel 等**稳健**统计而非绝对阈值检测,命中只**标记隔离待人工核查**,绝不直接删。

> tick 级额外关注:成交乱序需用 `(transact_time, tradeId)` 复合键;重复成交按 tradeId 幂等;**活跃 symbol 的零量 bar 多为合成占位 bar(可疑),但冷门 symbol 的零量桶是真实无成交(合法)**——按流动性区分,别一刀切。详见 references。

### ③ 通用时间戳 / 对齐

- **时间戳单位混淆 (ms/s/ns/us unit confusion)**:**致命**。秒/毫秒/微秒/纳秒混用,时间被放大或缩小 10³~10⁹ 倍,bar 落到 1970 或遥远未来,所有对齐/排序/窗口彻底错乱——且因数值仍是合法整数而静默通过。入口统一归一为单一内部单位(建议 epoch ms),`pd.to_datetime` 永远带 `unit`,转换后跑年份范围 sanity check(2020s 秒级约 1.7e9、毫秒约 1.7e12、微秒约 1.7e15)。
- **幸存者偏差 (survivorship bias in universe)**:**致命**。回测 universe 只用"当前还在交易"的 symbol,剔除了已退市/爆雷的合约,系统性高估收益。必须用 point-in-time universe——每个历史时点的成分由当时实际可交易标的决定,含后来退市的(crypto 含已下架币并记最后成交价)。
- **未来时间戳 / 停盘 vs 真缺口混淆 (future ts / halt vs real gap)**:**未来 bar**——`ts > now` 的记录(本地写错单位、时钟漂移、交易所事件时间与接收时间混用都会造成)必须**隔离待查**,绝不能进训练/回测(等于看到未来)。数据为空时**区分两种相反情形**:交易所维护/停牌/休市(标 `halted`,**不交易、不回补**)vs 采集丢数据(**真缺口,需回补**)——两者处理完全相反,认错方向要么填进假数据、要么丢掉真信号。修:入口加 `ts <= now()+容差` 断言;空数据分类前不做任何 ffill/回补。
- **时区错误 (timezone / naive-aware mismatch)**:把交易所 UTC 当本地、naive 与 aware 混用、按本地时区切日界。bar 整体偏移数小时、日线日界错位、DST 处出现重复/缺失小时。修:全链路内部统一 UTC tz-aware,只在展示层转本地。
- **时钟漂移 (clock skew)**:本地与交易所时钟偏差。签名请求因 timestamp 超 recvWindow 被拒(Binance `-1021`)、延迟测量失真。修:周期请求交易所 `/time` 测 `offset = serverTime - localTime`,签名补偿,本机 NTP 校时。
- **时间戳漂移 / 非单调 (drift / non-monotonic event time)**:混用本地接收时间与交易所事件时间。修:分别存 `event_time` 与 `recv_time`,聚合/对齐用 event,延迟监控用差值。
- **跨源对齐 / symbol 归一化 (cross-exchange alignment / symbol normalization)**:不同交易所日界、bar 对齐基准、符号(`BTCUSDT` vs `BTC-USDT` vs `XBTUSD`,spot vs perp)不同。**合法例外**:跨所聚合时盘口"交叉"是真实套利(见 ①),按 UTC 时间戳 join 时不同所同一时刻数据本就有差异,别把真实价差当对齐错误。修:按 UTC 时间戳 join 而非行号 zip,建符号映射表区分市场类型(spot/perp/季度),映射缺失 **fail-loud** 而非静默归并。

## 数据体检报告输出模板

> 审查结论按下面格式输出,可直接截图。严重度用 emoji + 中文档级(与本工具集 `commands/orderbook-sanity.md` 一致),每条带 `文件:行` 和实测校验,致命在前。命中可疑模式但读上下文后判为真实行情/合法用法的,写入"已排除"小节而非问题清单。

```
══════════════════════════════════════════
   数据体检报告 · orderbook-sanity
══════════════════════════════════════════
数据流: 采集(__) → 解析(__) → 落库(__) → 读取(__)
审查范围: <文件/数据样本清单>     扫描文件: N 个

总评: 🔴 致命 a 项 · 🟠 高危 b 项 · 🟡 中 c 项 · 🔵 低 d 项
判语: 「<这是干净行情,还是建在沙子上的脏数据?>」
      —— 一句不留情面但克制的整体结论;不背书"绝对干净"

────────────── 问题清单(按严重度) ──────────────

[🔴 致命] checksum 未校验(本地簿可能已静默失同步)
  位置: src/feed/orderbook_okx.py:––(订阅 books 频道但全文件无 crc32 比对)
  现象: OKX books 频道每帧带 checksum,代码只按 seq 衔接、从不比对 checksum
  毒害: seq 连续但档位被错误应用/精度截断时无法察觉 → 本地簿静默偏离
  实测: 复现 OKX 文档的 CRC32(top-25 档、bid/ask 价×量拼接)与帧内 checksum 比对;
        持续失配率 > 0 即本地簿有脏
  修复: 逐字复现 checksum,失配即丢弃本地簿重拉 snapshot;价格用定点整数当 key

[🔴 致命] 使用未闭合 bar 泄漏
  位置: src/feed/kline_ws.py:88
  现象: k.x 字段未判断,forming bar 的 close 直接入信号
  毒害: 回测未来函数 → 实盘对不上回测
  实测: last_bar.close_time > now() 即被使用了未闭合 bar
  修复: 仅 k.x==true 才采纳为终值;forming bar 标 provisional 永不入信号

[🔴 致命] 时间戳单位混淆
  位置: src/parse/ts.py:23
  现象: pd.to_datetime(ts) 未带 unit,秒级时间戳被当纳秒,bar 落到 1970
  实测: df['open_time'].dt.year.between(2017,2030).all() 为 False 即中招
  修复: 入口统一 epoch ms,to_datetime 永远带 unit,转换后跑年份 sanity check

[🟠 高危] snapshot/增量衔接未做区间断言 ...
[🟠 高危] K线缺口未分类(停牌真缺口 vs 采集丢包混处理) ...
[🟡 中]   活跃 symbol 零量占位 bar ...
[🔵 低]   缺缺口率/失配率监控埋点 ...

────────────── 已排除(命中但判为真实行情/合法用法) ──────────────
- 跨所聚合簿 A.bid > B.ask: 真实套利价差,非单簿交叉(aggregate.py:54)
- BTCUSDT 周末日线缺口: 该所 7×24 无此问题 / 或确认为休市非丢数据
- REST 翻页边界重复 bar: 已 upsert 幂等去重,非计量重复

────────────── 实测建议(请在真实数据上跑) ──────────────
说明: 以上是"代码里会让数据变脏的结构性坑 + 样本可静态看出的异常",
不等于全量数据扫描。命中是否成立、多严重,以你在真实数据上的实测数字为准。
建议断言: checksum 失配率、best_bid<best_ask 违例数、open_time 单调性与重复数、
          年份范围 sanity、ts<=now 违例数、缺口分类后真缺口/停牌占比。

优先处置: 先堵致命(checksum/未闭合 bar/单位混淆/衔接错误)→ 再修缺口分类与去重 → 最后补监控埋点。
══════════════════════════════════════════
```

报告原则:每条必带 `位置`;找不到证据的项标"未发现/需人工确认",不硬凑;命中可疑模式但读上下文后判为真实行情/合法用法的,写进"已排除"而非问题清单;判语克制、不羞辱、不夸张;**绝不承诺"修完就绝对干净"**——结论以实测数字为准。

## 严重度定义

| 严重度 | 含义 | 处置 |
|--------|------|------|
| 🔴 **致命** | 静默注入未来函数或让数据语义彻底错乱,直接导致回测虚高/实盘对不上(未闭合 bar、单位混淆、幸存者偏差、checksum 失配、snapshot/增量衔接错误、未来 bar) | **必须修**,修前别信任何基于该数据的回测结论 |
| 🟠 **高危** | 数据会偏离真实状态,污染因子/信号但不一定全错(序列/深度缺口、重复/乱序、盘口交叉、qty==0 删档错误、OHLC 破坏、时区错误);**注**:纯 K 线缺口须先做"停牌真缺口 vs 采集丢包"分类——分类前不直接定高危,误把真实休市当丢数据回补本身就是新的脏数据源 | **应尽快修**,修前对受影响区间的结论打折 |
| 🟡 **中** | 边缘场景或可恢复的质量问题(活跃 symbol 零量占位 bar、时钟漂移测量、离群尖刺隔离) | **建议修**,至少加监控指标 |
| 🔵 **低** | 健壮性 / 可观测性欠缺(缺数据质量埋点、缺缺口率/checksum 失配率统计) | **可选**,有余力再补 |

判定基准:**"这个值在该时点真能算出来吗?这根盘口/这桶 bar 是本地簿真实状态,还是失同步/采集错误的产物?这个时间戳单位/时区对吗?"** ——同时反问"**它会不会其实是真实市场状态(停牌/无量/极速/跨所)?**" 两问交叉后再定罪。

## 重要说明

- **这是工程审查,不是数据质检报告**。本 skill 照的是**代码里会让数据变脏的结构性坑**和**数据样本里能静态看出的异常**,它**不能**也**不会**替你跑全量数据扫描后背书"你的数据绝对干净"。
- **必须配合实测校验**。每条发现都给了可运行的断言/统计,**请在你的真实数据上跑一遍确认**——审查能告诉你"这里有坑、这样去验",但最终是否命中、命中多严重,要靠你在真实数据上的实测数字说话。
- **真实行情 ≠ 脏数据**。停牌的真缺口、冷门 symbol 的零量桶、极速行情里一瞬的锁价、跨所聚合的真实价差,都是**真实市场状态**,不能当坑误删/误填。每条陷阱的"合法例外"就是为防这种误伤——审查时务必区分。
- **不承诺数据完美**。脏数据来源太多(交易所推送本身的 bug、网络重排、补数交叠、时钟问题),工程审查只能系统性**降低**风险、把已知坑堵上,**无法保证零脏数据**。任何"修完就绝对干净"的话术都不要信。
- **不碰策略层**。为什么这个信号不赚钱、参数怎么调、alpha 在哪——不在本 skill 范围。本 skill 只保证喂给策略的数据本身不是脏的(已知坑层面),且这种"保证"也只是"未发现本清单覆盖的坑",不是"绝对干净"的背书。

