2. Tick级数据:Tick数据结构、Tick数据获取、Tick数据清洗、Tick数据存储
做量化交易的朋友都知道,K线数据是「压缩饼干」,而Tick数据才是「原粮」。说白了,K线把市场最原始的搏杀痕迹给抹平了。我刚开始做高频策略时,用K线回测效果不错,一上实盘就亏钱。后来才发现,问题出在数据颗粒度上。
今天我们就来聊聊Tick数据。这是市场微观结构的基石,也是我这些年花时间最多的一个环节。
2.1 Tick数据结构
先搞清楚Tick数据长什么样。不同交易所的Tick格式略有差异,但核心字段大同小异。我整理了一个通用结构:
| 字段名 | 类型 | 说明 | 示例 |
|---|---|---|---|
| Symbol | str | 合约代码 | rb2410 |
| DateTime | datetime | 交易所时间戳 | 2024-01-15 09:30:00.123 |
| LastPrice | float | 最新成交价 | 3985.0 |
| Volume | int | 当前成交量(累计) | 12567 |
| Turnover | float | 当前成交额(累计) | 50123456.0 |
| BidPrice1 | float | 买一价 | 3984.0 |
| BidVolume1 | int | 买一量 | 320 |
| AskPrice1 | float | 卖一价 | 3986.0 |
| AskVolume1 | int | 卖一量 | 180 |
这里有个坑要注意。很多新手以为Volume是「这一笔的成交量」,其实不是。交易所给的Volume是累计值,从开盘到现在的总成交量。要算单笔成交量,你得自己差分。
核心要点:Tick数据本质上是「快照」数据,不是「事件」数据。每一笔Tick代表交易所某一时刻的完整市场状态快照。
2.2 Tick数据获取
获取Tick数据,主要有三条路。我三条都走过,说说各自的优缺点。
2.2.1 直接从交易所获取
国内期货交易所(上期所、大商所、郑商所、中金所)都提供Tick级行情接口。但门槛不低——你得有期货公司席位,还得交行情费。我记得第一次申请上期所Level-2行情,光接口文档就看了两周。
2.2.2 通过数据服务商
这是大多数人的选择。国内常用的有:
- Wind:贵,但数据质量好,适合机构
- 聚宽/米筐:性价比高,适合个人
- 天勤:免费版够用,但历史数据有限
2.2.3 自己搭建采集系统
如果你有技术能力,可以自己写采集程序。我用Python写过一套,核心代码其实不长:
import pymongo
from ctp import MdApi
class TickCollector(MdApi):
def __init__(self, broker_id, user_id, password):
super().__init__()
self.broker_id = broker_id
self.user_id = user_id
self.password = password
self.db = pymongo.MongoClient()['tick_db']
def OnRtnDepthMarketData(self, data):
# 收到Tick数据,直接入库
tick = {
'symbol': data.InstrumentID,
'datetime': data.UpdateTime,
'last_price': data.LastPrice,
'volume': data.Volume,
'bid_price': data.BidPrice1,
'ask_price': data.AskPrice1
}
self.db[data.InstrumentID].insert_one(tick)
def start(self):
self.CreateFtdcMdApi()
self.RegisterFront('tcp://180.168.146.187:10010')
self.Init()
我的建议:刚开始做研究,先用数据服务商提供的现成数据。等策略稳定了,再考虑自建采集系统。别一上来就搞基础设施,容易跑偏。
2.3 Tick数据清洗
拿到原始Tick数据,千万别直接用。我敢说,90%的原始Tick数据都有问题。清洗是必须的。
2.3.1 常见的数据问题
- 时间戳错乱:网络延迟导致时间戳顺序颠倒
- 重复数据:同一笔Tick被推送了两次
- 价格异常:出现明显错误的价格(比如涨停价突然变成0)
- 缺失数据:某些时间段没有Tick数据
2.3.2 清洗流程
我一般按这个顺序处理:
def clean_tick_data(df):
# 1. 去重
df = df.drop_duplicates(subset=['datetime', 'symbol'])
# 2. 排序
df = df.sort_values('datetime')
# 3. 过滤异常价格
df = df[(df['last_price'] > 0) &
(df['last_price'] < df['last_price'].quantile(0.999))]
# 4. 计算单笔成交量
df['tick_volume'] = df['volume'].diff()
df = df[df['tick_volume'] > 0] # 去掉无成交的Tick
# 5. 填充缺失的中间价
df['mid_price'] = (df['bid_price'] + df['ask_price']) / 2
return df.reset_index(drop=True)
曾经踩过的坑:有一次我忘了处理集合竞价阶段的Tick数据。那些数据的时间戳是连续的,但价格是虚拟撮合的结果,不能用于策略计算。后来回测结果一直不对,查了三天才发现这个问题。
2.4 Tick数据存储
Tick数据量很大。一个活跃的期货品种,一天能产生几万到几十万条Tick。存储是个技术活。
2.4.1 存储方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| CSV文件 | 简单、通用 | 查询慢、占用空间大 | 小规模研究 |
| MongoDB | 写入快、灵活 | 查询复杂、内存占用高 | 实时采集 |
| ClickHouse | 查询极快、压缩比高 | 部署复杂 | 大规模分析 |
| Parquet | 列式存储、压缩好 | 不支持实时写入 | 离线分析 |
2.4.2 我的存储方案
我个人习惯用Parquet格式存储历史Tick数据。原因很简单:
- 压缩比高,能省80%的磁盘空间
- 读取速度快,特别是按时间范围查询时
- 支持Python的pandas直接读写
import pandas as pd
# 写入Parquet
def save_tick_to_parquet(df, filepath):
df.to_parquet(
filepath,
compression='snappy',
index=False,
partition_cols=['symbol', 'date']
)
# 读取Parquet
def load_tick_from_parquet(filepath, start_time, end_time):
df = pd.read_parquet(filepath)
return df[(df['datetime'] >= start_time) &
(df['datetime'] <= end_time)]
小技巧:按日期和合约代码做分区存储。这样查询特定日期、特定合约的数据时,不用扫描全量数据。我见过有人把所有Tick数据塞进一个CSV文件,查询一次要等十分钟...那体验太糟糕了。
2.5 知识体系总览
下面这张图,是我对Tick数据处理流程的总结。你可以把它当作一个「路线图」:
嗯,到这里Tick数据的基础知识就讲完了。记住一句话:数据质量决定策略上限。我见过太多人花几个月写策略,却不愿意花几天清洗数据。结果呢?策略在回测里跑得飞起,实盘一塌糊涂。
下一节我们会聊到「订单簿重建与微观结构特征提取」,那是Tick数据真正发挥价值的地方。到时候我会分享一些实战中总结出来的「骚操作」。
公众号:蓝海资料掘金营,微信deep3321