跳至内容

强化学习

安装包大小

强化学习依赖项包含一些大型包(如 torch),在执行 ./setup.sh -i 时需显式请求:当出现提示 "Do you also want dependencies for freqai-rl (~700mb additional space required) [y/N]?" 时回答 "y"。偏好使用 Docker 的用户应确保使用带有 _freqairl 后缀的镜像。

背景与术语

什么是强化学习,以及为什么 FreqAI 需要它?

强化学习包含两个关键组成部分:智能体(agent)和训练环境(environment)。在智能体训练过程中,智能体会逐根 K 线遍历历史数据,并在一组预设动作中做出选择:做多入场、做多离场、做空入场、做空离场或保持中性。在此训练过程中,环境会追踪这些动作的表现,并根据用户自定义的 calculate_reward() 函数给予智能体奖励(此处我们提供了一个默认奖励函数,供用户在此基础上进行扩展,详情见此)。该奖励用于训练神经网络中的权重。

FreqAI 强化学习实现中的另一个重要部分是使用状态信息。每一步都会将状态信息输入网络,包括当前收益、当前持仓状态和当前交易持续时间。这些信息用于在训练环境中训练智能体,并在模拟或实盘交易中对智能体进行强化(此功能在回测中不可用)。FreqAI 与 Freqtrade 是这一强化机制的理想组合,因为这些信息在实际部署中可直接获取。

强化学习是 FreqAI 的自然演进方向,因为它引入了一层新的适应性和市场响应能力,这是分类器和回归模型无法比拟的。然而,分类器和回归模型也有其优势,例如预测更加稳健。训练不当的强化学习智能体可能会找到“捷径”或“技巧”来最大化奖励,但实际上并未赢得任何交易。因此,强化学习更为复杂,要求使用者具备更高的理解水平。

强化学习接口

在当前框架下,我们旨在通过通用的“预测模型”文件暴露训练环境,该文件是一个用户继承的 BaseReinforcementLearner 对象(例如 freqai/prediction_models/ReinforcementLearner)。在这个用户类内部,可以通过 MyRLEnv 访问并定制强化学习环境,如下所示

我们预计大多数用户将主要精力集中在创造性地设计 calculate_reward() 函数上 详情见此,而保持环境其余部分不变。其他用户可能完全不修改环境,仅调整配置参数以及利用 FreqAI 中已有的强大特征工程功能。同时,我们也为高级用户提供支持,使其能够完全自定义自己的模型类。

该框架基于 stable_baselines3(torch)和 OpenAI gym 的基础环境类构建。但总体而言,模型类具有良好的隔离性,因此可以轻松集成其他竞争性库。对于环境部分,它继承自 gym.Env,这意味着若要切换到不同的库,则必须编写一个全新的环境。

重要注意事项

如上所述,代理是在一个人工交易“环境”中进行“训练”的。在我们的案例中,该环境可能看起来与真实的 Freqtrade 回测环境非常相似,但实际上它并不相同。事实上,强化学习训练环境被大大简化了,它并未包含任何复杂的策略逻辑,例如 custom_exitcustom_stoploss、杠杆控制等回调函数。相反,RL 环境是对真实市场的非常“原始”的表示,在此环境中,代理可以自由地学习策略(即:止损、止盈等),而这些策略由 calculate_reward() 函数来实施。因此,必须认识到,代理的训练环境与现实世界并不完全一致。

运行强化学习

设置并运行强化学习模型的过程与运行回归器或分类器相同。命令行中必须定义相同的两个标志:--freqaimodel--strategy

freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --config config.json

