TradingGym · GYMNASIUM / SB3

TradingGym 怎么接 Gymnasium 和 Stable-Baselines3:必须自己写一层适配

TradingGym 是「模仿 Gym 风格」,不是 Gym 生态的一部分:全仓库没有 import gym,环境类不继承 gym.Envobservation_space 声明成一维 512 而实际返回二维矩阵,step() 返回四元组。想用 SB3 训练,必须自己写适配层——这页给出可直接复制的写法。

  • 四条硬事实:非 gym.Env · 空间声明不符 · 四元组返回 · 无 seed/close
  • 适配层要做的四件事:包成 gym.Env · 修正空间 · 转五元组 · 提供 reset(seed=…)
  • 算法要自备:仓库里没有任何算法实现,README 里四个算法都是 WIP

TradingGym 适配层放在哪里?

TradingGym 原生 envgym.Env,四元组返回
自写适配层包成 gym.Env、修正空间、转五元组
Gymnasium APIreset(seed) / 五元组 step
Stable-Baselines3PPO/DQN 等算法入口
依据源码事实(无 import gymobservation_space 声明为 np.array([obs_len*feature_len,])step 返回四值)绘制的适配层示意(非官方流程图)。中间那一层必须由使用者自己实现。

TradingGym · 四条硬事实

为什么不能直接把 TradingGym 丢给 SB3

下面四条都来自源码与实测,任一条都会让「直接传入」失败。先认清这四点,再写适配层就不会白折腾。

与 Gym 生态的四处不兼容(逐条给源码位置)
问题事实源码/实测依据导致的失败适配层怎么处理
不是 gym.Env 子类类定义为 class trading_env:,全仓库无 import gymgrep import gym 零命中SB3 的 check_env 直接拒绝写一个继承 gym.Env 的包装类
空间声明与实际不符observation_space = np.array([obs_len*feature_len,])(一维 512),实际观测是 (obs_len, 特征数+8) 二维backtest_v1.py:41,48;实测 obs 形状 (64,16)SB3 校验形状不一致时报错按真实形状重建 Box/Discrete 空间
action_space 类型不统一v1 里是整数 3,v0 里是 np.array([3,])backtest_v1.py:41training_v0.py:38action_space.n 时报 AttributeError统一声明 gym.spaces.Discrete(3)
返回四元组且无 seed/closestep() 返回 (obs, reward, done, info)reset() 不接收 seed,也没有 close()backtest_v1.py:280training_v1.py:82Gymnasium 要求五元组与 reset(seed=…)(obs, reward, terminated, truncated, info),并自己管随机种子
观测含持仓信息return_transaction=True 时观测拼接 8 列持仓/成本/盈亏backtest_v1.py:118-128与论文设定的纯行情观测不一致按需裁剪观测维度,并在报告里写明
无算法实现README 里 simple dqn / policy gradient / actor-critic / A3C 全部标 WIPREADME + 文件树以为「装完就有模型」算法完全由使用者提供(如 SB3)

TradingGym · 适配层

可复制的适配层是什么?把 TradingGym 包成标准 gym.Env

这段代码做了四件事:继承 gym.Env、按真实形状声明空间、把四元组转成五元组、统一随机种子入口。它不修改 TradingGym 源码。

第 1 步 · 依赖与兼容补丁

pip install gymnasium stable-baselines3
# 另需 TradingGym 本身:git clone https://github.com/Yvictor/TradingGym.git && cd TradingGym && python setup.py install

Gymnasium 与 SB3 是外部依赖,TradingGym 的 setup.py 里没有它们;gym 老库也不需要装(项目本来就不用)。

预期输出:import gymnasiumimport trading_env 均成功。

第 2 步 · 包装类

import numpy as np, gymnasium as gym, trading_env

class TradingGymEnv(gym.Env):
    metadata = {"render_modes": []}

    def __init__(self, env_id="backtest_v1", obs_data_len=64, step_len=32, df=None,
                 fee=0.0, max_position=1, deal_col_name="Price", feature_names=None,
                 drop_position_info=False):
        super().__init__()
        self.drop = drop_position_info
        self._env = trading_env.make(env_id=env_id, obs_data_len=obs_data_len, step_len=step_len,
                                     df=df, fee=fee, max_position=max_position,
                                     deal_col_name=deal_col_name, feature_names=feature_names)
        n_feat = len(feature_names)
        n_obs  = n_feat if drop_position_info else n_feat + 8     # v1 观测拼接了 8 列附加信息
        lower  = np.full((obs_data_len, n_obs), -np.inf, dtype=np.float32)
        upper  = np.full((obs_data_len, n_obs),  np.inf, dtype=np.float32)
        self.observation_space = gym.spaces.Box(low=lower, high=upper, dtype=np.float32)
        self.action_space = gym.spaces.Discrete(3)                # 0=不动 1=做多方向 2=做空方向
        self._done = False

    def _obs(self, obs):
        o = np.asarray(obs, dtype=np.float32)
        return o[:, :o.shape[1] - 8] if self.drop else o

    def reset(self, *, seed=None, options=None):
        super().reset(seed=seed)
        if seed is not None:
            np.random.seed(seed)             # TradingGym 内部用全局 np.random 抽日
        obs = self._env.reset()
        self._done = False
        return self._obs(obs), {}

    def step(self, action):
        obs, reward, done, info = self._env.step(int(action))
        terminated = bool(done)
        truncated = False                    # v1 的 done 只有「当日 tick 用尽」一种来源
        if terminated and getattr(self._env, "backtest_done", False):
            truncated = False
        return self._obs(obs), float(reward), terminated, truncated, {"info": getattr(self._env, "info", None)}

    def close(self):
        self._env = None

