4、数据清洗:处理缺失值、异常值、时间戳对齐、快照重建
数据清洗这步,说白了就是给原始订单簿数据「洗澡」。我见过太多人,模型写得花里胡哨,结果数据一塌糊涂,最后回测曲线漂亮得像假的一样——嗯,其实就是假的。今天咱们就聊聊,怎么把这堆脏数据收拾得服服帖帖。
4.1 缺失值处理:别让空值毁了你的信号
订单簿数据里,缺失值太常见了。比如某只股票流动性差,某个价位上半天没单子;或者交易所数据推送偶尔抽风,丢了一帧。我个人习惯,先分清楚缺失的类型:
- 完全随机缺失:比如网络抖动丢包,直接扔掉或前向填充就行
- 非随机缺失:比如大单撤单导致某个价位变空,这得小心处理
我在项目中遇到过最坑的一次:某只小盘股,卖五价位经常是空的,我直接用0填充。结果不平衡度信号算出来全是极端值,回测收益高得离谱——后来发现,那些空价位其实代表「没人卖」,应该用卖一价或者前一个非空值才对。
核心原则:订单簿的缺失值,不要机械地用0或均值填充。要考虑市场微观结构。
常用的填充策略:
- 前向填充(ffill):用上一个非空值填充,适合高频场景
- 线性插值:适合价格序列,但订单簿的价量数据用这个要谨慎
- 标记填充:对缺失位置打标记,作为特征输入模型
import pandas as pd
import numpy as np
# 假设df是订单簿DataFrame,包含bid_price1~5, bid_vol1~5, ask_price1~5, ask_vol1~5
def clean_missing_orderbook(df):
# 前向填充:用上一个非空值填充
df = df.ffill()
# 如果某一行所有价格都缺失,直接删除
price_cols = [col for col in df.columns if 'price' in col]
df = df.dropna(subset=price_cols, how='all')
# 对成交量缺失,用0填充(表示该价位无挂单)
vol_cols = [col for col in df.columns if 'vol' in col]
df[vol_cols] = df[vol_cols].fillna(0)
return df
小技巧:处理前先看看缺失率。如果某个价位缺失超过30%,我建议直接放弃这个价位,别硬填。
4.2 异常值检测:揪出那些离谱的数据点
异常值在订单簿里很要命。比如突然冒出一个卖一价是0.01元,或者买一量是10亿股——这明显是数据错误。我曾经在回测时没处理这些,结果信号在某个时刻直接爆表,仓位管理全乱套。
常用的检测方法:
| 方法 | 适用场景 | 注意事项 |
|---|---|---|
| Z-score | 价格序列 | 假设正态分布,订单簿数据往往不满足 |
| IQR(四分位距) | 成交量、价差 | 对极端值不敏感,适合厚尾分布 |
| 固定阈值 | 价格涨跌幅限制 | 需要根据品种设定,比如A股±10% |
| 滚动窗口 | 高频数据 | 用过去N个点的均值±3倍标准差 |
def detect_outliers_iqr(series, factor=3):
"""用IQR方法检测异常值"""
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - factor * IQR
upper = Q3 + factor * IQR
return (series < lower) | (series > upper)
# 对买卖价差做异常检测
df['spread'] = df['ask_price1'] - df['bid_price1']
outliers = detect_outliers_iqr(df['spread'])
df.loc[outliers, 'spread'] = np.nan # 标记为缺失,后续填充
警告:别一刀切删除异常值。有些异常其实是市场极端事件(比如闪崩),删了反而丢失信息。我建议先标记,再人工判断。
4.3 时间戳对齐:让不同数据源步调一致
做量化交易最头疼的事之一,就是不同数据源的时间戳对不上。交易所的tick数据可能是微秒级,但你的行情软件只给到毫秒。更坑的是,有些数据源的时间戳是本地时间,有些是UTC。
我个人习惯,统一用纳秒级时间戳,全部转成UTC。为什么?因为跨市场交易时,时区差异能把你搞疯。
def align_timestamps(df, freq='100ms'):
"""将订单簿数据对齐到固定时间间隔"""
# 确保时间戳是datetime类型
df.index = pd.to_datetime(df.index, unit='ns')
# 按固定频率重采样,取每个区间最后一笔数据
aligned = df.resample(freq).last()
# 如果某个区间没有数据,前向填充
aligned = aligned.ffill()
return aligned
# 示例:对齐到100毫秒
df_aligned = align_timestamps(df, '100ms')
这里有个坑:重采样时用last()还是first()?我建议用last(),因为订单簿反映的是该时刻的「最新状态」。但如果你做的是事件驱动策略,可能用first()更合适。嗯,看策略需求。
4.4 快照重建:从增量数据还原完整订单簿
很多交易所为了节省带宽,只推送增量数据(比如「买一价变了」),不推送完整快照。这时候你得自己重建。我记得第一次做这个,没处理好增量叠加,结果订单簿越算越乱,最后价格都变成负数了。
重建的核心逻辑:
- 维护一个当前快照的字典
- 收到增量事件时,更新对应价位
- 定期用全量快照做校准(防止累积误差)
class OrderBookReconstructor:
def __init__(self, levels=5):
self.levels = levels
self.bids = {} # {price: volume}
self.asks = {}
def apply_snapshot(self, snapshot):
"""应用全量快照"""
self.bids = {p: v for p, v in snapshot['bids']}
self.asks = {p: v for p, v in snapshot['asks']}
def apply_delta(self, delta):
"""应用增量更新"""
for side, price, volume in delta:
if side == 'bid':
if volume == 0:
self.bids.pop(price, None)
else:
self.bids[price] = volume
elif side == 'ask':
if volume == 0:
self.asks.pop(price, None)
else:
self.asks[price] = volume
def get_top_n(self):
"""获取前N档买卖盘"""
sorted_bids = sorted(self.bids.items(), reverse=True)[:self.levels]
sorted_asks = sorted(self.asks.items())[:self.levels]
return sorted_bids, sorted_asks
关键点:增量更新时,成交量归零表示撤单,要删除该价位。别傻傻地保留0量。
4.5 知识体系总览
下面这张图,是我做数据清洗时的完整流程。你可以把它贴在工位上,每次处理数据前扫一眼。
数据清洗这件事,说白了就是「垃圾进,垃圾出」。你花80%的时间在清洗上,一点都不夸张。我见过太多人急着跑模型,结果数据没洗干净,最后白忙活一场。记住:干净的订单簿数据,是交易信号挖掘的地基。