首页
/ freqtrade 高级回测分析:基于 Entry/Exit 标签的信号归因与指标回溯(backtesting-analysis)

freqtrade 高级回测分析:基于 Entry/Exit 标签的信号归因与指标回溯(backtesting-analysis)

2026-09-05 16:08:40作者:翟萌耘Ralph

在 freqtrade 中,默认的回测报告只给出整体胜率、总收益等汇总数字;当你需要回答"某一个入场信号到底赚没赚钱""哪些交易是被 stop_loss 打掉的、哪些是被 ROI 带走的""信号出现那根 K 线上 RSI/EMA 各是多少"这类归因问题时,就需要文档所描述的高级回测分析流程:先用 freqtrade backtesting --export=signals 导出信号与成交数据,再用 freqtrade backtesting-analysis 按 enter_tag / exit_tag 多维度分组统计,甚至把信号 K 线上的指标值逐笔打印出来用于精细调参。读完本篇,你将掌握完整的命令用法、各分组(analysis group 0–5)的统计口径,以及该流程在源码层面的实现细节(信号 K 线如何与交易记录匹配、分组表格如何聚合计算、CSV 导出如何落盘)。

一、为什么需要信号级分析

freqtrade 策略通常通过多个互斥的买入条件构成入场逻辑,并用 enter_tag(旧称 buy tag)标记每次入场由哪个条件触发;出场侧同样有 exit_tag、ROI、stop_loss、trailing_stop_loss 等多种原因。默认回测输出无法按这些维度拆解盈亏,因此 docs/advanced-backtesting.md 给出的方案是:

  1. 回测时额外导出"触发信号的 K 线"数据;
  2. 用独立命令 backtesting-analysis 读取导出文件,输出按信号分组的多级统计表与指标明细。

需要注意文档中明确的前提:

  • 该买入/卖出原因分析仅适用于回测(backtesting),不适用于 hyperopt
  • 导出的信号文件可能非常大,应定期检查 user_data/backtest_results 目录并清理旧导出;
  • 在跑下一次回测前,应删除旧的 backtest 结果,或使用 --cache none 运行回测,确保不会命中缓存结果(缓存命中时不会重新导出信号数据)。

二、第一步:带 --export=signals 运行回测

要启用信号与成交的联合导出,回测命令必须显式指定 --exportsignals

freqtrade backtesting -c <config.json> \
  --timeframe <tf> \
  --strategy <strategy_name> \
  --timerange=<timerange> \
  --export=signals

cli_options.py 的参数定义可以看到,--export 的合法取值来自 constants.EXPORT_OPTIONS,默认值是 trades(仅导出成交);选择 signals 后,freqtrade 会在 user_data/backtest_results 目录中额外输出两类 pickled 数据(策略名、交易对、以及触发入场/出场信号的 K 线 DataFrame),成功时你可以看到形如:

  • backtest-result-{timestamp}_signals.pkl
  • backtest-result-{timestamp}_exited.pkl

对应的加载逻辑在 bt_fileutils.pyload_backtest_analysis_data() 中:它既支持从独立 .pkl 文件读取,也支持直接从回测结果 .zip 包中提取 {stem}_signals.pkl{stem}_exited.pkl{stem}_rejected.pkl 三份数据(函数签名中 name 只允许 "signals" | "rejected" | "exited")。因此新版 zip 打包结果与旧版散落 pkl 文件对 backtesting-analysis 都是兼容的。

三、第二步:运行 backtesting-analysis 并选择分组级别

拿到导出文件后,用如下命令分析入场/出场标签:

freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4 5

该命令默认读取最近一次回测结果(通过 user_data/backtest_results/.last_result.json 记录的最新文件名定位,见 bt_fileutils.pyget_latest_backtest_filename())。--analysis-groups 可传多个取值(0–5),从最粗到最细:

组号 分组维度 说明
0 enter_tag 总体胜率与盈亏摘要(wins/losses)
1 enter_tag 按入场标签聚合的盈亏摘要
2 enter_tag + exit_tag 入场标签 × 出场标签交叉聚合
3 pair + enter_tag 每个交易对 × 入场标签
4 pair + enter_tag + exit_tag 全维度交叉(可能很大)
5 exit_tag 按出场标签聚合

