跨度 13.1 点是什么概念
样例价格量级约 277 点,13.1 点相当于 4.7% 的价格波动幅度——仅仅来自随机种子的差异。
TradingGym · LEDGER & BASELINES
同一个环境里,收益数字有三个来源:step() 的返回值、env.info['reward']、env.info['reward_fluctuant']。本站实测它们的差距不是小数点级别——单日分别是窗口和、-0.5(与真实盈亏一致)与 +28.1。用错一处,「收益异常高」或「reward 上升但净值下降」就会出现。
env.info['reward'].sum() = 独立重算增量盈亏 = -0.5000reward_fluctuant 累加得 +28.1(差 56 倍)-19.500,账本只有 -9.300trading_env/envs/backtest_v1.py:265,280 与单日对账实测绘制的口径示意(非官方流程图)。写净值曲线只能用可累加的那一列,其余两列各有用途但不能相加。TradingGym · 三处口径
先把三个来源摆在一起。它们不是「同一个数的不同精度」,而是三种完全不同的定义。
| 来源 | 定义 | 源码位置 | 能否直接累加 | 实测(单日 / 5 日) | 适用场景 |
|---|---|---|---|---|---|
step() 返回的 reward | 本次观测窗口内 reward 数组之和 | backtest_v1.py:280(self.obs_reward.sum()) | 不能(窗口重叠,被重复计数) | — / 5 日 -19.500 | 喂给 RL 的即时反馈 |
env.info['reward'] | 逐 tick 的增量盈亏(含平仓结算,按笔扣 fee) | 账本列,来自 reward_arr(backtest_v1.py:170,178,223-238) | 可以 | -0.5000 / 5 日 -9.300 | 重建净值、算总收益与回撤 |
env.info['reward_fluctuant'] | 持仓浮动盈亏的水平值,并按每个持仓 tick 扣 fee | backtest_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=64、step_len=32 时观测窗口长度是步长的两倍,窗口之间重叠,同一个 tick 的 reward 会落进相邻两次返回值里。实测比值约 2.1 倍,与窗口/步长比例一致。TradingGym · 净值重建
重建的原则只有一条:只用增量列。下面三步同时做了「算净值」和「验算净值」两件事,任何一步不吻合就说明口径用错了。
det = env.info # 当日逐 tick 账本
inc = det['reward'].to_numpy(dtype=float) # 增量盈亏(可累加)
equity_day = inc.cumsum() # 当日资金曲线(价格单位)
print('当日合计 = %.4f' % inc.sum())
若某天出现 NaN,回到数据契约页检查特征列;账本出现 NaN 通常源头是观测里有 NaN。
预期输出:单日样例 当日合计 = -0.5000。
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。
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 的 v0 训练环境用全局 np.random 抽交易日、且没有 seed 参数;本站回测用固定种子的 RandomState,换种子就换一条动作序列。结果是:同一策略在三个种子下可以同时给出正收益和负收益。
| 随机种子 | 交易笔数 | 账本收益(fee=0) | 账本收益(fee=0.1) | Buy&Hold | 说明 |
|---|---|---|---|---|---|
| 0 | 802 | -9.300 | -49.400 | +1.100 | 唯一为负的种子;若只跑这一个,结论会是「策略必亏」 |
| 1 | 830 | +3.800 | -37.700 | +1.100 | 若只跑这一个,结论会是「随机策略有效」 |
| 2 | 812 | +1.700 | -38.900 | +1.100 | 三种子跨度 13.1 点,远大于与 Buy&Hold 的差异 |
样例价格量级约 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 · 基准对照
基准的作用是把「策略赚了钱」翻译成「策略比什么都不做更好吗」。下面四个基准口径都写清楚,便于你自己在同一段数据上复现。
| 基准 | 定义 | 实测 5 日 | 怎么构造 | 适用场景 | 注意点 |
|---|---|---|---|---|---|
| do nothing | 始终 action=0 | 0.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 累加当收益 | 会得到显著非零值 | — | 无 | 这是「收益异常高」类问题的典型来源 |
TradingGym · 可信度自检
这份清单可以直接当作实验报告的检查项。任何一项为「否」,结论都不该以「策略有效」的形式写出来。
| # | 检查项 | 判据 | 不合格的表现 | 怎么补 |
|---|---|---|---|---|
| 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 · 常见误判
下面四种现象在 RL 交易实验里反复出现。它们都不是模型问题,而是口径与设定问题。
训练环境的 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
不能。它是价格单位的盈亏,不是百分比,只有在 fee=0 且 max_position 固定时才与「价格差 × 持仓」的量级一致。要换算成收益率,需要你自己定义初始资金与持仓规模。本站所有数字均以「价格点」为单位并注明口径,以官方源码为准。
典型原因是口径混用:训练阶段看到的 reward 可能来自 v0 的 reward/fluc_div 缩放或 v1 的窗口求和,而回测账本用的是增量列。把训练曲线和回测净值放在同一个坐标系里比较,本身就不成立。正确做法是训练时看 reward,评估时一律用 env.info['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 个,报告时给区间而不是单点,并同步报告基准。
关键是敞口与区间一致。本站用 价格[末] − 价格[首] 表示 1 份敞口的 Buy&Hold,并把策略固定为 max_position=1。注意两点差异:策略的撮合价是观测窗口之后的第一根 tick,因此 always long 与价格口径的 Buy&Hold 会差一点点(实测 +1.000 vs +1.100);如果为了计算 MA 裁掉了头部若干行,基准也必须用同一区间重算(实测从 +1.100 变 +1.700)。
不能。这批样例数据只有 8 个交易日、单一品种(SGX TW tick),本站只用了其中 5 天,而且它同时充当训练集与测试集。所有实测数字的用途是「验证环境与口径是否被正确使用」,不是「证明策略有效」。任何策略结论都需要更长区间、独立样本与外推验证,并且同样要报告成本与基准。