第 6 章 · 01 回测引擎原理


文档摘要

第 6 章 · 01 回测引擎原理 本节摘要:本节是回测章的理论基石,讲透 freqtrade 回测引擎的工作方式与固有局限。回测(Backtesting)的本质是「逐根 K 线回放」——把历史 OHLCV 数据按时间顺序一根根喂给策略,策略算指标、产信号、模拟下单、记账,最终汇总表现。但回测永远不等于实盘,因为它必须做一系列假设:K 线内部价格怎么走的、订单是否成交、滑点和手续费如何。本节重点讲三件事:回测引擎的事件循环与「K 线内假设」、回测与实盘的固有差异、以及最危险的「未来函数(lookahead)」本质——如果你的指标在 K 线还没收盘时就用了它的收盘价,回测会「作弊」式盈利,实盘却会亏光。读完本节,你才能带着批判的眼光看回测报告。

第 6 章 · 01 回测引擎原理

本节摘要:本节是回测章的理论基石,讲透 freqtrade 回测引擎的工作方式与固有局限。回测(Backtesting)的本质是「逐根 K 线回放」——把历史 OHLCV 数据按时间顺序一根根喂给策略,策略算指标、产信号、模拟下单、记账,最终汇总表现。但回测永远不等于实盘,因为它必须做一系列假设:K 线内部价格怎么走的、订单是否成交、滑点和手续费如何。本节重点讲三件事:回测引擎的事件循环与「K 线内假设」、回测与实盘的固有差异、以及最危险的「未来函数(lookahead)」本质——如果你的指标在 K 线还没收盘时就用了它的收盘价,回测会「作弊」式盈利,实盘却会亏光。读完本节,你才能带着批判的眼光看回测报告。

内容来源:原项目文档 docs/backtesting.md 的 Assumptions 与 Improved backtest accuracy 章节、源码 freqtrade/optimize/backtesting.py,汉化并套用体系化模板。

⚠️ 风险提示:回测盈利绝不保证实盘盈利。任何回测都建立在假设之上,过度乐观的假设(无滑点、K 线内价格理想化)会让回测好看但实盘崩溃。回测是「必要但不充分」的验证手段。

学习目标

阅读完本节,你应当能够:

  1. 描述 freqtrade 回测引擎的逐根 K 线回放事件循环。
  2. 列举回测必须做出的关键假设(成交价、K 线内走势、滑点)。
  3. 解释回测与实盘的固有差异及其后果。
  4. 说清**未来函数(lookahead)**的本质与危害。
  5. 区分回测的三种粒度(普通、--timeframe-detail、Dry-Run 验证)。

一、回测引擎的事件循环

回测引擎(freqtrade/optimize/backtesting.py 中的 Backtesting 类)的核心逻辑可以概括为「加载 → 回放 → 汇总」:

关键点:

  1. 一次性加载 + 一次性算指标:回测会把整个时间范围的所有 K 线读进内存,然后一次性调用 populate_indicators 算好所有指标列。这与实盘「每来一根新 K 线才算」不同——正是这个差异,埋下了「未来函数」的隐患(第 04 节详讲)。
  2. 逐根回放:指标算完后,引擎按时间戳逐根遍历,在每根 K 线上读 enter_long / exit_long 等信号列,按一套假设决定是否成交。
  3. 汇总:遍历完所有 K 线后,把所有模拟交易汇总成回测报告(第 02 节)。

💡 为什么一次性算指标:为了速度。回测要在几秒内跑完几千根 K 线,如果每根都重算指标会慢几个数量级。但代价是策略作者必须自律,不能在指标计算里「偷看」未来数据。

二、回测的关键假设(必须理解)

回测「不知道 K 线内部价格怎么走」——一根 1h K 线只有 4 个数(开、高、低、收),但实际这一小时内价格可能走出任意路径。为此回测必须做假设:

成交与价格假设

场景 假设
入场 在 K 线开盘价成交(除非自定义了入场价逻辑)
出场信号 下一根 K 线的开盘价成交(信号在收盘才确认)
所有订单 只要价格在当根 K 线 high/low 范围内,就全额成交、无滑点
ROI 平仓 与 high 比较,但用 ROI 值(如 ROI=2%,high=5%,按 2% 成交);不会「低于 K 线」
止损 在止损价精确成交,即使 low 更低;但损失会比止损价多 2 * 手续费
出场信号 vs 止损 同根 K 线同时触发时,出场信号优先(假设信号在开盘触发)
止损 vs ROI 止损先于 ROI 评估(保护资本优先)
止损内 high/low 顺序 low 先于 high(保守,先假设最坏情况)

同根 K 线多信号的评估顺序

一根 K 线上可能同时有出场信号、止损、ROI、追踪止损,回测按这个顺序判断:

  1. 出场信号(假设在开盘触发)
  2. 止损(保护资本)
  3. ROI
  4. 追踪止损

⚠️ 这些假设都有偏向:「止损精确成交」「low 先于 high」「出场信号优先于止损」——这些都是保守假设。但「无滑点」「订单全额成交」是乐观假设。回测结果总是「真实表现的一个有偏估计」,方向取决于哪边假设更偏离实际。

三、回测与实盘的固有差异

即便没有未来函数,回测也永远不等于实盘,差异来自:

