Gym 交易环境

QTrade 提供一个 Gymnasium 环境(qtrade.env.TradingEnv),它包装的是和 Backtest 同一个 Broker。agent 一次推进一根 bar;账户逻辑、成交语义、SL/TP 行为完全一致 —— 只是用 step() 驱动而不是 Python 循环。

自定义 action / observation / reward 见自定义交易环境

初始化

import yfinance as yf
from qtrade.env import TradingEnv
from qtrade.core.commission import PercentageCommission

data = yf.download(
    "GC=F",
    start="2022-01-01",
    end="2024-01-01",
    interval="1d",
    multi_level_index=False,
)

# Indicators added to the DataFrame become observable features by default.
data['Rsi'] = data['Close'].pct_change().rolling(14).mean()  # placeholder for ta.rsi
data['Diff'] = data['Close'].diff()
data.dropna(inplace=True)

env = TradingEnv(
    data=data,
    cash=3000,
    commission=PercentageCommission(0.001),
    window_size=10,        # observation lookback (also defines warmup)
    max_steps=400,         # max bars per episode
    random_start=False,    # start at index `window_size`
    trade_on_close=True,   # market orders fill at current bar's close
)

默认的 ObserverScheme 返回除 OHLCV 之外所有列的 (window_size, n_features) 窗口。所以你加到 DataFrame 上的指标会自动变成观测特征。

step API

env.step(action) 返回标准 5 元组:

obs, reward, terminated, truncated, info = env.step(action)

字段

含义

obs

ObserverScheme 产生的内容。默认:形状 (window_size, n_features)np.ndarray

reward

RewardScheme.get_reward(env)。默认:本步关闭交易的对数收益减去佣金。

terminated

当且仅当 current_step >= len(data) - 1 时为 True —— 数据用完了。

truncated

当且仅当 current_step - start_idx >= max_steps 时为 True —— episode 到时间限制。

info

字典(见下文)。

terminatedtruncated 变 True 时,broker 自动调用 close_all_positions() —— episode 在返回前已「结算」。

info 里有什么?

每一步包含:

含义

equity

broker.equity(现金 + 浮动盈亏)。

unrealized_pnl

broker.unrealized_pnl

cumulative_return

broker.cumulative_returns —— 自 episode 开始的乘性增长。

position

带符号的 position.size 净值。

total_trades

本 episode 至今已平仓交易数。

trades_profit

所有已平仓交易 profit 之和。

avg_trade_duration

平均 exit_index entry_index(按 bar 计;无交易时为 0)。

is_success

trades_profit > 0。便于 SB3 EvalCallback 记录成功率。

如需额外字段(持仓中交易数、当前回撤、特定交易属性),继承 TradingEnv 并重写 step 来扩展这个 dict。

Episode:terminated vs truncated

Gymnasium 的约定是:

  • terminated:episode 到达自然终点(胜、负或可用状态耗尽)。

  • truncated:episode 被人为时间限制截断

TradingEnv 里:

  • terminated 在数据用完时触发(current_step == len(data) - 1)。Episode 「完整结束」。

  • truncated 在达到 max_steps 时触发。Episode 本可以继续,但你设置了预算。

stable-baselines3 的值函数 bootstrap 对二者处理不同 —— 把 max_steps 设得短到大多数 episode 是 truncate(让 agent 从多个短 episode 学),但不能短到策略没机会展开。

random_start 增加 episode 多样性

默认 random_start=False:每个 episode 都从 bar 索引 window_size 开始。random_start=True 时,每次 reset()[window_size, len(data) - max_steps) 范围随机选起点,让 episode 采样不同的市场 regime。

前置条件:len(data) > window_size + max_steps。不满足时构造函数抛出清晰的 ValueError —— 保持 max_steps < len(data) - window_size 留余地。

典型训练循环

from stable_baselines3 import PPO

model = PPO("MlpPolicy", env, verbose=1)
model.learn(total_timesteps=200_000)

obs, _ = env.reset(seed=42)
for _ in range(400):
    action, _ = model.predict(obs, deterministic=True)
    obs, reward, terminated, truncated, info = env.step(action)
    if terminated or truncated:
        break

env.show_stats()
env.plot()

env.show_stats()env.plot() 行为和 Backtest 实例完全一致 —— 相同指标、相同的多面板 Bokeh 报告 —— 因为底层都委托给同一个 Broker

渲染

env.render('human') 打开一个 mplfinance 实时蜡烛图,每步更新:

交易环境渲染

RGB 数组输出(用于 SB3 的 VecVideoRecorder 等):

env = TradingEnv(..., render_mode='rgb_array')
frame = env.render()  # → ndarray of shape (h, w, 3)

注意:渲染很慢。训练时关闭它(render_mode='human' 是默认值,但你不必调用 render()),只在评估时渲染。

常见坑

  • 稀疏奖励:默认奖励只在有交易平仓的 bar 触发。长 episode 没有平仓很难训练。要么用基于权益的奖励(见自定义交易环境),要么缩短 episode 让结尾的自动平仓提供规律信号。

  • 指标开头有 NaN:你加到 data 上的列会被原样观测。观测里出现 NaN 会让大多数策略崩溃。计算指标后用 data.dropna(inplace=True) 丢掉热身行。

  • Action / observation 漂移:实验中途换 scheme 后加载旧模型会形状不匹配。打算后续加载的模型和对应的 scheme 一起保存。