TradingGym · LEDGER & BASELINES

TradingGym 的 reward 能直接当收益吗?三处口径与净值重建

同一个环境里,收益数字有三个来源:step() 的返回值、env.info['reward']env.info['reward_fluctuant']。本站实测它们的差距不是小数点级别——单日分别是窗口和、-0.5(与真实盈亏一致)与 +28.1。用错一处,「收益异常高」或「reward 上升但净值下降」就会出现。

  • 单日对账env.info['reward'].sum() = 独立重算增量盈亏 = -0.5000
  • 同一天另一列reward_fluctuant 累加得 +28.1(差 56 倍)
  • step 返回值:5 天累加 -19.500,账本只有 -9.300

TradingGym 三处收益口径怎么分叉

step().reward观测窗口内 reward 数组求和(窗口重叠)
env.info['reward']增量盈亏,可累加(实测 -0.5)
reward_fluctuant水平值 + 每 tick 持仓费(实测 +28.1)
独立重算Σ(价格差 × 持仓),与环境账本一致
依据 trading_env/envs/backtest_v1.py:265,280 与单日对账实测绘制的口径示意(非官方流程图)。写净值曲线只能用可累加的那一列,其余两列各有用途但不能相加。

TradingGym · 三处口径

TradingGym 同一个环境,收益数字有三个来源:用哪个?

先把三个来源摆在一起。它们不是「同一个数的不同精度」,而是三种完全不同的定义。

三处 reward 口径对照(源码位置与实测数值均给出)
来源定义源码位置能否直接累加实测(单日 / 5 日)适用场景
step() 返回的 reward本次观测窗口内 reward 数组之和backtest_v1.py:280self.obs_reward.sum()不能(窗口重叠,被重复计数)— / 5 日 -19.500喂给 RL 的即时反馈
env.info['reward']逐 tick 的增量盈亏(含平仓结算,按笔扣 fee)账本列,来自 reward_arrbacktest_v1.py:170,178,223-238可以-0.5000 / 5 日 -9.300重建净值、算总收益与回撤
env.info['reward_fluctuant']持仓浮动盈亏的水平值,并按每个持仓 tick 扣 feebacktest_v1.py:265不能(水平值不是增量)+28.1 / —观察瞬时浮盈浮亏、成本敏感度
站点独立重算Σ (价格[t+1] − 价格[t]) × 持仓[t]本站脚本 _verify/reconcile.py可以-0.5000 / 5 日 -9.300交叉验证环境账本是否可信
Buy&Hold 基准价格[末] − 价格[首](1 份敞口)本站脚本 _verify/run_verify.py可以— / 5 日 +1.100判断策略有没有跑赢「什么都不做」
步数累计的 reward(常见误用)把每天 step() 返回值相加不能— / 会得到 -19.500 而非 -9.300无适用场景,属错误用法
为什么 step() 的返回值会偏大?因为 obs_data_len=64step_len=32 时观测窗口长度是步长的两倍,窗口之间重叠,同一个 tick 的 reward 会落进相邻两次返回值里。实测比值约 2.1 倍,与窗口/步长比例一致。

TradingGym · 净值重建

TradingGym 净值怎么重建?从账本到可核对的资金曲线

重建的原则只有一条:只用增量列。下面三步同时做了「算净值」和「验算净值」两件事,任何一步不吻合就说明口径用错了。

第 1 步 · 从账本取增量盈亏

det = env.info                                   # 当日逐 tick 账本
inc  = det['reward'].to_numpy(dtype=float)       # 增量盈亏(可累加)
equity_day = inc.cumsum()                        # 当日资金曲线(价格单位)
print('当日合计 = %.4f' % inc.sum())

若某天出现 NaN,回到数据契约页检查特征列;账本出现 NaN 通常源头是观测里有 NaN。

预期输出:单日样例 当日合计 = -0.5000

第 2 步 · 独立重算并对比(关键校验)

