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 时间戳对齐步骤
- 时区统一: 将所有时间戳转换为同一时区(通常为交易所本地时区,如Asia/Shanghai)。
- 精度归一化: 将毫秒、微秒、纳秒级时间戳统一截断或四舍五入到指定精度(如毫秒)。
- 去重与排序: 按时间戳和证券代码去重,确保每个时间点只有一条记录;按时间升序排列。
- 对齐到标准时间网格: 将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数据输出:
3.6 预处理质量评估指标
完成预处理后,建议从以下维度评估数据质量:
| 评估维度 | 指标 | 合格标准 |
|---|---|---|
| 完整性 | 缺失率(缺失记录数/总记录数) | < 1% |
| 准确性 | 异常跳跃占比(标记跳跃数/总记录数) | < 0.5% |
| 一致性 | 时间戳重复率(重复记录数/总记录数) | 0% |
| 时效性 | 最大时间戳间隔(重采样后相邻记录时间差) | ≤ 重采样间隔的2倍 |
| 合规性 | 涨跌停边界违规记录数 | 0 |
自动化建议:将上述预处理流程封装为Python类或Airflow DAG,每日运行一次,自动生成数据质量报告。报告应包括:处理记录数、缺失/异常/过滤比例、时间戳覆盖范围、以及关键字段的统计摘要。