3、数据源与数据清洗:Level2行情数据、Tick数据、数据对齐与去噪
做高频策略,说白了就是跟数据打交道。我见过太多人,策略逻辑写得天花乱坠,结果一跑回测全是噪音。为什么?数据源没选对,清洗没做到位。
今天我们就来聊聊数据这关怎么过。我个人习惯把数据比作「食材」——食材不新鲜,厨艺再好也白搭。
3.1 Level2行情数据:高频策略的「标配」
普通行情数据是3秒快照,Level2数据是逐笔成交+逐笔委托。你想想看,3秒在T+0的高频世界里,黄花菜都凉了。
Level2数据到底包含什么?我列个表给你看:
| 数据项 | 说明 | 更新频率 |
|---|---|---|
| 逐笔成交 | 每一笔真实成交的价格、数量、方向 | 实时 |
| 逐笔委托 | 每一笔挂单的价位、数量、时间 | 实时 |
| 十档行情 | 买卖各10档的挂单深度 | 约500ms |
| 买卖队列 | 前50笔挂单的明细 | 约500ms |
我在项目中遇到过一个问题:某家数据商的Level2数据,逐笔成交的时间戳居然精确到秒级。这在高频回测里根本没法用。所以我的建议是——拿到数据后,先检查时间戳精度。至少要到毫秒级,最好能到微秒级。
3.2 Tick数据:最细粒度的「显微镜」
Tick数据,就是每一笔行情变动的记录。它比Level2更细,细到每一笔成交、每一次报价变动。
为什么需要Tick数据?举个例子:
你写了一个基于「订单簿不平衡」的策略。普通分钟线根本看不到订单簿的变化,只有Tick数据才能捕捉到挂单的瞬间变化。我做过一个统计:同一个策略,用分钟线回测年化收益15%,用Tick数据回测只有3%。为什么?因为分钟线把很多反转信号平滑掉了。
Tick数据的典型结构:
# 一个典型的Tick数据行
{
"symbol": "600519.SH",
"timestamp": 20240105143000123, # 微秒级时间戳
"last_price": 1688.00,
"volume": 100,
"turnover": 168800.0,
"bid_price_1": 1687.99,
"bid_volume_1": 200,
"ask_price_1": 1688.01,
"ask_volume_1": 150,
"trade_flag": "B" # B=主动买, S=主动卖
}
嗯,这里要注意:Tick数据量极大。一只股票一天可能产生几万到几十万条Tick。如果你回测全市场5000只股票,一天的数据量轻松上亿条。存储和读取都是挑战。
3.3 数据对齐:高频回测的「隐形杀手」
数据对齐,听起来简单,做起来全是坑。
什么叫对齐?就是不同数据源、不同频率的数据,在时间轴上要「对上」。比如你的策略同时用到沪深300指数和个股的Tick数据,它们的Tick时间戳可能差了几毫秒。别小看这几毫秒,在高频策略里,这可能导致信号和成交顺序错乱。
我曾经踩过一个坑:用某数据商的期货Tick数据和现货Tick数据做套利回测。结果发现回测收益高得离谱,一查原因——期货数据的时间戳比现货慢了20毫秒。导致回测里「先看到现货涨,再开多期货」,实际交易中根本不可能。
数据对齐的常用方法:
- 时间戳对齐:把所有数据统一到同一个时钟源。比如都用交易所的撮合时间,不要用数据商收到数据的时间。
- 频率对齐:把高频数据降采样到统一频率。比如把Tick数据聚合成100ms的K线。
- 事件对齐:以某个事件为基准,对齐前后数据。比如以「大单成交」为锚点,对齐前后的订单簿变化。
我个人习惯用「最小时间戳对齐法」:先找出所有数据源中最早的时间戳,然后以此为基准,把其他数据源的时间戳向前对齐。这样能最大程度避免「未来信息」泄露。
3.4 数据去噪:把「沙子」筛出去
高频数据里噪声特别多。为什么?因为市场微观结构本身就有很多「假信号」。
常见的噪声类型:
- 报价跳跃:比如某股票突然从10.00跳到10.02,然后又跳回10.00。这可能是大单扫货,也可能是数据错误。
- 自成交:同一个账户自己买自己卖,制造虚假成交量。
- 数据缺失:某几秒的数据丢了,导致价格出现「断崖」。
- 异常值:比如价格突然变成0,或者成交量突然放大100倍。
去噪的方法,我总结了三板斧:
- 价格滤波:用中位数滤波或卡尔曼滤波,剔除价格突变。比如,当前价格偏离前N笔价格中位数超过3个标准差,就标记为异常。
- 成交量校验:单笔成交量不能超过该股票流通盘的某个比例。我一般设0.1%,超过就剔除。
- 时间连续性检查:相邻两笔Tick的时间差不能超过某个阈值。比如超过5秒,就认为中间有数据缺失,需要插值或标记。
# 一个简单的价格去噪函数
def denoise_price(ticks, window=10, std_thresh=3):
"""
用滚动中位数+标准差去噪
"""
ticks['price_median'] = ticks['last_price'].rolling(window, min_periods=1).median()
ticks['price_std'] = ticks['last_price'].rolling(window, min_periods=1).std()
# 标记异常
ticks['is_noise'] = (
(ticks['last_price'] - ticks['price_median']).abs()
> std_thresh * ticks['price_std']
)
# 用中位数替换噪声
ticks.loc[ticks['is_noise'], 'last_price'] = ticks.loc[ticks['is_noise'], 'price_median']
return ticks
3.5 知识体系总览
下面这张图,是我对本章知识结构的总结。你可以把它当作一个「数据清洗检查清单」:
数据清洗这件事,没有银弹。每个市场、每个品种都有自己的「脾气」。我的建议是:先花一周时间把数据摸透,写几个可视化脚本看看数据长什么样。你会发现很多问题,光靠统计指标是看不出来的。
好了,数据源和数据清洗就聊到这里。记住一句话:回测的精度,取决于数据的质量。数据没处理好,后面的一切都是空中楼阁。
公众号:蓝海数据掘金营,微信deep3321