CLI 侧的参数定义在 cli_options.pyanalysis_groupsnargs="+"choices=["0".."5"],帮助文本与上表一致;完整的命令行帮助输出可参见 docs/commands/backtesting-analysis.md

分组表格的统计口径(源码视角)

真正的聚合计算在 entryexitanalysis.py_do_group_table_output() 中:

  • Group 0:对每类 enter_reason 计算 total_num_buyswinslosses、盈利/亏损绝对额(profit_abs_wins / profit_abs_loss)、profit_totwl_ratio_pct(胜率百分比)、avg_winavg_loss,并额外算了一个期望收益比 exp_ratio = (1 + avg_win/|avg_loss|) × win率 − 1(源码中 exp_ratio 一行,行 105–107);
  • Group 1–5:按各自 group_maskgroupby().agg(),输出 num_buysprofit_abs_sum/median/meanmedian_profit_pctmean_profit_pcttotal_profit_pct(百分比列在输出前 ×100)。

这意味着 Group 0 适合快速看"哪个入场信号赢率/赔率最优",Group 2 适合看"某入场信号主要被哪类出场收割",Group 5 适合反向审视出场机制(例如 stop_loss 造成的亏损集中在哪些标签)。

四、指定历史结果:--backtest-filename 与 --backtest-directory

默认分析的是最新一次回测;要复盘更早的结果,使用 --backtest-filename 指定文件名(在 user_data/backtest_results 下):

# 先运行一次回测导出结果,日志中会给出带时间戳的文件名
freqtrade backtesting -c <config.json> --strategy <strategy_name> \
  --timerange <timerange> --export signals

# 之后指定该文件进行分析
freqtrade backtesting-analysis -c <config.json> \
  --backtest-filename=backtest-result-2025-03-05_20-38-34.zip

如果结果存放在别的目录,可再用 --backtest-directory 覆盖基础目录:

freqtrade backtesting-analysis -c <config.json> \
  --backtest-directory custom_results/ \
  --backtest-filename backtest-result-2025-03-05_20-38-34.zip

源码上,这两个参数分别映射到 exportfilenameexportdirectorycli_options.py--backtest-filename/--export-filename--backtest-directory/--export-directory 是同一参数的别名),最终在 entryexitanalysis.pyprocess_entry_exit_reasons() 里以 config["exportdirectory"] + config["exportfilename"] 传给 load_backtest_stats() / load_backtest_data()。注意 _normalize_filename() 的语义:传入目录时按"目录 + 文件名"解析,传入具体文件路径时直接使用该文件——所以 --backtest-filename 只写文件名即可。

五、标签过滤:--enter-reason-list 与 --exit-reason-list

只想看特定入场/出场标签时,用两个空格分隔的列表参数(默认都是 "all"):

--enter-reason-list : 要分析的入场信号列表,默认 all
--exit-reason-list  : 要分析的出场信号列表,默认 all

示例(同时过滤入场标签 enter_tag_a/enter_tag_b 与出场原因 roicustom_exit_tag_astop_loss):

freqtrade backtesting-analysis -c <config.json> \
  --analysis-groups 0 2 \
  --enter-reason-list enter_tag_a enter_tag_b \
  --exit-reason-list roi custom_exit_tag_a stop_loss

实现上非常直接:entryexitanalysis.py_select_rows_by_tags() 中,只要列表不含 "all",就用 df["enter_reason"].isin(...) / df["exit_reason"].isin(...) 过滤交易明细。一个实用的组合是 --analysis-groups 5 --exit-reason-list stop_loss,直接量化止损在所有标签中的分布。

六、信号 K 线指标打印:--indicator-list

这是 backtesting-analysis 最强的能力:把触发信号那根 K 线上策略计算的指标值逐笔打印出来,用于细粒度排查入场/出场时机。

freqtrade backtesting-analysis -c <config.json> \
  --analysis-groups 0 2 \
  --enter-reason-list enter_tag_a enter_tag_b \
  --exit-reason-list roi custom_exit_tag_a stop_loss \
  --indicator-list rsi rsi_1h bb_lowerband ema_9 macd macdsignal

