第 7 章 · 02 搜索空间与可优化参数 本节摘要:本节是 Hyperopt 章的实操核心,讲清怎么在策略里声明「可优化参数」。freqtrade 提供四种参数类: (整数)、 (限定位数小数,推荐)、 (全精度小数,少用)、 (枚举),外加 (Categorical 的简写)。本节先用一张表对比这四种的适用场景,再讲清声明的完整语法(范围、默认值、space、optimize、load 等选项),然后用一个完整示例展示「声明 → 在 populate 里用 引用 → 用 预算多列指标」的闭环。最后重点讲 space 的分配规则(命名约定 vs 显式 )、 / 的妙用,以及「为什么可优化参数不能用在 」这个高频踩坑。
本节摘要:本节是 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,汉化并套用体系化模板。
阅读完本节,你应当能够:
IntParameter / DecimalParameter / CategoricalParameter / RealParameter)。populate_* 里用 .value 引用当前值,用 .range 预算多列。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:
RealParameter在0.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=False:让某个参数「在策略里声明但固定值」。适合:你想保留参数化的灵活性,但这次只想优化别的参数。load=False:让某参数「每次 Hyperopt 都从 default 起步」,不继承上次结果。适合:上次优化的值你不确定好坏,想重新探索。⚠️ load=False 影响回测:
load=False不仅影响 Hyperopt,还影响回测——回测时这个参数也用 default,而不是上次 Hyperopt 找到的值。如果你想让回测用优化值,务必保持load=True(默认)。
一个参数属于哪个 space,有两种判定方式:
参数名以 buy_ / sell_ / enter_ / exit_ / protection_ 开头,会自动归到对应 space:
buy_rsi = IntParameter(10, 50, default=30) # 自动归 buy sell_rsi = IntParameter(50, 100, default=70) # 自动归 sell
space=命名不符合约定时,必须显式指定:
my_custom_threshold = IntParameter(10, 50, default=30, space="buy")
也可以用自定义 space 名(如 space="my_space"),然后用 --spaces my_space 优化它。注意 space 名不能是 all 或 default,且必须是合法 Python 标识符。
buy_adx = IntParameter(4, 24, default=14, space='sell') # 名字是 buy_,但显式归 sell
这种情况按 space='sell' 算。别写这种自相矛盾的代码,纯粹是给自己挖坑。
adx_period = IntParameter(4, 24, default=14) # 名字不带前缀,也没 space=
这种参数 Hyperopt 检测不到,不会参与优化,永远用 default。日志里不会有报错,只是默默忽略——这是隐蔽的坑。
💡 自检:跑 Hyperopt 前用
freqtrade list-strategies看参数计数,确认你声明的参数都被识别了。
声明后,在 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 注入,信号随之变化,回测出不同结果。
如果可优化的是指标周期(如 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
关键区别:
.range 返回完整范围([3, 4, ..., 50]),预算出 48 列,Hyperopt 从中选。.range 只返回当前 .value,只算 1 列。⚠️
.range只支持 Int / Decimal / Categorical:RealParameter因为空间无限,不提供.range。
这是最常见、最隐蔽的坑。官方明确:
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)。两个缓解手段:
populate_entry_trend:这个方法每个 epoch 都跑,不需要 .range,只算当前 .value 一列。代价是 CPU 增加。--analyze-per-epoch:把整个 populate_indicators 移到 epoch 进程里,每个 epoch 只算当前值。代价同样是 CPU 增加,但省内存。freqtrade hyperopt --strategy MyStrategy --analyze-per-epoch -e 500
💡 空间越小越好:无论用哪种方式,把参数范围设得尽可能小(
IntParameter(5, 20)比IntParameter(1, 200)快几十倍),是提升性能和降低过拟合的最简单手段。
Hyperopt 文档里有个经典的设计区分:
ADX > 25、price > EMA200。用 BooleanParameter 开关 + DecimalParameter / IntParameter 阈值。EMA5 上穿 EMA20、close 触及布林下轨。用 CategoricalParameter 选哪种触发。技术上两者无区别,但设计上区分能让信号「不粘连」(sticking)——粘连信号会在多根 K 线上持续触发,导致「信号快消失时才进场」,成功率低。Guard 控制过滤、Trigger 选时机,组合清晰。
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
它演示了「声明 → 引用」的最小闭环,新策略可以照着改。
IntParameter(整数)、DecimalParameter(限位数小数,推荐)、CategoricalParameter(枚举)、RealParameter(少用);BooleanParameter 是 Categorical 的简写。low / high / default 必填,space / decimals / optimize / load 可选;optimize=False 固定值,load=False 不继承上次结果(且影响回测)。buy_/sell_/enter_/exit_/protection_)隐式推断,或 space= 显式指定;显式优先;无 space 的参数被静默忽略。.value 引用:在 populate_entry/exit_trend 里用 self.参数.value 取当前值。.range 预算:指标周期类参数,在 populate_indicators 用 .range 循环预算所有可能列;Hyperopt 模式产多列,回测模式产 1 列。populate_indicators 只跑一次,可优化参数在里面用会永远是 default;要么用 .range,要么移到 populate_entry_trend。--analyze-per-epoch、或缩小参数范围,任选其一降内存。下一节,我们看损失函数——它定义了「什么叫好」,是 Hyperopt 的方向盘。