4、数据清洗:异常值过滤、时间戳对齐、乱序数据重排、去重策略

行情数据进了内存,你以为就能直接用了吗?

太天真了。我见过太多团队,数据还没跑完就崩了。原因很简单——原始行情数据,脏得离谱。

这一章,我们就来聊聊数据清洗的四个核心动作:异常值过滤、时间戳对齐、乱序数据重排、去重策略。说白了,就是让数据变得「干净、有序、可用」。

4.1 异常值过滤:别让一颗老鼠屎坏了一锅粥

什么是异常值?举个例子:某只股票突然出现一笔成交价是前一笔的100倍。这明显是交易所的撮合异常,或者是数据源传输错误。如果你不处理,策略会直接爆仓。

我个人习惯用三种方法过滤异常值:

  • 阈值过滤:设定价格上下限,比如涨跌幅超过10%的直接丢弃。
  • 统计过滤:用Z-score或IQR(四分位距)识别离群点。
  • 业务规则过滤:比如买卖价差为负、成交量小于等于0,这些明显不合理。

核心原则:宁可漏掉一笔正常数据,也不要放进来一笔异常数据。高频交易中,一次错误信号可能让你亏掉一周的利润。

我在项目中遇到过一件事:某次回测结果异常漂亮,年化收益超过200%。我一开始还以为是策略牛逼,后来一查,原来是数据里混入了大量「虚拟成交」——交易所测试环境的数据没被过滤掉。嗯,从那以后,我每次清洗都会加一道「数据源校验」。

def filter_outliers(df, price_col='price', volume_col='volume', z_thresh=3):
    """
    基于Z-score过滤异常价格和成交量
    """
    from scipy import stats
    
    # 价格异常过滤
    price_z = np.abs(stats.zscore(df[price_col]))
    df = df[price_z < z_thresh]
    
    # 成交量异常过滤(成交量不能为负或零)
    df = df[df[volume_col] > 0]
    
    # 业务规则:买卖价差必须为正
    if 'ask_price' in df.columns and 'bid_price' in df.columns:
        df = df[df['ask_price'] > df['bid_price']]
    
    return df

避坑指南:我曾经用全局Z-score过滤,结果发现早盘和尾盘的波动率天然不同,导致大量正常数据被误杀。后来改成「滚动窗口Z-score」,效果好了很多。

4.2 时间戳对齐:让不同数据源「说同一种语言」

行情数据来自多个交易所、多个数据商。每个数据源的时间戳格式可能都不一样:有的用Unix毫秒,有的用字符串,有的甚至带时区偏移。

你想想看,如果A交易所的tick是14:30:00.123,B交易所的tick是14:30:00.456,你拿它们做套利策略,差这333毫秒,可能就错过了最佳开仓点。

时间戳对齐,我一般分三步走:

  1. 统一格式:全部转为Unix纳秒或毫秒整数,避免浮点数精度问题。
  2. 时区归一:统一转为UTC时间,或者统一转为交易所本地时间。
  3. 对齐粒度:根据策略需求,对齐到毫秒、微秒或纳秒级别。
数据源 原始格式 对齐后格式
交易所A 2024-01-15 14:30:00.123 1705314600123000000 (纳秒)
交易所B 1705314600456 (毫秒) 1705314600456000000 (纳秒)
数据商C 2024-01-15T06:30:00.456Z 1705314600456000000 (纳秒)
def align_timestamps(df, time_col='timestamp', target_unit='ns'):
    """
    将时间戳统一对齐到目标精度
    """
    # 如果是字符串格式,先解析
    if df[time_col].dtype == 'object':
        df[time_col] = pd.to_datetime(df[time_col])
    
    # 转为纳秒整数
    if target_unit == 'ns':
        df[time_col] = df[time_col].astype(np.int64)
    
    # 排序(为后续乱序重排做准备)
    df = df.sort_values(time_col).reset_index(drop=True)
    
    return df

注意:不同数据源的时钟可能存在偏差。我见过某数据商的时间戳比交易所慢了整整2秒。建议定期用交易所的行情快照做时钟校准。

4.3 乱序数据重排:把「插队」的数据放回原位

