恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
深度强化学习股票交易:PPO/A2C/DDPG源码解析与风控实践
首页
资讯中心
/
深度强化学习股票交易:PPO/A2C/DDPG源码解析与风控实践
深度强化学习股票交易:PPO/A2C/DDPG源码解析与风控实践
发布时间:2026/9/24 18:23:54
简介基于深度强化学习的自动化股票交易策略设计源码面向金融科技研究者、量化交易爱好者及希望探索AI交易算法的投资者解决传统人工交易中情绪干扰与策略适应性问题。项目以PPO、A2C、DDPG三种Actor-critic算法为核心训练代理与股票市场环境交互通过历史数据识别交易模式并动态调整策略以最大化投资回报。压缩包共40个文件、大小6.4MB包含14个Python脚本、4个XML算法配置、多个CSV市场数据、PNG结果图表及Jupyter Notebook交互分析环境并附使用许可与说明文档目录结构清晰便于按功能模块学习。已有303人学习下载。读者可获得完整的深度强化学习交易策略实验框架既能查看回测与训练脚本也能复用真实股票数据与预处理流程适合作为入门实操和算法对比研究的参考。1. 深度强化学习做自动股票交易这份源码到底能跑出什么先说一个反直觉的结论用深度强化学习做股票交易回测里翻倍的策略换一段行情往往直接归零。这不是算法不行而是大部分开源项目把环境、奖励、数据切分做得太随意模型在训练集上背答案。这份源码之所以值得拆是因为它来自 ICAIF 2020 的 Ensemble Strategy 论文配套实现把 PPO、A2C、DDPG 三种 Actor-Critic 算法做成了可对比、可组合的完整实验框架并且带上了 turbulence index 风控——市场波动异常时强制降低仓位而不是让模型硬扛。项目覆盖 2009-2020 年道琼斯 30 只成分股的日线数据外加 SPY 作为基准从数据预处理、多股票交易环境、算法训练到回测评估全部跑通。它的核心价值不是稳赚而是给你一个标准化的强化学习交易实验台想验证新的奖励函数、想换动作空间、想对比不同算法的鲁棒性改配置就能跑。适合正在入门深度强化学习与金融交叉方向的研究生、想评估 RL 交易可行性的量化从业者以及那些被各种指标源码绕晕、想从零理解 agent-environment 闭环的开发者。2. 拆开看数据与预处理turbulence index 是这套代码的风控灵魂2.1 六个数据文件各自干什么别一股脑全喂给模型项目 data 目录下有四类数据很多新手直接 pd.read_csv 全读进来这是第一个坑。我先把它拆清楚文件内容用途dow_30_2009_2020.csv30 只道指成分股日线 OHLCV主角数据训练/验证/交易共用^DJI.csv道琼斯工业指数日线计算市场整体波动率dow30_turbulence_index.csv30 只股票的扰动指数序列风控开关阈值之上切空仓ETF_SPY_2009_2020.csvSPY 日线数据基准收益评估策略超额收益这里的 turbulence index 是 FinRL 系列项目的经典设计。它本质上是一个基于 ^DJI 收益率的标准差度量衡量市场异常程度。当指数处于极端波动区间比如 2015 年 A 股熔断、2018 年 Q4 暴跌、2020 年 3 月疫情冲击模型如果还按照正常市场学到的策略下单大概率被左右打脸。所以环境内置一个规则turbulence 超过历史 95 分位值时返回空动作并强制平仓。我一般会先单独读一遍 dow30_turbulence_index.csv确认它的时间范围和缺失情况而不是假装它不存在。2.2 预处理脚本真正做的事对齐、填缺失、生成技术指标preprocessors.py 的核心不是算指标而是把 30 只股票的数据对齐到同一时间轴上。道指成分股有停牌日、有上市时间差直接 concat 会让某些股票的行数不一致训练时 batch 维度直接炸掉。常见做法是按日期填充 forward fill然后对每只股票独立计算技术指标再按 ticker 堆叠成三维张量。维度是 [时间步, 股票数, 特征数]这一步错了后面全错。我用一个示例说明标准的数据装配流程import pandas as pd import numpy as np # 道指30成分股数据每个ticker一组 df pd.read_csv(data/dow_30_2009_2020.csv, sep,) # 剥离ticker之后按日期排序 df[date] pd.to_datetime(df[date]) df df.sort_values([ticker, date]).reset_index(dropTrue) # 对数值列做前向填充避免停牌日出现NaN numeric_cols [open, high, low, close, volume] df[numeric_cols] df.groupby(ticker)[numeric_cols].ffill() # 对每只股票独立算收益groupby之后shift1 df[daily_return] df.groupby(ticker)[close].pct_change() df[log_return] np.log(df[close] / df.groupby(ticker)[close].shift(1)) # 生成特征矩阵每只股票一行时间序列多股票堆叠成3D feature_cols [open, high, low, close, volume, daily_return, log_return] pivoted df.pivot_table(indexdate, columnsticker, valuesfeature_cols) # 输出形态: (时间, ticker*特征数) 的宽表后续reshape成[时间, ticker, feature]逻辑说明pct_change 计算当日相对昨日的收益率这是 reward 计算的基础groupby 保证每只股票的 shift 不会串到别的股票上。pivot_table 把长表转成宽表是为了后续按时间步切片时每个 step 能一次性拿到所有股票在当天的特征向量。参数说明ffill 只适合停牌等缺失场景如果某只股票长时间缺失超过 20 个交易日建议直接剔除而不是填充否则模型会把填充值当成真实市场状态学习。特征列不是越多越好——后面环境里动作空间是连续的持仓比例特征噪声过大会让 PPO 的 critic 网络很难收敛。2.3 为什么 SPY 是基准不是训练数据项目里 ETF_SPY_2009_2020.csv 专门用于回测对比。你可以把它理解为对照组策略跑出来的累计收益必须要跑赢买入持有 SPY才有意义。强化学习交易策略如果连指数都跑不赢那模型学到的可能只是市场 beta而不是 alpha。回测时通常把策略净值曲线和 SPY 净值曲线画在同一张图上看超额收益和最大回撤。figs 目录下的 performance.png 就是论文配套的结果图能看到三种算法在 2020 年之前的训练段表现接近进入 2020 年 3 月后 ensemble 策略的回撤明显更小——这就是 turbulence 风控在起作用。3. 交易环境是黑匣子拆开 StockTradingRLEnv 就全明白了3.1 三个环境文件为什么分 train、validation、tradeenv 目录下有三个 Python 文件EnvMultipleStock_train.py、EnvMultipleStock_validation.py、EnvMultipleStock_trade.py加上一个公共的 StockTradingRLEnv.py。很多新手以为三个环境逻辑完全不同其实它们共用同一个基类区别只在数据切分和是否启用 turbulence 风控。train 环境暴露 2009-2016 年的数据validation 环境暴露 2016-2019 年trade 环境暴露 2020 年整年。这么做是为了防止数据泄露模型在训练时绝对不能看到 2020 年的任何一根 K 线。项目原文里反复强调模型基于历史数据训练的局限性代码层面的防泄露就是靠这种硬切分。trade 环境与前两者的另一个区别是它默认开启 turbulence 指数过滤当市场扰动超过阈值环境会返回一个强制清仓信号模型在这个状态下只能选择空仓等待而不是继续买入。3.2 Reward 函数设计只给收益还不够强化学习交易和普通 RL 游戏任务最大的差别在于交易的奖励非常稀疏且噪声巨大。你今天赚钱可能只是运气明天亏钱也可能只是波动。如果 reward 设计只看绝对收益模型会学到频繁交易、追涨杀跌这类短期行为。StockTradingRLEnv 里的奖励计算逻辑值得细看。它不是一个简单的收益率而是引入了持有惩罚、交易成本、turbulence 惩罚三部分。核心原则是鼓励模型长期持仓、避免频繁换手、在极端行情下主动避险。用一段简化代码说明核心奖励逻辑def _calculate_reward(self, actions): # actions: 当前步所有股票的持仓比例范围[-1, 1] # 先算组合当日收益率 daily_returns self.price_history[:, 1] / self.price_history[:, 0] - 1.0 portfolio_return np.sum(actions * daily_returns) # 交易成本惩罚: 换手越大扣分越多抑制频繁交易 turnover np.abs(actions - self.actions_prev).sum() cost_penalty turnover * self.transaction_cost_pct # 常见设定 0.1% # turbulence 惩罚: 市场波动异常时强制降仓位 turbulence_penalty 0 if self.turbulence_score self.turbulence_threshold: # 超过阈值模型应该主动减仓拿着现金不动 turbulence_penalty np.abs(actions).sum() * 0.5 reward portfolio_return - cost_penalty - turbulence_penalty self.actions_prev actions.copy() return reward逻辑说明portfolio_return 是组合层面按持仓比例加权的收益率不是单只股票的涨跌。cost_penalty 用换手率的绝对值之和来惩罚交易频率这个设计比单纯减固定佣金更合理——市场冲击成本是随成交量非线性增加的。turbulence_penalty 在扰动指数超阈值时施加模型如果还在持仓会被扣分从而学会异常行情躲一躲。参数说明transaction_cost_pct 设 0.1% 对应美股常见交易成本如果做 A 股或加密货币这个值要调到 0.15%-0.2%否则策略会因摩擦成本过高而失真。turbulence_threshold 的选取一般取历史序列的 95 分位也可以直接用 dow30_turbulence_index.csv 里的最大值乘以 0.8 作为经验值。3.3 状态空间怎么组织不是把所有行情都塞进去环境每次 step 返回的状态是一个三维矩阵 [1, 股票数, 特征维度]包含了当天的 open、high、low、close、volume、技术指标和历史持仓比例。这里有个关键设计环境会维护一个 lookback 窗口默认 50 天把过去 50 个交易日的特征序列作为模型输入。为什么是 50 而不是 1 或 20因为单日 K 线包含的信息太少模型看不到趋势而超过 50 天后早期数据对当前决策的影响已经衰减到可忽略反而增加计算量。50 个交易日大约相当于两个半月一个能覆盖中期趋势又不至于太长的折中值。def _get_state(self, t): # 取过去50个交易日的数据作为当前状态 lookback self.lookback # 默认50 if t lookback: state self.data[t - lookback 1 : t 1] else: # 开头不足50天用重复填充补齐 pad np.repeat(self.data[:1], lookback - t, axis0) state np.vstack([pad, self.data[: t 1]]) # 归一化: 用每个特征的历史最大值缩小量纲避免梯度爆炸 state state / (np.max(np.abs(state), axis0, keepdimsTrue) 1e-8) return state逻辑说明数据开头不足 50 天时用第一天的数据重复补齐这是 RL 环境里常见的 padding 策略避免 episode 刚开始就因为状态不够长而报错。归一化用的是历史最大值而不是 mean-std 标准化因为金融数据分布不是高斯分布用均值方差标准化会把异常波动过分放大。参数说明lookback 不建议低于 20否则模型看不到任何中期趋势也不建议超过 100训练速度和 GPU 显存会成问题。如果你换到分钟级数据lookback 需要大幅提升到 240一天交易日因为分钟数据的单根 K 线信息量远低于日线。4. PPO、A2C、DDPG 三选一配置参数与 Ensemble 投票逻辑4.1 三个算法的选型逻辑不是越新越好项目核心训练脚本 run_DRL.py 支持三种算法config.py 里定义了各自的超参数。我在跑项目时最先做的事就是把三个算法各训一遍然后对比它们的训练曲线——这不是浪费时间而是建立哪个算法更适合这个任务的直觉。PPO 是默认首选原因在于它用 clipped surrogate objective 限制了每次策略更新的幅度训练稳定性很好对学习率的敏感度低。在金融数据这种噪声极强的环境里稳定性比激进探索重要得多。A2C 本质是同步版的 A3C多个 worker 并行采样靠并行降低方差但训练速度慢且对 reward 尺度敏感。DDPG 适合连续动作空间能输出平滑的持仓比例变化但最大的问题是超参数敏感critic 网络只要学偏一点actor 就跟着崩。三个算法的核心超参数我直接给表参数PPOA2CDDPG说明learning_rate5e-55e-51e-4金融数据建议偏小太大直接发散batch_size12825664DDPG 用经验回放batch 小更稳定gamma0.990.990.99折扣因子交易场景不需要太大tau--0.001DDPG 软更新系数控制 target 网络更新速度clip_param0.2--PPO 裁剪阈值0.1-0.3 之间调试entropy_coef0.010.01-熵正则防止策略过早收敛到局部最优这里最值得说的是 learning_rate。很多新手拿通用 RL 例子的 1e-3 直接跑结果 loss 曲线剧烈震荡模型完全学不到东西。原因很简单股票数据特征的值域不像 Atari 游戏那样规范收益率的分布是厚尾的学习率稍大就会让策略网络参数震荡太剧烈。4.2 Ensemble 策略三个臭皮匠怎么投票这个项目的名字里有 Ensemble Strategy它的做法不是等权重平均三个模型的输出而是先让三个模型各自独立训练然后在 trade 阶段根据每个模型的近期回测收益动态分配权重。投票逻辑在 run_DRL.py 里体现每次决策前计算当前时刻前 30 个交易日里三个模型的累计收益表现收益越好的模型在 final_action 里的权重越大。这种做法比固定权重平均更聪明——哪个模型在近期的市场风格下表现好就多信它一点。def ensemble_action(actions_ppo, actions_a2c, actions_ddpg, weights): # 三个模型各自的action shape: [num_stocks] # weights 由近期回测收益 softmax 归一化得到比如 [0.5, 0.3, 0.2] final_action ( weights[0] * actions_ppo weights[1] * actions_a2c weights[2] * actions_ddpg ) # 限制总仓位不超过1防止杠杆过度 if np.abs(final_action).sum() 1.0: final_action final_action / np.abs(final_action).sum() return final_action逻辑说明weights 的计算方式是每个模型在过去 30 天累计收益的 softmax 值收益越高权重越高。如果某个模型最近连续亏钱它的梯度权重会趋近于 0动态让位于表现好的模型。最终动作约束总仓位绝对值不超过 1这是为了防止多个模型同向加仓导致实际杠杆超过 1 倍。参数说明30 天的回看窗口是关键参数窗口太短比如 5 天会让权重剧烈震荡模型切换频繁反而增加交易成本窗口太长比如 120 天又无法快速适应市场风格切换。我实测下来20-40 天是一个相对稳健的区间。4.3 训练配置怎么改一个真正的落地参数经验config.py 里除了算法参数还有一些容易被忽略但实际很关键的环境参数包括risk_indicator_col 指定 turbulence 指数在数据里的列名这个不能填错否则风控直接失效。action_repeat 默认是 1如果你希望模型每次决策后保持仓位 N 天不动作可以把 action_repeat 调到 5。这相当于给决策加入了时间平滑能显著降低交易频率但也会让模型对突发行情的响应变慢。还有一点容易踩坑模型保存路径在 trained_models 目录下每次训练完会生成带时间戳的 .pt 文件。如果多次实验建议手动改模型文件名前缀否则同一算法的模型会互相覆盖想对比历史实验就找不回来了。这是我从实际跑项目里学到的教训——头两次跑完想对比参数调整前后的效果发现模型已经被覆盖了。5. 避坑手册跑这个项目最常见的五个翻车现场5.1 CSV 解析失败sep 参数和编码的坑现象pandas.read_csv 读取数据时报 ParserError或者日期列变成了一堆 NaN。原因dow_30_2009_2020.csv 这类文件是从不同来源拼的分隔符可能是逗号也可能是制表符而且部分文件带 BOM 头。解决读取时显式指定 sep 和 encoding不要靠 pandas 自动推断。df pd.read_csv(data/dow_30_2009_2020.csv, sep,, encodingutf-8-sig) # 如果还报错先看文件前两行原始内容 with open(data/dow_30_2009_2020.csv, r, encodingutf-8-sig) as f: for _ in range(2): print(f.readline())5.2 turbulence 指数过高导致环境交互中断现象训练跑到中途环境 step 返回全零动作然后 loss 变成 NaN或者直接抛 ValueError。原因turbulence_score 超过阈值后环境中某些数组变空部分实现里会触发除零错误。解决在环境初始化时对 turbulence 序列做缺失值填充和裁剪并在超阈值时跳过该步的 reward 更新。我在跑 2020 年 3 月那段数据时turbulence 指数冲到历史极值如果预处理没做平滑环境几乎每几步就触发一次异常状态训练完全无法进行。5.3 DDPG 训练不收敛reward 震荡发散现象DDPG 训练时critic loss 不断上升actor 输出的动作全部变成极端值全仓或全空仓。原因DDPG 对 reward 尺度极敏感股票收益的绝对值太小日收益通常 0.1%-1%而 Q 值误差被放大导致梯度爆炸。解决对 reward 做缩放把原始收益乘一个放大系数比如 100让 reward 量级落在 [-1, 1] 区间同时降低学习率到 1e-4 以下。5.4 训练集验证集数据重叠模型偷偷看到了未来现象回测曲线非常漂亮但换到 2021 年以后的数据立刻亏损。原因数据切分时没按时间顺序严格划分或者预处理时用了全样本的统计量做归一化导致特征里混入了未来信息。解决归一化的均值、最大值必须只从训练段计算验证和交易段直接用训练段的统计量做变换。5.5 模型文件被覆盖无法复现历史实验现象跑完某个参数组合后想和之前的结果对比发现 trained_models 目录下只剩最后一个模型。原因模型保存文件名固定没有时间戳或参数标识。解决训练脚本里把关键超参拼到文件名中比如 ppo_lr5e5_bs128_lookback50.pt这样每次实验都能被独立回溯。6. 进阶玩法把单次回测改成 walk-forward 滚动验证固定切分 2009-2020 只能得到一次回测结果说服力有限。我拿到这个项目后做的第一件事就是改成 walk-forward 滚动验证把数据切成 12 个窗口每个窗口用前 5 年训练、后 1 年验证然后滚动推进。这样能拿到 12 次独立的验证结果而不是一次性的幸存者曲线。具体做法是在 run_DRL.py 外面包一层循环每次把训练区间的结束日期往后推一年重新初始化环境重新训练模型再做一次回测。滚动验证的 12 次结果里如果大多数窗口策略都跑赢 SPY那才算真正验证了这个策略的泛化能力。这个项目本身没提供这个脚本但按上面的框架改起来只需要调整数据切分的日期边界。做完 walk-forward 之后还有一件事值得做把训练好的模型接到小时级或者分钟级数据上测试。模型在日线上的决策频率太低分钟级数据能让模型更充分地发挥连续动作空间的优势但注意环境里的 lookback 参数必须同步调大。换数据频率时technical indicators 也要重新计算——比如日线的 RSI 周期是 14 天在分钟级别可能需要 140 根 K 线才有同等信息量。最后说一个我的习惯从那以后我每次跑强化学习交易项目都会强制走一遍数据切分检查 归一化统计量隔离 模型文件命名带参数这套流程它省掉了我太多为复现实验而重跑模型的时间。这份源码不一定能让你赚钱但它能帮你看清强化学习交易从数据到策略的完整链路以及每一步可能埋的雷。希望帮到你。本文还有配套的精品资源点击获取