跳至内容

高级 Hyperopt

本页介绍了一些高级 Hyperopt 主题,相比创建普通的超优化类,这些主题可能需要更高的编程技能和 Python 知识。

创建和使用自定义损失函数

要使用自定义损失函数类,请确保在自定义的 hyperopt 损失类中定义了函数 hyperopt_loss_function。对于下面的示例,你需要在 hyperopt 调用中添加命令行参数 --hyperopt-loss SuperDuperHyperOptLoss,以便使用该函数。

以下是一个示例,其内容与默认的 Hyperopt 损失实现完全相同。完整示例可在 userdata/hyperopts 中找到。

from datetime import datetime
from typing import Any, Dict

from pandas import DataFrame

from freqtrade.constants import Config
from freqtrade.optimize.hyperopt import IHyperOptLoss

TARGET_TRADES = 600
EXPECTED_MAX_PROFIT = 3.0
MAX_ACCEPTED_TRADE_DURATION = 300

class SuperDuperHyperOptLoss(IHyperOptLoss):
    """
    Defines the default loss function for hyperopt
    """

    @staticmethod
    def hyperopt_loss_function(
        *,
        results: DataFrame,
        trade_count: int,
        min_date: datetime,
        max_date: datetime,
        config: Config,
        processed: dict[str, DataFrame],
        backtest_stats: dict[str, Any],
        starting_balance: float,
        **kwargs,
    ) -> float:
        """
        Objective function, returns smaller number for better results
        This is the legacy algorithm (used until now in freqtrade).
        Weights are distributed as follows:
        * 0.4 to trade duration
        * 0.25: Avoiding trade loss
        * 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
        """
        total_profit = results['profit_ratio'].sum()
        trade_duration = results['trade_duration'].mean()

        trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
        profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
        duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)
        result = trade_loss + profit_loss + duration_loss
        return result