要点:①空间按真实观测形状声明;②reset(seed) 里把种子传给全局 np.random,否则抽日不可复现;③done 只映射到 terminated,因为你无法从四元组里区分截断。

预期输出:TradingGymEnv(...) 实例化成功,env.observation_space.shape == (64, 16)

第 3 步 · 用 check_env 自检

from stable_baselines3.common.env_checker import check_env
check_env(TradingGymEnv(df=df, feature_names=FEATURES), warn=True)

check_env 会检查空间一致性、reset/step 返回结构、动作合法性。它能抓住「空间声明与实际不符」这类问题——也正是原环境直接传入时最会踩的坑。

预期输出:无异常(若报 observation 不在 space 内,说明维度或 dtype 写错了)。

适配层不改变 TradingGym 的行为:reward 仍是窗口求和、账本仍是 env.info。训练时的 reward 与评估时的净值是两套口径,不要混用(见「奖励与基准」页)。

TradingGym · 接口迁移

TradingGym 怎么从旧式 Gym 接口迁到 Gymnasium?逐项对照

TradingGym 用的是 OpenAI Gym 时代的老式接口,网上大量教程也停留在那一代。下表把差异集中在一处,便于你改写别人的示例代码。

旧接口 → Gymnasium 对照(TradingGym 现状在左列) 环节TradingGym 现状(老式)Gymnasium 要求适配写法注意点 reset 返回值只返回 obs(obs, info)return obs, {}info 可以给空字典 reset 参数无参数reset(*, seed=None, options=None)签名对齐后自行 np.random.seed(seed)不接 seed 就无法复现抽日 step 返回值(obs, reward, done, info)(obs, reward, terminated, truncated, info)terminated = done; truncated = False无法区分自然终止与超时,需在报告里说明 空间类型np.array([3,]) 或整数 3gym.spaces.Discrete(3)重新声明SB3 依赖 action_space.n 观测空间声明一维、实际二维必须与返回一致按真实形状建 Box这是最常被 check_env 拦下的点 close()不存在可选但推荐提供空实现或清理引用多环境并行时尤其重要 渲染render() 依赖 matplotlib 实时画图推荐 render_mode 规范关掉渲染,或迁移到独立绘图脚本训练循环里开渲染会显著拖慢

TradingGym · 训练前检查

TradingGym 开训之前要先确认哪六件事?

RL 训练跑了几小时才发现口径错了,是这类项目的常见浪费。下面六项在训前各花一分钟就能确认。

训练前自检(每项给判据与不通过的后果) #检查项判据不通过的后果怎么改 1观测维度与声明一致check_env 无告警SB3 直接报错或静默维度错配按实际列数重建 Box 2动作空间与 action_describe 对应Discrete(3) 且语义为 不动/做多/做空策略学出来的动作含义错位写注释并在奖励设计里保持一致 3reward 是否可解释能说清它是窗口求和还是增量训练曲线好但回测亏统一评估口径为账本 4种子是否可控同一 seed 两次跑结果一致实验无法复现reset(seed) 透传 + 固定 numpy 种子 5episode 边界是否合理一天 = 一个 episode,步数符合预期episode 过短导致学习不到东西检查每日 tick 数 ≥ 窗口+步长 6成本是否已计入fee 设置与实验目的相符学到高换手策略,实盘不可行fee 0/0.1 对照训练

TradingGym · 算法现实

TradingGym 自带算法吗?仓库里没有 PPO/DQN,README 全标 WIP

这是最容易被误解的一点:TradingGym 提供的是环境,不是算法。README 里列出的四个算法方向,仓库中都没有对应实现文件。

README 写了什么

在 Algorithms 一节的列表里,simple dqnpolicy gradientactor-criticA3C with RNN 都标着 WIP(进行中)。仓库的文件树里也没有这些模块——它只有 trading_env/ 包的四个环境与一个自测脚本。

你实际要准备什么

一套 RL 算法实现(如 Stable-Baselines3 的 PPO/DQN)、训练循环、评估流程与超参搜索。也就是说,「用 TradingGym 做 RL 交易」这件事的算法部分完全在你这边。

