TradingGym · DATA CONTRACT

TradingGym 要的数据长什么样:四道断言 + 一条索引契约

TradingGym 不下载数据、不做复权、不管交易规则,它只接受一份你整理好的 DataFrame。这份表必须通过 make() 里的四个断言,并且索引必须是 0..N 的整数——这两条决定了你是十分钟跑通,还是卡在KeyError 里。

  • 四个断言env_id 合法 · deal_col_name 在列 · serial_number 必须在列 · 每个特征列存在
  • 一条契约:索引要是 0..N 整数,否则 iloc 切片会错位
  • 官方样例:113,059 行 tick、8 个交易日,字段含 serial_number/Price/Ask_price/Bid_price/Updown

TradingGym 数据要过哪四道检查?

必需列校验make() 四个断言,缺列立即报错
serial_number 分日==0 切出每日首根 tick
整数索引iloc[label:label] 的前提
窗口切片特征矩阵 → 观测(obs_data_len)
依据 trading_env/__init__.py:13-17 的断言与官方样例数据实读绘制的数据契约示意(非官方流程图)。四道检查任一不过,都会在 make()reset() 阶段报错,而不是在训练时。

TradingGym · 列要求

TradingGym 需要哪些列?必需列与可选列分开看

make() 只检查四件事,但真正跑起来还依赖几列。下表按「谁要求它、缺了报什么」写,避免你把官方样例的列名当成唯一答案。

