第 7 章 · 02 搜索空间与可优化参数


文档摘要

第 7 章 · 02 搜索空间与可优化参数 本节摘要:本节是 Hyperopt 章的实操核心,讲清怎么在策略里声明「可优化参数」。freqtrade 提供四种参数类: (整数)、 (限定位数小数,推荐)、 (全精度小数,少用)、 (枚举),外加 (Categorical 的简写)。本节先用一张表对比这四种的适用场景,再讲清声明的完整语法(范围、默认值、space、optimize、load 等选项),然后用一个完整示例展示「声明 → 在 populate 里用 引用 → 用 预算多列指标」的闭环。最后重点讲 space 的分配规则(命名约定 vs 显式 )、 / 的妙用,以及「为什么可优化参数不能用在 」这个高频踩坑。

第 7 章 · 02 搜索空间与可优化参数

本节摘要:本节是 Hyperopt 章的实操核心,讲清怎么在策略里声明「可优化参数」。freqtrade 提供四种参数类:IntParameter(整数)、DecimalParameter(限定位数小数,推荐)、RealParameter(全精度小数,少用)、CategoricalParameter(枚举),外加 BooleanParameter(Categorical 的简写)。本节先用一张表对比这四种的适用场景,再讲清声明的完整语法(范围、默认值、space、optimize、load 等选项),然后用一个完整示例展示「声明 → 在 populate 里用 .value 引用 → 用 .range 预算多列指标」的闭环。最后重点讲 space 的分配规则(命名约定 vs 显式 space=)、optimize=False / load=False 的妙用,以及「为什么可优化参数不能用在 populate_indicators」这个高频踩坑。

内容来源:原项目文档 docs/hyperopt.md(Parameter types / Optimizing an indicator parameter)、模板 freqtrade/templates/sample_strategy.py,汉化并套用体系化模板。

学习目标

阅读完本节,你应当能够:

  1. 选对四种参数类(IntParameter / DecimalParameter / CategoricalParameter / RealParameter)。
  2. 写出完整声明语法(范围、default、space、optimize、load、decimals)。
  3. populate_* 里用 .value 引用当前值,用 .range 预算多列。
  4. 理解 space 的命名约定与显式赋值规则。
  5. 用 **optimize=False / load=False** 控制参数行为。
  6. 避开「可优化参数不能用在 populate_indicators」的坑。

一、四种参数类

适用 示例
IntParameter(low, high, default) 整数参数 RSI 周期 IntParameter(10, 50, default=30)
DecimalParameter(low, high, default, decimals=3) 小数(限位数,推荐) 止损比率 DecimalParameter(-0.2, -0.01, default=-0.05)
CategoricalParameter([a, b, c], default) 枚举选择 触发器 CategoricalParameter(["bb_lower", "macd_cross"], default="bb_lower")
RealParameter(low, high, default) 全精度小数(少用) 几乎不用,空间近乎无限
BooleanParameter(default=True) CategoricalParameter([True, False]) 的简写 开关 BooleanParameter(default=True)

💡 为什么推荐 Decimal 而非 Real:RealParameter0.10~0.15 之间有几乎无限个候选值(0.10000001, 0.10000002, ...),搜索空间爆炸、Hyperopt 跑不完且易过拟合。DecimalParameter(..., decimals=3) 把它限制到 0.100, 0.101, ..., 0.150 共 51 个值,空间可控、结果可复现。没有特殊理由就用 Decimal。

二、声明语法

完整声明长这样:

from freqtrade.strategy import ( IStrategy, IntParameter, DecimalParameter, CategoricalParameter, BooleanParameter, ) class MyStrategy(IStrategy): # 完整形式:范围、默认、space、是否优化、是否加载 buy_rsi = IntParameter( low=10, high=50, # 搜索范围 default=30, # 默认值(未优化时用) space="buy", # 归属哪个 space optimize=True, # 是否参与优化(False 则固定用 default) load=True, # 是否从 JSON 加载上次优化结果 ) # 简写:只写必要项 buy_ema_short = IntParameter(3, 50, default=10) sell_rsi = IntParameter(50, 100, default=70, space="sell") buy_trigger = CategoricalParameter(["bb_lower", "macd_cross"], default="bb_lower", space="buy") buy_adx_enabled = BooleanParameter(default=True, space="buy")

关键选项

