2026年九月3日 · 数据工程

不存在的那些分钟:OHLCV 历史数据中的缺口、停牌与零成交量 K 线

不存在的那些分钟:OHLCV 历史数据中的缺口、停牌与零成交量 K 线

一个日历年的 1-minute K 线应该有 525,600 行。我们拉取的 2025 年 BTCUSDT 永续合约数据有 525,557 行——少了 43 分钟,完整率为 99.992%。同一交易所、同一次拉取、同一段代码路径下,一种中市值山寨币永续合约少了 1,247 行:完整率为 99.76%。同一年的美国股票分钟线则比加密货币数据少了大约 427,000 行,这根本不是数据缺口,只是因为市场会休市。

其中 3 个数字平平无奇。真正值得长篇大论的是 43。

525,600非闰年的 1-minute K 线数量
0.008%2025 年 BTCUSDT 的缺失比例
61%的缺失分钟落在波动率最高的 10% 时段

4 种看起来都像 dataframe 缺了一块的情况

在说那 43 分钟之前,先讲清楚分类,因为大多数缺口处理代码在这一步就已经错了。本地 parquet 里少了一行时,你不知道是以下哪种情况造成的,而每种情况的正确处理方式都不一样。

类型实际发生的情况数据表现正确处理方式
没有成交市场正常开放;那一分钟里没有人主动吃单有些接口会返回零成交量 K 线(OHLC 全相等,trades=0),另一些接口则不返回这一行保留。这是真实信息:没人想交易。
交易所宕机撮合引擎离线,可能是计划内,也可能不是缺少一行,有时会连续缺少多行标记为过期。期间不要交易。
标的停牌触及 LULD 价格区间,等待新闻发布,或收到退市通知缺少一行,随后出现复牌集合竞价成交价标记为过期,并将复牌视为一次不连续变化。
你自己的问题分页请求被限流、重试时漏掉一页,或循环中的时区边界 bug缺少一行,无法与上述情况区分检测并重新拉取。这是唯一能修好的情况。

对于表格第一类情况,各交易所的处理方式不一样,而这正是容易出问题的地方。Coinbase 的 candles 接口会完全跳过空桶,所以流动性差的交易对历史里会满是实实在在的缺口。Binance 的 klines 通常会给你一根合成 K 线,成交量为 0,且 open=high=low=close,都沿用上一笔成交价。同一个事实,两种不同的数据形态;而一个把数据重新索引到完整分钟网格的加载器,会悄悄把其中一种变成另一种。写补缺代码之前,先检查你的交易所会返回什么,别等写完了才检查。

山寨币缺少的 1,247 分钟几乎全属于第一类:UTC 时间周日 04:00 的盘口很薄,没人交易。虽说烦人,但很好理解,而且基本无害,因为策略那时本来也不会交易。也正因如此,我才停下对它的排查,回头去看那 43 分钟。

那 43 分钟并非零散分布

如果缺失是均匀分布的,那么一整年里的 43 分钟会是 43 个孤立点,平均每 8 天半出现一个,每个都微不足道。但实际情况并非如此。它们分成了 6 段:一段连续 19 分钟,一段 11 分钟,两段各 4 分钟,还有两段各 2 分钟。是 6 次事件,不是 43 次偶发缺失。

而且,这些事件会随着你关心的因素而变化。交易所在负载过高时会宕机,而价格波动时负载也会升高。我按实现波动率将全年每个小时分组,再看缺失分钟落在哪里:其中 61% 落在波动率最高的 10% 时段。任意一分钟缺失的无条件概率是 0.008%。而处于波动率最高的 10% 时段时,缺失概率大约为 0.05%——高出 6 倍,而且缺失还会聚集出现。

所以,你的数据质量仪表盘上的完整率指标衡量错了东西。99.992% 听起来像是一个可以不用再操心的数据集。但它实际描述的是:策略无所作为的时段里,数据完整无缺;策略全力出击的时段里,数据却有缺口。一个在波动扩张时触发的动量系统,撞上缺口的概率远高于这个概括性数字所暗示的水平,而且可能是在交易进行中撞上缺口。

前向填充会在 3 行代码后造成什么后果

这就是让我动笔写这篇文章的问题。以那段连续 19 分钟的缺口为例。常见的数据清理步骤是:重新索引到完整的分钟网格,用前一根 K 线的收盘价前向填充 OHLC,将成交量设为 0。现在序列连续了,指标也能正常计算,看不到任何 NaN。

