缺失值处理:时间戳缺失检测、快照缺失时的重同步策略、价格档位空缺填充

做订单簿数据清洗,说白了就是跟「不完美」的数据打交道。我刚开始做高频回测那会儿,天真地以为交易所给的数据都是规规矩矩的。结果呢?第一次跑回测就发现收益率曲线像心电图一样乱跳——后来一查,原来是某段时间的快照丢了,策略在「真空」里瞎交易。

嗯,今天咱们就来聊聊缺失值处理。这活儿看着不起眼,但做不好,后面的特征工程全是垃圾。

时间戳缺失检测

订单簿数据是按时间戳排列的。正常情况下,快照应该以固定间隔到达。比如交易所每100ms推送一次。但现实是——网络抖动、系统过载、甚至交易所自己的bug,都会导致某些时间戳凭空消失。

怎么检测?我个人的习惯是两步走:

  1. 计算时间差序列:把相邻时间戳相减,得到一个间隔序列
  2. 标记异常间隔:如果某个间隔远大于正常值(比如超过1.5倍标准间隔),就认为中间有缺失

核心思路:不是看「有没有缺失」,而是看「间隔是否合理」。

举个例子,假设正常间隔是100ms,你发现某两个时间戳差了500ms。那中间大概率丢了4个快照。这时候不能简单跳过,得标记出来。

# 伪代码:时间戳缺失检测
def detect_missing_timestamps(timestamps, expected_interval_ms=100, tolerance=0.5):
    gaps = []
    for i in range(1, len(timestamps)):
        diff = (timestamps[i] - timestamps[i-1]) / 1e6  # 转成毫秒
        if diff > expected_interval_ms * (1 + tolerance):
            missing_count = round(diff / expected_interval_ms) - 1
            gaps.append({
                'start': timestamps[i-1],
                'end': timestamps[i],
                'missing_count': missing_count
            })
    return gaps

这里有个坑——精度问题。时间戳单位是纳秒,但很多交易所实际精度只有微秒甚至毫秒。我遇到过一种情况:两个时间戳看起来只差了99ms,但实际是因为精度截断导致的「假缺失」。所以检测时最好留个缓冲区间。

快照缺失时的重同步策略

检测到缺失之后怎么办?直接扔掉?那回测的连续性就断了。强行补一个?补错了更麻烦。

我一般用三种策略,看场景选:

策略 适用场景 风险
丢弃+重对齐 缺失比例很小(<1%) 可能丢失关键行情
前向填充 市场波动不大 延迟反应价格变化
重同步 缺失时间较长 需要额外数据源

重同步是我最推荐的做法。说白了就是:用下一个有效快照来「校准」当前状态。但要注意——不能简单地把下一个快照复制过来,因为中间可能发生了成交。

我的经验:重同步时,最好同时检查成交记录。如果缺失期间有成交,那快照里的累计成交量、最后一笔价格都得跟着调整。否则回测时你会发现「咦,怎么价格没动但成交了?」

具体做法是这样的:

  1. 找到缺失区间后的第一个有效快照
  2. 从成交记录中提取缺失期间的所有成交
  3. 用成交数据「回退」快照状态,得到缺失开始时的合理状态
  4. 用线性插值填充中间的快照

听起来复杂?其实核心就一句话:别凭空造数据,用成交记录来辅助重建

价格档位空缺填充

这个更常见。订单簿的买一到买十,卖一到卖十,经常有空位。比如买三的价格是10.00,买五直接跳到10.02,中间10.01没人挂单。

这种空缺要不要填?看你的策略需求。

  • 做市策略:必须填。因为你要知道每个价位的深度
  • 趋势策略:可以不填。空缺本身也是一种信息(流动性不足)

如果决定要填,我常用两种方法:

前向填充(Forward Fill)

最简单。空缺的价格用上一个有效价格代替,数量填0。比如:

原始:买一 10.00(100), 买二 10.01(0), 买三 10.02(200)
前向填充后:买一 10.00(100), 买二 10.01(0), 买三 10.02(200)

注意,数量填0而不是填上一个数量。因为空缺意味着「没人挂单」,不是「有人挂了但没记录」。

线性插值(Linear Interpolation)

更平滑,但风险也大。比如买一和买三之间空缺了买二,那就取价格的平均值:

买二价格 = (10.00 + 10.02) / 2 = 10.01
买二数量 = (100 + 200) / 2 = 150

注意:线性插值会「创造」出原本不存在的挂单。如果市场本身流动性很差,插值出来的数据会误导策略。我曾经因为这个在回测里赚得盆满钵满,实盘一跑就亏——因为插值出来的「假深度」根本没法成交。

异常空值识别

最后聊一个容易被忽略的点:空值不等于缺失

有些交易所会在某些档位上显式返回空值(null),表示「这个价位没有挂单」。这和「缺失」是两码事。缺失是数据没到,空值是数据到了但内容是空的。

怎么区分?我总结了一个小规则:

  • 时间戳连续,但某个档位是null → 正常空值,保留
  • 时间戳不连续,且档位全是null → 可能是快照缺失,需要重同步
  • 时间戳连续,但所有档位都是null → 异常!可能是交易所系统故障

最后一种情况我遇到过两次。一次是交易所的推送服务挂了,返回的全是空快照。另一次是网络中间层把数据包截断了,只传了时间戳没传内容。这两种情况都得直接丢弃,不能填充。

避坑指南:我建议在清洗流程里加一个「全空快照检测」。如果连续3个以上的快照全是空值,直接标记为异常区间,后续特征计算时跳过。别想着用插值去补——你补出来的是一段「假行情」,策略会死得很惨。

知识体系总览

下面这张图把整个缺失值处理的逻辑串起来了。你可以把它当作清洗流程的「检查清单」:

订单簿缺失值处理流程 ① 时间戳缺失检测 ② 快照重同步 ③ 档位空缺填充 间隔 > 阈值? 有成交记录? 档位为null? 标记缺失区间 用成交回退重建 前向填充/插值 清洗后的连续订单簿快照 ⚠ 异常空值(全空快照)→ 直接丢弃,不填充

你看,整个流程其实是个漏斗——先检测时间戳,再处理快照,最后填充档位。每一步都有对应的判断条件。别想着一步到位,数据清洗这事儿,急不得。

好了,关于缺失值处理就聊到这儿。记住一句话:宁可少一个快照,也别多一个假数据。回测里多赚的那几个点,实盘里都会还回去的。

专注资料整理