其中 ReinforcementLearner 将使用位于 freqai/prediction_models/ReinforcementLearner 的模板化 ReinforcementLearner(或用户自定义并存放于 user_data/freqaimodels 中的模型)。另一方面,策略遵循与其他典型回归器相同的特征工程流程,使用 feature_engineering_* 方法。不同之处在于目标值的生成——强化学习不需要显式的目标标签。然而,FreqAI 要求在动作列中设置一个默认(中性)值:

    def set_freqai_targets(self, dataframe, **kwargs) -> DataFrame:
        """
        *Only functional with FreqAI enabled strategies*
        Required function to set the targets for the model.
        All targets must be prepended with `&` to be recognized by the FreqAI internals.

        More details about feature engineering available:

        https://www.freqtrade.io/en/latest/freqai-feature-engineering

        :param df: strategy dataframe which will receive the targets
        usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
        """
        # For RL, there are no direct targets to set. This is filler (neutral)
        # until the agent sends an action.
        dataframe["&-action"] = 0
        return dataframe

大部分函数的功能保持不变,但以下函数展示了策略如何将原始价格数据传递给代理,以便其在训练环境中能够访问原始的 OHLCV 数据:

    def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame:
        # The following features are necessary for RL models
        dataframe[f"%-raw_close"] = dataframe["close"]
        dataframe[f"%-raw_open"] = dataframe["open"]
        dataframe[f"%-raw_high"] = dataframe["high"]
        dataframe[f"%-raw_low"] = dataframe["low"]
    return dataframe

最后,无需明确创建“标签”——而是需要分配 &-action 列,该列将在 populate_entry/exit_trends() 中被访问时包含代理的动作。在当前示例中,中性动作为 0。该值应与所使用的环境保持一致。FreqAI 提供了两个环境,均使用 0 作为中性动作。

当用户意识到无需设置标签后,他们很快就会明白,代理正在自行做出进出仓决策。这使得策略构建变得相当简单。入场和离场信号以整数形式由代理提供,并直接用于在策略中决定买卖操作:

    def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:

        enter_long_conditions = [df["do_predict"] == 1, df["&-action"] == 1]

        if enter_long_conditions:
            df.loc[
                reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"]
            ] = (1, "long")

        enter_short_conditions = [df["do_predict"] == 1, df["&-action"] == 3]

        if enter_short_conditions:
            df.loc[
                reduce(lambda x, y: x & y, enter_short_conditions), ["enter_short", "enter_tag"]
            ] = (1, "short")

        return df

    def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
        exit_long_conditions = [df["do_predict"] == 1, df["&-action"] == 2]
        if exit_long_conditions:
            df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1

        exit_short_conditions = [df["do_predict"] == 1, df["&-action"] == 4]
        if exit_short_conditions:
            df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1

        return df

需要注意的是,&-action 的含义取决于所选择的环境。上述示例展示了 5 种动作:0 表示中性,1 表示做多入场,2 表示做多离场,3 表示做空入场,4 表示做空离场。

配置强化学习器

为了配置 Reinforcement Learner,必须在 freqai 配置文件中存在以下字典:

        "rl_config": {
            "train_cycles": 25,
            "add_state_info": true,
            "max_trade_duration_candles": 300,
            "max_training_drawdown_pct": 0.02,
            "cpu_count": 8,
            "model_type": "PPO",
            "policy_type": "MlpPolicy",
            "model_reward_parameters": {
                "rr": 1,
                "profit_aim": 0.025
            }
        }

参数详情请参见此处,但通常来说,train_cycles 决定了智能体在其人工环境中对 K 线数据进行训练时循环的次数,以调整模型中的权重。model_type 是一个字符串,用于从 stable_baselines(外部链接)中选择可用的模型之一。

注意

如果你想尝试 continual_learning 功能,则应在主 freqai 配置字典中将该值设为 true。这会通知强化学习库在每次重新训练时,从先前模型的最终状态继续训练新模型,而不是每次都从头开始重新训练。

注意

请注意,通用的 model_training_parameters 字典应包含针对特定 model_type 的所有模型超参数自定义设置。例如,PPO 的参数可在 此处 找到。

创建自定义奖励函数

不适用于生产环境

