3、Tick级数据清洗:去重与对齐、异常值处理、时间戳标准化

数据清洗这事儿,说实话,是整套套利模型里最枯燥但最要命的一环。我见过太多人花大把精力搞策略、调参数,结果数据源里藏着几个重复的tick,回测曲线漂亮得不行,一上实盘就崩。嗯,咱们先把地基打牢。

3.1 去重与对齐:别让同一笔交易算两次

Tick数据从交易所出来,经过行情网关、网络传输,再到你的数据库,中间可能被重复推送。我遇到过最夸张的一次,某合约在毫秒级内收到了三笔完全相同的tick——价格、成交量、时间戳一模一样。你想想看,如果不处理,套利信号会被放大三倍,这谁受得了?

去重策略

我个人习惯用「交易对 + 时间戳 + 价格 + 成交量」四元组做唯一键。为什么不用交易对+时间戳?因为同一毫秒内确实可能发生多笔不同价格的交易,直接去重会丢数据。

# 伪代码示例:tick去重逻辑
def dedup_ticks(ticks):
    seen = set()
    clean = []
    for t in ticks:
        key = (t.symbol, t.timestamp, t.price, t.volume)
        if key not in seen:
            seen.add(key)
            clean.append(t)
    return clean
小技巧:如果数据量极大(比如全市场5000+合约),可以用布隆过滤器做近似去重,内存占用能降一个数量级。不过要容忍极低的误判率,我一般设0.1%。

时间对齐:不同合约的tick不在一个频道上

套利模型的核心是「同一时刻」的价差。但现实是,合约A的tick在09:30:00.123到达,合约B的tick在09:30:00.456到达。直接拿这两个价格算价差?那误差大了去了。

我常用的对齐方法有两种:

  • 最近邻对齐:以主合约的tick时间为基准,找副合约最近的一个tick。适合流动性好的品种,误差在几毫秒内。
  • 线性插值对齐:用副合约前后两个tick,按时间权重插出中间价格。适合流动性差、tick稀疏的品种。
注意:插值会引入「未来信息」。回测时一定要用前向插值(只用历史数据),别用中心插值,否则你的策略在回测里会「预知未来」,实盘直接完蛋。我曾经在这上面栽过跟头,回测年化50%,实盘亏了三个月才发现是插值方向搞反了。

3.2 异常值处理:那些离谱的价格从哪来的?

Tick数据里的异常值,说白了就是「一眼假」的价格。比如某合约正常在100块附近波动,突然蹦出一个1000块的tick。这种数据不处理,套利模型会以为出现了巨大的套利机会,直接冲进去接盘。

常见的异常类型

异常类型 典型特征 处理方式
价格跳变 偏离前一个tick超过N个标准差 用前一个tick替换,或标记为缺失
零值/负值 价格为0或负数 直接剔除,这明显是数据错误
成交量异常 成交量突然放大到正常值的100倍 结合盘口数据验证,可能是大单成交
时间戳乱序 后一个tick的时间早于前一个 按时间戳重新排序,丢弃无法修复的

我个人的经验是:宁可漏掉一个真异常,也别误杀一个正常tick。套利模型对价格敏感,误杀会导致价差序列出现空洞,影响后续计算。

异常检测的实操方法

最简单有效的方法——移动窗口标准差法。以当前tick为中心,取前后各50个tick,计算均值和标准差。如果当前价格偏离均值超过3倍标准差,就标记为异常。

def detect_outlier(tick, window=50, threshold=3):
    # 取窗口内的价格序列
    prices = [t.price for t in window_ticks]
    mean = np.mean(prices)
    std = np.std(prices)
    if abs(tick.price - mean) > threshold * std:
        return True
    return False
核心原则:异常值处理不是越严格越好。在套利场景下,真正的价差突破往往也表现为「异常」,如果你把异常值全删了,那套利机会也被删没了。我一般设3.5倍标准差作为阈值,既过滤掉明显的错误数据,又保留真实的极端行情。

3.3 时间戳标准化:让不同数据源说同一种语言

做套利的人,手头的数据源往往不止一个。上交所的tick时间戳是毫秒级,深交所的是微秒级,期货交易所的又是另一种格式。更头疼的是,有些数据源用UTC时间,有些用北京时间。不统一?那价差计算就是鸡同鸭讲。

标准化步骤

  1. 统一时区:全部转为UTC+8(北京时间),或者全部转成UTC。我个人习惯用UTC+8,因为回测时看盘时间更直观。
  2. 统一精度:全部转成毫秒级时间戳。微秒级精度太高,反而容易引入噪声。我一般把微秒部分四舍五入到毫秒。
  3. 处理闰秒:虽然概率低,但交易所确实会在某些时刻插入闰秒。遇到这种情况,直接丢弃闰秒对应的tick,或者用前后tick插值补上。
# 时间戳标准化示例
import pandas as pd

def standardize_timestamp(ts, source_tz='UTC', target_tz='Asia/Shanghai'):
    # 转为datetime对象
    dt = pd.to_datetime(ts, unit='ms')
    # 时区转换
    dt = dt.tz_localize(source_tz).tz_convert(target_tz)
    # 转回毫秒时间戳
    return int(dt.timestamp() * 1000)
避坑指南:我曾经遇到过某个数据商,他们的tick时间戳用的是「交易所本地时间」,但交易所服务器的时间本身就有几毫秒的漂移。后来我加了一步「NTP时间校准」,用交易所的行情快照时间作为基准,把每个tick的时间戳做微调。效果立竿见影,套利信号的延迟从10毫秒降到了2毫秒以内。

3.4 知识体系总览

下面这张图,是我自己梳理的tick级数据清洗流程。说白了,就三步:去重对齐、异常处理、时间标准化。每一步都有坑,但踩过去之后,你的数据质量就能上一个大台阶。

Tick级数据清洗流程 原始Tick数据 去重与对齐 四元组去重 | 最近邻/插值对齐 异常值处理 3σ检测 | 替换/剔除 时间戳标准化 时区统一 | 精度统一 | 闰秒处理 清洗后Tick数据 套利模型输入 数据源:交易所行情、第三方数据商 输出:干净、对齐、标准化的tick序列

数据清洗这件事,说白了就是「脏活累活」。但你想啊,如果连数据都是脏的,那模型再漂亮又有什么用?我做了这么多年量化,最大的感悟就是:花80%的时间在数据上,20%的时间在策略上,这才是正确的比例。别嫌麻烦,这一步省下来的时间,以后都会在实盘里加倍还给你。

最后说一句:清洗后的数据一定要做「回测验证」。拿一小段历史数据跑一遍,看看清洗前后的套利信号数量、胜率、盈亏比有没有明显变化。如果清洗后信号变少了但胜率提高了,说明清洗有效;如果信号和胜率都下降了,那可能是清洗过度了,需要调整参数。