3. Tick 数据清洗:缺失值处理、异常值检测与时间戳对齐

Tick 数据是高频交易的“原油”,但原始交易所数据往往包含大量噪声、缺失和异常。本章聚焦于清洗流程中的三个核心环节:缺失值处理、异常值检测(价格跳跃与涨跌停过滤)以及时间戳对齐与去重。清洗质量直接决定后续特征工程的可靠性。

3.1 缺失值处理

Tick 数据缺失通常由网络延迟、交易所撮合引擎快照间隔或数据供应商采样不均导致。缺失模式分为三类:

缺失类型 典型场景 推荐处理方式
随机缺失 单笔买卖报价缺失,相邻 Tick 正常 前向填充(ffill)或线性插值
连续缺失 盘中断线超过 500ms,连续 5 笔以上缺失 标记为“无报价区间”,不填充,策略中跳过
开盘/收盘缺失 集合竞价阶段无逐笔成交 使用集合竞价第一笔成交价填充开盘 Tick
实践建议: 对于高频策略,优先使用前向填充(ffill)而非均值填充,因为均值会引入未来信息。连续缺失超过 3 笔时,建议直接丢弃该区间,避免插值引入虚假价格序列。
import pandas as pd
import numpy as np

# 示例:Tick 数据前向填充与连续缺失标记
def clean_missing_ticks(df, max_gap=3):
    """
    df: 包含 'time', 'price', 'volume' 的 DataFrame
    max_gap: 允许连续缺失的最大笔数
    """
    # 标记缺失
    df['price_missing'] = df['price'].isna()
    # 计算连续缺失组
    df['gap_group'] = (~df['price_missing']).cumsum()
    gap_counts = df.groupby('gap_group')['price_missing'].sum()
    large_gaps = gap_counts[gap_counts > max_gap].index

    # 对连续缺失过多的区间,将价格设为 NaN(后续丢弃)
    df.loc[df['gap_group'].isin(large_gaps) & df['price_missing'], 'price'] = np.nan
    # 前向填充其余缺失
    df['price'] = df['price'].ffill()
    # 丢弃仍为 NaN 的行(开盘前或收盘后)
    df = df.dropna(subset=['price'])
    return df

3.2 异常值检测:价格跳跃与涨跌停过滤

异常 Tick 主要来自:错误报价(胖手指)、撮合异常(闪电崩盘中的极端单笔)以及涨跌停边界。检测策略需结合统计方法与市场规则。

3.2.1 价格跳跃检测

价格跳跃定义为相邻 Tick 价格变化超过阈值。阈值设定方法:

  • 固定阈值法: 基于合约最小变动价位(Tick Size)的 N 倍,例如 N=10 倍 Tick Size。
  • 动态阈值法: 使用滚动窗口的 Median Absolute Deviation (MAD),对波动率变化具有鲁棒性。
  • Z-Score 法: 计算价格变化率的 Z-Score,|Z| > 3 视为异常。
def detect_price_jumps(df, tick_size=0.2, n_mult=10):
    """
    基于固定阈值检测价格跳跃
    """
    df['price_diff'] = df['price'].diff().abs()
    threshold = tick_size * n_mult
    df['is_jump'] = df['price_diff'] > threshold
    return df[df['is_jump'] == False]  # 过滤跳跃点
注意: 涨跌停附近的跳跃可能是真实市场行为(封板/开板),不应直接过滤。建议结合涨跌停状态标记,仅过滤非涨跌停状态下的极端跳跃。

3.2.2 涨跌停过滤

交易所涨跌停规则:

  • 涨停/跌停价 基于前收盘价计算,例如 ±10%(A 股主板)。
  • 涨停时 卖一价可能为空,买一价等于涨停价;跌停时相反。
  • 过滤策略: 将价格等于涨跌停价的 Tick 标记为“涨跌停状态”,不删除,但策略中需特殊处理(如禁止开仓)。
def filter_limit_up_down(df, prev_close, limit_pct=0.10):
    """
    标记涨跌停状态
    """
    limit_up = prev_close * (1 + limit_pct)
    limit_down = prev_close * (1 - limit_pct)
    df['is_limit_up'] = df['price'] >= limit_up
    df['is_limit_down'] = df['price'] <= limit_down
    # 保留所有数据,仅添加标记
    return df
异常值检测流程 原始 Tick 数据 价格变化率计算 MAD 动态阈值检测 涨跌停边界过滤 清洗后 Tick 数据 并行处理

3.3 时间戳对齐与去重

不同数据源(交易所直连、第三方供应商)的时间戳精度和时区可能不一致。对齐与去重是保证时序正确性的关键。

3.3.1 时间戳对齐步骤

  1. 统一时区: 将全部时间戳转换为交易所本地时区(如 Asia/Shanghai),避免夏令时问题。
  2. 精度归一化: 将毫秒/微秒时间戳统一为纳秒级(Python 的 pd.Timestamp 默认支持)。
  3. 对齐到标准网格: 对于需要等间隔采样的策略,将 Tick 对齐到固定的 100ms/500ms 网格,使用前向填充。

3.3.2 去重策略

重复 Tick 通常由数据重传或多路订阅导致。去重规则:

去重维度 判断依据 保留策略
精确重复 时间戳 + 价格 + 成交量完全相同 仅保留第一条
时间戳重复但价格不同 同一毫秒内出现多笔不同价格 保留最后一条(最新撮合结果)
跨日重复 收盘后数据与次日开盘数据混淆 按交易日分组后去重
def align_and_deduplicate(df, time_col='time', price_col='price', volume_col='volume'):
    """
    时间戳对齐与去重
    """
    # 1. 确保时间戳为 datetime 类型
    df[time_col] = pd.to_datetime(df[time_col], utc=True)
    # 2. 按时间戳排序
    df = df.sort_values(time_col).reset_index(drop=True)
    # 3. 精确去重
    df = df.drop_duplicates(subset=[time_col, price_col, volume_col], keep='first')
    # 4. 时间戳重复但价格不同:保留最后一条
    df = df.drop_duplicates(subset=[time_col], keep='last')
    return df
时间戳对齐与去重流程 多源 Tick 数据 步骤1:统一时区 → 精度归一化 步骤2:精确去重 + 时间戳重复去重 对齐去重后 Tick

3.4 清洗效果评估指标

清洗完成后,建议使用以下指标量化清洗效果:

  • 缺失率: 清洗前后缺失值占比变化,目标降至 < 0.1%。
  • 异常点占比: 被标记为跳跃或涨跌停的 Tick 比例,通常 < 0.5%。
  • 重复率: 去重前后数据量减少比例,正常应在 0.1% ~ 2% 之间。
  • 时间戳连续性: 相邻 Tick 时间间隔的 MAD,越小说明对齐越好。
核心原则: 清洗不是“删除越多越好”,而是保留真实市场微观结构,同时剔除数据采集环节引入的噪声。每次清洗操作都应记录日志,便于回溯。