开通会员
首页›量化投研›正文

你的回测一天到底有几根 K 线?QuantConnect 又修了一个容易被忽略的时间陷阱

QuantConnect 又修了一个容易被忽略的时间陷阱

宽
宽宽
9月21日2 阅读
午休如何影响历史日K数量

你的回测一天到底有几根 K 线?QuantConnect 又修了一个容易被忽略的时间陷阱

你向回测程序请求最近 100 根日 K 线。

正常理解应该很简单:

我要 100 根,你就给我 100 根。

但在某些市场里,事情没这么简单。

2026 年 9 月,QuantConnect 的开源量化引擎 LEAN 修复了一个 History 数据问题。

问题标题很直接:

Fix daily history bar counts for markets with a lunch break。

也就是:

修复存在午间休市市场的日线历史数据数量错误。

这次问题最早出现在香港期货市场相关场景中。QuantConnect 的 issue 记录显示,当算法时间刚好处于午间休市阶段时,用 History(barCount) 请求日线数据,返回的 K 线数量可能比要求的少大约 17%。

程序没有崩。

数据也不是假的。

只是你要 100 根,它可能没有真的给你 100 根。

这种问题看起来小,但如果这 100 根数据是拿来初始化均线、波动率或者机器学习特征,后面的结果就可能跟着变。

先说清楚:什么叫 Bar Count?

在 QuantConnect 里,请求历史数据有几种常见方式。

比如你可以说:

history = self.history(symbol, 100, Resolution.DAILY)

意思就是:

给我这个品种最近 100 根日线。

也可以按照时间范围请求,比如最近 30 天,或者指定开始日期和结束日期。

这两种方式看起来差不多,其实不是一回事。

“最近 100 天”是时间概念。

“最近 100 根 K 线”是数据数量。

周末没有交易,节假日没有交易,有些市场还有特殊交易时段。

所以 100 个自然日,并不等于 100 根日 K。

QuantConnect 的文档也明确区分了这两种 History 请求方式:既可以按照一段时间请求,也可以直接指定需要多少根 Bar。

如果我明确说“给我 100 根”,正常情况下就应该尽量返回最近 100 个有效交易 Bar。

这次的问题就出在这里。

午休为什么会影响“日 K 线数量”?

乍一看其实有点反常。

日 K 线不是一天一根吗?

中午休不休市,为什么会影响日线?

原因在于,回测引擎不能只看日历日期。

它还要理解交易所的完整交易时间。

有些市场从开盘一直连续交易到收盘。

但还有不少市场一天被切成几段。

比如:

上午交易
↓
午间休市
↓
下午交易
↓
收盘

问题来了。

假设现在是某个交易日中午 12 点。

上午已经交易过,但下午还没开始。

这一天算不算一个已经完成的交易日?

当然不能算。

因为当天的日 K 线还没有结束。

QuantConnect 的日线定义也是如此:日 Bar 覆盖市场从开盘到收盘的数据,只有交易日结束之后,这根日 Bar 才完整。

如果历史数据系统在推算“为了找到最近 100 根完整日线,我应该往前找多远”时,没有正确处理这段午间休市,就可能少算历史范围。

最终出现一个很奇怪的结果:

你要的是 100 根。

程序往前找的数据却不够远。

最后真正返回的可能只有八十多根。

QuantConnect 收到的 issue 就报告了这种情况:对存在 lunch break 的 HKFE 市场,在午间休市时调用基于 Bar 数量的日线 History,请求结果大约少了 17%。

2026 年 9 月 18 日,这个问题对应的修复 #9806 被合并进 LEAN。

少十几根 K 线,有那么严重吗?

得看你拿它干什么。

如果只是画一张图,可能没什么大不了。

但量化策略里,History 数据经常不是拿来看的,而是拿来初始化模型的。

比如一个很普通的策略:

用过去 100 天价格计算移动平均线。

代码可能会先请求:

100 根日线

然后把这 100 个价格送进指标。

如果实际只回来 83 根,会发生什么?

最直接的问题是:

指标可能还没准备好。

在很多量化框架里,这叫 Warm-up,也就是预热。

假设你使用:

SMA(100)

它需要 100 个历史数据点。

只有 83 个数据点时,这个指标还没有完整形成。

于是程序可能需要继续等待新的市场数据。

策略原本计划今天开始交易,结果又多等了十几个交易日。

这已经不是“少显示几根 K 线”这么简单了。

策略真正开始工作的时间变了。

再复杂一点,问题会更隐蔽

假设你不是算一条均线,而是在做机器学习。

你需要最近 250 个交易日的数据,构建这些特征:

  • 20 日收益率
  • 60 日波动率
  • 120 日动量
  • 200 日均线距离
  • 成交量变化