px  = det['Price'].to_numpy(dtype=float)
pos = det['position'].to_numpy(dtype=float)
site = ((px[1:] - px[:-1]) * pos[:-1]).sum()     # 站点口径:价格差 × 持仓
print('环境账本 = %.4f   站点重算 = %.4f   差额 = %.4f' % (inc.sum(), site, inc.sum() - site))

两条路径完全独立:一条读环境数组,一条用「价格差 × 持仓」重算。本站实测两者在 fee=0 时完全一致,这是判断账本有没有被误读的最快办法。

预期输出:环境账本 = -0.5000 站点重算 = -0.5000 差额 = 0.0000

第 3 步 · 拼多日曲线并算风险指标

totals = pd.Series([d['reward'].sum() for d in days])   # 每日合计
equity = totals.cumsum()                               # 区间资金曲线
peak   = equity.cummax()
mdd    = (equity - peak).min()                          # 最大回撤(价格单位)
print('总收益 = %.3f   最大回撤 = %.3f   交易日 = %d' % (equity.iloc[-1], mdd, len(totals)))

多日曲线按「每日合计再累计」拼,不要直接把每天 step() 的返回值堆起来。

预期输出:随机策略 seed 0 的 总收益 = -9.300,交易日 5 天。

一个常见误区:把 reward_fluctuant 当作净值列画图。它是「水平值」,同一天累加会得到 +28.1 而不是 -0.5;如果其中还含每 tick 持仓费,量级会进一步失真。要画浮盈浮亏,请画该列本身的曲线而不是它的累计和。

TradingGym · 方差与复现

TradingGym 为什么必须跑多个随机种子:实测方差比策略差异还大

TradingGym 的 v0 训练环境用全局 np.random 抽交易日、且没有 seed 参数;本站回测用固定种子的 RandomState,换种子就换一条动作序列。结果是:同一策略在三个种子下可以同时给出正收益和负收益。

随机策略三种子实测(同一数据、同一参数、仅随机种子不同)
随机种子交易笔数账本收益(fee=0)账本收益(fee=0.1)Buy&Hold说明
0802-9.300-49.400+1.100唯一为负的种子;若只跑这一个,结论会是「策略必亏」
1830+3.800-37.700+1.100若只跑这一个,结论会是「随机策略有效」
2812+1.700-38.900+1.100三种子跨度 13.1 点,远大于与 Buy&Hold 的差异

跨度 13.1 点是什么概念

样例价格量级约 277 点,13.1 点相当于 4.7% 的价格波动幅度——仅仅来自随机种子的差异。

成本把差异压平

同一个 fee=0.1 下三个种子全为负(-49.4 / -37.7 / -38.9),说明高换手策略在这个成本口径下没有生存空间。

结论写法

正确表述是「在 N 个随机种子下,账本收益区间为 [−9.3, +3.8] 点,同期 Buy&Hold 为 +1.1 点」——而不是「本策略收益 X%」。

多随机种子复现步骤(照做即可复现上面的数字)
步骤做什么预期结果注意点
① 固定环境侧随机源把回测流程改为不依赖 np.random 全局状态(backtest_v1 不抽日,可确定性推进)同一脚本两次运行走同样的交易日训练环境 training_v1 会随机抽日,需要自己加 seed 包装
② 策略侧独立随机源策略内用 np.random.RandomState(seed)动作序列可复现别用 np.random.randint 全局调用
③ 至少 3 个种子跑 0/1/2 或 0..9得到收益区间而不是单点种子上限越多,结论越稳
④ 同步跑基准do nothing / always long / Buy&Hold基准收益作为参照线基准必须与策略同口径、同敞口
⑤ 落盘证据保存账本 CSV、参数、种子、脚本任何人可复跑本站 _verify/run_result.json 即此格式
⑥ 报告区间与边界写明数据区间、样本量、成本设定结论可被检验不要只报最好那个种子

TradingGym · 基准对照

TradingGym 四个基准的实测值是多少?不给基准的收益率没有意义

