3. Tick数据预处理:缺失值处理、异常值检测、时间戳对齐与重采样、交易日历过滤

高频Tick数据在采集、传输与存储过程中极易受到市场微观结构噪声、网络延迟、交易所规则及硬件故障的影响。原始Tick数据通常包含缺失值、异常价格跳跃、涨跌停边界、时间戳错位以及非交易时段冗余记录等问题。本章将系统讲解Tick数据预处理的四大核心环节,确保后续特征工程建立在干净、对齐、连续的数据基础之上。

核心原则:预处理的目标不是“删除所有异常”,而是“识别并保留有效信息,剔除或修正确定性错误”。过度清洗会丢失市场微观结构信号,清洗不足则引入噪声。

3.1 缺失值处理

Tick数据中的缺失值通常表现为:某只股票在连续交易时段内出现时间戳断层(无任何报价或成交记录),或单笔记录中部分字段(如卖一价、成交量)为空。缺失原因包括:交易所数据推送间歇性中断、行情网关丢包、数据库写入延迟等。

3.1.1 缺失类型与检测方法

缺失类型 典型表现 检测方法
完全随机缺失 单笔记录中个别字段为空,前后Tick正常 逐字段检查 isnull() 或 isna()
时间戳断层 连续Tick之间间隔超过阈值(如>1秒) 计算相邻Tick时间差 diff(),标记超阈值区间
整段缺失 某只股票在特定时间段内完全无数据 与基准交易日历对比,识别缺失区间

3.1.2 处理策略

  • 前向填充(Forward Fill): 适用于价格类字段(最新价、买一价、卖一价),使用最近一个有效值填充后续缺失,直到新Tick到达。这是高频场景下最常用的方法。
  • 线性插值(Linear Interpolation): 适用于短时间断层(< 3个Tick间隔),在缺失区间两端有效值之间做线性插值。注意:插值会引入非真实价格,需谨慎使用。
  • 删除记录: 当缺失字段为关键标识(如证券代码、时间戳)或缺失比例超过阈值(如>5%)时,直接删除该笔记录。
  • 标记缺失区间: 对于时间戳断层,不填充数据,而是生成一个“缺失标记”特征,供后续模型识别该区间的不确定性。
# Python示例:前向填充与时间戳断层检测
import pandas as pd

# 假设df包含列:timestamp, price, bid_price, ask_price
df['price'] = df['price'].ffill()                     # 前向填充价格
df['bid_price'] = df['bid_price'].ffill()             # 前向填充买一价
df['ask_price'] = df['ask_price'].ffill()             # 前向填充卖一价

# 检测时间戳断层(阈值设为500毫秒)
df['time_gap'] = df['timestamp'].diff().dt.total_seconds() * 1000
df['gap_flag'] = (df['time_gap'] > 500).astype(int)
经验建议:对于高频策略,优先使用前向填充而非插值。插值会破坏价格序列的“离散跳跃”特性,导致回测结果过于乐观。仅在回测模拟撮合场景下,才考虑使用插值填充极短断层。

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

Tick数据中的异常值主要分为两类:价格跳跃(Price Jump)和涨跌停边界(Price Limit)。前者通常由数据错误或市场突发事件引起,后者则是交易所规则导致的硬边界。

3.2.1 价格跳跃检测

价格跳跃指相邻Tick之间价格变化幅度远超正常市场波动。检测方法包括:

  • 固定阈值法: 设定绝对价格变化阈值(如0.5元)或相对变化阈值(如5%),超过则标记为跳跃。
  • 移动统计法: 基于过去N个Tick的价格标准差,标记超过均值±k倍标准差的价格点(常用k=5或k=10)。
  • 中位数绝对偏差法(MAD): 对极端值更鲁棒,使用 MAD = median(|price - median(price)|),标记超过3倍MAD的点。
# Python示例:基于MAD的价格跳跃检测
import numpy as np