关键规则:

  1. 指标必须存在于策略主 DataFrame 中(主时间框架或 informative 时间框架均可),否则会被静默忽略。源码印证:print_results() 中只有 if ind in res_df 的列才会进入 available_indsentryexitanalysis.py 行 253–259);
  2. 指标值默认在入场点与出场点各显示一次,列名会加 (entry) / (exit) 后缀;
  3. 指定 --indicator-list all 时为避免超大输出,只显示入场点的指标。

信号 K 线如何与交易匹配(源码原理)

匹配逻辑在 _analyze_candles_and_indicators()entryexitanalysis.py 行 40–72):

  • 入场侧:对每笔交易取 signal_candles严格早于 open_date 的最后一根信号 K 线(buyf.loc[(buyf["date"] < v["open_date"])].iloc[[-1]]),把该 K 线整行(含全部指标列)并入这笔交易记录,并记 signal_date
  • 出场侧:process_entry_exit_reasons() 会对 exited 数据重复同样的流程,只是把日期列换成 close_date
  • 最后 _merge_dfs()["pair", "open_date"] 为键把入场表与出场表拼成宽表,冲突列自动加 (entry) / (exit) 后缀——这就是输出表中 chikou_span (entry) / chikou_span (exit) 的来源(行 273–300)。

这个"严格小于"的取法与回测语义一致:信号在 K 线收盘时产生,成交发生在其后,因此取的是产生该笔交易的那根信号 K 线本身,不会引入未来数据。

内置的交易/ K 线字段

以下字段本身就包含在分析数据里,直接写进 --indicator-list 即可打印,无需策略定义:

字段 含义
open_date / close_date 开仓/平仓时间
min_rate / max_rate 持仓期间最低/最高价
open / close / high / low / volume 信号 K 线的开/收/高/低/量
profit_ratio / profit_abs 交易收益率 / 绝对盈利

完整的交易级列清单可参考 bt_fileutils.py 中的 BT_DATA_COLUMNSpairstake_amountamountopen_rateclose_ratefee_openfee_closetrade_durationexit_reasoninitial_stop_loss_absinitial_stop_loss_ratiostop_loss_absstop_loss_ratiois_openenter_tagleverageis_shortopen_timestampclose_timestampordersfunding_fees 等)。这些"贯穿整笔交易"的列在合并时不加 (entry)/(exit) 后缀——_merge_dfs()signal_wide_indicators = set(available_inds) - set(BT_DATA_COLUMNS) 把这类列与信号 K 线指标区分开(行 281)。

输出示例

freqtrade backtesting-analysis -c user_data/config.json \
  --analysis-groups 0 --indicator-list chikou_span tenkan_sen

文档给出的样例输出:

pair open_date enter_reason exit_reason chikou_span (entry) tenkan_sen (entry) chikou_span (exit) tenkan_sen (exit)
DOGE/USDT 2024-07-06 00:35:00+00:00 exit_signal 0.105 0.106 0.105 0.107
BTC/USDT 2024-08-05 14:20:00+00:00 roi 54643.440 51696.400 54386.000 52072.010

(entry) 是开仓时刻的指标值,(exit) 是平仓时刻的值,两者对比即可判断"信号强度在持仓期间是否衰减"。

只看入场或只看出场:--entry-only / --exit-only

  • --entry-only:只展示入场信号点的指标值;
  • --exit-only:只展示出场信号点的指标值;
  • 使用这两种过滤时,列名不再(entry)/(exit) 后缀;
  • 两者不能同时使用,process_entry_exit_reasons() 会抛出 Cannot use --entry-only and --exit-only at the same timeentryexitanalysis.py 行 336–339)。
freqtrade backtesting-analysis -c user_data/config.json \
  --analysis-groups 0 --indicator-list chikou_span tenkan_sen --entry-only

freqtrade backtesting-analysis -c user_data/config.json \
  --analysis-groups 0 --indicator-list chikou_span tenkan_sen --exit-only

七、按日期过滤:--timerange

回测区间是 20220101-20221231,但只想看 1 月的交易明细时:

