# 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` ```json // ❌ 错误(会报 schema 校验失败) "telegram": { "enabled": false } // ✅ 正确(即使 disabled 也要提供空值) "telegram": { "enabled": false, "token": "", "chat_id": "" } ``` ### 2. api_server 模块 **错误**:只写了 `"enabled": false` ```json // ❌ 错误 "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 回测配置文件**必须显式声明**这两个模块。 ```json // ✅ 回测 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 版本支持。 ```json // ❌ 导致配置解析错误 "ccxt_config": { "enableRateLimit": true, "proxies": { ... } } // ✅ 正确的极简配置 "ccxt_config": { "enableRateLimit": true }, "ccxt_async_config": { "enableRateLimit": true } ``` --- ## 二、策略文件的强制属性 Freqtrade 要求每个策略必须声明以下属性,否则回测直接失败: ```python 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**。 ```python # 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()` 返回前,显式填充所有布尔列: ```python 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`,会导致源列被覆盖。后续引用时记得带后缀: ```python # ✅ 正确引用 dataframe["trend_up_1d"] # 来自日线的趋势判断 dataframe["trend_up_1h"] # 来自1H的趋势判断 dataframe["swing_low_1h"] # 来自1H的Swing Low ``` --- ## 四、回测执行命令模板 ```bash cd d:/ft_userdata docker compose run --rm freqtrade backtesting \ --config user_data/config_backtest.json \ --strategy PriceActionStrategy \ --timerange 20250101- ``` ### 必备前置步骤 1. **策略文件已复制到 Docker 目录**: ```bash cp user_data/strategies/price_action_strategy.py \ d:/ft_userdata/user_data/strategies/PriceActionStrategy.py ``` 2. **历史数据已下载**(`d:/ft_userdata/user_data/data/binance/` 下有对应的 JSON 文件) 3. **回测配置文件 `config_backtest.json` 已就绪**,包含所有必需字段 --- ## 五、完整的最小可行回测配置 ```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 行: ```python _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 模式下**必须**设置: ```json "entry_pricing": { "use_order_book": true }, "exit_pricing": { "use_order_book": true } ``` ### 问题:`SampleStrategy` 也无法运行 futures 回测 **确认**:不是策略问题,是 freqtrade 2026.2 的 Binance futures 初始化 Bug。 **解决方案**: 1. `use_order_book: true`(绕过 ticker 检查) 2. 下载 mark price 数据:`download-data --trading-mode futures` 3. pair 名称必须带结算货币后缀:`ETH/USDT:USDT` ### 最小可行 futures 回测配置 ```json { "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% 宽止损 ```python # 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 查找逻辑脆弱 ```python # 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)) ] ``` 问题: 1. `potential_entry_low` 标记了**所有**信号 K 线,不是触发这笔交易的那个 2. ±1 小时窗口内有多个信号时,`iloc[-1]` 取的是最后一个,不一定正确 3. 时区稍有偏差(纳秒级)就找不到,直接掉进 `return None` **结论**:在 `custom_stoploss` 里查找 Entry Candle 是不可靠的,必须换方法。 ### 根因 #3:阶段二 `pass` 后没有设止损 ```python # 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 动态止损**: ```python # 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 配置 ```yaml 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 被排除在代理外 | ### 经验教训 1. **每次跑回测前**,确认代理服务(Clash/V2Ray)在运行 2. **不要随意修改 docker-compose.yml 的代理配置**——它一旦配置正确就不该动 3. **区分服务不可用 vs 配置错误**:同样配置之前能跑通现在不能 → 服务的锅不是配置的锅 4. **在踩坑文档中固化已验证的 docker-compose.yml**,不要每次手动改 | **✅** | **网络/代理经验固化完成** | — | — |