LumiBot / 未来函数与时间安全

LumiBot 的未来函数修复:一个 offset=-1 的 SMA,为什么能从 25 变成 1510

回测里最容易骗过自己的不是策略,而是指标。LumiBot 官方在自己的工程笔记里公开了一个真实的回归案例:旧实现把指标放在整个回测区间上算一次,再按策略时间取索引;结果是——当「次日收盘价」从 30 变成 3000 时,一个 offset=-1 的 SMA 读数从 25 跳成 1510。也就是说,第二天还没发生的数据,改变了今天的信号

这一页把官方的修复逻辑逐条摊开:现实现如何先按策略时间截取前缀再计算、独立时间窗的三条硬规则(闭区间、必须带显式时区偏移、拒绝越过策略时间的 end)、以及官方自己列出的仍未证明清单。最后给一份可操作的自查表。

需要说清边界:以下全部依据官方仓库的英文工程笔记与 llms.txt 的代码生成硬规则,本站没有在本机复现这些测试;LumiBot 官方文档本身也明确写了「不要从前缀回归外推其它保证」。请把这一页当成提问清单,而不是「这个框架没有未来函数」的保证书。

官方公开回归案例SMA offset=-1:25 → 1510来源:仓库 docs/ 工程笔记采集日期 2026-09-22

修复前后对照

计算范围与缓存键的变化

修复前整个回测区间一次算完,策略时间只取索引
修复后按策略时间截取前缀再计算,按观测值 digest 缓存
回归数字offset=-1 的 SMA:25 → 1510
指标时间安全修复前后对照示意(依据官方 docs/indicator-temporal-safety.md,2026-09-22 采集)。示意图只重述该文档陈述的事实;该文档同时列出仍未证明的部分,示意图不替官方扩大结论。
未来函数

未来数据是怎么溜进回测的:七种常见入口

「未来函数」听起来抽象,落地就是几件很具体的事:计算范围没截断、缓存键不对、时间来源用错、数据口径用了修订后的快照。下面这张表把入口、机制和典型症状对齐起来。

泄漏入口具体机制典型症状注意点
整段区间的指标缓存指标在整个回测数据上算一次,再按策略时间取索引。官方的例子就是一个 offset=-1 的 SMA,在「次日收盘从 30 变 3000」时从 25 变成 1510回测曲线平滑得反常;越往后越好;对参数极不敏感这是官方公开的回归缺陷本身。现实现已改为先按策略时间截取前缀再计算
自定义函数自己聚合未来行自定义指标函数若自己去取数,可以跨行聚合、把后续 bar 也算进去只有自定义指标的那一段异常好LumiBot 官方明确:自定义函数的时间安全由使用者自负
改动共享 provider 帧自定义函数就地修改了共享的数据帧,污染同一策略里其它指标的输入自己没有泄漏,但同策略其它指标开始漂移官方把「自定义输入隔离」单列进了测试覆盖项
策略里直接取当前时间datetime.now() / datetime.today() 取「今天」,而不是回测推进到的策略时间同样的代码在不同日期跑出不同结果;跨月边界跳变官方 llms.txt 的代码生成硬规则:必须用 self.get_datetime()
指标窗口 end 越过策略时间请求 start/end 窗口时把 end 写到策略时间之后,等于把未来 bar 要了回来临近结束的几个 bar 收益率异常突出现实现会拒绝任何越过策略时间的 end;每个请求在取数前先校验窗口
借用其他月/年的 warmup bar为了让指标「热起来」,从别的月份或年份借前置 bar 填进当前窗口月初/年初的信号特别准官方写明:计算只用该窗口,不跨月跨年借 warmup bar
用修订后的快照当历史宏观数据用今天的公开 CSV 快照跑历史模拟;这些端点里的值是被修订过宏观择时的回测好得不真实官方内置 FRED 工具因此不使用公开 CSV 回退,改走 ALFRED 的时点版本,详见第 5 个板块
为什么第一张表就把「症状」列出来:未来函数不会报错,它只会让结果变好看。所以实际排查时通常从症状出发——如果你的回测出现了「越往后越顺」「只有某一段特别好」「换台机器结果就变了」这三类现象,先怀疑输入口径,再怀疑策略。本站不对任何具体回测做「有没有未来函数」的判定,那是需要拿到代码和数据才能回答的问题。
修复逻辑

修复后变成了什么:按策略时间截前缀,再按观测值缓存

官方给出的关键改动集中在 Indicators._dispatch 一处:从「算完再查表」改成「先截断再算」。这个改动会牺牲一次算完的性能优化,官方是知情的、也是刻意这么做的。

