# 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**，不要每次手动改

| **✅** | **网络/代理经验固化完成** | — | — |