第4章:Gymnasium环境搭建——从安装到自定义交易环境

强化学习里有个核心概念叫「环境交互」。说白了,智能体跟环境的关系就像小孩学走路——摔倒了爬起来,慢慢就知道怎么迈腿了。在量化投资里,这个环境就是市场,智能体就是我们的交易策略。而Gymnasium,就是帮我们搭建这个「市场模拟器」的标准工具。

我个人习惯把Gymnasium比作「乐高底座」。它本身提供了一些经典环境(比如倒立摆、赛车游戏),但真正做量化投资,你得自己搭积木——创建自定义的交易环境。这一章,我们就从安装开始,一步步走到自定义交易环境的交互循环。

4.1 安装Gymnasium

先搞定环境安装。我建议用Python 3.8以上版本,用pip直接装:

pip install gymnasium

如果你需要用到经典控制类环境(比如CartPole),再加个扩展包:

pip install gymnasium[classic-control]

嗯,这里要注意:有些老教程还在用gym库,但那个已经停止维护了。Gymnasium是它的官方继承者,API基本兼容,但修复了不少坑。我曾经在项目里因为用了旧版gym,结果环境重置时状态初始化出了问题,排查了半天……后来果断切到Gymnasium,世界清净了。

验证安装是否成功,跑个简单测试:

import gymnasium as gym
env = gym.make("CartPole-v1")
obs, info = env.reset()
print(obs)

如果打印出一串数字(比如[0.0, 0.0, 0.0, 0.0]),说明装好了。

4.2 Gymnasium的核心概念

在动手搭环境之前,先理解Gymnasium的几个核心组件。你想想看,一个强化学习环境其实就干三件事:

  • 状态(Observation):当前市场是什么情况?比如价格、持仓、账户余额。
  • 动作(Action):智能体能做什么?比如买入、卖出、持有。
  • 奖励(Reward):这一步做得好不好?比如赚了钱给正奖励,亏了给负奖励。

Gymnasium用一套标准接口把这些封装好了。核心方法就这几个:

方法作用我常用的场景
reset()重置环境到初始状态每个交易周期开始前调用
step(action)执行动作,返回新状态、奖励、是否结束等每次交易决策后调用
render()可视化环境(可选)调试时看看交易曲线
close()关闭环境训练结束后清理资源

为什么要有这套标准?因为强化学习算法(比如DQN、PPO)都依赖这个接口。你只要实现了stepreset,任何算法都能直接跑你的环境。这就是「乐高底座」的威力。

4.3 创建自定义交易环境

好,重头戏来了。我们要创建一个股票交易环境。这个环境模拟了:你有一笔初始资金,每天可以决定买多少、卖多少,目标是最大化最终资产。

我会继承gym.Env类,然后实现几个关键方法。先看整体框架:

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

class TradingEnv(gym.Env):
    """自定义股票交易环境"""
    
    def __init__(self, price_data, initial_balance=10000):
        super().__init__()
        
        self.price_data = price_data  # 历史价格数据
        self.initial_balance = initial_balance
        self.current_step = 0
        
        # 定义动作空间:0=持有, 1=买入, 2=卖出
        self.action_space = spaces.Discrete(3)
        
        # 定义状态空间:价格、持仓量、账户余额
        self.observation_space = spaces.Box(
            low=0, high=np.inf, shape=(3,), dtype=np.float32
        )
        
    def reset(self, seed=None, options=None):
        super().reset(seed=seed)
        self.current_step = 0
        self.balance = self.initial_balance
        self.holdings = 0  # 持仓股数
        return self._get_obs(), {}
    
    def step(self, action):
        # 获取当前价格
        current_price = self.price_data[self.current_step]
        
        # 执行动作
        if action == 1:  # 买入
            can_buy = self.balance // current_price
            self.holdings += can_buy
            self.balance -= can_buy * current_price
        elif action == 2:  # 卖出
            self.balance += self.holdings * current_price
            self.holdings = 0
        
        # 移动到下一步
        self.current_step += 1
        
        # 计算奖励:资产变化
        new_total = self.balance + self.holdings * current_price
        reward = new_total - self._last_total
        self._last_total = new_total
        
        # 判断是否结束
        terminated = self.current_step >= len(self.price_data) - 1
        
        return self._get_obs(), reward, terminated, False, {}
    
    def _get_obs(self):
        current_price = self.price_data[self.current_step]
        return np.array([current_price, self.holdings, self.balance], dtype=np.float32)