环节修复前修复后注意点
计算范围整个回测 store按策略时间截取前缀后计算这是修复的核心;范围一变,「未来改变现在」在结构上就不可能发生
参数校验(官方未在笔记中描述校验)_dispatch 会先校验因果参数非因果的参数组合在服务端就被拒,而不是悄悄算出一个好看的数
缓存键按整段序列算一次观测值 digest 缓存键跟着实际观测到的值走,修正过的历史值不会命中旧缓存
性能取舍全未来序列只算一次(快,但不安全)该优化被刻意移除官方说明:要换成增量实现,必须带等效的时间与修正测试才允许替换
自定义函数与内置指标同等对待自己取数的函数自负时间安全自定义路径不会自动获得前缀截断的保护
批量请求列表式接口接受 requests_json(唯一 id + 每请求参数与时框);旧列表接口保留非法信封在计算前就失败;id、指标名、时框都是有界字符串,数组/对象/超长值在取数前被拒,而不是被字符串化
时框元数据(官方未在笔记中描述)存储的时框必须与请求一致官方原文:缺失的分钟序列不能拿日线缓存顶替并标成分钟输出;该请求要么走数据源取到对应时框,要么如实不可用
这一节最值得记住的一句:官方用「deliberately removes the unsafe full-future-series compute-once optimization」来描述这次改动——为了正确性放弃了一个性能优化。反过来说,如果你在别处看到「一次性把指标全算完再查表」的实现,那正是这类缺陷的温床。与本项目的回测数据源选择结合看,见数据源与路由
时间窗

独立时间窗的三条硬规则,与三条容易被忽略的补充

如果不用框架内置的指标接口,而是自己按窗口取一段历史来算,那么窗口本身的定义就成了安全边界。官方为「独立 start/end 窗口」定了六条可核对的约束。

规则具体约束为什么要这样注意点
闭区间startend 都是闭区间(含端点)避免「最后一天算不算」在不同实现间产生一字之差的结果自己写边界处理时别再用半开区间口径;两种情况在月末会对不上
必须显式时区偏移窗口要求显式 timezone offset同一份日线在不同交易所时区里属于不同的自然日不要把「本地时间字符串」当窗口端点传进去
拒绝越过策略时间的 end任何 end 超过策略时间都会被拒绝这是把「要未来数据」这件事直接变成错误,而不是静默裁剪这类请求会失败而不是返回近似值——看到失败先检查窗口,不要急着改数据源
取数前校验每个请求先验证窗口,再做数据工作非法请求不触发任何取数,避免用脏数据算出一个看着正常的数批量请求里单个请求的失败会显式保留,不会被静默吞掉
只用该窗口计算只用该窗口,不借用其他月/年的 warmup barwarmup bar 是最隐蔽的跨期泄漏来源代价是窗口前几根 bar 的指标可能为空,这是正确行为而不是 bug
等价时区不改时钟等价的时区偏移指向同一窗口,且不改动策略时钟否则一次查询就会把策略的「当前时间」带偏,后续所有判断跟着错同一窗口用 +08:00 与 +0800 写法都算同一窗口
空窗/不足返回 null窗口为空或数据不足时保持 null用 0 或前值填充会把「没有数据」伪装成「数据是 0」策略里必须显式处理 null,不要直接参与算术
证据边界

官方自己列的「仍未证明」清单,比任何第三方结论都值得读

这段内容在中文资料里几乎没人引:官方在说明修复之后,紧接着列了一串还不能从这次回归里推出的结论。它的价值在于把「机制已改好」和「所有场景都验证过了」明确区分开。

仍未证明的类别为什么还不能算证明你可以怎么自测注意点
适配器相关的已完成 bar不同数据适配器对「日线日期标签、交易时段、时区」的处理不同,前缀回归只覆盖了指标层,没有覆盖每家数据源的 bar 完成语义用你实际使用的数据源,取同一标的同一区间,用两个相邻策略时间点各算一次,比较前缀是否稳定不要用「官方已修复」覆盖数据源层面的差异
多指标的独立复算值文档的回归针对特定访问路径,未声明所有指标的批量组合都被独立复算验证把批量请求的结果与逐个请求的结果对拍批量接口的参数与时框都有边界校验,非法信封会直接失败
真实模型推理官方原文写明这是确定性测试,不是真实模型评测,也不构成 provider/session 完成语义的证明如果要评估 AI agent 决策,必须自己记录 model_call_id 与决策产物AI 决策的可复现性属于另一套机制,见记忆与回放
长历史性能改动移除了「一次算完」的优化,长历史区间上的表现没有被声明为已验证在你自己的目标区间上先跑一遍,记录耗时与内存性能与正确性在这里是有取舍的,不要默认两者都拿到了
测试本身未在本机复现仓库里存在 tests/test_indicator_temporal_safety.py,覆盖负偏移缺陷保持、自定义输入隔离、未来不变性、修正、时间回退与批量请求契约;但本站没有在本机运行它自己 clone 仓库后运行该测试文件,并把它接进你的 CI本站对这条只做文献陈述,不做「已验证」表述
修正与时间回退的边界测试覆盖项里包含「修正」与「时间回退」,但文档没有声明所有访问路径与所有数据源下都成立;换数据源等于换一套完成语义在你自己的数据源上做一次「改一根历史 bar 的值,看后续读数是否只有该时点之后才变」的对拍这类回归必须固定数据源版本,否则无法区分是代码变了还是数据变了
provider / session 完成语义官方原文写明:这是确定性测试,不是真实模型评测,也不是 provider/session 完成语义的证明接实时行情时,自己记录「bar 何时被声明为完成」,并与指标读数时间对齐把「指标没有前视」与「行情判定已完成」当成两个独立命题分别验证
官方原话的核心一句:Do not infer those guarantees from the prefix regression alone.(不要仅凭这次前缀回归就推断出上述保证。)这一句应该成为你阅读任何「某框架已彻底解决未来函数」结论时的检查项——先问它解决了哪一层、哪一类适配器、有没有把数据源口径算进去。本站同样不替 LumiBot 扩大这个结论。
数据口径