选项 默认 含义
low, high 必填 搜索范围上下界
default 必填 默认值;未优化或 load=False 时用
space 按命名推断 归属空间(见下节)
decimals(Decimal) 3 小数位数
optimize True False 则不参与优化,固定用 default
load True False 则不从上次结果加载,强制用 default

optimize 和 load 的妙用

  • optimize=False:让某个参数「在策略里声明但固定值」。适合:你想保留参数化的灵活性,但这次只想优化别的参数。
  • load=False:让某参数「每次 Hyperopt 都从 default 起步」,不继承上次结果。适合:上次优化的值你不确定好坏,想重新探索。

⚠️ load=False 影响回测:load=False 不仅影响 Hyperopt,还影响回测——回测时这个参数也用 default,而不是上次 Hyperopt 找到的值。如果你想让回测用优化值,务必保持 load=True(默认)。

三、space 的归属规则

一个参数属于哪个 space,有两种判定方式:

1. 命名约定(隐式)

参数名以 buy_ / sell_ / enter_ / exit_ / protection_ 开头,会自动归到对应 space:

buy_rsi = IntParameter(10, 50, default=30) # 自动归 buy sell_rsi = IntParameter(50, 100, default=70) # 自动归 sell

2. 显式 space=

命名不符合约定时,必须显式指定:

my_custom_threshold = IntParameter(10, 50, default=30, space="buy")

也可以用自定义 space 名(如 space="my_space"),然后用 --spaces my_space 优化它。注意 space 名不能是 alldefault,且必须是合法 Python 标识符。

冲突时以显式为准

buy_adx = IntParameter(4, 24, default=14, space='sell') # 名字是 buy_,但显式归 sell

这种情况按 space='sell' 算。别写这种自相矛盾的代码,纯粹是给自己挖坑。

没归到任何 space 的参数会被忽略

adx_period = IntParameter(4, 24, default=14) # 名字不带前缀,也没 space=

这种参数 Hyperopt 检测不到,不会参与优化,永远用 default。日志里不会有报错,只是默默忽略——这是隐蔽的坑。

💡 自检:跑 Hyperopt 前用 freqtrade list-strategies 看参数计数,确认你声明的参数都被识别了。

四、用 .value 引用当前值

声明后,在 populate_entry_trend / populate_exit_trend 里用 self.参数名.value 取当前值:

