3、数据清洗与预处理:去重、对齐、异常值处理、时间戳标准化、买卖价差计算
高频策略回测的可靠性,首先取决于输入数据的质量。真实市场数据往往存在重复记录、时间戳错位、异常跳变、买卖价差失真等问题。若未经清洗直接回测,策略绩效可能被虚假信号、未来函数或套利假象严重扭曲。本章围绕数据清洗的五个核心环节——去重、对齐、异常值处理、时间戳标准化、买卖价差计算,建立一套可落地的预处理流水线,确保回测引擎在“干净、一致、可复现”的数据基础上运行。
上图展示了数据清洗的完整流水线:原始行情数据依次经过去重、时间戳标准化、多标的对齐、异常值处理和买卖价差计算,最终形成可直接用于回测引擎的干净数据集。每个环节都有明确的输入输出和质量检查标准,确保数据在进入策略逻辑之前已经过充分校验。
3.1 核心原理:为什么数据清洗是回测的“生死线”
高频策略对数据质量极其敏感。一个重复的tick可能导致成交量虚高,一个错位的时间戳可能产生“未来函数”信号,一个异常的价格跳变可能触发虚假的套利机会。数据清洗的本质是建立一套规则,将原始数据转换为符合市场微观结构逻辑的、可复现的、无偏的数据集。
从工程角度看,数据清洗需要解决四个核心矛盾:
- 数据冗余与存储效率:交易所原始数据可能包含大量重复推送,需按业务语义去重。
- 时间基准不一致:不同数据源(交易所、第三方、模拟撮合)的时间戳精度和时区可能不同。
- 市场微观结构噪声:买卖价差倒挂、零价格、数量异常等噪声会干扰策略信号。
- 多标的数据频率不匹配:股票、期货、期权等品种的tick频率差异巨大,需统一对齐。
3.2 去重:消除数据冗余与重复信号
去重是数据清洗的第一步。高频数据中,重复记录可能来自交易所的重发机制、数据商的冗余存储或撮合引擎的重复推送。去重不当会导致成交量虚高、信号重复触发。
去重策略:
- 基于主键去重:以
(exchange_ts, symbol, side, price, volume)作为唯一键,保留第一条或最后一条记录。 - 基于时间窗口去重:若同一毫秒内出现多条相同价格的记录,仅保留一条。
- 基于业务语义去重:对于订单簿快照,若连续两条快照的买卖盘完全一致,可合并为一条。
# 伪代码示例:基于主键去重
df = df.drop_duplicates(subset=['exchange_ts', 'symbol', 'side', 'price', 'volume'], keep='first')
# 若需保留最后一条,则 keep='last'
| 去重维度 | 适用场景 | 风险提示 |
|---|---|---|
| 精确主键去重 | tick级数据,字段完整 | 若时间戳精度不足,可能误删有效数据 |
| 时间窗口去重 | 高频快照数据 | 窗口大小需根据品种流动性调整 |
| 业务语义去重 | 订单簿无变化时 | 需保留最后一条以反映最新状态 |
3.3 时间戳标准化:统一时区、精度与对齐基准
时间戳是高频回测的“心跳”。不同数据源可能使用不同的时间格式(Unix秒、毫秒、微秒)、时区(UTC、北京时间、交易所本地时间)和精度(秒级、毫秒级、微秒级)。时间戳标准化是确保策略信号与市场事件严格同步的前提。
标准化步骤:
- 统一时区:全部转换为UTC或交易所本地时区,并记录时区偏移。
- 统一精度:将时间戳统一为毫秒或微秒级整数,避免浮点数精度损失。
- 单调性检查:确保时间戳严格递增,若出现倒挂或跳变,需标记并处理。
- 对齐基准:对于多标的数据,需确定一个统一的时间网格(如每100ms一个bar),将tick数据映射到网格上。
# 时间戳标准化示例
df['ts_ms'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True).dt.tz_convert('Asia/Shanghai')
df = df.sort_values('ts_ms').reset_index(drop=True)
# 检查单调性
assert df['ts_ms'].is_monotonic_increasing, "时间戳非单调递增!"
3.4 多标的数据对齐:重采样与前向填充
高频策略常涉及多个标的(如股指期货与ETF、现货与期权)。不同标的的tick频率差异巨大,直接拼接会导致数据稀疏或错位。对齐的目标是生成一个统一时间网格上的多标的数据矩阵。
对齐方法:
- 时间网格重采样:设定固定频率(如1秒、100ms),将每个标的的tick数据聚合到网格上。
- 前向填充(ffill):对于网格上缺失的标的,使用最近一次有效价格填充,避免引入未来数据。
- 后向填充(bfill):仅在特殊场景(如计算已实现波动率)使用,需谨慎防止未来函数。
| 对齐方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 前向填充 | 无未来函数,简单 | 可能引入陈旧价格 | 回测主数据流 |
| 线性插值 | 平滑过渡 | 可能产生虚假价格 | 低频数据补全 |
| 仅保留共同时间点 | 数据纯净 | 样本量大幅减少 | 高频统计套利 |
3.5 异常值处理:识别并修正价格与数量的极端跳变
异常值可能来自交易所故障、数据商错误或撮合引擎的极端成交。若不处理,异常价格会触发虚假信号,异常数量会扭曲流动性估计。
异常值识别方法:
- 价格跳变检测:若相邻tick价格变动超过阈值(如中位数偏差的5倍),标记为异常。
- 买卖价差合理性:若 ask - bid ≤ 0 或价差超过历史分布的极端分位数,标记为异常。
- 成交量突变:若单笔成交量超过过去N笔的均值+5倍标准差,标记为异常。
# 价格跳变检测示例
median_abs_dev = (df['price'].diff().abs()).median()
threshold = 5 * 1.4826 * median_abs_dev
df['price_jump'] = df['price'].diff().abs() > threshold
# 对异常值进行修正(如用前值填充或剔除)
df.loc[df['price_jump'], 'price'] = df['price'].shift(1)
3.6 买卖价差计算:从订单簿到可交易信号
买卖价差(Bid-Ask Spread)是高频策略的核心输入之一,直接影响交易成本估算和套利信号判断。价差计算需基于订单簿快照数据,而非成交数据。
价差计算口径:
- 价差:ask1 - bid1,单位为价格最小变动单位(tick size)。
- 相对价差:(ask1 - bid1) / mid_price,用于跨品种比较。
- 加权价差:考虑买卖盘深度,计算加权平均价差。
# 价差计算示例
df['mid_price'] = (df['ask1'] + df['bid1']) / 2
df['spread_abs'] = df['ask1'] - df['bid1']
df['spread_rel'] = df['spread_abs'] / df['mid_price']
# 过滤无效价差
df = df[(df['spread_abs'] > 0) & (df['spread_abs'] < 10 * df['tick_size'])]
| 价差类型 | 计算公式 | 用途 |
|---|---|---|
| 价差 | ask1 - bid1 | 交易成本估算 |
| 相对价差 | (ask1 - bid1) / mid | 跨品种比较 |
| 有效价差 | 2 * |trade_price - mid| | 实际成交成本 |
3.7 案例拆解:某期货高频策略的数据清洗实战
背景与约束:某量化团队开发螺纹钢期货高频做市策略,使用某数据商提供的tick数据(含1档买卖盘),回测周期为2024年1月-3月。数据量为日均200万条tick,原始数据存在以下问题:重复记录约0.3%、时间戳精度不一致(部分为秒级,部分为毫秒级)、偶发买卖价差倒挂、某日出现价格跳变(从3800元/吨瞬间跳至4000元/吨)。
初始现象与目标:初步回测显示策略年化收益高达80%,但实盘模拟中却持续亏损。经排查,发现回测中使用了未经清洗的数据,导致虚假套利信号频发。
关键输入数据与指标:
- 原始tick数据:字段包括
timestamp,symbol,bid1,ask1,bid_vol1,ask_vol1,last_price,volume。 - 异常指标:重复率0.3%,价差倒挂占比0.05%,价格跳变事件3次。
分析判断过程:
- 首先进行去重,发现重复记录主要来自数据商的重发机制,按
(timestamp, symbol, bid1, ask1)去重后,数据量减少0.3%。 - 时间戳标准化:将秒级时间戳转换为毫秒级,并统一为UTC+8时区。检查发现部分时间戳存在毫秒级倒挂,按顺序重排。
- 异常值处理:对价格跳变事件,结合螺纹钢期货涨跌停板(±5%)判断,3800→4000元/吨涨幅5.26%,超出涨跌停限制,判定为数据错误,剔除该tick。
- 价差计算:过滤掉价差≤0的记录,并设置价差上限为10个tick size(tick size=1元/吨),过滤后保留99.95%的数据。
实际操作步骤:
# 清洗流水线
df = df.drop_duplicates(subset=['timestamp', 'symbol', 'bid1', 'ask1'])
df['ts_ms'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True).dt.tz_convert('Asia/Shanghai')
df = df.sort_values('ts_ms')
# 异常价格剔除
df = df[(df['last_price'] >= 3500) & (df['last_price'] <= 4000)]
# 价差过滤
df['spread'] = df['ask1'] - df['bid1']
df = df[(df['spread'] > 0) & (df['spread'] <= 10)]
结果对比:清洗后,策略年化收益从80%降至12%,与实盘模拟的10%基本一致。虚假信号被消除,策略绩效变得可解释、可复现。
风险与代价:清洗过程剔除了约0.4%的数据,可能导致部分真实极端行情被误删。为降低风险,团队保留了被剔除数据的日志,并定期人工复核。
复盘经验:数据清洗规则必须与市场规则(如涨跌停、tick size)结合,不能仅依赖统计阈值。同时,清洗过程应生成数据质量报告,作为回测报告的附件。
3.8 常见误区与总结复盘
常见误区:
- 过度清洗:将真实极端行情当作异常值剔除,导致策略在极端行情下失效。
- 未来函数:使用后向填充或全局统计量(如全样本均值)处理异常值,引入了未来信息。
- 忽略时间戳精度:秒级时间戳在毫秒级策略中会产生大量“同秒信号”,需在引擎中增加随机化处理。
- 价差计算错误:使用成交价而非订单簿价格计算价差,导致交易成本低估。