为什么宏观数据不能用今天的公开 CSV 跑历史模拟

前面六种泄漏入口都在「代码」层,第七种在「数据」层,而且更隐蔽:你拿到的历史宏观数据,可能是被修订过的版本。LumiBot 的产品决策在这里很值得抄——它的内置 FRED 工具宁愿要求 API Key,也不用公开 CSV。

数据口径它是什么优点风险注意点
公开 CSV 快照官方或第三方站点上「今天的」历史数据文件免费、无需 Key、随手可用宏观序列会被反复修订:你今天下载的「2020 年 GDP」,可能不是 2020 年当时就能看到的值拿它做历史模拟,等于让策略看过了后来的修正
ALFRED point-in-timeFRED 的档案服务,用 realtime_start/realtime_end 指定「在某个时点能看到的版本」能还原当时的信息状态,符合回测的因果要求需要 FRED_API_KEY;查询写法比读 CSV 复杂这正是 LumiBot 内置 FRED 工具采用的路径
内置 FRED 工具list_fred_series / get_fred_series / get_fred_latest / get_fred_snapshot把「时点版本」固化进工具接口,agent 拿到的就是当时口径没有 Key 就用不了这几个工具官方明确不使用公开 CSV 回退,理由写在文档里:那些端点含修订值、不是历史模拟的安全默认值
宏观数据的「最新值」get_fred_latest 这类取最新观测的用法做当下判断很方便放回历史回测里就是典型的前视区分「现在用」与「回测用」是两件事
行情数据的等价问题复权、分红、拆股、退市处理各家数据源都能给Yahoo 有复权与分红、Alpaca/Polygon/Tradier 有复权但不含分红(官方对照表)换数据源等于换口径,收益率会变;见数据源与路由
你自己下载的本地文件用 Pandas/CSV 走自有数据路径完全可控、离线可跑来源与时点是否一致,只有你自己知道建议在文件名或元数据里写死「下载日期 + 数据版本」,否则半年后无法复现
缺口与停牌的处理停牌、退市、节假日导致的缺失 bar 怎么补不同处理方式都合法用前向填充把缺口抹平会制造「价格不动」的假 bar,进而改变波动率与均线形态合格的答案要能指出缺口检测规则,以及被填充的 bar 在指标里如何被识别
自查清单

拿到一份回测结果,先问这八个问题

下面这份清单把上面五个板块的结论收拢成可执行提问。它的用法不是「逐条打勾」,而是逐条要证据——问不出答案的那一条,通常就是结果最可疑的地方。

