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%。过滤后务必输出被剔除样本的时间戳与原始值,人工抽查若干条,确认剔除的是真实异常而非行情剧烈波动导致的正常跳变——这是避免“清洗过度”的关键一步。
四、时间对齐:让快照落在统一网格上
采集时间戳往往不是整齐的 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 逐字段比对一致。任一项不通过,就回到对应阶段调整阈值重跑,而不是在后续计算里“打补丁”。
至此,我们得到了一份时间等间隔、字段合法、异常可控的快照数据集。它保留了原始行情的主要结构,同时把采集噪声隔离在 valid 标记之外,为下一步计算买卖不平衡度与微价格提供了可靠输入。