出处一:平仓算式(按笔扣一次)
# backtest_v1.py 的空头平仓分支(多头对称)
self.chg_reward[:] = ((self.chg_price - self.chg_price_mean) * (-1) - self.fee) * self.chg_makereal
这里 fee 从已实现盈亏里减一次,单位就是你自己价格列的单位。价格量级 277 点时,fee=0.1 约等于单笔 3.6 个基点。
TradingGym · PARAMETERS · 源码语义
TradingGym 的参数文档只有几行英文,真正决定实验是否成立的细节都在源码里:fee 同时出现在两处、含义不同;step() 返回的 reward 是窗口求和而不是单步收益;还有三个参数 README 完全没提。这页把 README 说法与源码行为逐条摆在一起。
env.info['reward']、env.info['reward_fluctuant']obs_data_len 根 tick 的特征矩阵step() 窗口右移trading_env/envs/backtest_v1.py 的 step() 实现与本机实测绘制的窗口推进示意(非官方流程图)。关键点:撮合价取的是观测窗口之后的第一根 tick,而返回的 reward 是窗口内 reward 之和。TradingGym · 逐条对照
对照依据是仓库 master HEAD(08bb534)的实际代码与本机实测。凡是 README 没写的,都在「源码实际行为」列里点了出来。
| 参数 | README 说法 | 源码实际行为 | 本机实测 | 适用场景 | 注意点 |
|---|---|---|---|---|---|
obs_data_len | observation data length | 观测窗口的 tick 根数;观测 = 该窗口的特征矩阵 | 取 64 时观测形状为 (64, 8)(不含附加列) | 做微观结构研究用短窗 | 窗口越长,持仓信息在观测里越滞后 |
step_len | when call step rolling windows will + step_len | 每次 step() 窗口右移的根数;相邻 step 的窗口不重叠 | 64/32 组合下每次 step 前进 32 根 tick | 调决策频率 | 返回值却是整窗求和,见第 3 节 |
fee | when each deal will pay the fee | 两处出现、含义不同:平仓算式里按笔扣一次;reward_fluctuant 列里按每个持仓 tick 扣一次 | 5 天 401 笔平仓 × 0.1 = 40.1 点差额 | 观察成本敏感度 | 它是绝对量,不是百分比费率 |
max_position | the max market position for you trading share | 双向持仓上限;达到上限时同向动作被改写为「不动」 | 取 1 时 5 天随机策略 802–830 笔交易 | 控制单标的风险敞口 | 上限同时是「开仓/加仓」的判定分界 |
deal_col_name | the column name for cucalate reward used | 盈亏计算所用价格列;必须存在于 DataFrame | 样例用 Price | 统一撮合口径 | 选买价还是最新价,会改变回测结论 |
feature_names | list contain the feature columns to use in trading status | 观测的特征列,逐列断言存在;顺序即观测列顺序 | 传 8 列时观测为 (64, 16)(8 特征 + 8 附加) | 构造状态空间 | 列名大小写敏感 |
serial_number(列,非参数) | serial num of deal at each day recalculating | 切分交易日的唯一依据(找 ==0 的行) | 样例 113,059 行 → 8 个交易日 | 所有环境 | 夜盘品种的「交易日」跨午夜,不能按日历日期切 |
fluc_div(README 未提) | — | v0 里把浮动盈亏缩放成 reward/fluc_div(默认 100.0) | — | 控制 v0 奖励量级 | v1 保留该参数但 step() 不再用它缩放 |
gameover_limit(README 未提) | — | v0 系:浮亏超过阈值提前结束 episode(默认 5);v1 的 step 里没有这个判定 | backtest_v1 全程未触发提前终止 | 限制最大回撤 | 想做止损式提前终止,v1 得自己加 |
return_transaction(README 未提) | — | v1 专有:True 时观测拼接 8 列持仓/成本/盈亏,False 时只给特征列 | 默认 True → 观测 16 列 | 让 Agent 看到持仓状态 | 关掉后 Agent 会「看不见自己仓位」 |
fluc_div、gameover_limit、return_transaction)都会显著改变实验,其中 gameover_limit 在 v1 环境里根本不生效——照 v0 教程写的「亏损超限自动结束」在 v1 上是不会发生的。TradingGym · 参数深挖
网上常见的写法是 fee=0.1 并解释成「0.1% 手续费」。这个解释在源码面前站不住:它既不是百分比,也不是统一的「每笔一次」。
# backtest_v1.py 的空头平仓分支(多头对称)
self.chg_reward[:] = ((self.chg_price - self.chg_price_mean) * (-1) - self.fee) * self.chg_makereal
这里 fee 从已实现盈亏里减一次,单位就是你自己价格列的单位。价格量级 277 点时,fee=0.1 约等于单笔 3.6 个基点。
# backtest_v1.py:265 每个 step 覆盖的窗口
self.chg_reward_fluctuant[:] = (self.chg_price - self.chg_price_mean) * self.chg_posi \
- np.abs(self.chg_posi) * self.fee
同一个 fee 在浮动盈亏列里按每个持仓 tick 扣。持仓 1 万根 tick 时,这一项会远大于价格波动本身——这是「收益被成本吃光」的最常见原因。
| 实验 | fee | 交易笔数 | 账本收益 | 与 fee=0 的差 | 怎么读 |
|---|---|---|---|---|---|
| 随机策略 seed 0 | 0 | 802 | -9.300 | — | 基线 |
| 随机策略 seed 0 | 0.1 | 802 | -49.400 | -40.100 | 恰好等于 0.1 × 401 笔平仓,反证「平仓按笔扣一次」 |
| 随机策略 seed 1 | 0 | 830 | +3.800 | — | 同一策略换种子由亏转盈 |
| 随机策略 seed 1 | 0.1 | 830 | -37.700 | -41.500 | 成本约 0.1 × 415 笔平仓 |
| 随机策略 seed 2 | 0.1 | 812 | -38.900 | — | 三个种子在 fee=0.1 下全部为负 |
| 双均线(20 tick 均线交叉) | 0 | 132 | +1.300 | — | 交易频率低,成本影响小 |
| 双均线(20 tick 均线交叉) | 0.1 | 132 | -5.300 | -6.600 | 66 笔平仓 × 0.1 = 6.6,与差额一致 |
fee=0.1 的正确读法是「每笔平仓从盈亏里减 0.1 个价格单位,同时在浮动盈亏列里每个持仓 tick 再减 0.1」。不要写成 0.1%,也不要直接和 A 股佣金费率换算。以官方源码为准。TradingGym · 窗口机制
这两个参数不只控制「看多久」,还决定了撮合价落在哪里、以及 step() 返回的 reward 覆盖多大区间。
| 环节 | 取值来源 | 行为 | 适用场景 | 注意点 |
|---|---|---|---|---|
| 本次决策用到的观测 | 前一步的观测窗口 | 形状 (obs_data_len, 特征数+8) | Agent 决策 | 观测里已含当前持仓(return_transaction=True) |
| 窗口右移 | step_len | 先 step_st += step_len 再取新窗口 | 调决策频率 | step_len 越大,单位时间内决策越少 |
| 撮合价 | 新窗口之后的第一根 tick | enter_price = self.chg_price[0] | 保证「先看到、后成交」的顺序 | 它与观测窗口之间相隔了 step_len 根 tick |
| 返回的 reward | 窗口内 reward 数组求和 | return ... self.obs_reward.sum() | 给 RL 用的即时反馈 | 相邻 step 的窗口叠加会让累加值偏大 |
| episode 终点 | 当日 tick 用尽 | done=True,同时生成账本 | 逐日回测 | 每日行数少于 obs_data_len+step_len 时会立刻结束 |
| 参数与数据规模的关系 | 两者共同决定步数 | 单日步数 ≈ (当日 tick 数 − obs_data_len) / step_len | 估算训练/回测时长 | 样例最长日 25,617 根、最短日 8,550 根,步数差 3 倍 |
决策频率高、能更快反应,但噪声也被放大。本站实测 5 天就有 800 多笔交易,成本项变成主导因素。
观测更平稳、交易更少,但窗口内持仓信息滞后更多;单日步数会明显下降,样本效率变差。
日线下 1 根 = 1 天,obs_data_len=20 就是「看过去 20 个交易日」。此时 serial_number 常设为 0,整段区间会被当成一个超长交易日。
TradingGym · 持仓规则
v1 环境的 action_space 是整数 3,动作语义写在 action_describe 里:{0: 'do nothing', 1: 'long', 2: 'short'}。也就是说动作 1 表示「做多方向」,2 表示「做空方向」,具体是开仓还是加仓由当前持仓决定。
| 当前持仓 | 动作 | 环境行为 | 适用场景 | 注意点 |
|---|---|---|---|---|
| 0 | 1(long) | 开多 1 单位,记录开仓价 | 趋势入场 | 开仓价写入 price_mean,后续盈亏以它为基准 |
| 大于 0 且小于上限 | 1(long) | 同向加仓 1 单位,entry_cover=2(increase) | 金字塔加仓 | 加仓会改变持仓均价 |
| 大等于上限 | 1(long) | 同向动作被改写为「不动」 | 风险上限保护 | 达到上限后 Agent 的加仓信号全部失效,容易误判为「策略不响应」 |
| 大于 0 | 2(short) | 减少多头 1 单位;到 0 时平仓,entry_cover=-1(cover) | 止盈止损 | 平仓瞬间才结算已实现盈亏并扣 fee |
| 0 | 2(short) | 开空 1 单位 | 做空/对冲 | 空头盈亏符号与多头相反,账本里看 position 正负 |
| 小于 0 | 1(long) | 减少空头;到 0 时平仓 | 空头回补 | 平仓同样扣一次 fee |
| 任意 | 0 | 持仓不变,但仍按持仓量结算浮动盈亏 | 持有观察 | 不动不等于不亏:持仓成本项照算 |
max_position 设成 1 与设成 5 是两种实验:前者做「单份持仓的方向决策」,后者的胜负由加仓节奏主导。本站所有对照实验都固定在 1,才能让随机策略与 Buy&Hold 可比。TradingGym · 未文档化参数
这三个参数在 README 里没有说明,但对实验的影响不比文档化的参数小。下面给出它们的源码位置与行为。
| 参数 | 默认值 | 源码位置 | 行为 | 什么时候要动它 | 注意点 |
|---|---|---|---|---|---|
fluc_div | 100.0 | training_v0.py 构造参数 | v0 把浮动盈亏缩放到 reward/fluc_div | v0 奖励量级过大导致训练不稳定时 | v1 的 step() 不再用它缩放,改参数在 v1 上无效果 |
gameover_limit | 5 | training_v0.py:276、backtest_v0.py:270 | 浮亏超过阈值时提前结束 episode | 想模拟「爆仓/止损后停止交易」 | v1 的 step 里没有该判定,在 backtest_v1 上设了也不生效 |
return_transaction | True | backtest_v1.py:17 及 reset/step | True 时观测拼接 8 列持仓/成本/盈亏 | 要区分「Agent 看得见仓位」与「看不见仓位」 | 关掉后观测只剩特征列,Agent 需要自己记忆仓位 |
deal_col_name 的大小写 | 'price'(小写默认) | __init__.py:9 | 默认值是小写 price,而官方样例列名是 Price | 直接按默认值建环境会断言失败 | 必须显式传 deal_col_name='Price' 或统一改列名 |
feature_names 的默认值 | ['price','volume'] | __init__.py:9 | 默认只取两列 | 不传时会因列名不存在而断言失败 | 默认值是占位符性质,实际使用必须显式传入 |
| 观测里的附加列顺序 | 固定 8 列 | backtest_v1.py:118-128 | position、position_variation、entry_cover、price、price_mean、reward_fluctuant、reward_makereal、reward | 你在做特征工程时要按位置索引 | 附加列不可配置、不可改名,只能整体开关 |
TradingGym · 选型建议
网上流传的「高频用 50–100、日内 200–500、趋势 1000 以上」并没有源码或实验依据。下面这张表给的是一组可复现的起点,并写明为什么。所有数字都要在你自己的数据上重跑验证。
| 目标 | obs_data_len | step_len | fee 起点 | 理由 | 注意点 |
|---|---|---|---|---|---|
| 先确认环境能跑(冒烟测试) | 64 | 32 | 0 | 窗口小、步数多,最快暴露数据与索引问题 | 本站实测即用此组合,8,550 根的一日也能跑出上百步 |
| 观察成本敏感度 | 64 | 32 | 0 → 0.1 | 固定其他变量,只改 fee,差额可直接归因 | 实测 5 天 401 笔平仓对应的差额是 40.1 点 |
| 低频规则策略(如均线) | 64 | 32 | 0 与 0.1 各跑一次 | 与随机策略共用口径,便于横向比较 | 实测 132 笔交易时,fee 0.1 使收益由 +1.300 变 -5.300 |
| 降低交易频率 | 128 或 256 | 64 或 128 | 按实际成本设定 | 窗口变长、决策变少,成本项影响下降 | 单日步数会明显减少,样本效率变差 |
| 日线/多日窗口 | 20 | 5 | 0 | 「看过去 20 个交易日、每 5 日决策」 | 需把 serial_number 设为 0,整段区间成为一个 episode |
| 奖励塑形研究 | 64 | 32 | 0 | 先零成本,确认奖励改动方向正确再叠加成本 | 改 reward 后必须重建净值,不能直接比较 step 返回值 |
TradingGym · FAQ
不是。源码里 fee 是绝对量:平仓时从已实现盈亏里减一次,同时在 reward_fluctuant 列里按每个持仓 tick 再减一次(backtest_v1.py:170/178 与 :265)。本站实测:5 天 401 笔平仓、fee=0.1 时账本比 fee=0 少 40.1 点,正好等于 0.1 × 401。要换算成费率,请拿你价格列的量级自己算,别直接抄教程里的「0.1%」。以官方源码为准。
没有官方最佳值。README 只说明它是观测长度。网上「高频 50–100 / 日内 200–500 / 趋势 1000+」的说法没有源码或实验依据。建议从 64/32 起步做冒烟测试,再按你的决策频率调整;任何取值都要在你自己的数据上重跑,并同时报告交易笔数与成本影响——本站实测显示窗口变小会让交易笔数上升到数百笔,成本项随之主导结果。
它是双向持仓的绝对上限。达到上限时,同方向的加仓动作会被环境改写成「不动」(不是报错),所以看起来像「策略突然不响应」。设为 5 意味着允许加仓到 5 份,回测结果会由加仓节奏主导;本站所有对照实验固定为 1,目的是让随机策略与 Buy&Hold 在同等敞口下可比。
因为该参数只在 v0 系的两个环境里参与 step() 判定(training_v0.py:276、backtest_v0.py:270),而 backtest_v1 的 step() 里没有这段逻辑。想在 v1 上实现「亏损超限终止」,只能自己在外层循环里判断 env.info['position'] 与盈亏,然后主动 break。以官方实现为准。
关掉后观测只包含 feature_names 指定的特征列(不含持仓/成本/盈亏 8 列)。对 Agent 来说,它「看不见自己的仓位」,必须自己维护状态;对研究者来说,这更接近纯行情观测的传统设定。默认是 True(观测 16 列),这也是本站实测所用的配置。
不能。改动 obs_data_len/step_len 会改变决策频率与步数;改动 fee 会改变成本项;改动 max_position 会改变敞口规模;改动 return_transaction 会改变观测。任何一项变化都意味着换了一个实验,必须重跑包括基准策略在内的全部对照,并在报告里写明参数。