3、数据清洗与预处理:去重、对齐、异常值处理、时间戳标准化、买卖价差计算

高频策略回测的可靠性,首先取决于输入数据的质量。真实市场数据往往存在重复记录、时间戳错位、异常跳变、买卖价差失真等问题。若未经清洗直接回测,策略绩效可能被虚假信号、未来函数或套利假象严重扭曲。本章围绕数据清洗的五个核心环节——去重、对齐、异常值处理、时间戳标准化、买卖价差计算,建立一套可落地的预处理流水线,确保回测引擎在“干净、一致、可复现”的数据基础上运行。

数据清洗与预处理流水线 原始行情数据 Tick / 1s / 1min ① 去重 (exchange_ts, symbol, price) ② 时间戳标准化 统一时区 / 精度 / 对齐 ③ 多标的对齐 重采样 / 前向填充 ④ 异常值处理 价格跳变 / 买卖价差 / 量能突变 ⑤ 买卖价差计算 ask1 - bid1 / 相对价差 清洗后数据集 回测引擎输入 质量检查清单 ✔ 无重复记录(exchange_ts + symbol + side 唯一) ✔ 时间戳单调递增,无缺失/跳变 ✔ 买卖价差 > 0,且不超过阈值(如中位数±5σ) ✔ 多标的数据对齐后无NaN(或已填充) ✔ 价格/数量无异常跳变(如价格翻倍) ✔ 回测结果可复现(随机种子固定)

上图展示了数据清洗的完整流水线:原始行情数据依次经过去重、时间戳标准化、多标的对齐、异常值处理和买卖价差计算,最终形成可直接用于回测引擎的干净数据集。每个环节都有明确的输入输出和质量检查标准,确保数据在进入策略逻辑之前已经过充分校验。

3.1 核心原理:为什么数据清洗是回测的“生死线”

高频策略对数据质量极其敏感。一个重复的tick可能导致成交量虚高,一个错位的时间戳可能产生“未来函数”信号,一个异常的价格跳变可能触发虚假的套利机会。数据清洗的本质是建立一套规则,将原始数据转换为符合市场微观结构逻辑的、可复现的、无偏的数据集。

从工程角度看,数据清洗需要解决四个核心矛盾:

  • 数据冗余与存储效率:交易所原始数据可能包含大量重复推送,需按业务语义去重。
  • 时间基准不一致:不同数据源(交易所、第三方、模拟撮合)的时间戳精度和时区可能不同。
  • 市场微观结构噪声:买卖价差倒挂、零价格、数量异常等噪声会干扰策略信号。
  • 多标的数据频率不匹配:股票、期货、期权等品种的tick频率差异巨大,需统一对齐。
核心原则:清洗规则必须“可解释、可复现、可审计”。每条清洗规则都应记录在案,并能在回测报告中追溯,否则策略绩效无法归因。

3.2 去重:消除数据冗余与重复信号

去重是数据清洗的第一步。高频数据中,重复记录可能来自交易所的重发机制、数据商的冗余存储或撮合引擎的重复推送。去重不当会导致成交量虚高、信号重复触发。

去重策略:

  1. 基于主键去重:以 (exchange_ts, symbol, side, price, volume) 作为唯一键,保留第一条或最后一条记录。
  2. 基于时间窗口去重:若同一毫秒内出现多条相同价格的记录,仅保留一条。
  3. 基于业务语义去重:对于订单簿快照,若连续两条快照的买卖盘完全一致,可合并为一条。
# 伪代码示例:基于主键去重
df = df.drop_duplicates(subset=['exchange_ts', 'symbol', 'side', 'price', 'volume'], keep='first')
# 若需保留最后一条,则 keep='last'
去重维度适用场景风险提示
精确主键去重tick级数据,字段完整若时间戳精度不足,可能误删有效数据
时间窗口去重高频快照数据窗口大小需根据品种流动性调整
业务语义去重订单簿无变化时需保留最后一条以反映最新状态

3.3 时间戳标准化:统一时区、精度与对齐基准

时间戳是高频回测的“心跳”。不同数据源可能使用不同的时间格式(Unix秒、毫秒、微秒)、时区(UTC、北京时间、交易所本地时间)和精度(秒级、毫秒级、微秒级)。时间戳标准化是确保策略信号与市场事件严格同步的前提。

标准化步骤:

  1. 统一时区:全部转换为UTC或交易所本地时区,并记录时区偏移。
  2. 统一精度:将时间戳统一为毫秒或微秒级整数,避免浮点数精度损失。
  3. 单调性检查:确保时间戳严格递增,若出现倒挂或跳变,需标记并处理。
  4. 对齐基准:对于多标的数据,需确定一个统一的时间网格(如每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 异常值处理:识别并修正价格与数量的极端跳变

异常值可能来自交易所故障、数据商错误或撮合引擎的极端成交。若不处理,异常价格会触发虚假信号,异常数量会扭曲流动性估计。

异常值识别方法:

  1. 价格跳变检测:若相邻tick价格变动超过阈值(如中位数偏差的5倍),标记为异常。
  2. 买卖价差合理性:若 ask - bid ≤ 0 或价差超过历史分布的极端分位数,标记为异常。
  3. 成交量突变:若单笔成交量超过过去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)
经验提醒:异常值处理需区分“真实极端行情”与“数据错误”。例如,2010年美股闪崩时价格瞬间下跌10%,这是真实事件,不应被清洗掉。建议结合市场状态(如涨跌停、熔断)设置动态阈值。

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次。

分析判断过程:

  1. 首先进行去重,发现重复记录主要来自数据商的重发机制,按 (timestamp, symbol, bid1, ask1) 去重后,数据量减少0.3%。
  2. 时间戳标准化:将秒级时间戳转换为毫秒级,并统一为UTC+8时区。检查发现部分时间戳存在毫秒级倒挂,按顺序重排。
  3. 异常值处理:对价格跳变事件,结合螺纹钢期货涨跌停板(±5%)判断,3800→4000元/吨涨幅5.26%,超出涨跌停限制,判定为数据错误,剔除该tick。
  4. 价差计算:过滤掉价差≤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 常见误区与总结复盘

常见误区:

  • 过度清洗:将真实极端行情当作异常值剔除,导致策略在极端行情下失效。
  • 未来函数:使用后向填充或全局统计量(如全样本均值)处理异常值,引入了未来信息。
  • 忽略时间戳精度:秒级时间戳在毫秒级策略中会产生大量“同秒信号”,需在引擎中增加随机化处理。
  • 价差计算错误:使用成交价而非订单簿价格计算价差,导致交易成本低估。
总结:数据清洗不是一次性的“过滤”,而是贯穿回测全流程的持续工程。建议建立标准化的清洗流水线,每次回测前自动执行,并输出数据质量报告。清洗规则应版本化管理,任何修改都需重新回测验证。