维度 回测 实盘
行情 历史 K 线回放 实时真实行情(含 tick 级噪声)
K 线内信息 只有 OHLC,猜内部走势 看到真实 tick 路径
成交 假设无滑点、全额 有滑点、可能部分成交、可能被拒
数据延迟 网络延迟、API 限流
心理 无情绪干扰 贪婪恐惧影响干预
流动性 假设无限 大单会推高/砸低价格

最致命的差异是K 线内走势的未知。一根 1h K 线,你只知道开高低收,但不知道「是先涨后跌,还是先跌后涨」。如果是先涨后跌,止损可能在高位触发;如果是先跌后涨,止损可能在低位触发——这俩结果天差地别。回测只能假设一个顺序(默认 low 先于 high)。

四、--timeframe-detail:用更细周期模拟 K 线内部

为了缓解「K 线内未知」问题,freqtrade 提供了 --timeframe-detail:

freqtrade backtesting --strategy AwesomeStrategy --timeframe 1h --timeframe-detail 5m

它的逻辑:主周期(1h)用于策略信号;但当某根 1h K 线「有活动」(有信号或正持仓)时,引擎会切到更细的 5m 周期去模拟这一小时内的价格走势。

效果:

  • 更真实地模拟 K 线内的高低价顺序。
  • 释放持仓槽位更早(5m 上触发出场信号后,下一根 5m 就能开新仓,而不必等下一个 1h)。
  • custom_exit / custom_stoploss 等回调会在每根 5m 上跑(1h 内跑 12 次)。

⚠️ 代价:需要额外下载 5m 数据、内存翻倍、运行时间变长。建议作为策略开发的最后一步验证——如果普通回测和 --timeframe-detail 结果接近,说明策略没在「K 线内假设」上钻空子,实盘存活概率更高。

五、未来函数(lookahead):回测最大的敌人

未来函数(lookahead bias)是回测最隐蔽、最危险的陷阱:策略在 K 线还没收盘时,就用了这根 K 线的收盘价(甚至更晚的数据)来决策

它是怎么产生的

回测引擎「一次性加载 + 一次性算指标」的工作方式,让未来函数极易混入而不自知:

  • dataframe.shift(-10):往后看 10 根 K 线(直接用未来数据)。
  • dataframe.iloc[...]populate_* 里访问特定行:可能读到当前时间点之后的数据。
  • for 循环遍历 DataFrame:循环边界没控制好就读到未来。
  • .mean() / .min() / .max() 不带 rolling:对整列求统计量,信号那根 K 线「看到」的值里混入了未来数据。
  • ta.MACD(dataframe, 12, 26, 1):signalperiod=1 会引入未来函数。

它为什么可怕

未来函数让回测看起来「印钞」,但实盘里那些信号根本不会产生(因为实盘是「逐根 K 线」算的,看不到未来)。90% 的「好得难以置信」的回测,都是未来函数在作祟。

怎么检测和避免

  • 检测:用 lookahead-analysis 命令(第 04 节详讲),它会故意「切断」未来数据,看信号是否变化。
  • 避免:
    • 永远只用「当前及以前」的数据,即 shift(正数) 或 rolling 窗口往回看。
    • 不在 populate_* 里用 iloc / loc 按位置访问。
    • 聚合统计一定带 rolling(N).expanding(),绝不裸用 .mean()
    • 慎用 for 循环,改用向量化操作。

💡 第 04 节预告:lookahead 太容易引入又太难肉眼发现,所以 freqtrade 提供了 lookahead-analysis 自动检测。任何策略上实盘前,都必须过这一关。

六、回测的三种粒度

把回测理解成「从粗到细」的三种粒度,有助于选择合适的验证手段:

粒度 命令 特点 用途
普通回测 backtesting 主周期 K 线回放,K 线内靠假设 快速迭代、调参
细节回测 backtesting --timeframe-detail 5m 用更细周期模拟 K 线内 上线前最终验证
Dry-Run trade --dry-run 实时真实行情,模拟下单 接近实盘的最终验证

三者「越往下越接近实盘,也越慢」。推荐流程:普通回测调逻辑 → 细节回测验证 K 线内假设 → Dry-Run 验证配置和流程 → 实盘。

本节要点回顾

  1. 事件循环:回测 = 「一次性加载 + 一次性算指标 + 逐根 K 线回放 + 汇总」;为了速度牺牲了「逐根算」的真实性。
  2. 关键假设:入场按开盘价、出场信号按下一根开盘、止损精确成交、无滑点全额成交;同根 K 线评估顺序为 出场信号 → 止损 → ROI → 追踪止损。
  3. K 线内未知:回测只有 OHLC,假设 low 先于 high(保守);但这与实际 tick 路径可能完全不同。
  4. 固有差异:回测 vs 实盘在行情、K 线内信息、成交、延迟、心理、流动性上都有差异;回测是「有偏估计」。
  5. --timeframe-detail:用更细周期模拟 K 线内,更真实但更慢更耗内存;作为上线前最终验证。
  6. 未来函数:回测最大的敌人;shift(-N)、裸 .mean()iloc、signalperiod=1 等都会引入;让回测「印钞」、实盘亏光。
  7. 三种粒度:普通回测 → 细节回测 → Dry-Run,逐级逼近实盘,信任逐级升级。

下一节,我们实打实跑一次回测,逐表解读回测报告——总收益、胜率、最大回撤、Sharpe、按交易对/出场原因的拆分。


发布者: 作者: 灏天文库 转发
评论区 (0)
U