3. Tick数据结构:Tick数据字段详解与预处理
Tick数据是金融市场最细粒度的交易记录,每一笔成交或每一次报价变动都会生成一条Tick记录。理解Tick数据的字段含义、掌握清洗与预处理方法,是构建高频回测系统的基石。本节将逐一拆解Tick数据的核心字段,并给出工业级的数据清洗流程。
3.1 Tick数据核心字段详解
一条完整的Tick记录通常包含以下字段,按功能可分为时间标识、成交信息、报价信息和辅助信息四类。
| 字段类别 | 字段名称 | 数据类型 | 说明 | 典型示例 |
|---|---|---|---|---|
| 时间标识 | Timestamp | datetime / int64 | 交易所撮合时间,精度通常为毫秒或微秒 | 2025-03-21 09:30:00.123 |
| Exchange | string | 交易所代码(如SSE、SZSE、CME) | SSE | |
| 成交信息 | LastPrice | float64 | 最新成交价 | 15.28 |
| Volume | int64 | 当前Tick的成交量(非累计) | 200 | |
| Turnover | float64 | 当前Tick的成交金额 | 3056.00 | |
| 买卖盘口 | BidPrice1 ~ BidPrice5 | float64 | 买一到买五档位价格 | 15.27, 15.26, ... |
| BidVolume1 ~ BidVolume5 | int64 | 买一到买五档位挂单量 | 500, 800, ... | |
| AskPrice1 ~ AskPrice5 | float64 | 卖一到卖五档位价格 | 15.29, 15.30, ... | |
| AskVolume1 ~ AskVolume5 | int64 | 卖一到卖五档位挂单量 | 300, 600, ... | |
| 辅助信息 | Symbol / InstrumentID | string | 合约代码或股票代码 | 600519.SH |
关键概念: LastPrice 与 Bid/Ask 的关系决定了市场微观结构。当 LastPrice 等于 AskPrice1 时,表示主动买入(吃单);当 LastPrice 等于 BidPrice1 时,表示主动卖出。这一关系是构建订单流不平衡指标的基础。
3.2 Tick数据清洗与预处理
原始Tick数据通常存在缺失值、异常值、重复记录和时间错乱等问题。以下是一套标准化的预处理流程。
3.2.1 清洗流程总览
- 时间排序与去重:按 Timestamp 升序排列,删除完全重复的行(所有字段相同)。
- 缺失值处理:对价格字段(LastPrice、BidPrice1、AskPrice1)的缺失值,使用前向填充(ffill);对成交量缺失值,直接填充为0。
- 异常价格过滤:剔除价格超出 [1, 100000] 区间的记录,以及价格变动超过阈值(如 ±20%)的跳变点。
- 盘口逻辑校验:确保 BidPrice1 < AskPrice1,且各档位价格单调(BidPrice1 > BidPrice2 > ...)。
- 非交易时段过滤:根据交易所规则,剔除集合竞价、午休、收盘后的无效Tick。
实战经验: 在回测中,不要对 Tick 数据做平滑或插值处理,这会引入未来信息。前向填充是唯一安全的缺失值填充方式。
3.2.2 代码实现示例
import pandas as pd
import numpy as np
def clean_tick_data(df: pd.DataFrame) -> pd.DataFrame:
"""
标准Tick数据清洗函数
输入df必须包含列:Timestamp, LastPrice, Volume,
BidPrice1, AskPrice1, BidVolume1, AskVolume1
"""
# 1. 时间排序
df = df.sort_values('Timestamp').reset_index(drop=True)
# 2. 去重
df = df.drop_duplicates()
# 3. 缺失值前向填充(价格类)
price_cols = ['LastPrice', 'BidPrice1', 'AskPrice1']
df[price_cols] = df[price_cols].ffill()
# 4. 成交量缺失补0
df['Volume'] = df['Volume'].fillna(0)
# 5. 异常价格过滤
df = df[(df['LastPrice'] > 1) & (df['LastPrice'] < 100000)]
# 6. 盘口逻辑校验
df = df[df['BidPrice1'] < df['AskPrice1']]
# 7. 价格跳变过滤(单笔变动超过20%视为异常)
df['price_change_pct'] = df['LastPrice'].pct_change().abs()
df = df[df['price_change_pct'] < 0.20]
df = df.drop(columns=['price_change_pct'])
return df
3.3 Tick数据字段关系与处理流程
下面的SVG图展示了Tick数据从原始记录到清洗后数据的完整处理链路,以及各字段之间的逻辑关系。
3.4 常见数据质量问题与处理策略
| 问题类型 | 典型表现 | 影响 | 处理策略 |
|---|---|---|---|
| 时间戳重复 | 同一毫秒出现多条相同价格记录 | 回测中重复计算收益 | 保留第一条,或按Volume合并 |
| 价格为零或负数 | LastPrice = 0 或 -1 | 导致收益率计算崩溃 | 直接删除该行 |
| 盘口倒挂 | BidPrice1 > AskPrice1 | 价差为负,逻辑错误 | 删除该Tick或交换Bid/Ask |
| 成交量突增 | Volume 超过前100个Tick的99%分位数 | 可能是数据合并错误 | 用中位数替换或删除 |
| 非交易时段数据 | 午休11:30-13:00出现Tick | 引入无效信号 | 按交易所日历过滤 |
重要提醒: 在回测中,绝对不要对Tick数据进行未来数据填充(如使用后向填充或插值)。任何基于未来信息的填充都会导致回测结果严重失真,产生虚假的高收益。
3.5 数据预处理后的字段衍生
清洗完成后,通常需要衍生以下关键字段用于回测策略:
- Spread = AskPrice1 - BidPrice1(买卖价差)
- MidPrice = (BidPrice1 + AskPrice1) / 2(中间价)
- OrderFlowImbalance = (AskVolume1 - BidVolume1) / (AskVolume1 + BidVolume1)(订单流不平衡)
- LogReturn = log(LastPrice / LastPrice.shift(1))(对数收益率)
- VolumeWeightedPrice = Turnover / Volume(加权均价,用于大单拆分)
# 字段衍生示例
df['Spread'] = df['AskPrice1'] - df['BidPrice1']
df['MidPrice'] = (df['BidPrice1'] + df['AskPrice1']) / 2
df['OrderFlowImbalance'] = (df['AskVolume1'] - df['BidVolume1']) / (df['AskVolume1'] + df['BidVolume1'] + 1e-8)
df['LogReturn'] = np.log(df['LastPrice'] / df['LastPrice'].shift(1))