def populate_entry_trend(self, dataframe, metadata): dataframe.loc[ (dataframe["rsi"] < self.buy_rsi.value) & # 引用当前 RSI 阈值 (dataframe["volume"] > 0), "enter_long", ] = 1 return dataframe

Hyperopt 每个 epoch 会把不同的 .value 注入,信号随之变化,回测出不同结果。

五、用 .range 预算多列(优化指标周期)

如果可优化的是指标周期(如 EMA 用几周期),需要先把所有可能值都算成列,Hyperopt 才能从中选。用 .range:

class MyStrategy(IStrategy): buy_ema_short = IntParameter(3, 50, default=10, space="buy") buy_ema_long = IntParameter(15, 200, default=50, space="buy") def populate_indicators(self, dataframe, metadata): # 把所有可能的 ema_short 值都算成列:ema_short_3, ema_short_4, ..., ema_short_50 for val in self.buy_ema_short.range: dataframe[f'ema_short_{val}'] = ta.EMA(dataframe, timeperiod=val) # 同理 ema_long for val in self.buy_ema_long.range: dataframe[f'ema_long_{val}'] = ta.EMA(dataframe, timeperiod=val) return dataframe def populate_entry_trend(self, dataframe, metadata): # 用 .value 选当前 epoch 的列名 conditions = [ qtpylib.crossed_above( dataframe[f'ema_short_{self.buy_ema_short.value}'], dataframe[f'ema_long_{self.buy_ema_long.value}'], ), (dataframe['volume'] > 0), ] dataframe.loc[reduce(lambda x, y: x & y, conditions), 'enter_long'] = 1 return dataframe

关键区别:

  • Hyperopt 模式:.range 返回完整范围([3, 4, ..., 50]),预算出 48 列,Hyperopt 从中选。
  • 回测/Dry-Run/实盘:.range 只返回当前 .value,只算 1 列。

⚠️ .range 只支持 Int / Decimal / Categorical:RealParameter 因为空间无限,不提供 .range

六、关键踩坑:可优化参数不能用在 populate_indicators 计算

这是最常见、最隐蔽的坑。官方明确:

Hyperoptable parameters cannot be used in populate_indicators - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case.

意思是:populate_indicators 只跑一次(为了速度),所以里面用的可优化参数会一直是 default 值,不会随 epoch 变化。

# ❌ 错误:buy_ema_period 在 populate_indicators 里用,永远是 default=10 def populate_indicators(self, dataframe, metadata): dataframe['ema'] = ta.EMA(dataframe, timeperiod=self.buy_ema_period.value) return dataframe # ✅ 正确:用 .range 预算所有可能值 def populate_indicators(self, dataframe, metadata): for val in self.buy_ema_period.range: dataframe[f'ema_{val}'] = ta.EMA(dataframe, timeperiod=val) return dataframe # ✅ 或者:把指标计算移到 populate_entry_trend(每 epoch 重算) def populate_entry_trend(self, dataframe, metadata): dataframe['ema'] = ta.EMA(dataframe, timeperiod=self.buy_ema_period.value) ...

如果你发现「Hyperopt 找出的 EMA 周期在回测里对不上」,首先查这个坑。

七、性能优化:减少内存

.range 预算多列,内存会随参数数量线性增长(乘以并行 -j)。两个缓解手段:

  1. 指标计算移到 populate_entry_trend:这个方法每个 epoch 都跑,不需要 .range,只算当前 .value 一列。代价是 CPU 增加。
  2. --analyze-per-epoch:把整个 populate_indicators 移到 epoch 进程里,每个 epoch 只算当前值。代价同样是 CPU 增加,但省内存。
freqtrade hyperopt --strategy MyStrategy --analyze-per-epoch -e 500

💡 空间越小越好:无论用哪种方式,把参数范围设得尽可能小(IntParameter(5, 20)IntParameter(1, 200) 快几十倍),是提升性能和降低过拟合的最简单手段。

八、Guards 与 Triggers 的设计思路

Hyperopt 文档里有个经典的设计区分:

  • Guard(守卫):「不满足条件就不进场」的过滤,如 ADX > 25price > EMA200。用 BooleanParameter 开关 + DecimalParameter / IntParameter 阈值。
  • Trigger(触发):真正决定进场的信号,如 EMA5 上穿 EMA20close 触及布林下轨。用 CategoricalParameter 选哪种触发。

技术上两者无区别,但设计上区分能让信号「不粘连」(sticking)——粘连信号会在多根 K 线上持续触发,导致「信号快消失时才进场」,成功率低。Guard 控制过滤、Trigger 选时机,组合清晰。

九、官方 sample_strategy 的参数声明

freqtrade 模板 sample_strategy.py 里有完整的可优化参数示例,值得参考:

# 出自 freqtrade/templates/sample_strategy.py buy_rsi = IntParameter(low=1, high=50, default=30, space="buy", optimize=True, load=True) sell_rsi = IntParameter(low=50, high=100, default=70, space="sell", optimize=True, load=True) # 在 populate_entry_trend 里引用 dataframe.loc[ ( (qtpylib.crossed_above(dataframe["rsi"], self.buy_rsi.value)) # ... 其他条件 ), "enter_long", ] = 1

它演示了「声明 → 引用」的最小闭环,新策略可以照着改。

本节要点回顾

  1. 四种参数类:IntParameter(整数)、DecimalParameter(限位数小数,推荐)、CategoricalParameter(枚举)、RealParameter(少用);BooleanParameter 是 Categorical 的简写。
  2. 声明语法:low / high / default 必填,space / decimals / optimize / load 可选;optimize=False 固定值,load=False 不继承上次结果(且影响回测)。
  3. space 归属:命名前缀(buy_/sell_/enter_/exit_/protection_)隐式推断,或 space= 显式指定;显式优先;无 space 的参数被静默忽略
  4. .value 引用:在 populate_entry/exit_trend 里用 self.参数.value 取当前值。
  5. .range 预算:指标周期类参数,在 populate_indicators.range 循环预算所有可能列;Hyperopt 模式产多列,回测模式产 1 列。
  6. 核心踩坑:populate_indicators 只跑一次,可优化参数在里面用会永远是 default;要么用 .range,要么移到 populate_entry_trend
  7. 性能:指标移到 entry_trend、或 --analyze-per-epoch、或缩小参数范围,任选其一降内存。
  8. Guards vs Triggers:设计上区分「过滤」和「触发」,避免粘连信号。

下一节,我们看损失函数——它定义了「什么叫好」,是 Hyperopt 的方向盘。


发布者: 作者: 灏天文库 转发
评论区 (0)
U