「以为有」与「实际有」对照 你可能以为实际情况依据替代方案注意点 装完就能训练 PPO没有 PPO 实现README 标 WIP,仓库无对应文件自备 SB3 等库需要先写适配层 有训练脚本模板只有 100 步的包内自测(python -m trading_env.test)官方自测脚本自己写训练循环自测只验证接口可用 有超参建议没有任何超参文档README 无相关内容用算法库默认值起步报告里要写明超参 能接实盘实时交易是 README 里的将来计划README 首段无(本项目不提供)不要以此做实盘改造 有多品种/多市场支持只接受你给的单个 DataFramemake() 签名自己循环多标的环境每只标的一份数据一个环境 有数据下载无任何数据接口源码 import 清单自备数据源样例数据只有一个品种 8 天

TradingGym · 训练与评估

TradingGym 接上 SB3 之后:训练口径与评估口径怎么分开

训练看 reward,评估看账本——这两件事必须分开,否则你会得到「训练曲线很好、回测是负的」这类结论。

训练:用 reward,但要知道它是什么

from stable_baselines3 import PPO
model = PPO("MlpPolicy", TradingGymEnv(df=df, feature_names=FEATURES), verbose=1, seed=0)
model.learn(total_timesteps=200_000)

这里 PPO 用的 reward 是 step() 的返回值(观测窗口求和)。它适合做优化信号,但不是可直接累计的收益。

预期输出:无报错即可;训练时长与步数取决于你的机器与 total_timesteps

评估:把模型动作喂回原生环境,读账本

env = trading_env.make('backtest_v1', 64, 32, df=df, fee=0.0, max_position=1,
                       deal_col_name='Price', feature_names=FEATURES)
env.reset(); done = False
while not done:
    action, _ = model.predict(env.obs_return, deterministic=True)
    obs, reward, done, info = env.step(int(action))
det = env.info.copy()
print('账本收益 = %.3f  笔数 = %d' % (det['reward'].sum(), (det['position_variation'] != 0).sum()))

评估阶段绕开适配层,直接用原生环境读账本,可以避免「适配层把 reward 换了口径」导致的误判。

预期输出:打印该交易日的账本收益与交易笔数(口径与「奖励与基准」页一致)。

对照:至少跑三组基线再下结论

# 1) 随机策略(多随机种子)
# 2) always long / Buy&Hold
# 3) 双均线规则策略
# 每一组都用同一个评估循环与同样的 fee

本站实测:随机策略三种子在 -9.300 到 +3.800 之间波动,双均线 +1.300,Buy&Hold +1.100。没有这些参照线,单看模型的收益率无法判断好坏。

预期输出:一张含策略、笔数、账本收益、Buy&Hold 的对照表(可参见回测循环页的实测对照表)。

TradingGym · FAQ

TradingGym 接 Gymnasium 与 SB3 常见问题是什么?

TradingGym 支持 Gymnasium 吗?

不支持开箱即用。它不依赖 gymgymnasium,环境类不继承 gym.Envstep() 返回四元组,reset() 不接收 seed。要接入 Gymnasium/SB3 必须自己写一层适配(本页给了完整代码)。以官方源码为准。

为什么 check_env 会报观测空间不一致?

因为原环境的 observation_space 声明为 np.array([obs_data_len * feature_len,])(一维),而 reset()/step() 实际返回二维矩阵 (obs_data_len, 特征数+8)(v1 默认拼接 8 列持仓/成本/盈亏)。适配层里按真实形状重建 Box 即可;也可以用 return_transaction=False 或手动裁剪去掉附加列。

项目自带 PPO / DQN 吗?

没有。README 的算法列表里 simple dqn、policy gradient、actor-critic、A3C with RNN 全部标注 WIP,仓库中也不存在对应实现文件。算法完全由使用者提供,例如用 Stable-Baselines3。训练脚本、评估脚本同样需要自己写。

reset() 的 seed 怎么处理?

TradingGym 的 reset() 不接受 seed,而训练环境内部用全局 np.random 抽取交易日。适配层的 reset(seed=…) 里需要显式调用 np.random.seed(seed) 才能让「同一种子两次跑结果一致」。回测环境 backtest_v1 按日顺序推进,不依赖随机抽日,因此更适合作可复现实验的载体。

训练出来的模型怎么评估?

把模型动作喂回原生环境(不经适配层),跑完整交易日,然后读 env.info['reward'] 重建净值,并同时输出交易笔数与 Buy&Hold 对照。训练时看到的 reward 是窗口求和,不能直接当收益。

可以多环境并行训练吗?

技术上可以(SB3 的 make_vec_env),但要注意两点:①TradingGym 内部使用全局 np.random,多进程/多线程下抽日的可复现性会变差;②每个环境实例需要独立的 DataFrame(项目没有标的维度)。多环境并行属于实验工程问题,本站未做实测,请以自己的运行结果为准。

下一步该怎么做?遇到报错怎么定位

环境、数据、账本、适配四类问题都在这页按「报错原文 → 原因 → 定位 → 修法」列好了,包括本站实测过的那几条原文。