--timerange : 过滤输出交易的时间范围,起始含、结束不含,格式 YYYYMMDD-[YYYYMMDD]
freqtrade backtesting-analysis -c <config.json> --timerange 20220101-20220201

实现见 _select_rows_within_dates()entryexitanalysis.py 行 192–198):用 TimeRange.parse_timerange() 解析后,对 open_date(入场表)/close_date(出场表)做"起点 ≥ 且终点 < "过滤。注意它只过滤分析输出,不会改变已计算好的结果文件。

八、打印被拒绝的信号:--rejected-signals

并非每个入场信号都会变成成交——价格过滤、手续费过滤、仓位已满、pairlock 等都会"拒绝"信号。加上 --rejected-signals 可以列出这些信号(列:pairdateenter_tag,见 _do_rejected_signals_output()):

freqtrade backtesting-analysis -c <config.json> --rejected-signals

它读取的是导出包中的 {stem}_rejected.pkl。如果没有任何被拒信号,输出 There were no rejected signals.。这个功能对排查"信号很多但交易寥寥"的策略尤其有用——可以判断是过滤器在拦截还是资金/仓位限制在拦截。

九、导出 CSV:--analysis-to-csv 与 --analysis-csv-path

Group 3/4 的表格可能非常大,终端打印并不方便。--analysis-to-csv 会把所有选中的表格写入 CSV 文件(同时关闭终端打印):

freqtrade backtesting-analysis -c <config.json> --analysis-to-csv

例如:

freqtrade backtesting-analysis -c <config.json> --analysis-to-csv --rejected-signals --analysis-groups 0 1

会在 user_data/backtest_results 写出:rejected_signals.csvgroup_0.csvgroup_1.csv——文件名规则在 _print_table() 中:取表格名(如 Group 0:Rejected Signals:)小写化、空格替换为下划线、去掉冒号后加 .csv

--analysis-csv-path 可覆盖输出目录(默认取 exportdirectory,即 user_data/backtest_results):

freqtrade backtesting-analysis -c <config.json> \
  --analysis-to-csv --analysis-csv-path another/data/path/

注意源码中有两个前置校验(entryexitanalysis.py 行 336–341):--entry-only--exit-only 互斥;--analysis-to-csv 指定的目录必须已存在,否则直接报错。

十、端到端工作流与注意事项小结

把整条链路串起来:

# 1. 清缓存/删旧结果,导出信号并回测
freqtrade backtesting -c user_data/config.json --strategy MyStrat \
  --timerange 20240101-20241231 --export signals --cache none

# 2. 宏观:各入场标签的胜率/盈亏
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 1

# 3. 微观:某入场标签在信号 K 线上的 RSI/EMA 值
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 2 \
  --enter-reason-list my_entry_tag --indicator-list rsi ema_9 close profit_abs \
  --analysis-to-csv

实践要点:

  • 分析命令走的是"离线工具"路径:analyze_commands.pystart_analysis_entries_exits()RunMode.UTIL_NO_EXCHANGE 初始化配置,即该命令不需要交易所连接,只需配置文件中策略/路径信息有效;
  • 指标列找不到时会静默丢弃而非报错,如果输出缺少某个指标,先确认策略 populate_indicators 里是否真的生成了该列;
  • 回测结果缓存(--cache)命中时不重新导出信号文件,因此改策略后务必 --cache none 或清理 user_data/backtest_results,再运行 backtesting-analysis
  • 多策略回测的结果中,分析流程按 backtest_stats["strategy"] 中的每个策略名逐一加载与输出(process_entry_exit_reasons() 的 for 循环),因此多策略场景下每个策略都会得到各自的分组表。

这套"导出信号 K 线 → 按标签分组 → 打印指标值 → CSV 落盘"的组合,把 freqtrade 回测从"结果汇总"推进到"逐笔信号审计"层面:你能精确回答每个入场/出场标签的贡献、被拒信号的去向,以及信号出现那一刻市场指标的状态,从而为策略调优提供可直接验证的证据。

相关入口文件:高级回测分析文档分析命令入口核心分析实现结果文件加载命令行参数定义测试用例

登录后查看全文
热门项目推荐
相关项目推荐