这 19 根 K 线的 high == low == close。每一根的真实波幅都是 0。在交易所宕机前的 ATR(14) 约为 240 USDT;在这个窗口上计算 ATR(14),到交易所恢复时,数值会逐渐降到大约 34——窗口里仅存的 5 根真实 K 线撑起了整个均值。接着把这个数值输入一个常见的波动率缩放仓位计算器,size = risk_budget / ATR 类型。仓位会放大 7 倍。

下一根真实 K 线是复牌成交价,而且它并不平静。我们遇到的情况是,开盘价比停机前最后一笔收盘价偏离了 1.8%。回测欣然以 7 倍仓位买入,承受 1.8% 的跳空;成交价根本不可能成交,当时也没有人报出这个价格。这笔合成交易在权益曲线上贡献的价值,超过了一个月的真实盈亏——而且方向还错了。它完全源自一行用于整理 dataframe 的数据清理代码。

把这些行删掉也不是解决办法,只是换了副面孔的同一个 bug。删掉后,按整数索引计算的回看窗口会说谎:“20-bar EMA”现在跨越了宕机期间的 39 个实际分钟;跨越缺口边界的逐 K 线收益率,会把整段 1.8% 的跳变当作 1 分钟的波动;任何逐 K 线波动率估计都会把它读成 60-sigma 事件。没有任何提示。索引依然单调递增。

重采样会让问题消失在视野里

大多数研究并非基于 1-minute K 线,而是基于聚合后的数据,聚合会把问题洗掉。重采样到 5-minute K 线后,19 分钟的缺口会变成 4 根 K 线,其中首尾两根是不完整的。Pandas 会根据仅存的 2 分钟数据算出看起来完全合理的 OHLC,并给它贴上和 5 分钟完整 K 线相同的标签。输出中没有任何东西能区分两者。

我知道最省事的修复方法:在每次重采样时都保留一个 bars_in_window 列,永远不要丢掉它。每行一个整数,所有下游环节都能据此判断一根 K 线是否可信。我们还会保留 seconds_since_last_real_print,这是同一信息的另一种形式,执行层可以据此采取行动。

我们的处理规则

我们的 agents 现在按以下顺序处理:

  1. 绝不悄悄重新索引。 加载器会生成缺口清单,列出开始时间、结束时间、长度,以及它判断属于上述 4 类中的哪一类。若连续缺失少于 3 根 K 线,且前后 K 线的成交量都很低,就视为无成交分钟。在活跃时段发生的更长缺口,在确认情况之前一律视为宕机。
  2. 先重新拉取,再做判断。我们早期发现的缺口中,有一半是分页 bug。通过不同接口或不同数据供应商再次拉取,就能识别第 4 类问题,在需要任何判断之前先缩小问题范围。
  3. 设置过期门槛,不填补缺口。向策略提供 data_age 输入,并制定硬性规则:最近一笔真实成交超过 N 根 K 线时,不得开新仓;已有仓位只能在复牌时通过市价单平仓,并在定价时明确计入跳空风险折扣。不可实现的成交,比没有发生的交易更糟。
  4. 指标看到的是 NaN,不是编造的数据。前向填充的价格绝不会进入特征层。如果无法计算 ATR,它就是未定义;未定义就意味着空仓。明确报错,胜过悄悄放大 7 倍。
  5. 单独报告缺口相关的表现。我们发布的每份回测报告都会同时展示两种 P&L:一份包含所有交易,另一份剔除宕机前后附近的交易。如果这类交易撑起了结果,那结果就是数据处理造成的假象。

今天就能做个快速审查:把缺失分钟按连续区间分组,再检查有多少比例的回测交易在区间边界前后 30 分钟内开仓或平仓。若低于 1%,缺口大概没有造成什么影响。若达到 5% 或更高,权益曲线讲述的就有一部分是交易所停机的故事。

我逐渐学会信任的判断依据,是缺失数据的形态,而非数量。数千个散落在冷清时段的缺口通常没什么问题。少数几个紧密聚集的缺口则是在告诉你:系统会在负载过高时发生故障,而策略恰恰就在负载过高的地方运行。

OHLCV 缺口数据工程回测重采样加密货币期货
← 全部文章