基准的作用是把「策略赚了钱」翻译成「策略比什么都不做更好吗」。下面四个基准口径都写清楚,便于你自己在同一段数据上复现。

基准定义与实测值(同一段数据、同一 max_position=1
基准定义实测 5 日怎么构造适用场景注意点
do nothing始终 action=00.000策略恒返回 0验证账本在无持仓时严格为 0它必须是 0;不是 0 说明账本被误读
always long第一步就做多并一直持有+1.000(10 笔)策略恒返回 1最朴素的「买入并持有」注意 max_position=1 时后续加仓会被改写为不动
Buy&Hold(价格口径)价格[末] − 价格[首]+1.100直接从行情算,不经环境最简参照线与 always long 差 0.1,差额来自撮合价位置(窗口之后第一根 tick)
双均线(20 tick 交叉)价格上穿均线做多、下穿做空+1.300(132 笔)规则策略,见回测循环页规则策略参照MA 头部裁剪 20 行后,同期 Buy&Hold 变为 +1.700,比较时要用同区间
随机策略(下限参照)随机 0/1/2-9.300 ~ +3.800多个种子各跑一遍判断策略是否优于瞎猜区间跨度大,单种子不可比
「什么都不做」的错误版本reward_fluctuant 累加当收益会得到显著非零值这是「收益异常高」类问题的典型来源
本站实测的口径顺序是:先确认 do nothing 严格为 0 → 再确认 always long 与 Buy&Hold 量级一致 → 最后才看策略相对基准的差异。跳过前两步,后面的数字都不可信。

TradingGym · 可信度自检

TradingGym 的结果怎么判断可不可信?8 项自检清单

这份清单可以直接当作实验报告的检查项。任何一项为「否」,结论都不该以「策略有效」的形式写出来。

结果可信度自检(逐项给判据与不合格时的表现)
#检查项判据不合格的表现怎么补
1是否用可累加的口径净值来自 env.info['reward'] 或独立重算收益量级明显偏大(如 5 天 -19.5 而非 -9.3)换列,见净值重建三步
2是否做了独立重算「价格差 × 持仓」与环境账本一致两者差额非 0 且无法解释先查索引契约与持仓列
3是否报告交易笔数有明确的成交笔数无法判断换手与成本影响position_variation != 0 统计
4是否计入成本fee 至少跑两档对照结论只在高换手、零成本下成立fee 0 与 0.1 各跑一遍
5是否跑多个随机种子≥3 个种子并给区间只报最好的那个种子按本页步骤 ③ 执行
6是否有基准对照至少含 Buy&Hold无法判断是否优于持有按本页基准表构造
7是否报告最大回撤给出回撤数值与计算口径只报总收益,忽略风险cumsum + cummax 计算
8是否说明样本边界写明单一品种、单一数据区间、样本内把样本内结果当作可外推结论明确写「不可外推」

TradingGym · 常见误判

TradingGym 四个真实误判:reward 上升但净值下降,是怎么发生的

下面四种现象在 RL 交易实验里反复出现。它们都不是模型问题,而是口径与设定问题。

① 训练 reward 一直涨,回测却亏

训练环境的 reward 与回测账本口径不同:v0 系把浮动盈亏缩放到 reward/fluc_div,v1 的 step() 返回窗口求和。用训练 reward 当业绩看,等于用尺子量温度。正确做法是训练时看 reward,评估时一律回到账本。

② 收益异常高(几十倍)

最常见原因是把 reward_fluctuant 累加当收益。它是水平值:实测单日 +28.1,而真实盈亏 -0.5,差 56 倍。另一个原因是 max_position 设得很大而误以为仍是 1 份敞口。

③ 交易笔数异常多(几百笔/天)

窗口小、步长小(如 64/32)时,一天会有 250–480 次决策,随机策略下大部分都变成成交。这不是 bug,但会放大成本项——实测 5 天 802 笔交易在 fee=0.1 时多扣 40.1 点。

④ 长期只买不卖 / 只开不平

因为 max_position 达到上限后同向动作被改写为不动,若策略持续输出同一方向,看起来就是「一动不动」。也可能是因为 gameover_limit 在 v1 不生效,你预期的提前终止没有发生。

误判 → 定位 → 修法
误判现象最可能的原因一行定位命令修法注意点
收益量级偏大数十倍用了 reward_fluctuant 累加det['reward_fluctuant'].sum()det['reward'].sum() 对比改用 reward两者单日可差 56 倍
步数累加 != 账本step() 返回窗口求和step 返回值与 info['reward'].sum() 同时打印净值只用账本窗口重叠导致约 2 倍偏差
训练 reward 与回测背离训练/回测 reward 口径不同确认用的是 v0 还是 v1 环境评估统一用账本v0 才有 fluc_div 缩放
笔数远超预期窗口/步长过小或策略抖动(det['position_variation'] != 0).sum()加大窗口或加动作过滤笔数直接决定成本量级
持仓卡在上限不动达到 max_position 后同向被改写打印 det['position'].max()策略内先判断上限(见回测循环页)环境不会报错,只会静默改写
亏损没有提前终止gameover_limit 在 v1 不生效查环境是否为 backtest_v1自己在外层加终止判断v0 系才有该判定

TradingGym · FAQ

TradingGym reward 与结果可信度常见问题是什么?

TradingGym 的 reward 可以直接当收益率吗?

不能。它是价格单位的盈亏,不是百分比,只有在 fee=0max_position 固定时才与「价格差 × 持仓」的量级一致。要换算成收益率,需要你自己定义初始资金与持仓规模。本站所有数字均以「价格点」为单位并注明口径,以官方源码为准。

reward 上升但收益下降是怎么回事?

典型原因是口径混用:训练阶段看到的 reward 可能来自 v0 的 reward/fluc_div 缩放或 v1 的窗口求和,而回测账本用的是增量列。把训练曲线和回测净值放在同一个坐标系里比较,本身就不成立。正确做法是训练时看 reward,评估时一律用 env.info['reward'] 重建净值。

为什么我把 reward 累加得到的结果特别大?

多半是用错了列。reward_fluctuant 是浮动盈亏的水平值(还含每 tick 持仓费),单日实测累加为 +28.1,而真实盈亏是 -0.5,差 56 倍。能累加的只有 env.info['reward'],以及你自己用「价格差 × 持仓」重算的结果。

要跑多少个随机种子才够?

没有官方标准,但本站实测表明 1 个种子完全不够:同一随机策略在 seed 0/1/2 下 5 天账本收益分别是 -9.300 / +3.800 / +1.700,跨度 13.1 点(约合价格量级的 4.7%),比与 Buy&Hold 的差异还大。建议至少 3 个,报告时给区间而不是单点,并同步报告基准。

Buy&Hold 基准怎么算才和策略可比?

关键是敞口与区间一致。本站用 价格[末] − 价格[首] 表示 1 份敞口的 Buy&Hold,并把策略固定为 max_position=1。注意两点差异:策略的撮合价是观测窗口之后的第一根 tick,因此 always long 与价格口径的 Buy&Hold 会差一点点(实测 +1.000 vs +1.100);如果为了计算 MA 裁掉了头部若干行,基准也必须用同一区间重算(实测从 +1.100 变 +1.700)。

5 天数据能得出策略结论吗?

不能。这批样例数据只有 8 个交易日、单一品种(SGX TW tick),本站只用了其中 5 天,而且它同时充当训练集与测试集。所有实测数字的用途是「验证环境与口径是否被正确使用」,不是「证明策略有效」。任何策略结论都需要更长区间、独立样本与外推验证,并且同样要报告成本与基准。

下一步该怎么做?把它接进 Gymnasium 与 Stable-Baselines3

TradingGym 不是 gym.Env 子类、空间声明与实际形状不一致、step 返回四元组。要接现代 RL 库必须自己写一层适配——这页给出可复制的适配层与自检清单。