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

15 KiB
Raw Blame History

Freqtrade 回测部署踩坑记录

记录于 2026-06-07 | PriceActionStrategy v0.2 首次回测
环境Docker freqtrade (d:\ft_userdata)ETH/USDTBinance


一、配置文件 (config.json) 的隐性必需字段

Freqtrade 的配置校验非常严格,即使某些模块设为 disabled,也必须提供完整的字段结构。 以下字段在首次部署时缺失,逐一触发报错:

1. telegram 模块

错误:只写了 "enabled": false,缺少 tokenchat_id

// ❌ 错误(会报 schema 校验失败)
"telegram": {
    "enabled": false
}

// ✅ 正确(即使 disabled 也要提供空值)
"telegram": {
    "enabled": false,
    "token": "",
    "chat_id": ""
}

2. api_server 模块

错误:只写了 "enabled": false

// ❌ 错误
"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

回测配置文件必须显式声明这两个模块。

// ✅ 回测 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 版本支持。

// ❌ 导致配置解析错误
"ccxt_config": {
    "enableRateLimit": true,
    "proxies": { ... }
}

// ✅ 正确的极简配置
"ccxt_config": {
    "enableRateLimit": true
},
"ccxt_async_config": {
    "enableRateLimit": true
}

二、策略文件的强制属性

Freqtrade 要求每个策略必须声明以下属性,否则回测直接失败:

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

# 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() 返回前,显式填充所有布尔列:

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,会导致源列被覆盖。后续引用时记得带后缀:

# ✅ 正确引用
dataframe["trend_up_1d"]   # 来自日线的趋势判断
dataframe["trend_up_1h"]   # 来自1H的趋势判断
dataframe["swing_low_1h"]  # 来自1H的Swing Low

四、回测执行命令模板

cd d:/ft_userdata
docker compose run --rm freqtrade backtesting \
    --config user_data/config_backtest.json \
    --strategy PriceActionStrategy \
    --timerange 20250101-

必备前置步骤

  1. 策略文件已复制到 Docker 目录
    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 已就绪,包含所有必需字段

五、完整的最小可行回测配置

{
    "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 行:

_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 模式下必须设置:

"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 回测配置

{
    "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% 宽止损

# v1.2 custom_stoploss 末尾(第 559 行)
return None  # ← 致命错误!

custom_stoploss 返回 Nonefreqtrade 忽略自定义止损,使用类属性 stoploss。 v1.2 的 stoploss = -0.2525% 硬止损),导致所有找不到 Entry Candle 的交易都用 25% 宽止损,"硬扛单"。

修复custom_stoploss 永远不要返回 None,始终返回显式止损比率。

根因 #2Entry Candle 查找逻辑脆弱

# 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 后没有设止损

# 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 动态止损

# 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.055%)防止极端行情,但 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 配置

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_PROXYHTTPS_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,不要每次手动改

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