4、交易环境搭建(Gymnasium)

好,咱们进入实战环节了。环境搭建这一步,说白了就是给强化学习算法一个「 playground 」—— 一个它能反复试错、不断进化的交易模拟器。我个人习惯把这一步看作整个系统的地基,地基不稳,后面模型训练得再花哨也是白搭。

4.1 安装 Gymnasium 库

Gymnasium 是 OpenAI Gym 的继承者,社区活跃,接口也干净。安装其实就一行命令,但我建议你在虚拟环境里操作,别污染全局 Python。

pip install gymnasium

嗯,这里要注意。如果你打算用 GPU 加速,或者后面要接入自定义环境,最好顺手装上这几个依赖:

pip install gymnasium[all]

我在项目中遇到过一个问题 —— 装完 Gymnasium 后跑示例报错,说缺少「mujoco」。其实不用慌,如果你不做机器人仿真,交易环境用不到它。装个最小集就够了:

pip install gymnasium[classic-control]

验证安装是否成功,跑下面这段代码:

import gymnasium as gym
env = gym.make("CartPole-v1")
print(env.observation_space)
print(env.action_space)

能正常打印出状态空间和动作空间,说明环境装好了。你想想看,这一步其实就相当于你买了一台新电脑,先开机看看能不能点亮。

4.2 自定义交易环境类(继承 Env)

Gymnasium 内置的环境大多是游戏或机器人控制,没有现成的股票交易环境。所以我们要自己写一个,继承 gym.Env 基类。

为什么非要继承?说白了,Gymnasium 有一套固定的接口规范 —— resetsteprender 这些方法。强化学习算法库(比如 Stable-Baselines3)只认这套规范。你按规矩来,算法就能直接跑。

我刚开始做的时候,图省事自己写了个类,方法名随便起。结果对接算法库时各种报错,改了一下午。所以,老老实实继承 Env 吧。

import gymnasium as gym
from gymnasium import spaces
import numpy as np

class TradingEnv(gym.Env):
    """自定义股票交易环境"""
    metadata = {"render_modes": ["human"]}

    def __init__(self, data, render_mode=None):
        super().__init__()
        self.data = data
        self.current_step = 0

        # 后面会定义状态空间和动作空间
        self.observation_space = None
        self.action_space = None

    def reset(self, seed=None, options=None):
        # 重置环境状态
        pass

    def step(self, action):
        # 执行动作,返回新状态、奖励等
        pass

    def render(self):
        # 可视化(可选)
        pass

这个骨架搭好了,接下来就是往里面填肉。

4.3 定义状态空间与动作空间

这是整个环境设计的核心。状态空间决定了智能体能看到什么,动作空间决定了智能体能做什么。

状态空间

交易环境的状态,通常包含两部分:市场数据和持仓信息。我个人习惯用 spaces.Box 来表示连续状态空间。

self.observation_space = spaces.Box(
    low=-np.inf,
    high=np.inf,
    shape=(10,),  # 假设用10个特征
    dtype=np.float32
)

具体到特征,我一般会选这些:

  • 过去 N 天的收盘价(归一化后)
  • 成交量变化率
  • 技术指标(如 RSI、MACD 的差值)
  • 当前持仓数量
  • 账户现金余额

你可能会问:「特征越多越好吗?」 我在项目中踩过这个坑。一开始我加了 30 多个特征,模型训练慢得像蜗牛,而且效果反而变差。后来精简到 8-10 个核心特征,效果立竿见影。记住,特征不是越多越好,关键是有效。

动作空间

动作空间有两种常见设计:

类型 空间定义 适用场景
离散动作 spaces.Discrete(3) 买入、卖出、持有
连续动作 spaces.Box(-1, 1, shape=(1,)) 输出仓位比例(-1 全仓卖,1 全仓买)

我个人更推荐离散动作,尤其是初学者。为什么?因为离散空间探索起来更简单,算法收敛也快。连续动作虽然理论上更精细,但训练难度大不少。我刚开始做量化交易时,用连续动作折腾了两周没收敛,换成离散动作后两天就出效果了。

# 离散动作:0=持有,1=买入,2=卖出
self.action_space = spaces.Discrete(3)

4.4 实现 reset 与 step 方法

这两个方法是环境的核心,算法全靠它们来交互。

reset 方法

每次训练开始,或者一个 episode 结束,算法都会调用 reset。它的任务很简单:把环境恢复到初始状态。

def reset(self, seed=None, options=None):
    super().reset(seed=seed)
    self.current_step = 0
    self.balance = 10000  # 初始资金
    self.holdings = 0     # 初始持仓
    self.total_value = self.balance

    # 返回初始状态
    observation = self._get_observation()
    info = {}

    return observation, info

注意,Gymnasium 新版要求 reset 返回两个值:observation 和 info。我刚开始升级版本时没注意,一直报错,后来才发现接口变了。嗯,版本兼容性是个坑,记得留意。

step 方法

step 是环境最核心的方法。算法每走一步,都会调用它。它接收一个动作,返回四个值:

  • observation:执行动作后的新状态
  • reward:这一步获得的奖励
  • terminated:是否达到终止条件(如爆仓)
  • truncated:是否因为时间限制而结束
  • info:额外信息(用于调试)
def step(self, action):
    # 1. 执行动作
    current_price = self.data[self.current_step]
    if action == 1:  # 买入
        self.holdings += self.balance / current_price
        self.balance = 0
    elif action == 2:  # 卖出
        self.balance += self.holdings * current_price
        self.holdings = 0

    # 2. 更新步数
    self.current_step += 1

    # 3. 计算奖励(这里用简单的资产变化率)
    new_total = self.balance + self.holdings * current_price
    reward = (new_total - self.total_value) / self.total_value
    self.total_value = new_total

    # 4. 判断是否结束
    terminated = self.total_value <= 0  # 爆仓
    truncated = self.current_step >= len(self.data) - 1  # 数据用完

    # 5. 获取新状态
    observation = self._get_observation()
    info = {"total_value": self.total_value}

    return observation, reward, terminated, truncated, info

这里有个细节:奖励函数的设计。我见过很多人直接用「盈利金额」做奖励,结果模型学会了「死扛亏损等反弹」。后来我改用「夏普比率」或「收益波动比」,模型行为就稳健多了。你想想看,奖励函数其实就是在告诉智能体「什么是对的」,设计不好,它就会钻空子。

核心要点总结:

  • 继承 gym.Env,遵循标准接口
  • 状态空间用 spaces.Box,动作空间用 spaces.Discretespaces.Box
  • reset 返回初始状态,step 返回五元组
  • 奖励函数是灵魂,设计要谨慎

小技巧: 调试环境时,可以写一个随机策略来测试。让智能体随机选动作,看看环境能不能正常跑完一个 episode。我每次写完新环境都会先跑这个测试,能省下不少 debug 时间。

注意: Gymnasium 从 v0.26 开始,step 返回五元组(多了 truncated),不再是四元组。如果你用的算法库比较老,可能需要做兼容处理。我曾经因为这个不兼容问题,排查了整整一个下午。

知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以把它当作搭建交易环境的「路线图」:

交易环境搭建核心流程 安装 Gymnasium 继承 Env 基类 定义状态空间 定义动作空间 reset() step() render() 四步走:安装 → 继承 → 定义空间 → 实现核心方法

好了,环境搭建的核心内容就这些。你按照这个流程走,一个能跑的交易环境就出来了。下一节我们会在这个环境上跑一个简单的强化学习算法,看看智能体到底能不能学会交易。


公众号:蓝海资料掘金营,微信deep3321