3、数据源与数据清洗:Level2行情数据、Tick数据、数据对齐与去噪

做高频策略,说白了就是跟数据打交道。我见过太多人,策略逻辑写得天花乱坠,结果一跑回测全是噪音。为什么?数据源没选对,清洗没做到位。

今天我们就来聊聊数据这关怎么过。我个人习惯把数据比作「食材」——食材不新鲜,厨艺再好也白搭。

3.1 Level2行情数据:高频策略的「标配」

普通行情数据是3秒快照,Level2数据是逐笔成交+逐笔委托。你想想看,3秒在T+0的高频世界里,黄花菜都凉了。

Level2数据到底包含什么?我列个表给你看:

数据项 说明 更新频率
逐笔成交 每一笔真实成交的价格、数量、方向 实时
逐笔委托 每一笔挂单的价位、数量、时间 实时
十档行情 买卖各10档的挂单深度 约500ms
买卖队列 前50笔挂单的明细 约500ms

我在项目中遇到过一个问题:某家数据商的Level2数据,逐笔成交的时间戳居然精确到秒级。这在高频回测里根本没法用。所以我的建议是——拿到数据后,先检查时间戳精度。至少要到毫秒级,最好能到微秒级。

注意:不同交易所的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只股票,一天的数据量轻松上亿条。存储和读取都是挑战。

我的经验:Tick数据不要存关系型数据库。用Parquet或HDF5格式,按日期分区存储。读取时用多进程并行加载,能快10倍以上。

3.3 数据对齐:高频回测的「隐形杀手」

数据对齐,听起来简单,做起来全是坑。

什么叫对齐?就是不同数据源、不同频率的数据,在时间轴上要「对上」。比如你的策略同时用到沪深300指数和个股的Tick数据,它们的Tick时间戳可能差了几毫秒。别小看这几毫秒,在高频策略里,这可能导致信号和成交顺序错乱。

我曾经踩过一个坑:用某数据商的期货Tick数据和现货Tick数据做套利回测。结果发现回测收益高得离谱,一查原因——期货数据的时间戳比现货慢了20毫秒。导致回测里「先看到现货涨,再开多期货」,实际交易中根本不可能。

数据对齐的常用方法:

  • 时间戳对齐:把所有数据统一到同一个时钟源。比如都用交易所的撮合时间,不要用数据商收到数据的时间。
  • 频率对齐:把高频数据降采样到统一频率。比如把Tick数据聚合成100ms的K线。
  • 事件对齐:以某个事件为基准,对齐前后数据。比如以「大单成交」为锚点,对齐前后的订单簿变化。

我个人习惯用「最小时间戳对齐法」:先找出所有数据源中最早的时间戳,然后以此为基准,把其他数据源的时间戳向前对齐。这样能最大程度避免「未来信息」泄露。

核心原则:回测中,任何一条数据的时间戳,都不能晚于它实际产生的时间。否则就是「未来函数」,回测结果全是假的。

3.4 数据去噪:把「沙子」筛出去

高频数据里噪声特别多。为什么?因为市场微观结构本身就有很多「假信号」。

常见的噪声类型:

  • 报价跳跃:比如某股票突然从10.00跳到10.02,然后又跳回10.00。这可能是大单扫货,也可能是数据错误。
  • 自成交:同一个账户自己买自己卖,制造虚假成交量。
  • 数据缺失:某几秒的数据丢了,导致价格出现「断崖」。
  • 异常值:比如价格突然变成0,或者成交量突然放大100倍。

去噪的方法,我总结了三板斧:

  1. 价格滤波:用中位数滤波或卡尔曼滤波,剔除价格突变。比如,当前价格偏离前N笔价格中位数超过3个标准差,就标记为异常。
  2. 成交量校验:单笔成交量不能超过该股票流通盘的某个比例。我一般设0.1%,超过就剔除。
  3. 时间连续性检查:相邻两笔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 知识体系总览

下面这张图,是我对本章知识结构的总结。你可以把它当作一个「数据清洗检查清单」:

高频策略数据流水线 数据源层 Level2行情数据 Tick数据 其他数据源 数据清洗层 时间戳对齐 统一时钟源 频率对齐 降采样/插值 去噪处理 滤波+校验 存储层 Parquet/HDF5格式 按日期分区 多进程并行读取 高质量回测数据

数据清洗这件事,没有银弹。每个市场、每个品种都有自己的「脾气」。我的建议是:先花一周时间把数据摸透,写几个可视化脚本看看数据长什么样。你会发现很多问题,光靠统计指标是看不出来的。

一个小技巧:把清洗前后的数据画在同一张图上,用不同颜色标注。一眼就能看出清洗效果好不好。我每次做数据清洗,都会先画图,再写代码。

好了,数据源和数据清洗就聊到这里。记住一句话:回测的精度,取决于数据的质量。数据没处理好,后面的一切都是空中楼阁。


公众号:蓝海数据掘金营,微信deep3321