警告!Freqtrade 源代码中提供的奖励函数仅用于功能展示,旨在尽可能多地演示/测试环境控制的各种特性,同时也设计为可在小型计算机上快速运行。这是一个基准示例,不可用于实际生产。请注意,你需要自行创建自定义的 reward() 函数,或使用其他用户在 Freqtrade 源代码之外构建的模板。

当你开始修改策略和预测模型时,很快就会意识到强化学习器与回归器/分类器之间的一些重要区别。首先,策略不会设定目标值(即没有标签!)。相反,你需要在 MyRLEnv 类中设置 calculate_reward() 函数(见下文)。默认的 calculate_reward() 函数位于 prediction_models/ReinforcementLearner.py 中,用于展示创建奖励所需的必要组件,但这并不适用于生产环境。用户必须创建自己的自定义强化学习模型类,或使用来自 Freqtrade 源代码之外的预建模型,并将其保存至 user_data/freqaimodels 目录。正是在 calculate_reward() 中,你可以表达关于市场的创造性理论。例如,当智能体完成一笔盈利交易时给予奖励,亏损交易时予以惩罚;或者你希望在智能体开仓时给予奖励,持仓时间过长时进行惩罚。以下我们展示了这些奖励的具体计算方式:

提示

最佳的奖励函数应具备连续可导性和良好的缩放性。换句话说,对罕见事件施加单次大幅负向惩罚并不是一个好的做法,神经网络难以从中有效学习。更好的方法是对常见事件施加较小的负向惩罚,这有助于智能体更快地学习。不仅如此,你还可以通过线性或指数函数使奖励/惩罚随严重程度变化,从而提升其连续性。例如,随着持仓时间延长,逐步增加惩罚力度,这比在某一时刻突然施加一次重大惩罚效果更好。

from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions


class MyCoolRLModel(ReinforcementLearner):
    """
    User created RL prediction model.

    Save this file to `freqtrade/user_data/freqaimodels`

    then use it with:

    freqtrade trade --freqaimodel MyCoolRLModel --config config.json --strategy SomeCoolStrat

    Here the users can override any of the functions
    available in the `IFreqaiModel` inheritance tree. Most importantly for RL, this
    is where the user overrides `MyRLEnv` (see below), to define custom
    `calculate_reward()` function, or to override any other parts of the environment.

    This class also allows users to override any other part of the IFreqaiModel tree.
    For example, the user can override `def fit()` or `def train()` or `def predict()`
    to take fine-tuned control over these processes.

    Another common override may be `def data_cleaning_predict()` where the user can
    take fine-tuned control over the data handling pipeline.
    """
    class MyRLEnv(Base5ActionRLEnv):
        """
        User made custom environment. This class inherits from BaseEnvironment and gym.Env.
        Users can override any functions from those parent classes. Here is an example
        of a user customized `calculate_reward()` function.

        Warning!
        This is function is a showcase of functionality designed to show as many possible
        environment control features as possible. It is also designed to run quickly
        on small computers. This is a benchmark, it is *not* for live production.
        """
        def calculate_reward(self, action: int) -> float:
            # first, penalize if the action is not valid
            if not self._is_valid(action):
                return -2
            pnl = self.get_unrealized_profit()

            factor = 100

            pair = self.pair.replace(':', '')

            # you can use feature values from dataframe
            # Assumes the shifted RSI indicator has been generated in the strategy.
            rsi_now = self.raw_features[f"%-rsi-period_10_shift-1_{pair}_"
                            f"{self.config['timeframe']}"].iloc[self._current_tick]

            # reward agent for entering trades
            if (action in (Actions.Long_enter.value, Actions.Short_enter.value)
                    and self._position == Positions.Neutral):
                if rsi_now < 40:
                    factor = 40 / rsi_now
                else:
                    factor = 1
                return 25 * factor

            # discourage agent from not entering trades
            if action == Actions.Neutral.value and self._position == Positions.Neutral:
                return -1
            max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
            trade_duration = self._current_tick - self._last_trade_tick
            if trade_duration <= max_trade_duration:
                factor *= 1.5
            elif trade_duration > max_trade_duration:
                factor *= 0.5
            # discourage sitting in position
            if self._position in (Positions.Short, Positions.Long) and \
            action == Actions.Neutral.value:
                return -1 * trade_duration / max_trade_duration
            # close long
            if action == Actions.Long_exit.value and self._position == Positions.Long:
                if pnl > self.profit_aim * self.rr:
                    factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
                return float(pnl * factor)
            # close short
            if action == Actions.Short_exit.value and self._position == Positions.Short:
                if pnl > self.profit_aim * self.rr:
                    factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
                return float(pnl * factor)
            return 0.