列的角色与缺失后果(以源码断言与官方样例数据为准)
谁要求它缺了会怎样适用场景注意点
serial_numbermake() 第 3 个断言,硬性要求AssertionError,环境根本建不起来所有环境必须按交易日重置编号,否则分日逻辑失效
deal_col_name(如 Pricemake() 第 2 个断言AssertionError所有环境,决定盈亏计算口径用收盘价还是成交价,要与你回测假设一致
feature_names 里的每一列make() 第 4 个断言,逐列检查任一列不存在即 AssertionError所有环境,构成观测列名大小写敏感;观测里会按你给的顺序排列
datetime非断言,但回测/日志与账本都靠它账本时间列缺失,逐日推进无法核对逐日回测、结果留痕建议解析成真正的时间类型,不要留字符串
Volume非断言,官方样例与教程默认特征若列入 feature_names 则断言失败价量类特征tick 数据里常是累计量,是否需要差分由你决定
买卖盘列(Ask_price/Bid_price 等)非断言不报错,但你没有盘口特征可用tick 级微观结构研究没有这些列时不要把它们写进 feature_names
Updown/Updown_Cum非断言,官方样例自带不报错期货等带涨跌字段的品种A 股日线没有这两列,直接不传即可
v1 的两个环境在观测里额外拼接 8 列持仓/成本/盈亏(positionposition_variationentry_coverpriceprice_meanreward_fluctuantreward_makerealreward),这些列不需要你提供,由环境自己生成。

TradingGym · 分日机制

TradingGym serial_number 怎么把一张大表切成一天一天

环境用 df[df['serial_number'] == 0].index 找出每个交易日的起点,再据此切分。所以 serial_number 不是「行号」,而是每个交易日内从 0 开始的连续编号

官方样例数据实读(trading_env/test/data/SGXTWsample.csv,2026-09-22 本机按 (serial_number==0).cumsum() 分组统计)
交易日(环境口径)tick 数起始时间结束时间跨几个日历日期注意点
第 1 日8,5502013-04-09 10:45:032013-04-10 01:59:392首根 tick 必须标 0,否则这一天被整体跳过
第 2 日10,4512013-04-10 08:45:002013-04-11 01:59:562编号要重置,不能跨日累加
第 3 日14,0712013-04-11 08:45:002013-04-12 01:59:122夜盘尾巴落在次日凌晨
第 4 日10,1822013-04-12 08:45:002013-04-13 01:59:552次日凌晨那段属于本交易日
第 5–7 日15,543 / 15,329 / 13,316均 08:45:00次日 01:59 左右2单日 tick 数在 8.5k–25.6k 之间波动
第 8 日25,6172013-04-19 08:45:002013-04-20 01:59:572最长的一个交易日(占比最大)
整表113,0592013-04-09 起2013-04-20 止11 个日历日期 → 8 个交易日行数 ≠ 可用天数:每天还要够 obs_data_len + step_len 根才能走完 episode
groupby(dt.date)会切出 11 天其中 3 天只有 283 / 422 / 202 根(夜盘尾巴),会得到与官方不一致的回测划分
这是官方样例里最容易被忽略的一条事实:SGX TW 是带夜盘的品种,2013-04-09 08:45 开的那个交易日一直跑到 2013-04-10 01:59。所以「按日历日期分组」与「按交易日起点分组」结果不同——前者 11 天、后者 8 天。生成 serial_number 时必须用交易日起点(例如「时间早于当日 08:45 的 tick 归到前一交易日」),或直接沿用数据里已有的 serial_number
生成 serial_number 的最小写法(自有数据的通用做法):先按时间排序,再把「交易日期变化」当成新一天:df['serial_number'] = df.groupby(df['datetime'].dt.date).cumcount()。日线数据每天只有 1 根,编号恒为 0。

TradingGym · 索引契约

TradingGym 为什么索引必须是 0..N 的整数:一个最容易踩的坑

这不是风格问题。源码里取出「每日起点」后,用的是 self.df.iloc[begin_point:end_point]——把索引标签当位置用。只要索引不是从 0 开始的连续整数,切片就会静默取错数据。

正确写法

df = df.reset_index(drop=True)   # 索引变成 0..N-1
assert list(df.index[:3]) == [0, 1, 2]

官方样例 CSV 用 pd.read_csv(..., index_col=0) 读取:第一列恰好是 0..N 的整数,所以能直接跑。这不是巧合,而是这份数据被整理过。

典型错误写法

df = pd.read_csv('mydata.csv', parse_dates=['datetime'])
df = df.set_index('datetime')     # 索引变成时间戳
env = trading_env.make(..., df=df)  # 不报错,但取的是错位的行

用时间戳做索引时,.index[label] 返回的是 Timestamp,传给 iloc 会报 TypeError;如果索引是像 1000, 2000, ... 这样的整数,则不会报错但取到的是错的区间——后者更危险。

索引形态对照:哪一种能用(本机按源码逻辑推导 + 实测确认)
索引形态能否用现象适用场景注意点
RangeIndex 0..N-1能用切片正确默认推荐官方样例就是这种;读完 CSV 后 reset_index(drop=True) 最稳
时间戳索引不能用iloc 收到 Timestamp 报 TypeError想按时间查数据,另存一列 datetime 即可
从 1 开始不能直接使用每日起始偏移一行,账本与真实交易日错开表现为「第一天少了第一根 tick」,很难察觉
非连续整数(如按股票代码)不能用不报错但区间错位,回测结果无意义多标的数据务必先按单一标的切片再 reset_index
多标的混在一张表不能用不同标的的 tick 被当成同一天序列TradingGym 不含标的维度,必须一只标的一份 df、一个环境
按日期做 MultiIndex不能用断言与索引逻辑都不兼容先把层级压平(reset_index()

TradingGym · 适配模板

TradingGym 三种数据怎么适配:tick、OHLC 日线、A 股日线

下面三段代码把自有数据整理成可用的形态。注意变量含义:obs_data_len 是观测窗口根数,step_len 是每次 step 前进的根数——日线数据一天只有 1 根,所以要按「多天滚动窗口」来理解。

模板 A · tick 数据(最贴近官方设计)

df = pd.read_csv('tick.csv', parse_dates=['datetime']).sort_values('datetime')
df = df.reset_index(drop=True)                       # 索引契约
df['serial_number'] = df.groupby(df['datetime'].dt.date).cumcount()   # 每日从 0 编号
features = ['Price', 'Volume', 'Ask_price', 'Bid_price']
assert {'serial_number', 'Price', *features} <= set(df.columns)
env = trading_env.make('backtest_v1', obs_data_len=64, step_len=32, df=df,
                       fee=0.0, max_position=1, deal_col_name='Price', feature_names=features)

要点:先排序、再 reset_index、最后生成 serial_number。顺序反了会导致编号与行序不一致。

预期输出:make() 不报错;若列名拼错会立刻抛 AssertionError

模板 B · OHLC 日线

df = pd.read_csv('daily.csv', parse_dates=['datetime']).sort_values('datetime')
df = df.reset_index(drop=True)
df['serial_number'] = 0            # 每天只有 1 根,编号恒为 0 → 一天=一根 tick
df['Price'] = df['close']
features = ['Price', 'open', 'high', 'low', 'volume']
env = trading_env.make('backtest_v1', obs_data_len=20, step_len=5, df=df, fee=0.0,
                       max_position=1, deal_col_name='Price', feature_names=features)

要点:日线下 obs_data_len=20 表示「看过去 20 个交易日」,step_len=5 表示每 5 个交易日决策一次。此时 serial_number=0 意味着整张表被当成「一个超长的交易日」。

预期输出:单日 episode 长度 ≈ (len(df) - obs_data_len) / step_len 步,全部合并在一个 episode 里。

模板 C · A 股日线(含 T+1 提醒)

df = ak.stock_zh_a_hist(symbol='600519', period='daily', adjust='hfq')   # 需自备数据源
df = df.rename(columns={'日期':'datetime','收盘':'close','成交量':'volume'})
df['datetime'] = pd.to_datetime(df['datetime'])
df['serial_number'] = 0
df['Price'] = df['close']
df = df.reset_index(drop=True)
features = ['Price', 'volume']

要点:TradingGym 只管「按价格撮合」,不管 T+1、涨跌停、停复牌与最小交易单位。这些规则必须在外层自己加约束(例如用观测里的持仓列判断是否可卖)。

预期输出:环境可以建起来,但所有 A 股特有规则都由你自己保证——这是本项目的能力边界,不是 bug。

TradingGym · 错误对照

TradingGym 数据类报错怎么定位?现象、原因、定位与修法

数据问题几乎都在 make() 或第一次 reset() 暴露,不会拖到训练后期。按这张表逐行对,比反复读源码快。

数据类问题速查(报错原文取自源码断言与本机实测)
现象 / 报错原因怎么定位修法注意点
AssertionError(无消息)四个断言之一失败:env_id 非法 / deal_col_name 不在列 / 无 serial_number / 特征列缺失逐个打印 env_id in available_envs()deal_col_name in df.columns'serial_number' in df.columnsset(feature_names) <= set(df.columns)补齐缺失列,或改 deal_col_name/feature_names断言不带提示信息,必须自己逐项检查
KeyError: 'Price'列名大小写或命名不符打印 list(df.columns)统一列名(df.rename(columns=...)空格与全角字符也会导致不匹配
TypeError(iloc 相关)索引不是整数(如时间戳)打印 type(df.index)df = df.reset_index(drop=True)只在 reset() 阶段暴露
回测「只跑了一天」serial_number 未按日重置,只识别出一个交易日打印 (df['serial_number'] == 0).sum(),应为交易日数按日期 groupby(...).cumcount() 重新编号编号跨日累加时最难察觉
episode 立刻结束当日 tick 数少于 obs_data_len + step_len打印每日行数分布减小 obs_data_len/step_len,或补足数据停牌日、半日市都会触发
观测里出现 NaN特征含缺失值(如 rolling 头部未裁剪)df[features].isna().sum()裁剪头部或 fillna本站在双均线实验里直接切掉了 MA 未定义的前 20 行
结果明显不合理时间未排序 / tick 与 OHLC 混用 / 多标的混表检查 datetime 是否单调递增、每行是否同一标的排序、拆分标的、统一频率多标的混表不报错但结果无意义

TradingGym · 自检清单

TradingGym 建环境之前要过哪些自检?8 项清单

这份清单对应源码里的真实约束,逐项确认能省掉绝大多数「跑不通」的时间。

结构与列

  • ① 索引是 0..N-1 的整数(reset_index(drop=True) 过一遍)。
  • ② 有 serial_number 列,且每天从 0 重新编号。
  • deal_col_name 指向的价格列真实存在,且与你回测假设一致。
  • feature_names 里每一列都存在于 DataFrame 中。

内容与规模

  • datetime 已解析为时间类型且单调递增。
  • ⑥ 特征列全为数值型、无 NaN(rolling 头部已裁剪)。
  • ⑦ 每个交易日的行数 ≥ obs_data_len + step_len
  • ⑧ 单只标的一份表;多标的先拆分再分别建环境。
自检项不满足会怎样(对应本机实测与源码路径)
自检项不满足的表现发现时机严重度注意点
① 整数索引切片错位或 TypeError首次 reset()高(结果不可信)静默错位比报错更糟
② serial_number断言失败或只识别 1 天make() / reset()两种失败方式分别对应「没这列」与「没重置」
③ deal_col_name断言失败make()列存在但语义不符(如用买价算盈亏)不会报错
④ feature_names断言失败make()逐列检查,任一缺失即失败
⑤ datetime账本时间列不可用结果分析阶段不影响运行,影响可复现与对账
⑥ NaN观测异常、reward 出现 NaN 风险reset() 之后rolling 指标最容易带入头部 NaN
⑦ 每日行数episode 立即结束首个 step停牌/半日市数据尤其常见
⑧ 单一标的不报错但结果无意义结果分析阶段项目不含标的维度,必须自己拆

TradingGym · FAQ

TradingGym 数据格式常见问题是什么?

TradingGym 能直接用 A 股日线数据吗?

机制上可以:只要满足四道断言并把索引整理成整数,日线也能建成环境(模板 C 已给出写法)。但要注意三点——官方没有任何 A 股数据源,复权要自己处理;日线每天只有 1 根,serial_number 恒为 0,整个区间会被当成一个长 episode;T+1、涨跌停、停复牌、最小交易单位这些规则它完全不管,必须在外层加约束。以官方源码的实际行为为准。

tick 数据一定要有买卖盘列吗?

不一定。只有 serial_numberdeal_col_namefeature_names 里的列是硬要求。官方样例带 Ask_price/Bid_price 等盘口字段,但如果你不把它们写进 feature_names,缺了也不会报错——只是你没有盘口特征可用。特征怎么选属于研究设计,不是安装问题。

一天只有几百根 bar,能跑完一个 episode 吗?

可以,但要保证每日行数 ≥ obs_data_len + step_len。若一天的 bar 数小于这个值,第一个 step() 之后就会直接 done。处理方式是调小窗口参数,或者改为中心化的多日窗口(把 serial_number 统一设 0,把整段区间当成一个交易日)。两种做法改变的是实验设定,需要在报告里写清楚。

serial_number 必须是「每日从 0 开始的连续编号」吗?

是。环境用 df[df['serial_number'] == 0] 找每个交易日的起点,所以 0 的位置决定切分点;跨日累加或从 1 开始都会让切分错位或漏掉第一天。生成方式见本页 BAND 2 的最小写法,仍建议跑完后打印 (df['serial_number'] == 0).sum() 与交易日数对照,以官方实现为准。

我的 DataFrame 有几千行,为什么环境说取不到数据?

先检查两件事:①是不是用了时间戳索引(iloc 会拿到 Timestamp);②serial_number 列里有没有 0。这两条占到数据类报错的绝大多数。其余情况按本页「错误对照表」逐行排查,每一项都给了定位命令。

多只股票可以放在同一张表里吗?

不可以。TradingGym 的环境没有标的维度,serial_number 只表达「日内第几根」,混表会让不同标的的 tick 被当成同一条时间序列,且不会报错。正确做法是每只标的一份 DataFrame、一个环境实例,最后在环境之外汇总结果。

下一步该怎么做?把回测循环跑起来

数据整理好之后,真正的坑在回测:README 里的 backtest 示例在当前版本会直接报错。这页给出可运行的日间循环与账本读取方式。