深度强化学习交易系统源码解析:PPO/A2C/DDPG三Agent实验台 简介一套基于深度强化学习的自动化股票交易策略设计源码面向金融量化研究者、算法交易开发者及有一定编程基础的投资者用于解决传统交易中情绪干扰与策略自适应问题。包体共40个文件以14个Python脚本为核心实现PPO、A2C、DDPG三种Actor-Critic算法的训练与回测配套4个XML配置、4个CSV数据文件、1个Jupyter Notebook及图像、文档等压缩包约6.4MB。已有306人学习下载。通过该源码可掌握从数据预处理、多股票环境搭建到深度强化学习代理训练与策略评估的完整流程便于在真实市场数据上模拟交易并验证算法表现。需要提醒的是历史数据训练模型存在局限实战使用需结合风险管理。1. 基于深度强化学习的自动化股票交易源码三个 Actor-Critic 代理的完整实验台把一份基于深度强化学习的自动化股票交易策略源码拿到手后我第一件事不是看公式而是看它能不能在自己机器上直接把训练跑起来。这个项目里道琼斯30只成分股的历史行情、波动率指数、四个交易环境、三种算法模型和回测 Notebook 被组织在一个目录里目标很清楚让代理通过与环境反复交互自动学会最大化投资回报的买卖策略而不是靠人工写规则。对入门者它是一个能照跑的实验台对想改策略的开发者它是改数据、改奖励、改模型的起点。唯一要提前说明的是它验证的是历史行情上的学习过程不是稳赚的实盘信号。下面从技术栈和目录结构拆起。2. 技术栈与目录结构三种算法、四套环境与三阶段流水线2.1 技术选型为什么是 PPO、A2C、DDPG 三兄弟项目摘要里明确写了三种核心算法PPO、A2C、DDPG它们都属于 Actor-Critic 家族。对股票交易这个场景单靠一种算法跑出来的结果很难说明问题因为市场数据是非平稳的训练出来的策略对不同算法特性非常敏感。PPO 用截断的代理目标限制每轮参数更新的幅度训练过程最稳定适合做主力模型A2C 通过多个并行环境异步采样来加速收敛但方差更大适合做对照DDPG 是确定性策略输出连续动作适合处理连续交易比例这类问题。把它们放在同一个入口脚本里本质上就是把“算法选择”变成一个可切换的实验变量而不是绑定在某一种实现上。用一个表格把三者的对比收拢算法策略类型更新方式在本项目里的角色PPO随机策略重要性采样 截断目标主训练模型回测效果基线A2C随机策略多环境并行优势估计对照模型验证收敛速度DDPG确定性策略经验回放 延迟更新对照连续动作表达差异跑这类多算法项目时我不会让三个模型共用同一组超参而是先按算法默认值各跑一遍记录三个模型的最终净值再挑表现最好的那个去调参。因为股票数据噪声大某个算法第一次跑崩不代表它不行很可能是学习率或批大小没对上环境的状态尺度。2.2 目录结构与文件职责35 个文件如何分工展开整个目录后能看到这个项目的组织方式比较规整Python 脚本、数据、配置、模型、结果分开存放。核心的 9 个 Python 脚本负责数据预处理、环境模拟、模型构建和训练入口8 个数据文件里既有道琼斯成分股行情也有波动率指数和基准 ETF 数据4 个 XML 配置文件集中在 .idea 目录是 IDE 工程配置4 个数据记录文件主要是回测过程产生的账户净值记录2 个图像文件是运行后生成的净值对比图剩下的就是许可协议、说明文档和回测用的 Notebook。把读写路线对应到目录上更直观路径职责run_DRL.py训练、验证、交易三段模式的统一入口env/EnvMultipleStock_train.py训练阶段环境数据覆盖完整历史区间env/EnvMultipleStock_validation.py验证阶段环境不更新梯度只评估策略env/EnvMultipleStock_trade.py交易阶段环境输出模拟交易动作序列env/StockTradingRLEnv.py三个环境共用的交易内核状态、动作、奖励models.pyPPO/A2C/DDPG 的模型构建与保存preprocessors.py行情数据清洗、特征构造、波动率指数计算config.py全局参数数据路径、训练步数、初始资金、模式开关backtesting.ipynb交互式回测加载结果对比 SPY 基准data/行情、波动率、基准 ETF 等原始数据trained_models/训练后保存的模型文件results/训练/回测产生的账户净值 CSVrequirements.txt依赖清单从这张表能看出设计取舍环境按阶段拆成三个文件而不是在一个环境里塞模式开关。好处是训练、验证、交易三阶段的数据切片、手续费、初始资金可以独立调整改验证集不会碰坏训练逻辑。缺点也很明显改共有逻辑时得同步改三个文件后续二次开发我会讲怎么避免漏改。2.3 整体流水线从训练到交易的三段式设计run_DRL.py 是整条流水线的发动机。它通过命令行参数选择算法再根据 mode 选择对应环境流程可以简化为# run_DRL.py 入口流程示意 if args.mode train: env EnvMultipleStock_train( dfload_data(config.DATA_PATH), turbulenceload_data(config.TURBULENCE_PATH) ) model build_model(args.algorithm, env) model.learn(total_timestepsconfig.TRAIN_TIMESTEPS) model.save(ftrained_models/{args.algorithm}) elif args.mode validation: env EnvMultipleStock_validation(...) # 关闭探索噪声只在验证集上评估累计收益 elif args.mode trade: env EnvMultipleStock_trade(...) # 加载 model输出每日动作和账户变化代码里三个环境各有自己的数据窗口train 用全量历史数据让模型见多识广validation 换一段未参与训练的行情节选trade 则按时间顺序推进模拟交易。每个阶段跑完都会在 results/ 目录写一份账户净值 CSVbacktesting.ipynb 从这些 CSV 读取数据和 SPY 基准画在同一张图上。这里有个关键参数值得单独说TRAIN_TIMESTEPS。它决定模型和环境交互的总步数步数太少策略学不到买卖规律太多又容易过拟合到训练集的特定走势。我一般会先设一个较小的值跑通链路确认环境没有报错再翻倍步数跑正式训练。3. 核心模块拆解交易环境、策略网络与回测实现3.1 交易环境状态空间、动作空间与奖励设计三个环境文件的共同内核是 StockTradingRLEnv.py。它对外暴露的接口是标准的 gym 环境接口reset 初始化账户step 接收动作并返回新状态、奖励和是否结束。环境内部维护现金、持仓、当前股价和波动率。状态空间常见做法是把当前所有股票的归一化价格、账户现金、持仓比例和当日波动率指数拼成一个向量喂给策略网络。动作空间需要注意一点三套算法对动作的要求不同PPO/A2C 适合离散动作DDPG 输出连续值。为了让三种算法共享同一套环境环境把交易动作抽象成 5 档0 清仓、1 卖半仓、2 持有、3 买半仓、4 买满仓每只股票对应一个动作。DDPG 的连续输出在进环境前会被映射到最近的档位。# StockTradingRLEnv.py step 核心逻辑示意 class StockTradingRLEnv(gym.Env): def __init__(self, df, turbulence, initial_capital10000): self.df df.reset_index(dropTrue) self.turbulence turbulence self.cash initial_capital self.holdings np.zeros(self.df.shape[1] - 1) self.current_step 0 def step(self, actions): # actions: 每只股票一个 0~4 的整型动作 self._execute_actions(actions) # 按档位调仓 self.current_step 1 total_asset self.cash np.sum(self.holdings * self._current_prices()) reward total_asset - self.prev_asset # 差分收益作为即时奖励 self.prev_asset total_asset done self.current_step len(self.df) - 1 obs self._build_observation() return obs, reward, done, {total_asset: total_asset}这里奖励的设计值得一提。reward 用的是当回合总资产减去上一回合总资产即差分收益而不是累计收益。这样模型每一步都能直接感受到买卖动作带来的资金变化梯度信号不会等到回合结束才回传训练初期的收敛会快很多。参数 initial_capital 控制起始资金turbulence 序列则让环境感知市场剧烈波动一旦波动率超过阈值环境会限制最大仓位防止模型在下跌行情里反复抄底把净值打崩。3.2 策略网络models.py 里三种模型如何构建models.py 本质上是模型工厂。它接收算法名和环境实例返回一个已经绑好环境的强化学习模型对象。常见的封装写法# models.py 模型构建示意 def build_model(algorithm, env, seed42): if algorithm ppo: model PPO( MlpPolicy, env, seedseed, verbose0, learning_rate0.0005, n_steps2048, batch_size128, gamma0.99 ) elif algorithm a2c: model A2C( MlpPolicy, env, seedseed, verbose0, learning_rate0.0007, n_steps5, gamma0.99 ) elif algorithm ddpg: model DDPG( MlpPolicy, env, seedseed, verbose0, learning_rate0.001, buffer_size1_000_000, batch_size128, gamma0.99 ) return model这里三个算法的超参差异是有讲究的。PPO 的 n_steps 设到 2048因为重要性采样需要足够多的轨迹样本来估计优势函数A2C 的 n_steps 只有 5它靠多个并行环境补足采样量所以单条轨迹长度可以很短DDPG 必须配经验回放缓冲buffer_size 直接决定样本多样性太小会让连续动作的策略网络很快陷入局部最优。选 MlpPolicy 而不是更复杂的网络是因为在这个项目里状态向量维度不高全连接网络已经能表达买卖动作和持仓变化的关系。如果后续要加入 LSTM 捕捉时序依赖需要额外处理环境返回的状态序列改动量会大不少。3.3 回测链路backtesting.ipynb 与两条曲线的对比逻辑回测是判断策略好坏的关键环节。backtesting.ipynb 做的事情很纯粹从 results/ 读训练阶段记录的账户净值从 data/ 读 SPY 基准 ETF 数据把两者画在同一张图上再算收益率和回撤指标。# backtesting.ipynb 回测对比示意 import pandas as pd import matplotlib.pyplot as plt result pd.read_csv(results/account_value_train.csv, index_coldate) baseline pd.read_csv(data/ETF_SPY_2009_2020.csv, index_coldate) # 把基准净值按首日资金归一化和代理账户起点对齐 baseline_asset baseline[close] / baseline[close].iloc[0] * result[total_asset].iloc[0] plt.plot(result[total_asset], labelDRL Agent) plt.plot(baseline_asset, labelSPY Baseline) plt.legend() plt.savefig(figs/performance.png)参数说明baseline 的净值必须做首日归一化否则基准是从 100 元的价格出发代理是从 10000 元账户出发两条曲线起点不对齐就看不出相对收益。figs 目录里的 performance.png 和 stock_trading.png 就是这个环节的产物前者看整体收益对比后者看持仓变化和换手频率。回测数据覆盖 2009 到 2020 年时间跨度足够长包含牛熊转换和多次剧烈波动这对验证策略的鲁棒性很有价值。不过也要注意区间越长训练和验证的数据切片越容易重叠后面避坑章节我会专门讲这个问题。3.4 数据预处理preprocessors.py 与波动率指数data 目录里的 dow30_turbulence_index.csv 是整个项目里最容易忽略又最能影响结果的文件。preprocessors.py 负责把它和行情数据一起加工成环境能用的格式。波动率指数的计算思路我一般会这样处理# preprocessors.py 波动率指数计算示意 def turbulence_index(df, window252): returns df.pct_change().dropna() mean returns.rolling(window).mean() std returns.rolling(window).std() # 衡量多只股票收益率相对均值的离散程度 tu np.sqrt((returns - mean) ** 2 / (std ** 2 1e-8)).mean(axis1) return tu.fillna(0)这个指数的用途在环境里表现得很直接当数值超过阈值时环境认为市场进入异常波动状态会限制买入动作避免策略在恐慌行情中高仓位运行。所以回测曲线如果在某个时间点突然变平或者收益不再增长优先去看该时间点附近波动率指数是否冲高十有八九是触发了风险开关。这也是我在第 5 章避坑清单里会再提的一个点。4. 从零跑通项目依赖安装、配置修改与三阶段执行4.1 环境准备requirements.txt 与虚拟环境代码能不能跑起来第一步是环境。建议先建一个干净的虚拟环境再按 requirements.txt 安装依赖避免和机器上其他项目的包版本冲突。python -m venv venv source venv/bin/activate pip install -r requirements.txtrequirements.txt 里锁定的主要是强化学习库、gym、pandas、numpy 和 matplotlib。这里有个血泪经验不要图省事直接 pip install 最新版强化学习库对版本接口非常敏感有时候只是 numpy 大版本不一样环境初始化就会报 gym 的接口错误。如果安装时遇到不兼容先看看项目是哪个年代维护的再反过来锁定对应版本的依赖。4.2 训练阶段run_DRL.py 的参数与运行训练入口是 run_DRL.py它从 config.py 读全局参数。config.py 里值得关注的参数集中在这几个参数作用建议值DATA_PATH行情数据路径./data/dow_30_2009_2020.csvTURBULENCE_PATH波动率指数路径./data/dow30_turbulence_index.csvTRAIN_TIMESTEPS训练总步数先跑 1e5 验证链路再翻倍INITIAL_CAPITAL模拟账户初始资金10000RESULTS_PATH / MODEL_PATH结果和模型保存目录./results / ./trained_models三种算法的训练命令可以依次执行python run_DRL.py --mode train --algorithm ppo python run_DRL.py --mode train --algorithm a2c python run_DRL.py --mode train --algorithm ddpg每条命令跑完后trained_models/ 下会多出对应算法的模型文件results/ 下会多出账户净值 CSV。跑完一个先打开 CSV 看一眼数值有没有异常增长或持续为零再决定要不要继续跑下一个。这一步能省下后面大量排查时间。4.3 验证与交易把模型放到陌生行情上走一遍训练完不能直接看回测中间还有验证和模拟交易两步python run_DRL.py --mode validation --algorithm ppo python run_DRL.py --mode trade --algorithm ppovalidation 模式会把模型放到验证环境里走一遍但不更新梯度纯粹评估策略在未见过的数据上的表现。trade 模式则输出每个交易日的动作序列和账户变化。两步都完成后打开回测 Notebook 对比三条曲线训练净值、验证净值、SPY 基准。jupyter notebook backtesting.ipynb这里要特别提醒验证结果和训练结果差距太大通常不是模型有问题而是数据切片交叉了。训练时模型已经把验证段的数据看过一遍验证自然会虚高。解决方法是严格控制训练和验证的时间窗口保持两者不重叠。这个细节我放在下一章展开。5. 避坑指南五个高频问题的现象、原因与解决5.1 三个最容易让人翻车的运行报错先说运行层面的报错这一节对新手最实用。现象一启动训练时报维数不匹配错误信息指向环境的状态维度但改来改去都对不上。原因大多数是加载了不同版本的模型 checkpoint模型网络输入维度和当前环境构造的状态长度不一致。解决方法是删掉 trained_models 里旧的模型文件用当前环境重新训练或者检查是否误用 validation 环境加载了 train 环境的模型。现象二训练过程中账户净值在某一步之后突然变成一条水平线。这种现象容易让人以为策略学会了空仓实际上是波动率指数超过阈值环境把仓位锁死了。去看 dow30_turbulence_index.csv 对应时间的数值确认是风险开关触发而不是模型崩溃。解决方法是调整阈值或者把波动率序列重算一遍先确认数据没有缺失值。现象三Notebook 里 import 项目脚本报 ModuleNotFoundError。原因不是包没装而是 Notebook 的工作目录不在项目根目录Python 解释器找不到项目里的模块。解决方法是在 Notebook 第一个单元格里加两行路径处理import sys, os sys.path.insert(0, os.path.abspath(..))或者干脆在命令行先 cd 到项目根目录再启动 jupyter。这一点看似小但对新手来说是最容易被卡住的地方。5.2 训练效果不理想时的排查清单训练能跑通不代表结果好看。如果发现三个算法都跑不出理想收益按下面的顺序检查基本能定位问题。第一检查学习率是否过大。表现是 loss 波动剧烈、净值曲线抖得厉害。把核心算法的学习率降到原来的十分之一再试一次如果曲线变平滑就是学习率的问题。第二检查 reward 是否长期不变。训练前期 reward 全是零说明环境没有有效信号通常是因为动作档位映射写错比如模型输出的动作经过取整后永远落在“持有”这一档。在 step 里临时打印 actions 分布看看五个档位是否都有覆盖。第三检查训练和验证的数据窗口是否重叠。前面提到一旦重叠验证指标会虚高这时你会误以为策略很好但真正到了模拟交易阶段立刻打回原形。我一般会在 config 里显式配置 TRAIN_START、TRAIN_END、VALIDATION_START、VALIDATION_END 四个时间点保证窗口严格不重叠。第四检查随机种子。强化学习训练对种子敏感是常态同一个参数换一个种子结果差一大截并不是玄学。好在项目里 build_model 接入了 seed 参数排查问题时固定 seed保证每轮训练可复现才能区分是策略问题还是随机性带来的波动。表格形式收拢这张清单现象优先排查项处理方式loss 剧烈震荡学习率过大降低学习率重训reward 长期为零动作映射失效检查动作档位分布验证虚高、实盘回落数据窗口重叠显式拆分四段时间区间多次运行结果差异大随机种子不固定固定 seed 复现6. 二次开发指南换数据、改奖励、加集成决策拿到这套源码后大多数人的第一步不是重新训练而是换一批股票试试。修改点高度集中在 config.py 和数据入口把 DATA_PATH 指向自己的行情 CSV格式保持和 dow_30_2009_2020.csv 一致包含日期、每只股票的 OHLC 列再用 preprocessors.py 重新生成对应的波动率指数。改完数据后先小步长训练几百步验证环境能正常跑再放长训练。改奖励函数是让策略行为发生质变的最快路径。默认奖励是差分收益模型只关心资金增长完全不管波动和回撤。如果想训练一个更稳健的策略可以把奖励改成带波动惩罚的形式# 从纯差分收益改为带回撤惩罚的奖励 rolling_std returns.rolling(30).std().iloc[-1] sharpe_like (total_asset - prev_asset) / (rolling_std 1e-8) reward sharpe_like - 0.5 * max(0, prev_asset - total_asset)这里的 rolling_std 是近 30 个交易日的收益波动分母加上小常数是为了防止除零。第一项让模型偏好区间收益第二项对单步亏损做惩罚系数 0.5 可以按实际回测调节。改奖励后必须重训模型旧模型不能直接用。最后说一个扩展方向把三种算法变成集成决策。在 trade 阶段同时加载 PPO、A2C、DDPG 三个模型对每只股票分别得到动作然后取多数投票作为最终动作。这种做法的代价是决策速度变慢但能显著降低单一算法对某段行情的过拟合。实现时注意三个模型的状态输入必须完全一致否则投票没有意义。我自己在改这类项目时踩过不少坑最典型的一次是只改了训练环境没同步改验证环境结果验证阶段模型直接报维度错误。从那以后我每次动环境、奖励或者数据格式都强制先跑一个几十步的超短训练脚本确认训练、验证、交易三套环境全部同步更新再放全长训练。这个习惯帮我省掉了大量返工希望帮到你。本文还有配套的精品资源点击获取