使用 Tensorboard

强化学习模型需要跟踪训练过程中的各项指标。FreqAI 已集成 Tensorboard,允许用户跨所有币种及多次重训练持续追踪训练和评估性能。可通过以下命令启用 Tensorboard:

tensorboard --logdir user_data/models/unique-id

其中 unique-id 是在 freqai 配置文件中设置的 identifier。该命令必须在单独的 shell 中运行,以便在浏览器中访问 127.0.0.1:6006 查看输出(6006 是 Tensorboard 使用的默认端口)。

tensorboard

自定义日志记录

FreqAI 还提供了一个内置的回合制摘要记录器,名为 self.tensorboard_log,用于向 Tensorboard 日志添加自定义信息。默认情况下,该函数已在环境中的每一步调用一次,以记录代理的动作。在单个回合中所有步骤累积的所有值将在该回合结束时统一报告,随后所有指标将重置为 0,为下一个回合做好准备。

self.tensorboard_log 也可以在环境内的任意位置使用,例如,可以将其添加到 calculate_reward 函数中,以收集有关奖励函数中各个部分被调用频率的更详细信息:

    class MyRLEnv(Base5ActionRLEnv):
        """
        User made custom environment. This class inherits from BaseEnvironment and gym.Env.
        Users can override any functions from those parent classes. Here is an example
        of a user customized `calculate_reward()` function.
        """
        def calculate_reward(self, action: int) -> float:
            if not self._is_valid(action):
                self.tensorboard_log("invalid")
                return -2

注意

self.tensorboard_log() 函数仅用于跟踪递增对象,即训练环境中的事件或动作。如果关注的事件是一个浮点数,则可将该浮点数作为第二个参数传入,例如 self.tensorboard_log("float_metric1", 0.23)。在这种情况下,指标值不会累加。

选择基础环境

FreqAI 提供了三种基础环境:Base3ActionRLEnvironmentBase4ActionEnvironmentBase5ActionEnvironment。顾名思义,这些环境分别适用于可以选择 3 种、4 种或 5 种动作的代理。Base3ActionEnvironment 最为简单,代理可选择持有、做多或做空。此环境也可用于仅做多的机器人(它会自动遵循策略中的 can_short 标志),其中做多表示入场条件,做空表示出场条件。而在 Base4ActionEnvironment 中,代理可以选择做多入场、做空入场、保持空仓或平仓。最后,在 Base5ActionEnvironment 中,代理具备与 Base4 相同的操作,但不同于单一的平仓操作,它将平多头和平空头分开。选择不同环境带来的主要变化包括:

  • calculate_reward 中可用的动作
  • 用户策略所消费的动作

所有 FreqAI 提供的环境都继承自一个与动作/持仓无关的基础环境对象 BaseEnvironment,该对象包含所有共享逻辑。整个架构设计为易于定制。最简单的定制方式是重写 calculate_reward()(详见此处)。然而,定制还可以进一步扩展到环境内的任意函数。你可以通过在预测模型文件中的 MyRLEnv 内直接重写这些函数来实现。对于更高级的定制,建议创建一个全新并继承自 BaseEnvironment 的环境。

注意

只有 Base3ActionRLEnv 支持仅做多的训练/交易(需设置用户策略属性 can_short = False)。