于是程序启动时:

History(symbol, 250, Daily)

你以为每个标的都拿到了 250 根。

实际上,某些市场只返回了 210 多根。

这时候不同特征可用的数据长度可能开始不一致。

你可能继续 dropna()。

然后数据又少一截。

最终真正进入模型训练的数据,可能和你以为的数量差很多。

程序照样能跑。

模型照样能给预测结果。

回测甚至照样可以赚钱。

但研究过程已经悄悄改变了。

这种 Bug 最麻烦的地方就在这里:

它不一定制造一个明显错误,而是让你的策略和你以为的策略变得不一样。

交易日历比很多人想象中复杂

刚开始做量化时,很容易把市场时间理解成:

周一到周五
9:30 开盘
16:00 收盘

实际市场远比这个复杂。

不同资产可能有:

  • 午间休市
  • 夜盘
  • 跨午夜交易
  • 提前收市
  • 节假日特殊交易时间
  • 夏令时切换
  • 临时休市
  • 不同交易所时区

期货尤其明显。

有些期货合约的一个“交易日”,甚至并不等于自然日的 0 点到 24 点。

QuantConnect 在这次修复前后,还有另一项针对 CME 系列期货缩短交易时段和节假日时间的修改工作,也能说明交易日历维护本身就是量化基础设施里一项长期工作。

所以一个成熟的回测系统不能只知道:

今天是星期几。

它还得知道:

这个品种在哪个交易所交易?
今天是不是交易日?
今天几点开?
中间有没有休市?
几点真正结束?
现在这根 Bar 到底完成没有?

少处理其中一个环节,历史数据边界就可能算错。


量化回测也要考虑午间休市


这和上一篇 Look-ahead Bias 有什么关系?

上一篇我们讲 QuantConnect 修复了一个前视偏差问题。

那个问题是:

回测程序拿到了本来还不应该知道的数据。

这一次恰好相反。

程序应该给你的历史数据,却可能没给够。

一个是:

拿多了。

一个是:

拿少了。

但背后其实是同一个问题。

时间。

量化回测不只是:

价格 + 策略公式

中间还藏着一整套时间系统:

交易所日历
→ 交易时段
→ 数据时间戳
→ Bar 开始时间
→ Bar 结束时间
→ History 查询
→ 指标预热
→ 交易决策

任何一步错一点,最后的回测结果都有可能变化。


History 数据还有一个容易误解的地方

K 线通常不是以“开始时间”来判断你什么时候知道它。

更重要的是它什么时候结束。

QuantConnect 的 History 数据会按照 Bar 的 EndTime 来索引,官方文档也专门提醒过这一点。

比如一根日 K 线包含了:

Open
High
Low
Close
Volume

在今天收盘之前,你不可能知道今天最终的:

High
Low
Close
Volume

所以一根完整的日线,只有交易结束之后才真正存在。

这也是为什么午间休市这个看起来很普通的时间段,会让日线历史查询变得复杂。

中午的时候:

今天已经交易了一部分。

但今天又还没有结束。

程序必须知道:

“今天”现在到底算不算一个完整 Bar。


自己做回测时,可以检查什么?

以后在策略里大量使用 History 时,我建议先做一个很简单的检查。

比如:

history = self.history(symbol, 100, Resolution.DAILY)

self.debug(len(history))

不要默认:

我请求 100 根
=
我肯定拿到 100 根

先看看实际返回多少。

然后再检查:

  • 第一根数据是什么日期?
  • 最后一根是什么日期?
  • 有没有缺失交易日?
  • 当前交易日有没有被错误算进去?
  • 指标是不是已经 Ready?
  • 不同市场返回的数据数量是否一致?
  • 回测和实盘请求历史数据时结果是否一致?

尤其当策略涉及:

海外市场、期货、不同交易所、多资产组合

这些检查很有必要。

因为不同市场根本不是同一套交易时间。


回测里,时间经常比公式更危险

很多人研究量化策略,会把大部分精力花在这些事情上:

用 SMA 还是 EMA?

参数用 20 还是 30?

Sharpe 能不能再提高 0.2?

但真正能让整个结果失真的,有时候不是公式。

可能只是:

少了十几根 K 线。

上一篇 QuantConnect 修复的是“未来的数据提前进来了”。

这一次修复的是“历史数据没有拿够”。

它们都说明了一件事:

一条回测曲线的背后,不只是策略逻辑。

还藏着数据、时间戳、交易日历、交易时段、历史数据查询和指标初始化。

所以下次写:

history(symbol, 100)

别只把它理解成一句取数据的代码。

最好再确认一下:

你要的 100 根,程序真的给了你 100 根吗?

//参与评论
?
还没有评论,来说第一句