4. 数据获取与清洗实战

微价格的计算公式本身并不复杂,真正决定结果可信度的是喂给公式的那份数据。订单簿快照在采集、传输、落盘的过程中,几乎必然出现时间戳错位、档位缺失、价格跳变、重复记录等问题。本节以一份模拟的逐笔订单簿快照为对象,完整走一遍“加载—缺失处理—异常过滤—时间对齐—产出干净数据集”的流程,并说明每一步的判断依据。

一、先明确数据对象与字段约定

我们假设采集端每 100ms 对某交易对(示例用 BTC/USDT)的订单簿做一次快照,每条记录包含:ts(采集时间戳,毫秒)、bid_px_1..5、bid_qty_1..5、ask_px_1..5、ask_qty_1..5。微价格只依赖买一卖一,但保留五档便于后续做深度加权与异常交叉验证。方向约定:买价恒小于卖价,价差 spread = ask_px_1 - bid_px_1 恒为非负,全文与图示均按此约定。

加载阶段用 pandas 读取 CSV,并强制指定时间戳为整型、价格数量为浮点,避免字符串混入导致后续比较失效:

import pandas as pd, numpy as np

cols = ["ts"] + [f"{s}_{k}" for s in ("bid_px","bid_qty","ask_px","ask_qty") for k in range(1,6)]
df = pd.read_csv("ob_snapshot.csv", usecols=cols,
                 dtype={"ts":"int64", **{c:"float64" for c in cols[1:]}})
df = df.sort_values("ts").drop_duplicates("ts", keep="last").reset_index(drop=True)

这里先做两件“加载即清洗”的事:按 ts 排序保证时序单调,按时间戳去重并保留最后一条(同一毫秒重复采集时,后到的通常更完整)。去重后应打印 len(df) 与原始行数对比,确认丢弃量在合理范围(例如不超过 1%),否则说明采集端存在重复写入问题,需要回到源头排查。

二、缺失值:区分“真缺失”与“结构性缺失”

订单簿的缺失有两种性质完全不同的情况。第一种是某档位数量为 0 或为空,这往往意味着该侧深度不足五档,属于结构性缺失,不应填补成随机值;第二种是整行关键字段(买一价、卖一价)为空,属于采集失败,必须剔除。处理策略因此分层:

  • 买一/卖一价格或数量为空的行:无法计算微价格,直接删除,并记录删除条数。
  • 第 2~5 档缺失:保留该行,缺失档位数量置 0,价格置 NaN,仅用于深度分析时跳过。
  • 数量为负或价格 ≤ 0:视为非法值,按缺失处理。
key = ["bid_px_1","bid_qty_1","ask_px_1","ask_qty_1"]
before = len(df)
df = df.dropna(subset=key)
df = df[(df[key] > 0).all(axis=1)]
print("剔除关键字段异常行:", before - len(df))

三、异常值过滤:用业务约束而非纯统计阈值

订单簿异常最典型的形态有三类:买卖价倒挂(bid_px_1 ≥ ask_px_1)、价差异常放大(远超正常水平)、相邻快照价格瞬间跳变。前两类用横截面约束即可判定,第三类需要纵向比较。实践中建议组合使用:

# 1) 横截面:价差必须为正且不超过合理上限
df["spread"] = df["ask_px_1"] - df["bid_px_1"]
df = df[(df["spread"] > 0) & (df["spread"] < df["bid_px_1"] * 0.01)]

# 2) 纵向:相邻快照中间价跳变超过 2% 视为异常
df["mid"] = (df["bid_px_1"] + df["ask_px_1"]) / 2
jump = df["mid"].pct_change().abs()
df = df[jump.fillna(0) < 0.02]

注意 2% 是示例阈值,应按标的波动率调整:高波动品种可放宽到 3%~5%,稳定币对可收紧到 0.5%。过滤后务必输出被剔除样本的时间戳与原始值,人工抽查若干条,确认剔除的是真实异常而非行情剧烈波动导致的正常跳变——这是避免“清洗过度”的关键一步。

订单簿快照清洗流水线与数据形态变化 原始快照 含重复 ts 空值 / 负数量 价差倒挂 时间戳乱序 加载与去重 ts 排序 同 ts 保留末条 类型强制转换 丢弃量 < 1% 缺失值分层 买一卖一空 → 删 2~5 档空 → 置 0 负数量 → 按缺失 记录剔除条数 异常值过滤 价差 ≤ 0 剔除 价差 > 1% 剔除 中间价跳变 > 2% 人工抽查剔除样本 时间对齐(重采样到 1s 网格) ts → 秒级桶,取桶内最后一条快照 空桶前向填充,限制最大填充间隔 3s 超限空桶标记为无效,不参与计算 干净数据集(可计算) ts 单调唯一 · 买卖价有效 · 价差为正 等间隔 1s · 无非法值 可直接计算 microprice 与 imbalance 产出校验: 行数留存率 ≥ 95% ts 差分恒为 1000ms spread > 0 全通过 抽样 20 条与原始快照比对一致 任一校验不通过 → 回溯对应清洗阶段,调整阈值后重跑

四、时间对齐:让快照落在统一网格上

采集时间戳往往不是整齐的 100ms 倍数,且存在丢包导致的间隔不均。微价格若在不等间隔序列上直接做差分或滚动统计,会引入偏差。做法是把时间戳归入固定网格(示例取 1 秒),每个桶取桶内最后一条快照作为该时刻状态:

df["sec"] = (df["ts"] // 1000) * 1000
aligned = df.groupby("sec").tail(1).set_index("sec").sort_index()

full_idx = pd.RangeIndex(aligned.index.min(), aligned.index.max()+1000, 1000)
aligned = aligned.reindex(full_idx)
aligned = aligned.ffill(limit=3)          # 最多前向填充 3 个空桶
aligned["valid"] = aligned["bid_px_1"].notna()

前向填充的语义是“沿用上一有效快照”,对订单簿而言是合理近似,但必须限制填充长度:连续缺失超过 3 秒通常意味着采集中断,此时沿用旧状态会制造虚假的平稳行情,应标记 valid=False 并在计算时排除。填充后要检查 valid 为真的比例,低于 90% 说明数据源质量不足,需先解决采集稳定性。

五、产出与验收

清洗完成后输出一份可直接进入微价格计算的表:ts、bid_px_1、bid_qty_1、ask_px_1、ask_qty_1、spread、mid、valid。验收不靠“看起来没问题”,而靠几条硬性检查:时间戳差分恒为 1000ms;spread 全部为正;关键字段无空值;随机抽 20 条与原始 CSV 逐字段比对一致。任一项不通过,就回到对应阶段调整阈值重跑,而不是在后续计算里“打补丁”。

建议把清洗参数(价差上限比例、跳变阈值、最大填充间隔、留存率下限)集中写进一个配置字典并随数据集一起落盘。同一套参数复用于不同日期时,若留存率突然从 98% 掉到 85%,这本身就是数据源异常的信号,比事后看微价格曲线更早发现问题。
常见误区:用全局均值填补买一价、或对价差为负的行取绝对值“修正”。前者会抹平真实行情结构,后者会把倒挂这种明确的采集错误伪装成正常数据,二者都会让微价格在异常时段给出看似合理却完全错误的信号。缺失与异常应当剔除或标记,而不是“修得好看”。

至此,我们得到了一份时间等间隔、字段合法、异常可控的快照数据集。它保留了原始行情的主要结构,同时把采集噪声隔离在 valid 标记之外,为下一步计算买卖不平衡度与微价格提供了可靠输入。