docs: 补充回测结果摘要 + 策略文档 + Dashboard 后端 + 整理目录结构

This commit is contained in:
2026-06-11 23:32:27 +08:00
parent bd598f0203
commit fa5e177caf
22 changed files with 8999 additions and 0 deletions

498
docs/backtest-pitfalls.md Normal file
View File

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