498 lines
15 KiB
Markdown
498 lines
15 KiB
Markdown
# 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**,不要每次手动改
|
||
|
||
| **✅** | **网络/代理经验固化完成** | — | — | |