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数据处理流程 数据获取 交易所直连 | 数据服务商 | 自建采集系统 数据结构解析 时间戳 | 价格 | 成交量 | 买卖盘口 | 累计值 vs 单笔值 数据清洗 去重 | 排序 | 异常过滤 | 成交量差分 | 集合竞价处理 数据存储 CSV | MongoDB | ClickHouse | Parquet(推荐)

嗯,到这里Tick数据的基础知识就讲完了。记住一句话:数据质量决定策略上限。我见过太多人花几个月写策略,却不愿意花几天清洗数据。结果呢?策略在回测里跑得飞起,实盘一塌糊涂。

下一节我们会聊到「订单簿重建与微观结构特征提取」,那是Tick数据真正发挥价值的地方。到时候我会分享一些实战中总结出来的「骚操作」。


公众号:蓝海资料掘金营,微信deep3321