15 KiB
Freqtrade 回测部署踩坑记录
记录于 2026-06-07 | PriceActionStrategy v0.2 首次回测
环境:Docker freqtrade (d:\ft_userdata),ETH/USDT,Binance
一、配置文件 (config.json) 的隐性必需字段
Freqtrade 的配置校验非常严格,即使某些模块设为 disabled,也必须提供完整的字段结构。
以下字段在首次部署时缺失,逐一触发报错:
1. telegram 模块
错误:只写了 "enabled": false,缺少 token 和 chat_id
// ❌ 错误(会报 schema 校验失败)
"telegram": {
"enabled": false
}
// ✅ 正确(即使 disabled 也要提供空值)
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
}
2. api_server 模块
错误:只写了 "enabled": false
// ❌ 错误
"api_server": {
"enabled": false
}
// ✅ 正确
"api_server": {
"enabled": false,
"listen_ip_address": "0.0.0.0",
"listen_port": 8080,
"username": "freqtrader",
"password": "password",
"jwt_secret_key": "somethingRandom123"
}
3. entry_pricing / exit_pricing
回测配置文件必须显式声明这两个模块。
// ✅ 回测 config 必须包含
"entry_pricing": {
"price_side": "same",
"use_order_book": false,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"price_side": "same",
"use_order_book": false,
"order_book_top": 1
}
4. ccxt_config 中的 proxies
在 Docker 环境下,proxies 字段会导致配置解析异常。不要在 ccxt_config 中加入代理配置,除非明确知道 freqtrade 版本支持。
// ❌ 导致配置解析错误
"ccxt_config": {
"enableRateLimit": true,
"proxies": { ... }
}
// ✅ 正确的极简配置
"ccxt_config": {
"enableRateLimit": true
},
"ccxt_async_config": {
"enableRateLimit": true
}
二、策略文件的强制属性
Freqtrade 要求每个策略必须声明以下属性,否则回测直接失败:
class PriceActionStrategy(IStrategy):
# ── 必需属性 ──
timeframe = "5m" # 主时间框架
can_short = True # 是否支持做空(spot 模式必须为 False)
stoploss = -0.10 # 硬止损比例(即使有 custom_stoploss 也要声明)
use_custom_stoploss = True # 是否使用动态止损
minimal_roi = {"0": 100} # 时间止盈表(不用就写 {"0": 100})
max_open_trades = 1 # 最大同时持仓数
# ── 必需方法 ──
def populate_indicators(self, dataframe, metadata): ...
def populate_entry_trend(self, dataframe, metadata): ...
def populate_exit_trend(self, dataframe, metadata): ...
| 属性 | 说明 | 缺失后果 |
|---|---|---|
stoploss |
硬止损比例,如 -0.10 = 10% |
回测直接报错退出 |
minimal_roi |
时间止盈表,不用则写 {"0": 100} |
回测直接报错退出 |
use_custom_stoploss |
有 custom_stoploss() 方法时必须设为 True |
方法不被调用 |
Spot vs Futures 的 can_short 差异
| 模式 | can_short |
说明 |
|---|---|---|
trading_mode: "spot" |
必须是 False | 现货不支持做空 |
trading_mode: "futures" |
可以 True | 合约支持双向 |
最佳实践:策略中保持 can_short = True(你的实盘方向),创建单独的回测配置指定 trading_mode: "futures" 来测完整逻辑。如果必须用现货数据跑 spot 回测,临时改为 False。
三、多时间框架合并的数据清洗陷阱
问题:merge_informative_pair 导致 NaN
当使用 merge_informative_pair() 合并 D1/1H 数据到 5M 主框架时,前 N 根 K 线的衍生列(布尔值、EMA、Swing 点)全是 NaN。
# merge_informative_pair 返回的 dataframe 前面有 NaN 行
dataframe = merge_informative_pair(
dataframe, daily, self.timeframe, "1d", ffill=True,
)
# 结果:前几十行中 trend_up_1d、bullish_pinbar_1h 等列为 NaN
错误表现:ValueError: The truth value of a Series is ambiguous 或 NaN 通过布尔条件传递导致崩溃。
修复方案:在 populate_indicators() 返回前,显式填充所有布尔列:
def populate_indicators(self, dataframe, metadata):
# ... 所有指标计算 ...
# ⚠️ 关键:填充合并后的 NaN 布尔列
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_1h", "trend_down_1h",
"bullish_pinbar", "bearish_pinbar",
"bullish_engulfing", "bearish_engulfing",
"volume_surge",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False).infer_objects(copy=False)
return dataframe
列名冲突问题
merge_informative_pair 默认 append_timeframe=True(给合并的列加 _1d、_1h 后缀)。不要传 append_timeframe=False,会导致源列被覆盖。后续引用时记得带后缀:
# ✅ 正确引用
dataframe["trend_up_1d"] # 来自日线的趋势判断
dataframe["trend_up_1h"] # 来自1H的趋势判断
dataframe["swing_low_1h"] # 来自1H的Swing Low
四、回测执行命令模板
cd d:/ft_userdata
docker compose run --rm freqtrade backtesting \
--config user_data/config_backtest.json \
--strategy PriceActionStrategy \
--timerange 20250101-
必备前置步骤
- 策略文件已复制到 Docker 目录:
cp user_data/strategies/price_action_strategy.py \ d:/ft_userdata/user_data/strategies/PriceActionStrategy.py - 历史数据已下载(
d:/ft_userdata/user_data/data/binance/下有对应的 JSON 文件) - 回测配置文件
config_backtest.json已就绪,包含所有必需字段
五、完整的最小可行回测配置
{
"max_open_trades": 1,
"stake_currency": "USDT",
"stake_amount": 10000,
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "USD",
"dry_run": true,
"trading_mode": "spot",
"margin_mode": "",
"exchange": {
"name": "binance",
"key": "",
"secret": "",
"ccxt_config": {
"enableRateLimit": true
},
"ccxt_async_config": {
"enableRateLimit": true
}
},
"pairlists": [
{"method": "StaticPairList"}
],
"telegram": {
"enabled": false,
"token": "",
"chat_id": ""
},
"api_server": {
"enabled": false,
"listen_ip_address": "0.0.0.0",
"listen_port": 8080,
"username": "freqtrader",
"password": "password",
"jwt_secret_key": "somethingRandom123"
},
"bot_name": "backtest",
"entry_pricing": {
"price_side": "same",
"use_order_book": false,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"price_side": "same",
"use_order_book": false,
"order_book_top": 1
}
}
六、错误顺序时间线
按实际触发顺序排列,方便快速定位:
| 序号 | 错误 | 根因 | 修复 |
|---|---|---|---|
| 1 | 配置解析错误 | ccxt_config 中含 proxies 字段 |
移除 proxies |
| 2 | Schema 校验失败 | telegram 缺少 token/chat_id |
补全空值字段 |
| 3 | Schema 校验失败 | api_server 缺少完整结构 |
补全所有必需字段 |
| 4 | Schema 校验失败 | 缺少 entry_pricing/exit_pricing |
添加两个模块 |
| 5 | 策略加载失败 | spot 模式不支持 can_short=True |
临时设 False |
| 6 | 策略校验失败 | 缺少 stoploss 属性 |
添加 stoploss |
| 7 | 策略校验失败 | 有 custom_stoploss() 但未声明 use_custom_stoploss |
设为 True |
| 8 | 策略校验失败 | 缺少 minimal_roi |
添加 {"0": 100} |
| 9 | NaN 导致崩溃 | merge_informative_pair 后布尔列为 NaN |
fillna(False) 清洗 |
| ✅ | 回测成功 | — | — |
七、Freqtrade 2026.2 Binance Futures 回测专用坑
问题:Ticker pricing not available for Binance
触发条件:trading_mode: "futures" + use_order_book: false
根因:freqtrade/exchange/binance.py 第 57 行:
_ft_has_futures = {
"tickers_have_price": False, # ← Binance futures 不支持 ticker.price
}
validate_pricing() 检查 self._ft_has["tickers_have_price"],若为 False 且 use_order_book=false,直接抛异常。
修复方案:futures 模式下必须设置:
"entry_pricing": { "use_order_book": true },
"exit_pricing": { "use_order_book": true }
问题:SampleStrategy 也无法运行 futures 回测
确认:不是策略问题,是 freqtrade 2026.2 的 Binance futures 初始化 Bug。
解决方案:
use_order_book: true(绕过 ticker 检查)- 下载 mark price 数据:
download-data --trading-mode futures - pair 名称必须带结算货币后缀:
ETH/USDT:USDT
最小可行 futures 回测配置
{
"trading_mode": "futures",
"margin_mode": "cross",
"liquidation_buffer": 0.05,
"exchange": {
"name": "binance",
"key": "",
"secret": "",
"ccxt_config": {
"enableRateLimit": true,
"options": {"defaultType": "swap"}
},
"pair_whitelist": ["ETH/USDT:USDT"],
"pair_blacklist": []
},
"entry_pricing": {
"use_order_book": true, // ← futures 必须 true
"order_book_top": 1
},
"exit_pricing": {
"use_order_book": true, // ← futures 必须 true
"order_book_top": 1
}
}
v1.1 回测结果(首次 futures 成功)
| 指标 | 数值 |
|---|---|
| 交易笔数 | 65 |
| 总盈亏 | +61.52% |
| 做多利润 | +5,193 USDT |
| 做空利润 | +959 USDT |
| 硬止损问题 | 31 笔全部亏损,-8,839 USDT |
教训:futures 可以做空,熊市不再被动挨打。但硬止损位置不对(放在结构失效点太远),v1.2 改为 Entry Candle 失效点。
| ✅ | v1.1 futures 回测成功 | — | — |
八、v1.2 止损逻辑 Bug 分析(关键经验)
v1.2 回测结果回顾
| 指标 | 数值 |
|---|---|
| 交易笔数 | 100 |
| 硬止损出场 | 50 笔,全部亏损,-8,468 USDT |
| trailing_stop 出场 | 46 笔,34.8% 胜率,+13,671 USDT |
50 笔硬止损全部亏损 = 止损逻辑根本性失效。
根因 #1(致命):return None 退回到 25% 宽止损
# v1.2 custom_stoploss 末尾(第 559 行)
return None # ← 致命错误!
custom_stoploss 返回 None 时,freqtrade 忽略自定义止损,使用类属性 stoploss。
v1.2 的 stoploss = -0.25(25% 硬止损),导致所有找不到 Entry Candle 的交易都用 25% 宽止损,"硬扛单"。
修复:custom_stoploss 永远不要返回 None,始终返回显式止损比率。
根因 #2:Entry Candle 查找逻辑脆弱
# v1.2 的查找方式(第 507-519 行)
entry_mask = (
(dataframe["potential_entry_low"].notna())
| (dataframe["potential_entry_high"].notna())
)
entry_candidates = dataframe[
entry_mask
& (dataframe["date"] <= trade.open_date_utc + timedelta(hours=1))
& (dataframe["date"] >= trade.open_date_utc - timedelta(hours=1))
]
问题:
potential_entry_low标记了所有信号 K 线,不是触发这笔交易的那个- ±1 小时窗口内有多个信号时,
iloc[-1]取的是最后一个,不一定正确 - 时区稍有偏差(纳秒级)就找不到,直接掉进
return None
结论:在 custom_stoploss 里查找 Entry Candle 是不可靠的,必须换方法。
根因 #3:阶段二 pass 后没有设止损
# v1.2 第 528-539 行
if current_profit > self.profit_to_structure_sl_pct.value:
pass # 继续到阶段二
else:
return max(sl_ratio, -0.25)
# ← 如果阶段二条件不满足,函数一路走到 return None!
盈利超阈值后跳到阶段二,但如果 resistance_4h / support_4h 为 NaN,阶段二没有任何 return,最终落到 return None。
v1.3 修复方案
弃用 Entry Candle 查找,改用 ATR 动态止损:
# v1.3 custom_stoploss 核心逻辑
atr = last_candle['atr_1h']
if not trade.is_short:
if current_profit <= 0.01:
sl_price = open_rate - atr * 1.0 # 阶段一:紧止损
elif current_profit <= 0.02:
sl_price = open_rate * 0.999 # 阶段二:保本
else:
sl_price = current_rate - atr * 1.0 # 阶段三:追踪止损
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.05) # 永不返回 None!
为什么用 ATR 而不是 Entry Candle:
- ATR 来自当前 K 线数据,100% 可靠,不需要查找历史
- ATR 是波动率的自适应指标,低波动时止损紧,高波动时止损宽
- 符合"早早认输"哲学:价格反向跑 1 ATR 就止损,不硬扛
经验总结
| 规则 | 说明 |
|---|---|
❌ 永不 return None |
始终返回显式止损比率,宁可紧不要松 |
| ✅ 用 ATR 做动态止损 | 可靠、自适应、不依赖历史 K 线查找 |
| ✅ 三阶段设计 | 紧止损 → 保本 → 追踪,符合趋势交易逻辑 |
✅ stoploss 属性设安全网 |
-0.05(5%)防止极端行情,但 custom_stoploss 应永远先于此触发 |
| ✅ | v1.3 止损逻辑重写完成 | — | — |
九、Docker 网络与代理环境(高频踩坑区)
核心事实
freqtrade 2026.2 在启动回测时强制执行 reload_markets,即使回测使用的全部是本地 OHLCV 数据。这意味着 Docker 容器必须有外网访问能力,否则任何操作都会在 _load_async_markets 阶段失败。
freqtrade.exceptions.TemporaryError: Error in reload_markets due to ExchangeNotAvailable.
Message: binance GET https://api.binance.com/api/v3/exchangeInfo
已验证可工作的 docker-compose.yml 配置
services:
freqtrade:
environment:
- HTTP_PROXY=http://127.0.0.1:7890
- HTTPS_PROXY=http://127.0.0.1:7890
- NO_PROXY=localhost,127.0.0.1
- TZ=Asia/Shanghai
关键点:
HTTP_PROXY和HTTPS_PROXY必须同时设置,缺一不可NO_PROXY中不要包含api.binance.com——Binance 必须走代理- 端口号
7890是用户 Clash 代理的默认端口
故障排查清单
| 现象 | 诊断方法 | 常见原因 |
|---|---|---|
ExchangeNotAvailable |
docker run --rm curlimages/curl curl --proxy http://127.0.0.1:7890 https://api.binance.com/api/v3/ping |
代理服务未运行 |
| 容器连不上代理 | 从宿主机 curl http://127.0.0.1:7890 |
代理端口不对或服务挂了 |
| 配置正确但连不上 | 检查 NO_PROXY 是否误包含 api.binance.com |
Binance 被排除在代理外 |
经验教训
- 每次跑回测前,确认代理服务(Clash/V2Ray)在运行
- 不要随意修改 docker-compose.yml 的代理配置——它一旦配置正确就不该动
- 区分服务不可用 vs 配置错误:同样配置之前能跑通现在不能 → 服务的锅不是配置的锅
- 在踩坑文档中固化已验证的 docker-compose.yml,不要每次手动改
| ✅ | 网络/代理经验固化完成 | — | — |