3. 数据获取与清洗:高频数据源(CTP、Level-2)、数据对齐、去噪与缺失值处理
做跨期套利高频策略,说白了就是跟数据打交道。你策略再牛,模型再花哨,数据源一塌糊涂,那全是白搭。我见过太多人把精力花在优化参数上,结果连 tick 数据的时间戳都没对齐,这能赚钱才怪。
这一章,咱们就聊聊高频数据的那些坑。从怎么拿到数据,到怎么把它洗干净,我会把我在实战中踩过的雷、总结的经验,一股脑倒出来。
3.1 高频数据源:CTP 与 Level-2
国内期货市场,CTP(综合交易平台)是绝对的主流。它提供的是 tick 级数据,也就是每一笔成交的细节。你想想看,一秒钟可能成交几十笔,每一笔都有价格、成交量、买卖方向,这就是高频策略的“原油”。
对于股指期货(IF、IC、IH)或者商品期货,CTP 的数据基本够用。但如果你要做更精细的套利,比如 ETF 与期货之间的套利,那就得考虑 Level-2 行情了。
核心区别:
- CTP(期货): 逐笔成交 + 快照行情(5档买卖盘口)。频率约 500ms 一次快照。
- Level-2(股票/ETF): 逐笔成交 + 逐笔委托 + 10档盘口。频率更高,信息更全。
我个人习惯,做期货跨期套利时,主力合约和次主力合约都用 CTP 数据。但如果你发现两个合约的流动性差异很大,比如一个合约一秒成交 100 笔,另一个一秒才 5 笔,那就要小心了。数据对齐会非常痛苦。
一个小技巧: 订阅 CTP 行情时,记得同时订阅 mduserapi 和 traderapi。前者拿行情,后者拿账户信息。别问我为什么,我曾经因为只订阅了行情,结果策略跑起来才发现下单接口没连上,白白错过一波行情。
3.2 数据对齐:高频策略的生死线
跨期套利,核心是看两个合约之间的价差。如果两个合约的数据时间戳没对齐,你算出来的价差就是错的。错一个 tick,可能就导致一次错误的开仓。
为什么会这样?因为 CTP 的行情推送不是同步的。主力合约的 tick 先到,次主力合约的 tick 可能晚 1-2 毫秒。如果你直接用最新收到的两个 tick 去算价差,那结果就是“伪价差”。
我的对齐方法:
- 时间戳对齐: 以交易所时间为准。CTP 的
UpdateTime和UpdateMillisec要合并使用。注意,有些行情源给的毫秒数可能不准,我遇到过某家数据商把毫秒位全填成 0 的情况,坑死。 - 事件驱动对齐: 维护两个队列,分别存主力合约和次主力合约的 tick。每次新 tick 进来,检查两个队列头部的时间差。如果时间差小于 1ms,就认为这对 tick 是对齐的,可以计算价差。
- 插值法: 如果实在对不齐,比如一个合约 10ms 没来新 tick,那就用线性插值估算一下当前价格。但这个方法我一般不用,因为高频套利对精度要求极高,插值会引入噪声。
# 伪代码:事件驱动对齐
class TickAligner:
def __init__(self):
self.queue_a = deque()
self.queue_b = deque()
def on_tick(self, tick, contract):
if contract == '主力':
self.queue_a.append(tick)
else:
self.queue_b.append(tick)
# 检查对齐
while self.queue_a and self.queue_b:
diff = abs(self.queue_a[0].time - self.queue_b[0].time)
if diff < 0.001: # 1ms 以内
yield (self.queue_a.popleft(), self.queue_b.popleft())
elif self.queue_a[0].time < self.queue_b[0].time:
self.queue_a.popleft() # 丢弃旧的
else:
self.queue_b.popleft()
注意: 对齐时不要用“最新价”直接相减。一定要用同一时刻的 snapshot。我见过有人用主力合约的最新成交价减去次主力合约的卖一价,这完全是在胡闹。
3.3 去噪:别让毛刺毁了你的策略
高频数据里,噪声是家常便饭。比如一笔 1 手的成交把价格打到了涨停价,然后下一秒又回来了。这种“毛刺”如果不处理,你的策略可能会误以为价差出现了极端值,从而错误开仓。
常见的去噪方法:
- 价格过滤: 如果当前 tick 价格与上一笔 tick 价格之差超过 N 个最小变动价位(比如 5 个 tick),就认为是异常,直接丢弃。我一般设 3-5 个 tick,具体看品种波动性。
- 成交量过滤: 如果一笔成交的成交量特别小(比如 1 手),而且价格偏离较大,也建议过滤掉。这种往往是散户的“试单”。
- 中值滤波: 取最近 3-5 笔 tick 价格的中位数,作为当前有效价格。这个方法对去除孤立噪声点很有效。
嗯,这里要注意。去噪不能太狠。如果你把正常的波动也滤掉了,那策略就变成了“死策略”。我刚开始做的时候,把过滤阈值设得太小,结果策略一直不开仓,因为所有信号都被当成噪声了。
避坑指南: 我曾经在回测时发现策略收益很高,但实盘一跑就亏。后来查了半天,发现是回测时用了“未来数据”——去噪时用了未来 tick 的信息。记住,去噪只能用历史数据,不能看未来的。
3.4 缺失值处理:行情断流怎么办?
做高频的,最怕行情断流。网络波动、交易所切换、数据源故障,都可能导致几秒甚至几分钟没有数据。对于跨期套利来说,缺失值意味着价差无法计算,策略必须暂停。
我的处理策略:
- 实时监控: 如果超过 1 秒没有收到某个合约的 tick,立刻报警。同时暂停该合约的交易。
- 回测中的缺失值: 回测时,如果遇到缺失值,我一般直接跳过那段时间。不要用插值去“脑补”行情,因为高频数据里,缺失的几秒可能发生了重大事件。
- 重连机制: CTP 行情接口有断线重连功能。我建议自己写一个心跳检测,每 3 秒检查一次连接状态。如果断线,自动重连,并清空之前的队列。
# 心跳检测示例
import time
def heartbeat_check(last_tick_time, timeout=3):
now = time.time()
if now - last_tick_time > timeout:
print("行情断流!触发重连")
# 重连逻辑
reconnect_ctp()
return False
return True
警告: 千万不要在缺失值期间继续运行策略。我见过有人用“前值填充”来处理缺失的 tick,结果行情恢复后,策略发现价差“突然”变大,直接开仓,然后被反向行情打爆。缺失值期间,市场可能已经发生了翻天覆地的变化。
3.5 知识体系总览
下面这张图,是我自己总结的高频数据获取与清洗的完整流程。从数据源到最终可用的价差序列,每一步都不能马虎。
你看,从数据源到策略决策,中间要经过对齐、去噪、缺失值处理这三道关卡。每一关都可能让你的策略从“赚钱”变成“亏钱”。
我个人习惯,在实盘之前,先用历史数据跑一遍完整的清洗流程,看看清洗前后的价差曲线有什么变化。如果清洗后价差曲线变得平滑了,说明去噪有效;如果曲线变得“断断续续”,那可能是缺失值处理得太粗暴了。
好了,数据获取与清洗就聊到这儿。记住一句话:数据干净了,策略才能赚钱。