目前,该函数的参数包括:

  • results:包含交易结果的 DataFrame。results 中包含以下列(对应于使用 --export trades 时回测输出文件中的数据):
    pair, profit_ratio, profit_abs, open_date, open_rate, fee_open, close_date, close_rate, fee_close, amount, trade_duration, is_open, exit_reason, stake_amount, min_rate, max_rate, stop_loss_ratio, stop_loss_abs
  • trade_count:交易数量(等同于 len(results)
  • min_date:所用时间范围的起始日期
  • max_date:所用时间范围的结束日期
  • config:使用的配置对象(注意:如果某些策略相关参数属于 hyperopt 参数空间,则此处可能不会更新其值)。
  • processed:包含用于回测数据的 Dataframe 字典,键为交易对。
  • backtest_stats:回测统计信息,格式与回测文件中的“strategy”子结构相同。可用字段可参考 optimize_reports.py 中的 generate_strategy_stats() 函数。
  • starting_balance:回测所用的初始余额。

该函数必须返回一个浮点数(float)。返回值越小,表示结果越好。具体参数选择和权衡由你自行决定。

注意

此函数每个训练周期(epoch)调用一次,因此请尽量优化其实现,避免不必要地拖慢 hyperopt 的运行速度。

*args**kwargs

请在接口中保留 *args**kwargs 参数,以便我们未来能够扩展此接口。

重写预定义参数空间

要重写预定义的参数空间(如 roi_spacegenerate_roi_tablestoploss_spacetrailing_spacemax_open_trades_space),请定义一个名为 Hyperopt 的嵌套类,并按如下方式定义所需的参数空间:

from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal

class MyAwesomeStrategy(IStrategy):
    class HyperOpt:
        # Define a custom stoploss space.
        def stoploss_space():
            return [SKDecimal(-0.05, -0.01, decimals=3, name='stoploss')]

        # Define custom ROI space
        def roi_space() -> List[Dimension]:
            return [
                Integer(10, 120, name='roi_t1'),
                Integer(10, 60, name='roi_t2'),
                Integer(10, 40, name='roi_t3'),
                SKDecimal(0.01, 0.04, decimals=3, name='roi_p1'),
                SKDecimal(0.01, 0.07, decimals=3, name='roi_p2'),
                SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'),
            ]

        def generate_roi_table(params: Dict) -> dict[int, float]:

            roi_table = {}
            roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
            roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
            roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
            roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0

            return roi_table

        def trailing_space() -> List[Dimension]:
            # All parameters here are mandatory, you can only modify their type or the range.
            return [
                # Fixed to true, if optimizing trailing_stop we assume to use trailing stop at all times.
                Categorical([True], name='trailing_stop'),

                SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'),
                # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive',
                # so this intermediate parameter is used as the value of the difference between
                # them. The value of the 'trailing_stop_positive_offset' is constructed in the
                # generate_trailing_params() method.
                # This is similar to the hyperspace dimensions used for constructing the ROI tables.
                SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'),

                Categorical([True, False], name='trailing_only_offset_is_reached'),
        ]

        # Define a custom max_open_trades space
        def max_open_trades_space(self) -> List[Dimension]:
            return [
                Integer(-1, 10, name='max_open_trades'),
            ]

注意

所有重写操作均为可选,可根据需要任意组合使用。

动态参数

参数也可以动态定义,但必须在 bot_start() 回调函数 被调用后对实例可用。

class MyAwesomeStrategy(IStrategy):

    def bot_start(self, **kwargs) -> None:
        self.buy_adx = IntParameter(20, 30, default=30, optimize=True)

    # ...

警告

以这种方式创建的参数不会出现在 list-strategies 命令的参数计数中。

重写基础估计器

你可以通过在 Hyperopt 子类中实现 generate_estimator() 方法,来为 Hyperopt 定义自己的 optuna 采样器。

class MyAwesomeStrategy(IStrategy):
    class HyperOpt:
        def generate_estimator(dimensions: List['Dimension'], **kwargs):
            return "NSGAIIISampler"

可选值包括 "NSGAIISampler"、"TPESampler"、"GPSampler"、"CmaEsSampler"、"NSGAIIISampler"、"QMCSampler"(详细信息请参见 optuna-samplers 文档),或一个继承自 optuna.samplers.BaseSampler 的类的实例。

例如,你可能需要自行研究以查找更多采样器(如来自 optunahub 的采样器)。

注意

虽然可以提供自定义估计器,但作为用户,你需要自行研究可能的参数,并分析/理解应使用哪些参数。如果你对此不确定,最好使用默认选项之一(例如 "NSGAIIISampler" 已被证明是最通用的),且无需额外参数。

在 Optunahub 中使用 AutoSampler

AutoSampler 文档

安装必要的依赖项

pip install optunahub cmaes torch scipy 
在你的策略中实现 generate_estimator()

# ... from freqtrade.strategy.interface import IStrategy from typing import List import optunahub # ...  class my_strategy(IStrategy): class HyperOpt: def generate_estimator(dimensions: List["Dimension"], **kwargs): if "random_state" in kwargs.keys(): return optunahub.load_module("samplers/auto_sampler").AutoSampler(seed=kwargs["random_state"]) else: return optunahub.load_module("samplers/auto_sampler").AutoSampler() 

显然,同样的方法也适用于 Optuna 支持的所有其他采样器。

参数空间选项

对于额外的参数空间,scikit-optimize(与 Freqtrade 结合使用)提供了以下类型的空间:

  • 分类(Categorical) - 从类别列表中选择(例如 Categorical(['a', 'b', 'c'], name="cat")
  • Integer - 从一组整数范围内选择(例如 Integer(1, 10, name='rsi')
  • SKDecimal - 从有限精度的小数范围内选择(例如 SKDecimal(0.1, 0.5, decimals=3, name='adx'))。仅在 freqtrade 中可用
  • Real - 从高精度小数范围内选择(例如 Real(0.1, 0.5, name='adx')

你可以从 freqtrade.optimize.space 导入上述所有类型,其中 CategoricalIntegerReal 实际上只是对应 scikit-optimize 空间的别名。SKDecimal 由 freqtrade 提供,用于实现更快的优化。

from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Real  # noqa

SKDecimal 与 Real 的对比

在几乎所有情况下,我们都推荐使用 SKDecimal 而不是 Real 空间。虽然 Real 空间提供了完整的精度(可达约 16 位小数),但这种精度很少被需要,并会导致超参数优化时间不必要的延长。

假设定义一个相对较小的空间(SKDecimal(0.10, 0.15, decimals=2, name='xxx'))—— SKDecimal 将只有 6 种可能值([0.10, 0.11, 0.12, 0.13, 0.14, 0.15])。

而对应的 Real 空间 Real(0.10, 0.15, name='xxx') 则具有近乎无限的可能性([0.10, 0.010000000001, 0.010000000002, ... 0.014999999999, 0.01500000000]