def detect_jump_mad(series, window=20, threshold=3.0):
    rolling_median = series.rolling(window=window, center=True).median()
    rolling_mad = (series - rolling_median).abs().rolling(window=window, center=True).median()
    # 避免除以0
    rolling_mad = rolling_mad.replace(0, np.nan)
    z_score_mad = 0.6745 * (series - rolling_median) / rolling_mad
    return np.abs(z_score_mad) > threshold

df['jump_flag'] = detect_jump_mad(df['price'])

3.2.2 涨跌停边界处理

A股市场设有±10%(普通股票)或±20%(科创板/创业板)的涨跌停限制。Tick数据在触及涨跌停时,价格不会超出边界,但可能出现以下异常:

  • 价格粘滞: 连续多个Tick价格完全相同(封死涨停/跌停),成交量可能骤降或放大。
  • 虚假突破: 某些数据源可能因撮合规则(如集合竞价)短暂记录超出涨跌停的价格,需过滤。
  • 涨跌停打开: 价格从涨跌停板回落,此时Tick序列出现大幅反向跳跃,属于有效市场行为,不应视为异常。
场景 处理方式
价格超出涨跌停边界 直接剔除该笔记录,或修正为涨跌停价
连续Tick价格相同且为涨跌停价 保留数据,但添加 limit_status 标记(涨停/跌停/正常)
涨跌停打开瞬间的大幅反向价格 保留为有效Tick,不视为异常跳跃
注意:涨跌停期间的Tick数据具有特殊的信息价值(如封单量变化、打开概率),不应简单删除。建议单独构建“涨跌停状态”特征,而非直接过滤。

3.3 时间戳对齐与重采样

不同数据源、不同交易所、不同证券品种的Tick时间戳精度和时区可能不一致。时间戳对齐的目标是建立统一的、可比较的时间基准。

3.3.1 时间戳对齐步骤

  1. 时区统一: 将所有时间戳转换为同一时区(通常为交易所本地时区,如Asia/Shanghai)。
  2. 精度归一化: 将毫秒、微秒、纳秒级时间戳统一截断或四舍五入到指定精度(如毫秒)。
  3. 去重与排序: 按时间戳和证券代码去重,确保每个时间点只有一条记录;按时间升序排列。
  4. 对齐到标准时间网格: 将Tick数据对齐到固定的时间网格(如每100ms、每500ms、每1s),生成等间隔的时间序列。

3.3.2 重采样方法

重采样是将不规则Tick数据转换为规则时间间隔数据的过程。常用方法包括:

重采样方式 价格聚合 成交量聚合 适用场景
最后Tick法 取区间内最后一个Tick的价格 累加区间内所有成交量 快速生成OHLC,计算简单
加权平均法 按时间间隔加权平均价格 累加成交量 减少尾盘效应,更平滑
成交量加权法 按成交量加权平均价格 累加成交量 反映真实交易成本
Tick数聚合 取区间内所有Tick的均价 累加成交量 适用于低流动性品种
# Python示例:将Tick数据重采样为1秒OHLC
df['timestamp'] = pd.to_datetime(df['timestamp'])
df.set_index('timestamp', inplace=True)

# 1秒重采样,使用最后Tick法
resampled = df.resample('1S').agg({
    'price': 'last',           # 收盘价
    'volume': 'sum',           # 成交量
    'bid_price': 'last',       # 买一价
    'ask_price': 'last',       # 卖一价
    'bid_volume': 'sum',       # 买一量
    'ask_volume': 'sum'        # 卖一量
})

# 计算开盘价、最高价、最低价(需要原始数据支持)
resampled['open'] = df['price'].resample('1S').first()
resampled['high'] = df['price'].resample('1S').max()
resampled['low'] = df['price'].resample('1S').min()
选择建议:对于高频策略(持仓时间<1分钟),建议使用最后Tick法或成交量加权法,保留价格序列的突变特征。对于中低频策略(持仓时间>5分钟),加权平均法能减少微观结构噪声。

