Files
beast-trader-strategies/docs/backtest-pitfalls.md

498 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Freqtrade 回测部署踩坑记录
> 记录于 2026-06-07 | PriceActionStrategy v0.2 首次回测
> 环境Docker freqtrade (d:\ft_userdata\)ETH/USDTBinance
---
## 一、配置文件 (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`**,始终返回显式止损比率。
### 根因 #2Entry 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**,不要每次手动改
| **✅** | **网络/代理经验固化完成** | — | — |