网络延迟、数据包重传、多路并发写入……这些都会导致数据乱序。简单说,就是后发生的事件先到了,先发生的事件反而后到。

乱序数据对策略的影响有多大?举个例子:你收到一笔14:30:00.500的成交,然后收到一笔14:30:00.300的成交。如果你不重排,策略会以为价格先涨后跌,但实际上价格是先跌后涨。方向完全反了。

我建议的做法是:

  • 滑动窗口排序:维护一个固定大小的窗口(比如100ms),窗口内的数据按时间戳排序后再输出。
  • 延迟输出:故意延迟50-100ms再输出数据,给乱序数据一个「归队」的时间。
  • 心跳检测:如果超过一定时间没收到新数据,强制输出当前窗口内的所有数据。
class ReorderBuffer:
    """
    乱序数据重排缓冲区
    """
    def __init__(self, window_ms=100, max_latency_ms=200):
        self.buffer = []
        self.window_ms = window_ms
        self.max_latency_ms = max_latency_ms
        self.last_output_time = 0
    
    def add(self, tick):
        self.buffer.append(tick)
        self.buffer.sort(key=lambda x: x['timestamp'])
        
        # 检查是否可以输出
        now = tick['timestamp']
        while self.buffer:
            if now - self.buffer[0]['timestamp'] > self.window_ms * 1e6:
                yield self.buffer.pop(0)
            else:
                break
    
    def flush(self):
        """强制输出所有剩余数据"""
        for tick in self.buffer:
            yield tick
        self.buffer = []

避坑指南:我曾经把窗口设得太小(10ms),结果乱序数据根本来不及归队,策略信号频繁闪烁。后来改成50ms窗口+100ms延迟,效果稳定多了。具体数值要根据你的网络延迟和交易所数据频率来调。

4.4 去重策略:别让同一笔数据算两次

数据重复的原因很多:网络重传、数据商重复推送、多路订阅导致同一笔数据被接收多次。

重复数据会导致什么后果?成交量翻倍、价格被重复计算、策略信号失真。最要命的是,如果你用重复数据做回测,结果会严重偏离真实情况。

我常用的去重策略有三种:

  • 基于唯一ID去重:交易所通常会给每笔成交分配一个唯一的TradeID或SequenceNumber。这是最可靠的方法。
  • 基于时间戳+价格+成交量去重:如果三个字段完全一致,大概率是重复数据。
  • 基于滑动窗口去重:维护一个最近N条数据的哈希集合,新数据进来先查重。
def deduplicate(df, id_col='trade_id', time_col='timestamp'):
    """
    基于唯一ID去重
    """
    before = len(df)
    df = df.drop_duplicates(subset=[id_col])
    after = len(df)
    
    print(f"去重前: {before} 条, 去重后: {after} 条, 移除: {before - after} 条重复数据")
    return df

def deduplicate_by_fields(df, fields=['timestamp', 'price', 'volume']):
    """
    基于多字段组合去重(适用于没有唯一ID的场景)
    """
    return df.drop_duplicates(subset=fields)

核心原则:去重一定要在时间戳对齐和乱序重排之后做。否则,同一笔数据可能因为时间戳格式不同而被误判为不同数据。

知识体系总览

下面这张图,把数据清洗的四个步骤串起来了。你可以把它当作一个「清洗流水线」:

数据清洗流水线 原始行情数据 步骤1 异常值过滤 步骤2 时间戳对齐 步骤3 乱序重排 步骤4 去重策略 输出 干净有序数据 数据完整性校验(可选环节) 注:步骤顺序不可颠倒,去重必须在时间戳对齐和乱序重排之后进行

这张图展示了数据清洗的完整流程。你可能会问:为什么去重要放在最后?

原因很简单:如果先做去重,再做时间戳对齐和乱序重排,同一笔数据可能因为时间戳格式不同而被误判为不同数据,导致去重失败。顺序很重要,别搞反了。

个人经验:我建议你在清洗流水线的每个步骤都加一个「计数器」,记录过滤/对齐/重排/去重的数据量。这样一旦策略表现异常,你可以快速定位是哪个环节出了问题。


公众号:蓝海资料掘金营,微信deep3321