3.4 交易日历过滤

交易日历过滤的目的是剔除非交易时段的数据,确保只保留在交易所正常交易时间内产生的Tick记录。不同市场、不同品种的交易时段差异很大。

3.4.1 过滤规则

  • 剔除非交易日: 周末、法定节假日、交易所临时休市日。
  • 剔除非交易时段: 盘前集合竞价(9:15-9:25)、午间休市(11:30-13:00)、盘后固定价格交易(15:05-15:30)等时段的数据,根据策略需求决定是否保留。
  • 剔除异常时段: 交易所系统测试、临时停牌、盘中熔断等特殊时段。

3.4.2 交易日历数据结构

建议维护一个独立的交易日历表,包含以下字段:

字段 类型 说明
trade_date 日期 交易日(如2025-03-15)
market 字符串 市场标识(SH/SZ/HK/US)
is_trading_day 布尔 是否为交易日
pre_open_start 时间 盘前集合竞价开始时间
pre_open_end 时间 盘前集合竞价结束时间
continuous_start 时间 连续竞价开始时间
continuous_end 时间 连续竞价结束时间
post_close_start 时间 盘后交易开始时间(如有)
post_close_end 时间 盘后交易结束时间(如有)
# Python示例:基于交易日历过滤Tick数据
import pandas as pd

# 加载交易日历
trade_calendar = pd.read_csv('trade_calendar.csv', parse_dates=['trade_date'])

# 过滤非交易日
df['date'] = df['timestamp'].dt.date
df = df[df['date'].isin(trade_calendar[trade_calendar['is_trading_day']]['trade_date'])]

# 过滤非连续竞价时段(仅保留9:30-11:30, 13:00-15:00)
df['time'] = df['timestamp'].dt.time
morning_mask = (df['time'] >= pd.Timestamp('09:30:00').time()) & (df['time'] <= pd.Timestamp('11:30:00').time())
afternoon_mask = (df['time'] >= pd.Timestamp('13:00:00').time()) & (df['time'] <= pd.Timestamp('15:00:00').time())
df = df[morning_mask | afternoon_mask]
工程实践:交易日历应每年更新一次,并包含交易所官方发布的休市安排。建议将日历表存储在数据库中,通过API自动更新,避免手动维护出错。

3.5 预处理流程总览

下图展示了Tick数据预处理的完整流程,从原始数据输入到清洗对齐后的标准Tick数据输出:

Tick数据预处理流程 Tick数据预处理流程总览 原始Tick数据(多源异构) 步骤1:缺失值处理 前向填充 / 插值 / 删除 / 标记 步骤2:异常值检测 价格跳跃检测 / 涨跌停边界处理 步骤3:时间戳对齐与重采样 时区统一 / 精度归一 / 等间隔重采样 步骤4:交易日历过滤 剔除非交易日 / 非交易时段 标准Tick数据(干净、对齐、连续) 关键检查点 • 缺失比例 < 5% • 跳跃标记准确率 > 95% • 时间戳无重复/无断层 • 交易时段覆盖完整 • 涨跌停状态正确标记 • 重采样间隔一致 • 成交量非负

3.6 预处理质量评估指标

完成预处理后,建议从以下维度评估数据质量:

评估维度 指标 合格标准
完整性 缺失率(缺失记录数/总记录数) < 1%
准确性 异常跳跃占比(标记跳跃数/总记录数) < 0.5%
一致性 时间戳重复率(重复记录数/总记录数) 0%
时效性 最大时间戳间隔(重采样后相邻记录时间差) ≤ 重采样间隔的2倍
合规性 涨跌停边界违规记录数 0
自动化建议:将上述预处理流程封装为Python类或Airflow DAG,每日运行一次,自动生成数据质量报告。报告应包括:处理记录数、缺失/异常/过滤比例、时间戳覆盖范围、以及关键字段的统计摘要。