第 6 章 · 05 递归偏移检测 recursive-analysis 本节摘要:本节讲清回测诊断里比未来函数更隐蔽的一类问题——递归指标偏移。所谓「递归」,是指当前值依赖前一值(如 EMA: )。这类指标在「数据起点不同」时会收敛到略不同的值,导致回测(数据长)和 Dry-Run/实盘(交易所只给最近 N 根)算出来的指标值有偏差,进而让信号不一致。freqtrade 的 命令专门检测这个:它用不同长度的 startup 数据算指标,对比最后一根 K 线上的值差异。本节先讲递归偏移的数学直觉,再讲为什么默认 startup 用奇数、命令的工作流程,然后逐指标解读输出表(方差百分比),最后给出「判断阈值」和 shift 修正的实务建议——目标不是零方差,而是「方差小到不影响买卖决策」。
本节摘要:本节讲清回测诊断里比未来函数更隐蔽的一类问题——递归指标偏移。所谓「递归」,是指当前值依赖前一值(如 EMA:
EMA_t = α·price + (1-α)·EMA_{t-1})。这类指标在「数据起点不同」时会收敛到略不同的值,导致回测(数据长)和 Dry-Run/实盘(交易所只给最近 N 根)算出来的指标值有偏差,进而让信号不一致。freqtrade 的recursive-analysis命令专门检测这个:它用不同长度的 startup 数据算指标,对比最后一根 K 线上的值差异。本节先讲递归偏移的数学直觉,再讲为什么默认 startup 用奇数、命令的工作流程,然后逐指标解读输出表(方差百分比),最后给出「判断阈值」和 shift 修正的实务建议——目标不是零方差,而是「方差小到不影响买卖决策」。
内容来源:原项目文档
docs/recursive-analysis.md,汉化并套用体系化模板。
阅读完本节,你应当能够:
recursive-analysis 并读懂输出表(各方差百分比)。递归公式定义「当前项依赖前一项」。一个最简单的例子叫 steps:第一行 = 0,之后每行 = 前一行 + 1。
steps = 999。steps = 499。同一个时间点,数据长度不同,算出来的值就不同。 这就是递归偏移。
真实指标里,EMA 是典型递归指标:
EMA_t = α · close_t + (1 - α) · EMA_{t-1} 其中 α = 2 / (period + 1)
EMA 的初值需要某根历史 K 线的 close 来「种子化」。数据起点不同,种子不同,经过有限次迭代后收敛到的值也会有微小差异。RSI、MACD(含 EMA)等也都隐含递归。
关键在于 freqtrade 在不同模式下拿到的数据长度不同:
| 模式 | 数据长度 |
|---|---|
| 回测 | 你下载的全部历史(可能几千根) |
| Dry-Run / 实盘 | 交易所 API 每次给的有限根数(如 Binance 单次 1000 根,最多连 5 次 = 5000 根) |
所以回测时 EMA 用了(比如)3000 根数据收敛,实盘只用最近 4999 根——两者算出的 EMA 值在最后一根 K 线上会有微小差异。如果你的策略恰好在这种边界上触发信号(比如 close > EMA),回测说「买」,实盘可能说「不买」——这就是递归偏移造成的回测/实盘不一致。
⚠️ 隐蔽性:这个差异通常很小(百分之零点几),但足以让临界信号翻转,让回测结果无法在实盘复现。它不像未来函数那样「印钞式失真」,而是「悄无声息地让信号对不上」。
这是 freqtrade 的一个细节:默认的 startup_candle_count 推荐用奇数(如 999 而非 1000)。原因是交易所 API 的分页机制。
以 Binance 为例,单次 API 调用最多返回 1000 根 K 线:
而且 Binance 限制连续 5 次批量调用,所以最多能拿 5000 根,即 startup 上限约 4999(奇数)。
💡 规则:
startup_candle_count用奇数(如 199、499、999、1999、4999),让 API 调用恰好对齐分页,避免浪费。
命令的逻辑很直接——改变 startup 长度,看指标值变不变:
它不做回测,只算指标。关注的焦点是「最后一根 K 线上,各指标值随 startup 变化的方差」。
freqtrade recursive-analysis \ --strategy MyStrategy \ -p BTC/USDT \ --timerange 20260101-20260801
推荐实践:
| 项 | 建议 | 原因 |
|---|---|---|
-p |
指定一个高价位、中等波动币(如 BTC、ETH) | 避免价格太低导致浮点精度问题扭曲结果 |
--timerange |
至少 5000 根 K 线(5m 周期约 18 天) | 让基准计算本身的递归问题小到可忽略 |
--cache |
强制 none |
避免加载旧缓存 |
不指定 -p 时,默认用白名单第一个币。
典型输出(有递归问题的策略):
| indicators | 20 | 40 | 80 | 100 | 150 | 300 | 999 | |--------------+---------+---------+--------+--------+---------+---------+--------| | rsi_30 | nan% | -6.025% | 0.612% | 0.828% | -0.140% | 0.000% | 0.000% | | rsi_14 | 24.141% | -0.876% | 0.070% | 0.007% | -0.000% | -0.000% | - |
startup_candle_count(20、40、80、100、150、300、999)。nan%:数据不够,算不出。比如 rsi_30 在只有 21 根(1 当前 + 20 startup)时无法计算(需要至少 30 根)。-:零方差(完美收敛)。rsi_14 在 startup=100 之后都在 0.01% 以内):说明 startup 取到这个值就够用了。rsi_30 在 startup=40 时还有 -6%):说明这个指标对 startup 敏感,需要更大的 startup_candle_count。💡 目标不是零方差:追求绝对零方差往往需要不切实际的超长 startup(可能超出交易所 API 上限)。目标是「方差小到不影响买卖决策」。比如指标值方差 < 0.1%,通常就不会让
close > EMA这类条件翻转。
populate_indicators 和 @informative:如果你把指标计算写在 populate_entry_trend / populate_exit_trend 里,它不会被检测。指标应该放在 populate_indicators(这也是最佳实践)。lookahead-analysis。经验上:
| 方差量级 | 含义 | 行动 |
|---|---|---|
| < 0.1% | 可忽略 | 安全 |
| 0.1% ~ 1% | 边界,需评估 | 看策略条件是否在临界区 |
| > 1% | 显著偏移 | 提高 startup 或换指标 |
最直接的修正:把策略的 startup_candle_count 设到「方差已收敛到可忽略」的那个值。比如 rsi_30 在 startup=150 后方差 < 0.15%,那就设 startup_candle_count = 199(奇数,留余量)。
注意别超过交易所上限(Binance 约 4999)。
某些场景下,作者会用 dataframe['ema'].shift(1) 把指标往后挪一根,确保「当前 K 线只用截止昨日的数据」。这能消除「当前根 EMA 还没最终确定」的问题,但会改变信号时机(信号延后一根 K 线)。这是设计选择,不是万能修正,需要回测验证延后的代价。
如果某个指标对 startup 极度敏感、又非要很长的 startup 才收敛,考虑换一个等价的非递归版本,或缩短周期。例如用 WMA(权重线性)替代 EMA,在某些场景下递归敏感性更低。
| 检测 | 关注 | 严重性 |
|---|---|---|
lookahead-analysis |
偷看未来(作弊) | 致命,策略无效 |
recursive-analysis |
数据长度导致的值偏移 | 隐蔽,信号对不上 |
两者都要跑。lookahead 是「定性」(有没有作弊),recursive 是「定量」(偏多少)。一个策略上线前,应该两个都通过。
startup_candle_count 用奇数(999 等),对齐交易所 API 分页,避免浪费调用。-p 选 BTC/ETH 等高价币、--timerange 至少 5000 根、--cache none。nan% = 算不出,- = 零方差。startup_candle_count 是首选修正,shift 修正慎用;指标计算必须放 populate_indicators 才会被检测。下一节(第 6 章末节),我们看 Edge 边缘分析模块——以及它在最新版 freqtrade 中已被移除的重要事实。