这段代码看起来简单,但有几个关键点我要强调一下:

动作空间设计:我用的是离散动作(0,1,2),因为交易决策本质上是分类问题。如果你要做连续仓位控制(比如买入30%仓位),那就得用spaces.Box

奖励函数的小技巧:我习惯用「资产变化量」作为即时奖励,而不是直接用总资产。因为强化学习对奖励的尺度很敏感,用变化量可以让训练更稳定。曾经我直接用总资产做奖励,结果智能体学会了「躺平」——因为只要不交易,资产就不变,奖励为0,反而比亏钱好……

注意状态归一化:价格和余额的量级可能差很多(价格几十块,余额几万块)。如果不做归一化,神经网络训练时会出问题。我一般会把价格除以初始价格,余额除以初始余额,让所有状态都在0~1之间。

4.4 环境交互循环

环境搭好了,怎么用?标准交互循环长这样:

# 准备数据(假设有100天的价格)
price_data = np.random.randn(100) * 10 + 100  # 模拟价格

# 创建环境
env = TradingEnv(price_data, initial_balance=10000)

# 交互循环
obs, _ = env.reset()
total_reward = 0
done = False

while not done:
    # 随机策略(实际会用训练好的模型)
    action = env.action_space.sample()
    
    # 执行动作
    obs, reward, terminated, truncated, info = env.step(action)
    total_reward += reward
    done = terminated or truncated
    
    print(f"Step {env.current_step}: Action={action}, Reward={reward:.2f}, Balance={env.balance:.2f}")

print(f"Total Reward: {total_reward:.2f}")
env.close()

这个循环就是强化学习的「心脏」。每次step返回五个值:

  • obs:新的状态
  • reward:这一步的奖励
  • terminated:是否达到终止条件(比如交易结束)
  • truncated:是否被截断(比如超时)
  • info:额外信息(调试用)

为什么要有terminatedtruncated两个标志?这是Gymnasium的新设计。前者表示「自然结束」(比如数据用完了),后者表示「人为截断」(比如为了防止无限循环设置的最大步数)。在量化交易里,我通常把数据用完设为terminated,把单次交易最大持仓天数设为truncated

4.5 知识体系总览

这一章的内容,我用一张图来总结:

Gymnasium环境搭建知识体系 安装与配置 核心概念 自定义环境 pip install gymnasium 状态/动作/奖励 继承gym.Env 环境交互循环 reset() → step(action) → 收集数据 → 训练 → 重复

这张图把本章的知识点串起来了。从安装开始,理解核心概念,然后动手搭自定义环境,最后用交互循环跑起来。每一步都是下一环的基础。

4.6 避坑指南

最后分享几个我踩过的坑:

  • 状态空间维度不匹配observation_space定义的shape必须和_get_obs()返回的数组维度一致。我曾经少写了一个维度,结果算法训练时直接报错,排查了半天才发现是shape写错了。
  • 奖励尺度问题:如果奖励值太大(比如几百万),神经网络梯度会爆炸。我一般会把奖励除以一个常数(比如1000),让它在-1到1之间。
  • 环境重置不彻底reset()方法里一定要把所有状态变量都初始化。我遇到过忘记重置holdings,结果第二个交易周期一开始就带着上一轮的持仓……

嗯,这一章的内容就到这里。环境搭好了,下一章我们就可以开始训练智能体了。记住:环境是强化学习的基石,花时间把环境搭好,后面会省很多事。


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