简介深度强化学习与时间序列预测方向的学习者常面临理论多、可运行实例少的问题此压缩包正好提供一个DRL预测项目。项目以DQN等经典算法为基础结合正弦函数等模拟数据展示智能体如何通过与环境交互学习预测未来序列值覆盖环境搭建、智能体实现、训练循环及结果可视化等完整流程。资源共35个文件以Python脚本和预编译pyc为主辅以JSON配置、pickle数据及环境配置文件env.yml其中src目录存放核心代码data目录提供训练与测试数据集README和LICENSE便于快速上手与合规使用。整个压缩包约760KB轻量易部署。已有525人下载学习适合希望通过实际代码理解强化学习在时间序列预测中应用的开发者可据此快速搭建实验、修改超参数并观察不同策略效果深入掌握DRL模型设计中的关键环节。1. 为什么时序预测要转向深度强化学习预测误差最小化不等于决策收益最大化如果你做过 LSTM 或者 Transformer 做股票价格预测、销量预测这类任务大概率遇到过同一个尴尬局面测试集上的 MSE 已经压得很低画出来的预测曲线和真实值几乎贴在一起但你把这个预测结果拿去做交易回测账户曲线却难看得很。问题不出在模型出在目标函数。你训练模型的时候用的是「预测误差最小化」而真正关心的是「决策收益最大化」这两件事在数学上不是一回事。深度强化学习换了个思路不再要求模型把未来数值猜准而是让模型直接输出动作——买、卖、持有、仓位多少——然后按动作产生的收益给奖励。这套路线在金融时序预测、能源负荷预测这类「预测完了还要做决策」的场景里比单纯的序列预测算法更贴合真实诉求。本文按我实际落地的路径把环境设计、算法选型、数据工程、训练调参和踩坑点完整讲一遍适合已经会跑 LSTM 时序预测、想往上走一层做决策优化的从业者。2. 深度强化学习做时序预测的原理状态、动作、奖励设计与算法选型2.1 为什么监督学习在时序决策上吃亏目标函数和真实目标错位先做一个思想实验。你用 LSTM 预测明天的收盘价预测值是 10.2真值是 10.0MSE 是 0.04看起来很好。但如果你按这个预测去交易系统告诉你「明天会涨 2%」于是你满仓买入结果真值显示只涨了 0.1%甚至跌了 0.5%。MSE 角度这只是个小误差从收益角度这是致命的误判。问题就在于误差分布和收益分布不是线性对应的预测偏一点点决策结果可能差很远。第二个问题是单点预测丢失了序列决策的连续性。交易不是一次性赌大小今天的仓位决定明天的浮盈明天的动作又基于今天的持仓状态。这种带状态转移的决策链条监督学习很难显式建模。你当然可以把未来 5 天的预测结果拼起来做成信号但那本质上还是「先预测、后决策」的两段式预测误差会在第二阶段被放大。深度强化学习把整个问题重新表述成一个马尔可夫决策过程状态是当前能观测到的市场特征窗口动作是仓位或买卖指令奖励是执行动作后的收益扣除成本。模型不再关心「明天价格是多少」只关心「现在这个动作能不能让账户曲线变好」。这也是为什么金融时序预测、超短期光伏功率预测这类场景里深度强化学习算法的讨论热度逐年上升——因为它们的最终目标本来就是决策而不是数值预测本身。2.2 动作空间与奖励设计连续仓位还是离散三态收益怎么给动作空间的选择直接决定了环境怎么搭。常见做法是两种离散动作空间一个三维向量买入、持有、卖出。优点是实现简单、和交易指令天然对齐、便于和传统策略对比缺点是仓位粒度太粗没法表达「半仓」这类中间状态而且离散动作会让策略在边界动作附近学得不够平滑。连续动作空间一个 [-1, 1] 之间的实数代表仓位比例1 代表满仓做多-1 代表满仓做空。优点是表达能力强能精细控制敞口缺点是训练难度更高动作的微小变化对奖励的影响是非线性的而且实盘执行时还要把连续仓位映射到实际委托量。我一般建议交易成本高的标的、流动性差的标的、或者你只需要方向性信号时选离散做指数、流动性好的品种或者你的目标本身就是动态仓位管理选连续。奖励设计是整个环境里最容易被低估的部分。最朴素的奖励是单步收益r_t ret_t * position_{t-1} - cost_t其中cost_t是换手成本。这里有一个必须注意的细节reward 里不扣手续费模型一定会学会高频换手因为它发现来回折腾能拿到更多「账面收益」而手续费在它看来不存在。另一个细节是收益的分布问题。金融时序里的单步收益是尖峰厚尾的直接拿原始收益当奖励少数大波动样本会主导梯度。常见解法是给奖励做变换比如用符号函数乘以收益的平方根或者对收益做 rank 归一化降低极端值的影响。2.3 算法家族对比DDPG / TD3 / SAC / PPO 谁更耐金融数据的性子深度强化学习算法在时序预测上真正常用的就四个DDPG、TD3、SAC 和 PPO。它们之间的区别决定了你调参时候血泪经验的方向。算法策略类型适用动作稳定性样本效率主要毛病DDPG确定性off-policy连续差高Q 值过估计超参数敏感训练曲线容易崩TD3确定性off-policy连续中高比 DDPG 稳但对探索噪声仍有要求SAC随机off-policy连续中高熵系数要调探索充分但收敛后可能过度随机PPO随机on-policy离散/连续好低样本利用率低但胜在稳金融数据的特点是信噪比极低、非平稳、分布随时在变。在这种数据上算法的稳定性比样本效率重要得多。PPO 的 clipped surrogate objective 限制了每次更新的步长不容易出现 DDPG 那种训练到一半 reward 突然崩掉的情况所以我的建议是第一次搭环境、验证流程能不能通用 PPO流程稳定之后再换上 SAC 去试天花板。如果你看到网上各种深度强化学习算法列表对比的帖子纠结选型记住一句话——金融场景先求稳再求好PPO 是下限最高的那个。3. 时序数据工程与特征构造标准化、滚动窗口与不泄漏的切分方式3.1 特征怎么选收益率、波动率与技术指标的组合时序预测的数据工程比模型结构更决定上限。直接用原始价格作为输入是新手最常见的错误价格序列非平稳均值随时间漂移模型学到的大多是「昨天的价格大概等于今天」这种无聊的恒等映射。正确的起点是把它转成平稳性更好的衍生特征。我通常是这么构造特征集import pandas as pd import numpy as np df pd.read_csv(price.csv, parse_dates[date]).set_index(date) # 对数收益消除绝对价格水平的影响 df[log_ret] np.log(df[close] / df[close].shift(1)) # 滚动波动率市场状态的直接度量 df[vol_20] df[log_ret].rolling(20).std() # 动量因子过去 N 日的累计涨幅 df[mom_5] df[close] / df[close].shift(5) - 1 # RSI超买超卖区间识别 delta df[close].diff() gain delta.clip(lower0).rolling(14).mean() loss (-delta.clip(upper0)).rolling(14).mean() rs gain / loss df[rsi_14] 100 - 100 / (1 rs) # 删除有 NaN 的行按时间顺序保留 feature_cols [log_ret, vol_20, mom_5, rsi_14] clean_df df.dropna(subsetfeature_cols) features clean_df[feature_cols].values.astype(np.float32) close clean_df[close].values.astype(np.float32)代码逻辑先用对数收益率替代原始价格解决非平稳问题再加滚动波动率表达市场当前的风险状态动量因子捕捉中短期趋势RSI 提供超买超卖的位置信息。这套特征组合对股票价格预测、销量预测、电力负荷预测都适用区别在于波动率和动量的窗口长度要按数据频率调整。参数说明rolling(20)是 20 个交易日对应自然月长度rolling(14)是 RSI 的经典周期。日线数据用这两个值起步是合理的如果是小时级数据周期要放大到 4060 根 K 线才有统计意义。数据中的缺失值在时间序列里只能用前向填充或直接删除不能用均值填充否则会引入未来信息。3.2 训练 / 验证 / 测试切分按时间轴切不是随机打乱这一点对做过结构化数据建模的人来说反直觉。分类任务里随机打乱数据是标准操作但时序预测里一旦打乱模型就能从未来样本里学到本不该看到的信息验证集和测试集全部失真。更隐蔽的错误是有人不打乱数据但仍然用 KFold 交叉验证KFold 默认也是随机切分这在时序场景里同样是泄漏。正确的做法是按时间轴切分# 按时间顺序切分前 60% 训练接下来 20% 验证最后 20% 测试 n len(clean_df) train_end int(n * 0.6) val_end int(n * 0.8) train_df clean_df.iloc[:train_end] val_df clean_df.iloc[train_end:val_end] test_df clean_df.iloc[val_end:]逻辑说明训练集只覆盖最早的时间段验证集次之测试集最后。模型在训练过程中永远看不到验证集和测试集的数据这才能保证评估指标真实反映模型的泛化能力。更严格的做法是 walk-forward 验证先把数据切成 K 段第一轮用第 1 段训练、第 2 段验证第二轮用第 1、2 段训练、第 3 段验证以此类推。这种做法模拟了实盘里「模型滚动更新」的节奏代价是训练次数成倍增加。初学阶段先做一次静态切分就够了等方案基本成型再上 walk-forward。3.3 归一化的隐藏陷阱全局统计量是未来信息归一化在深度学习里是标配操作但时序预测里的归一化和普通场景有本质区别。假设你用 sklearn 的 MinMaxScaler 对全量数据做 fit_transformscaler 计算最小值最大值时用到了测试段的数据——这相当于模型在训练阶段就知道了未来的价格区间。训练集表现还行测试集一换就全面崩盘你排查半天还以为模型结构有问题。解决方案是滚动窗口归一化每个时间点的特征值只用该时间点之前的数据计算统计量。# 滚动 z-score用过去 120 天的均值标准差做归一化不偷看未来 def rolling_zscore(series: pd.Series, window: int 120) - pd.Series: mean series.rolling(window).mean() std series.rolling(window).std() return (series - mean) / std for col in [log_ret, vol_20, mom_5, rsi_14]: clean_df[col _z] rolling_zscore(clean_df[col])参数说明window 取 120 天因为 120 天大约是半年交易时间能覆盖一个中周期的市场状态窗口太短会跟着短期波动乱跳窗口太长又反应迟钝。注意第一段 120 天内的数据会变成 NaN需要 dropna 丢掉。训练环境使用的特征就是这些_z列。实现里还有一个细节验证集和测试集的归一化不能用测试段数据重新计算均值应该直接用训练段最后 120 天的统计量继续滚动下去。换句话说整个归一化过程应该发生在切分之前按时间顺序一路滚动到底这样任何一步都不存在「用未来算过去」的问题。4. 可运行的强化学习时序预测基线用 PPO 在本地训练一版最小实现4.1 用 gymnasium 封装交易环境状态、动作与收益的计算深度强化学习的训练过程依赖环境提供「状态 → 动作 → 奖励 → 新状态」的闭环。时序预测场景里这个环境就是历史数据集本身。我习惯用 gymnasium 写环境因为它和 Stable-Baselines3 的兼容性最好。import numpy as np import gymnasium as gym from gymnasium import spaces class TimeSeriesTradingEnv(gym.Env): def __init__(self, features, close, window30, fee0.001): super().__init__() self.features features # (T, F) 归一化后的特征矩阵 self.close close # (T,) 原始收盘价用于计算收益 self.window window # 观测窗口长度 self.fee fee # 单边交易成本 # 动作[-1, 1] 连续仓位正数做多负数做空 self.action_space spaces.Box(low-1.0, high1.0, shape(1,), dtypenp.float32) # 观测最近 window 天的特征序列 self.observation_space spaces.Box( low-np.inf, highnp.inf, shape(window, features.shape[1]), dtypenp.float32 ) self.t window self.position 0.0 # 当前持仓仓位初始为空仓 def reset(self, seedNone, optionsNone): super().reset(seedseed) self.t self.window self.position 0.0 obs self.features[self.t - self.window:self.t] return obs.astype(np.float32), {} def step(self, action): action np.clip(action, -1.0, 1.0)[0] t self.t # 当前持仓仓位持有 t-1 到 t 这一周期产生的收益 ret self.close[t] / self.close[t - 1] - 1.0 # 换手成本只在仓位改变时产生 cost self.fee * abs(action - self.position) reward ret * self.position - cost self.position action self.t 1 terminated self.t len(self.close) - 1 truncated False obs self.features[self.t - self.window:self.t] return obs.astype(np.float32), reward, terminated, truncated, {}逻辑说明环境的状态是最近 30 天的特征窗口动作是模型输出的目标仓位。奖励分两部分——上一周期持仓产生的浮动收益以及调仓产生的交易成本。terminated设为数据尾部保证训练到最后一个时间点自然结束。参数说明window30意味着模型每次做决策时能看到的是一根 30 天的特征窗口这个值和特征的记忆长度配套。fee0.001代表单边 0.1% 的成本做股票短线交易够用如果对标期货或者加密货币这个值要按实际费率调整。4.2 用 Stable-Baselines3 训练 PPO关键参数与收敛信号环境写好后训练部分比想象中短。Stable-Baselines3 把 PPO 实现封装得很完整需要你操心的只剩参数。from stable_baselines3 import PPO from stable_baselines3.common.vec_env import DummyVecEnv # 训练环境只用训练段数据 train_env DummyVecEnv([lambda: TimeSeriesTradingEnv(train_features, train_close)]) model PPO( MlpPolicy, train_env, learning_rate3e-4, n_steps2048, batch_size64, gamma0.99, gae_lambda0.95, ent_coef0.01, verbose1, seed42, ) model.learn(total_timesteps100_000) model.save(ppo_ts_model)逻辑说明DummyVecEnv把单环境包装成向量环境这是 Stable-Baselines3 训练的标准入口。MlpPolicy代表策略网络和价值网络都是全连接网络特征窗口进入网络前会被自动展平成一维向量。参数说明learning_rate3e-4是 Adam 优化器在 PPO 里的保守默认值在这个值附近上下浮动三倍都算正常区间n_steps2048表示每轮更新前收集 2048 步的经验数据量小的时候要降到 512 或 1024否则一个 epoch 都填不满gamma0.99是折扣因子代表模型对未来奖励的看重程度金融场景建议 0.980.995 之间ent_coef0.01给熵奖励加了一个小权重防止策略过早收敛到「永远空仓」的懒惰解seed42固定随机种子保证实验可复现。训练时的收敛信号和普通深度学习不一样不要盯着 loss 看要看ep_rew_mean这个指标。它在 verbose 输出里会打印表示平均每轮累积奖励。正常的训练过程是前期有一段波动然后ep_rew_mean缓慢爬升并稳定在一个正区间。如果这个值一直不涨先别急着调网络结构检查 reward 的计算逻辑有没有问题。4.3 回测与评估夏普比率、最大回撤与换手率训练结束只是第一步模型好不好要看在测试段上的表现。回测代码我一般单独写一个脚本避免和训练逻辑混在一起def evaluate_model(model, features, close, window30, fee0.001, deterministicTrue): env TimeSeriesTradingEnv(features, close, windowwindow, feefee) obs, _ env.reset() done False returns [] while not done: action, _ model.predict(obs, deterministicdeterministic) obs, reward, terminated, truncated, _ env.step(action) returns.append(reward) done terminated or truncated returns np.array(returns) # 夏普比率年化衡量单位风险的超额收益 sharpe returns.mean() / (returns.std() 1e-8) * np.sqrt(252) # 累计收益 cum_return float(np.prod(1 returns) - 1) # 最大回撤 equity np.cumprod(1 returns) peak np.maximum.accumulate(equity) drawdown float(np.max(1 - equity / peak)) # 换手率动作变化的平均幅度反映交易频率 actions [] obs, _ env.reset() done False while not done: action, _ model.predict(obs, deterministicdeterministic) actions.append(action[0] if hasattr(action, __len__) else action) obs, _, terminated, truncated, _ env.step(action) done terminated or truncated actions np.array(actions) turnover float(np.mean(np.abs(np.diff(actions.squeeze())))) return { sharpe: sharpe, cum_return: cum_return, max_drawdown: drawdown, turnover: turnover, }逻辑说明回测循环让模型在测试段的特征数据上重新走一遍每个时间点输出动作、环境计算奖励最后汇总成四个指标。deterministicTrue让 PPO 输出策略均值而不是采样值回测结果更稳定。参数说明np.sqrt(252)是年化系数因为用的是日线数据一年大约 252 个交易日。如果你的数据是小时级这个系数要换成np.sqrt(252 * 交易小时数)。看回测结果时四个指标要一起看。夏普比率高但最大回撤大的策略实盘体验很差累计收益好但换手率极高的策略对成本的敏感度太高成本略微上浮就不赚钱了。我给自己设的及格线是夏普大于 1、最大回撤小于 20%、日均换手率低于 0.3。达不到就继续调特征和参数而不是急着换算法。5. 强化学习时序预测的 5 个高频翻车点从数据泄漏到实盘偏差5.1 全样本归一化验证集和测试集一起泄漏现象训练集和验证集上策略收益都很漂亮夏普能到 2 以上一到测试集就变成负收益模型就像换了一个人。原因归一化时用了全量数据的均值和标准差测试段的统计信息在训练阶段已经暴露给模型。模型学到的不是「规律」而是「对测试段范围的记忆」。解决按时间顺序做滚动归一化每个时间点的特征只依赖它过去 120 天的数据。验证这个坑有没有踩平的方法很简单——把测试段单独拎出来只用训练段末尾的统计量做标准化对比两组预测分布是否一致。5.2 reward 不含手续费模型学会高频换手现象回测里的交易次数极其频繁平均持仓时间不到一天收益曲线看起来很丰满一算扣掉手续费后的净收益直接变负。原因训练环境的 reward 只算了价格收益没扣换手成本。在模型看来反复调仓没有惩罚它自然愿意通过高频交易去捕捉每一个小波动。解决在环境的 step 函数里加上成本项cost fee * abs(new_position - old_position)并且把 fee 设成略高于实际值。这是一个反直觉但很实用的技巧——训练时用 0.0015 的实际费率实盘按 0.001 计算多出的 0.0005 是在给模型留安全边际。5.3 gamma 设成 0.999智能体只看远期不看当下现象训练过程中 reward 一直上升但回测策略表现很差经常出现「该止盈不止盈、该止损不止损」的决策。原因gamma0.999意味着模型几乎不区分即时奖励和远期奖励金融数据本身信噪比低远期奖励的预测误差极大模型等于在追逐一个充满噪声的远期目标。解决按决策周期反推 gamma。一天调一次仓的场景gamma 取 0.980.99一周调一次仓取 0.950.98。如果换仓频率本身不固定就把 gamma 视为超参数参与搜索。5.4 随机种子没固定模型预测股票涨跌每次结果不一样现象完全相同的代码、相同的数据跑两次训练得到的策略收益曲线完全不同甚至一次正收益一次负收益。原因深度强化学习的训练过程本身就包含大量随机性——网络权重初始化、经验采样顺序、探索噪声。不固定随机种子每一次训练都是从不同起点出发的随机过程。解决训练前固定三个种子——Python 的random.seed、NumPy 的np.random.seed、PyTorch 的torch.manual_seed。但这还不够同一套超参数应该用 5 个不同的种子各训练一次报告指标时取中位数而不是某一次的最好结果。只看一次训练结果就下结论是深度强化学习项目里最昂贵的乐观偏差。5.5 回测环境与真实交易规则不一致连续仓位到实盘不可用现象回测曲线平滑向上实盘小资金跑了一周成交价总是比模型假设的差收益明显缩水。原因回测环境假设可以持有任意连续仓位比如 0.37 手、-0.82 手。真实交易里有最小交易单位、有涨跌停、有盘口深度连续仓位映射到实际委托时必然产生偏差滑点还会进一步侵蚀收益。解决在环境中加两个修正——第一把动作离散成几个固定档位比如空仓、半仓、满仓三档贴近真实可执行的粒度第二在成交价上加入滑点模型买入价加一档、卖出价减一档用保守的成交假设过滤那些「回测美如画、实盘就翻车」的策略。6. 进阶验证让强化学习时序预测结果可信的最后一公里6.1 多段 walk-forward 验证把回测的偶然性摊开一次静态回测只能证明模型在某个特定时间段有不错的赚钱能力证明不了它在变化的市场里依然有效。更可信的做法是 walk-forward 验证把数据按时间切成多段每段都重复「训练 → 验证 → 测试」的流程。实际操作分四步第一把数据按季度切成 8 段第二第一轮用前 4 段训练、第 5 段测试第三第二轮用前 5 段训练、第 6 段测试以此类推第四汇总每一轮的夏普比率如果大多数轮次都能稳定跑赢买入持有基线这个策略才算初步过关。如果 8 段里只有两段赚钱剩下的全亏那说明模型学到的是某段特定行情的路径依赖而不是可复用的决策规律。6.2 与强基线对比先跑赢 LSTM 和买入持有再谈强化学习的价值深度强化学习不是银弹它比 LSTM 复杂得多对数据量和工程能力的要求也更高。所以在宣布「强化学习方案有效」之前先按复杂度从低到高对齐三个基线买入持有、LSTM 预测 规则交易、PPO。买入持有是最低的及格线如果策略连「躺着不动」都跑不赢说明模型学到的东西没有价值。LSTM 预测 规则交易是目前金融时序预测领域的主流实践用 LSTM 预测未来收益方向收益为正就持仓、为负就空仓。如果强化学习方案跑不赢这个基线那背后的原因大概率不是算法问题而是环境建模和特征构造还没做到位。只有强化学习方案在费用扣除后稳定跑赢 LSTM 基线你才有底气说「强化学习在这个场景里有价值」。这是个很朴素的标准但能过滤掉大量自我感动的实验。6.3 滚动重训从静态模型到持续适应的时间表时序数据的非平稳性决定了模型会随着时间推移而衰减。把模型训练一次就永久部署三个月后收益衰减到零几乎是必然的。业界常见做法是设定固定重训节奏每周五收盘后把最新一周的数据并入训练集重新训练一次模型然后在下周一开盘前完成模型替换。重训逻辑本身不复杂但有两个必须监控的指标策略收益与买入持有基线的滚动差值以及换手率的滚动均值。如果连续四周策略跑输基线或者换手率突然放大到正常水准的三倍触发预警强制检查特征分布和训练日志。用定时任务触发重训脚本配合日志监控每天扫一眼结果这套流程跑起来后强化学习在时序预测里的价值才算是真正落地了。我在这类项目上吃过最大的亏是过于信任单次回测结果眼看着回测曲线一路向上实盘却连续回撤。后来才明白深度强化学习的每一个环节——特征、环境、奖励、超参数、随机种子——都在微妙地影响最终结果任何一步的侥幸都会在真实数据上被加倍放大。从那以后我养成一个习惯每调整一个参数就跑完整套 walk-forward 验证把结果记下来再动下一个。这个习惯让我少走了很多弯路也希望帮到你。本文还有配套的精品资源点击获取