要问的问题为什么重要合格的答案长什么样
策略里的「当前时间」是怎么取的?datetime.now() 取真实当前时间,在回测里就是前视;跨日期跑还会得到不同结果能指出代码里用的是 self.get_datetime();官方 llms.txt 把这条列为硬规则
指标是在什么范围上算的?整段区间算一次再取索引,就是 25 变 1510 那个缺陷的结构能说明是按策略时间截取前缀后计算,并说明缓存键跟着观测值走
有没有自定义指标函数?它自己取数吗?自定义函数的时间安全自负,不会自动获得前缀截断保护能给出该函数的取数范围,以及它是否改动过共享的数据帧
时间窗的 end 有没有越过策略时间?越过策略时间的 end 会被拒绝;若没被拒绝,说明你绕过了校验路径能给出窗口定义方式,以及是否出现「空窗返回 null」的处理分支
指标窗口前几根 bar 是空的,还是被填了值?用前值填充或跨期借 warmup bar 都是伪装成正常数据的泄漏承认前几根为空,并说明策略里如何跳过这一段
宏观数据是修订后的快照还是时点版本?修订值会让策略「提前知道」后来的修正能指出用的是 ALFRED 的 realtime_start/realtime_end 口径,或明确承认用的是快照并接受该限制
换数据源后结果还成立吗?分红/复权口径差异会直接改变收益率能说出各数据源在除权/分红上的差别,并做过至少一次跨源对拍
这段表现是样本外还是样本内?参数在样本内调好再看样本内结果,等于自己给自己发奖状能给出样本外区间、随机种子、以及至少一次参数扰动后的结果
手续费与滑点在回测里怎么算的?成本假设直接决定高频策略的盈亏符号;扣与不扣可以得出相反结论能指出设置在哪里、数值来源(固定值还是按交易所档位)、以及是否随时间变化
数据缺口是怎么处理的?用前向填充抹平停牌缺口会制造「价格不动」的假 bar,改变波动率与均线形态能说出缺口检测规则,以及被填充的 bar 在指标里如何被识别或跳过
FAQ

关于未来函数与回测可信度的高频问题

以下回答基于 2026-09-22 对官方仓库 docs/ 工程笔记、llms.txt 与 PyPI 元数据的核对;涉及具体实现行为的,以官方仓库实现为准。本站未在本机运行官方测试,也不对任何具体策略回测下「有没有未来函数」的结论。

LumiBot 到底修了什么?一句能听懂的话

把指标的计算范围从「整个回测区间」改成「到策略当前时间为止的前缀」,并让缓存键跟着实际观测到的值走。改动之后,未来某天的数据在结构上不可能再影响今天的指标读数;官方同时主动删掉了那个快但不安全的「一次算完」优化。代价是性能,收益是因果正确性。具体实现以官方仓库为准。

那个「25 变成 1510」是怎么发生的?

旧实现对整个回测 store 只算一次指标,策略时间只用来取索引。于是一个 offset=-1 的 SMA 在计算时已经用到了后面的 bar;当「次日收盘从 30 变成 3000」时,同一根 bar 上的读数就从 25 变成 1510。读数的变化完全由未来数据驱动——这就是未来函数最典型的形态。案例数字来自官方工程笔记原文。

修复之后,我的回测就一定能信任了吗?

不能这样推。官方在同一份文档里列了四类仍未证明的部分:适配器相关的已完成 bar(日线日期标签、交易时段、时区)、多指标的独立复算值、真实模型推理、长历史性能,并写明「不要从前缀回归外推这些保证」。所以正确读法是:指标层这一类的缺陷已被针对性修复,数据源层与模型层的口径仍要你自己核对。

我用的是自定义指标,会被保护到吗?

不会自动生效。官方明确写了:自己去取数的自定义函数,时间安全由使用者自负。也就是说前缀截断、观测值缓存这些保护是给框架内置指标路径的;你的函数如果绕过它去直接读数据,就可能重新引入跨行聚合或改动共享帧的问题。建议:自定义指标只接受框架已经截好的输入,不要在函数内部自己发起取数。

为什么宏观数据不能用公开 CSV?我下载的也是官方数据啊

因为宏观序列会被反复修订。你今天下载的「某年 GDP」,很可能不是当年当时能看到的版本;拿它跑历史模拟,等于让策略看过后来的修正。LumiBot 的选择是:内置 FRED 工具要求 FRED_API_KEY,走 ALFRED 的 realtime_start/realtime_end 取时点版本,并明确不使用公开 CSV 回退这个取舍值得抄;如果你只能用 CSV,至少把下载日期与版本写进文件名。

时间窗为什么必须带显式时区偏移?我本地时间不行吗?

不行,因为同一根日线在不同交易所时区里属于不同的自然日,而窗口是闭区间。官方要求窗口带显式 timezone offset,并且说明等价的时区偏移会指向同一窗口、且不会改动策略时钟——后者很关键,否则一次查询就会把策略的「现在」带偏,后面所有时间判断跟着错。空窗或数据不足时返回 null,策略必须显式处理。

我怎么知道自己的回测有没有这类问题?

从症状反推比从代码反推更快:①曲线平滑得反常、越往后越好;②只有某一段(尤其是月初/年初、或你自定义指标的那一段)特别好;③同样的代码换台机器或换个日期跑,结果就变了。这三类现象出现时,先按本页第 7 个板块的八个问题逐条要证据,再谈策略是否有效。本站不给「合格收益」这类判断,只提供提问框架。

下一步:同一份 Strategy 在回测、模拟和实盘里真的等价吗?

未来函数解决的是「指标有没有偷看未来」;紧接着还有一个常被营销话术盖过去的问题——官方说的「same code」到底复用了什么、哪些部分必须按券商另行处理。