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 行情时,记得同时订阅 mduserapitraderapi。前者拿行情,后者拿账户信息。别问我为什么,我曾经因为只订阅了行情,结果策略跑起来才发现下单接口没连上,白白错过一波行情。

3.2 数据对齐:高频策略的生死线

跨期套利,核心是看两个合约之间的价差。如果两个合约的数据时间戳没对齐,你算出来的价差就是错的。错一个 tick,可能就导致一次错误的开仓。

为什么会这样?因为 CTP 的行情推送不是同步的。主力合约的 tick 先到,次主力合约的 tick 可能晚 1-2 毫秒。如果你直接用最新收到的两个 tick 去算价差,那结果就是“伪价差”。

我的对齐方法:

  1. 时间戳对齐: 以交易所时间为准。CTP 的 UpdateTimeUpdateMillisec 要合并使用。注意,有些行情源给的毫秒数可能不准,我遇到过某家数据商把毫秒位全填成 0 的情况,坑死。
  2. 事件驱动对齐: 维护两个队列,分别存主力合约和次主力合约的 tick。每次新 tick 进来,检查两个队列头部的时间差。如果时间差小于 1ms,就认为这对 tick 是对齐的,可以计算价差。
  3. 插值法: 如果实在对不齐,比如一个合约 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. 实时监控: 如果超过 1 秒没有收到某个合约的 tick,立刻报警。同时暂停该合约的交易。
  2. 回测中的缺失值: 回测时,如果遇到缺失值,我一般直接跳过那段时间。不要用插值去“脑补”行情,因为高频数据里,缺失的几秒可能发生了重大事件。
  3. 重连机制: 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 知识体系总览

下面这张图,是我自己总结的高频数据获取与清洗的完整流程。从数据源到最终可用的价差序列,每一步都不能马虎。

高频数据获取与清洗流程 数据源 CTP / Level-2 数据对齐 时间戳对齐 / 事件驱动 去噪处理 价格过滤 / 中值滤波 缺失值处理 监控 / 跳过 / 重连 价差计算 主力 - 次主力 策略决策 开仓 / 平仓 / 等待 关键要点 • 时间戳精度 < 1ms • 去噪阈值动态调整 • 缺失值期间暂停交易 • 回测与实盘逻辑一致

你看,从数据源到策略决策,中间要经过对齐、去噪、缺失值处理这三道关卡。每一关都可能让你的策略从“赚钱”变成“亏钱”。

我个人习惯,在实盘之前,先用历史数据跑一遍完整的清洗流程,看看清洗前后的价差曲线有什么变化。如果清洗后价差曲线变得平滑了,说明去噪有效;如果曲线变得“断断续续”,那可能是缺失值处理得太粗暴了。

好了,数据获取与清洗就聊到这儿。记住一句话:数据干净了,策略才能赚钱

专注资料整理