简介:一套基于深度强化学习的自动化股票交易策略设计源码,面向金融量化研究者、算法交易开发者及有一定编程基础的投资者,用于解决传统交易中情绪干扰与策略自适应问题。包体共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.py | PPO/A2C/DDPG 的模型构建与保存 |
| preprocessors.py | 行情数据清洗、特征构造、波动率指数计算 |
| config.py | 全局参数:数据路径、训练步数、初始资金、模式开关 |
| backtesting.ipynb | 交互式回测,加载结果对比 SPY 基准 |
| data/ | 行情、波动率、基准 ETF 等原始数据 |
| trained_models/ | 训练后保存的模型文件 |
| results/ | 训练/回测产生的账户净值 CSV |
| requirements.txt | 依赖清单 |
从这张表能看出设计取舍:环境按阶段拆成三个文件,而不是在一个环境里塞模式开关。好处是训练、验证、交易三阶段的数据切片、手续费、初始资金可以独立调整,改验证集不会碰坏训练逻辑。缺点也很明显,改共有逻辑时得同步改三个文件,后续二次开发我会讲怎么避免漏改。
2.3 整体流水线:从训练到交易的三段式设计
run_DRL.py 是整条流水线的发动机。它通过命令行参数选择算法,再根据 mode 选择对应环境,流程可以简化为:
# run_DRL.py 入口流程示意 if args.mode == "train": env = EnvMultipleStock_train( df=load_data(config.DATA_PATH), turbulence=load_data(config.TURBULENCE_PATH) ) model = build_model(args.algorithm, env) model.learn(total_timesteps=config.TRAIN_TIMESTEPS) model.save(f"trained_models/{args.algorithm}") elif args.mode == "validation": env = EnvMultipleStock_validation(...) # 关闭探索噪声,只在验证集上评估累计收益 elif args.mode == "trade": env = EnvMultipleStock_trade(...) # 加载 model,输出每日动作和账户变化代码里三个环境各有自己的数据窗口:train 用全量历史数据让模型见多识广,validation 换一段未参与训练的行情节选,trade 则按时间顺序推进模拟交易。每个阶段跑完都会在 results/ 目录写一份账户净值 CSV,backtesting.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_capital=10000): self.df = df.reset_index(drop=True) 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, seed=42): if algorithm == "ppo": model = PPO( "MlpPolicy", env, seed=seed, verbose=0, learning_rate=0.0005, n_steps=2048, batch_size=128, gamma=0.99 ) elif algorithm == "a2c": model = A2C( "MlpPolicy", env, seed=seed, verbose=0, learning_rate=0.0007, n_steps=5, gamma=0.99 ) elif algorithm == "ddpg": model = DDPG( "MlpPolicy", env, seed=seed, verbose=0, learning_rate=0.001, buffer_size=1_000_000, batch_size=128, gamma=0.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_col="date") baseline = pd.read_csv("data/ETF_SPY_2009_2020.csv", index_col="date") # 把基准净值按首日资金归一化,和代理账户起点对齐 baseline_asset = baseline["close"] / baseline["close"].iloc[0] * result["total_asset"].iloc[0] plt.plot(result["total_asset"], label="DRL Agent") plt.plot(baseline_asset, label="SPY 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, window=252): 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(axis=1) 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.csv |
| TURBULENCE_PATH | 波动率指数路径 | ./data/dow30_turbulence_index.csv |
| TRAIN_TIMESTEPS | 训练总步数 | 先跑 1e5 验证链路,再翻倍 |
| INITIAL_CAPITAL | 模拟账户初始资金 | 10000 |
| RESULTS_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 三个模型,对每只股票分别得到动作,然后取多数投票作为最终动作。这种做法的代价是决策速度变慢,但能显著降低单一算法对某段行情的过拟合。实现时注意三个模型的状态输入必须完全一致,否则投票没有意义。
我自己在改这类项目时踩过不少坑,最典型的一次是只改了训练环境没同步改验证环境,结果验证阶段模型直接报维度错误。从那以后,我每次动环境、奖励或者数据格式,都强制先跑一个几十步的超短训练脚本,确认训练、验证、交易三套环境全部同步更新,再放全长训练。这个习惯帮我省掉了大量返工,希望帮到你。
本文还有配套的精品资源,点击获取