简介这是一份基于深度强化学习的自动化股票交易策略设计源码面向金融科技研究者、量化交易入门者及对智能投顾感兴趣的开发者。项目采用PPO、A2C和DDPG三种Actor-Critic算法训练交易代理通过与股票市场环境交互来学习最优交易决策目标是最大化投资回报。包内共40个文件以Python脚本为核心辅以行情数据存储、XML参数配置、PNG图表和Jupyter Notebook交互文档压缩包约6.4MB整体结构清晰涵盖环境模拟、模型训练、回测评估等完整模块。当前已有306人学习下载。通过学习这套源码读者可以完整掌握深度强化学习在金融交易领域的落地路径包括历史数据处理、多股票交易环境构建、算法实现与效果对比、回测分析等核心环节也可在Notebook中直接运行策略测试与结果可视化快速上手智能交易策略的研究与验证。1. 一个把交易当连续控制问题来学的项目不预测涨跌直接学仓位预测股票明天涨不涨和真正靠交易赚钱是两件完全不同的事。前者只要方向对后者还得回答“买多少、什么时候买、什么时候卖”。如果把仓位当成连续动作、把账户净值变化当成奖励信号让代理自己在历史行情里反复试错学出来的就是一套自动化股票交易策略——这正是这套源码在做的事。项目用深度强化学习同时训练 PPO、A2C、DDPG 三个演员-评论家算法产出一个集成交易策略并配好了数据、环境、训练与回测整套工程train 环境训练、validation 环境选参、trade 环境做样本外验证。适合想入门量化策略的研究者也适合拿现成代码做二次开发的工程师。2. 技术架构拆解PPO、A2C、DDPG 如何共存在一个交易系统里这套系统的架构可以拆成四层数据预处理层负责把 CSV 变成特征环境层负责模拟交易和计算奖励算法层用三个强化学习算法独立训练决策层把三个模型的输出合并成一个最终动作。三个算法都走 Actor-Critic 路线一个网络负责输出策略Actor一个网络负责估计状态价值Critic两者交替更新。之所以没有选 DQN是因为 DQN 面向离散动作空间而仓位比例天然是连续的用连续动作空间的算法更贴合交易场景。2.1 三种算法的定位与配合算法策略类型收敛特点工作流中的角色PPO随机策略稳定对学习率敏感主力策略训练主模型A2C随机策略方差较小数据效率一般提供策略多样性DDPG确定性策略收敛快但探索弱直接输出明确仓位推理快PPO 的核心是裁剪目标函数把每次更新的步长限制在可信范围内所以整体训练过程稳定项目里拿它当主力模型。A2C 用的是优势函数来降低方差训练节奏比较“稳”但同样的样本量下不一定比 PPO 走得更远它的价值更多在于给集成系统提供不同偏置的策略。DDPG 则是确定性策略输入状态直接输出一个确定的动作值推理阶段最干净但它对超参数和奖励尺度相当敏感训练时稍不注意就会翻车。三个模型在同一份观测上给出的动作大概率不一致集成决策的意义就在这里把三个“委员”的意见做加权平均单模型的随机噪声会被压掉一部分这在信噪比极低的股票序列里非常实用。2.2 交易环境里的 MDP状态、动作、奖励与湍流开关把交易问题写成马尔可夫决策过程关键在于三件事。状态不是某一天的行情快照而是一个长度为 window_size 的窗口过去 10 天或 20 天的开高低收、成交量、湍流指数以及若干技术指标拼成一个特征矩阵再展平成向量。模型看到的是一段历史片段这样才能让网络学到动量、反转这类时序模式而不是只对单日行情做反应。动作空间是一个连续标量范围一般取 [-1, 1] 或 [0, 1]。正数代表做多仓位比例负数在允许做空的配置里代表空头仓位。比如动作 0.8就表示拿 80% 的资金买入动作 -0.5 表示拿一半资金做空。这个连续动作可以直接被 PPO、DDPG 这类连续控制算法消费这也是整个项目选择强化学习而不是监督学习的关键输出的是仓位而不是一个“涨/跌”标签。奖励函数默认取对数收益率也就是 r ln(NAV_t / NAV_{t-1})账户净值从上一时刻到这一时刻的对数变化。用对数而不是普通百分比是因为对数收益可加把一段时间的对数收益累加起来等于期末净值除以期初净值的对数训练信号在时间维度上是一致的。另外数据文件里专门有一份 dow30_turbulence_index.csv这是预计算好的市场湍流序列。当湍流指数超过配置里的阈值时交易环境会强制把仓位压到 0相当于给策略加了一道风控闸门——市场情绪剧烈波动时模型不交易就是最好的交易。2.3 三个环境文件不是冗余train / validation / trade 的分工env 目录下有三个高度相似的文件EnvMultipleStock_train.py、EnvMultipleStock_validation.py、EnvMultipleStock_trade.py加上一个公共的 StockTradingRLEnv.py。这不是代码重复而是刻意把实验分成三个阶段。train 环境从训练时间段内随机挑选起点每次 episode 走的都是不同的行情片段增加代理见过的市场状态多样性。validation 环境固定起点、固定路径反复运行多次来评估模型稳定性用来挑 checkpoint 和超参数。trade 环境则模拟真实逐日交易严格按时间顺序推进不允许任何未来信息配合湍流阈值做“该避就避”的风控。理解这套分工之后二次开发时就不会犯“拿 train 环境做绩效评估”这种误用。3. 目录结构与数据流水线35 个文件里真正要碰的是哪几个一眼看去文件很多但大部分是 IDE 配置和系统元数据真正影响收益的代码文件只有几个。先把目录按逻辑重新组织一遍再逐个说清楚每个文件的作用。3.1 文件地图代码、数据、配置与可忽略项data/ ^DJI.csv # 道琼斯指数行情 dow30_turbulence_index.csv # 湍流指数序列 dow_30_2009_2020.csv # 道指30只成分股历史行情 ETF_SPY_2009_2020.csv # SPY 基准行情 env/ EnvMultipleStock_train.py # 训练环境 EnvMultipleStock_validation.py # 验证环境 EnvMultipleStock_trade.py # 回测/交易环境 StockTradingRLEnv.py # 公共环境基类 preprocessing/ preprocessors.py # 数据清洗与特征工程 config/ config.py # 全局配置中心 trained_models/ # 训练产出的模型权重 results/ # 回测结果图 run_DRL.py # 训练与回测入口 backtesting.ipynb # 交互式回测 Notebook models.py # 模型与集成决策层 requirements.txt # 依赖清单文件列表里那 4 个 XML 其实是 IDE 工程配置不算运行时依赖可以直接忽略。init.py 只是把目录标记成 Python 包真正要动的是 data、env、preprocessing、config 这四个目录以及根目录下的三个入口文件。trained_models 和 results 是产物目录模型权重和回测图都会落到那里。3.2 数据预处理从五列行情到能进网络的特征preprocessors.py 承担的工作可以用一句话概括把原始 K 线数据清洗成环境可以直接消费的特征矩阵。数据来源有四份^DJI.csv是单标的指数行情dow_30_2009_2020.csv 是 30 只成分股的宽表数据ETF_SPY_2009_2020.csv 是对标基准dow30_turbulence_index.csv 是湍流序列。我一般实现这类预处理时会写类似下面的逻辑import numpy as np def add_log_return(df, price_colclose): # 对数收益率比普通涨跌幅更适合做强化学习特征 df[log_return] np.log(df[price_col]).diff() return df def zscore_fit_transform(df, feature_cols, fit_index): # 标准化只允许在 fit 区间计算均值方差 # 验证/回测阶段必须复用训练段统计量 scaler StandardScaler() scaler.fit(df[feature_cols].iloc[fit_index]) df[feature_cols] scaler.transform(df[feature_cols]) return df注意 zscore 这步的 fit 区间非常关键它决定了后面回测是否掺入未来信息标准化统计量只能从训练段拟合再应用到验证段和回测段。如果手滑对全表直接 fit_transform模型会在回测时偷偷看到“未来”的均值和方差训练曲线好看但实盘根本复现不了。3.3 config.py换数据、换参数、换模型的唯一入口config 目录是整个项目的参数中心训练窗口长度、湍流阈值、是否允许做空、模型保存路径全在这里。我整理了一份最常改的参数表参数含义调整建议DATA_DIR数据目录路径换自己的 CSV 时先改这里WINDOW_SIZE状态回看窗口长度典型值 10做长周期可调 20TURBULENCE_THRESHOLD湍流风控阈值按训练期湍流分位数取LEARNING_RATE各算法学习率PPO 常见 3e-4不稳就往 1e-4 降SEED随机种子复现实验必须固定项目默认的训练区间覆盖 2009 到 2020 年既有 2008 年后恢复期的慢牛也有 2020 年那样的高湍流行情数据集本身就包含了好几种市场状态。改配置时优先动数据路径和日期范围其次再动学习率算法结构反而不用急着动。4. 核心代码连读从环境状态到集成策略的完整执行链路读这套源码我建议按“环境 → 模型 → 入口”的顺序来因为强化学习项目的灵魂不在训练循环里而在环境怎么定义奖励、怎么给状态。先把交易环境看明白后面训练代码就很好理解。4.1 交易环境骨架状态构建与动作映射class StockTradingRLEnv: def __init__(self, df, feature_cols, window_size10, turbulence_thresholdNone, initial_cash1e6): self.df df.reset_index(dropTrue).copy() self.feature_cols feature_cols self.window_size window_size self.turbulence_threshold turbulence_threshold self.initial_cash initial_cash # 动作空间单标的持仓比例取 [-1, 1]负数表示做空 self.action_space Box(low-1.0, high1.0, shape(1,), dtypenp.float32) # 观测空间窗口天数 * 特征数展平成一维向量 self.observation_space Box( low-np.inf, highnp.inf, shape(window_size * len(feature_cols),), dtypenp.float32) def _get_state(self, t): # 只截取 t 时刻之前的窗口数据天然防止未来信息泄漏 start max(0, t - self.window_size) state self.df[self.feature_cols].iloc[start:t].values.flatten() return state.astype(np.float32) def step(self, action): # 动作映射为仓位 position float(np.clip(action[0], -1.0, 1.0)) prev_price self.df.loc[self.t, close] next_price self.df.loc[self.t 1, close] ret next_price / prev_price - 1.0 # 简化净值模型现金按仓位比例暴露于涨跌幅 self.cash self.cash * (1.0 position * ret) reward np.log(self.cash / self.prev_cash) ...动作空间的 Box 类型定义了上下界下面代码里 np.clip 再卡一次双保险防止模型输出越界动作。观测空间没有做上下界限制因为 Z-score 标准化之后特征理论上可以在任意范围浮动强行 clip 反而会丢掉信息。_get_state 里只截取 t 之前的窗口数据这是环境层防止未来函数的关键设计。reward 用对数收益好处是跨时间步可加模型优化的目标函数和“期末净值最大化”完全一致。4.2 集成策略层三个 Agent 如何形成一个最终动作models.py 是策略集成的地方。训练阶段三个算法各自独立优化推理阶段要合流def ensemble_predict(agents, obs, weightsNone): # agents: [ppo_agent, a2c_agent, ddpg_agent] votes [] for agent in agents: # 推理时统一走确定性输出避免概率采样带来噪声 action agent.predict(obs, deterministicTrue) votes.append(np.clip(action, -1.0, 1.0)) if weights is None: weights np.ones(len(votes)) / len(votes) return float(np.sum(weights * np.array(votes)))这段代码做的事情是软投票soft voting三个代理分别给出自己的仓位建议然后按权重合成一个。默认等权平均也可以根据验证集表现给表现好的模型更高权重。DDPG 输出的是确定性动作PPO 和 A2C 推理时要把随机采样关掉统一走 deterministic否则集成结果会带上不必要的方差。这样做的本质是用模型多样性换稳定性——股票序列信噪比太低单模型很容易被一段异常行情带偏平均之后决策边界平滑很多。4.3 run_DRL.py 与 backtesting.ipynb训练回测一条龙入口有两个run_DRL.py 适合批量训练backtesting.ipynb 适合边跑边看中间结果。通常的做法是先训练再回测# 先确认 requirements.txt 依赖已装齐数据路径已配置 python run_DRL.py # 训练完成后启动交互式回测 jupyter notebook backtesting.ipynb跑起来之后trained_models 目录下会出现各算法对应的模型权重文件results 目录会生成 performance.png 这类回测图。绩效图里重点关注三件事净值曲线是否稳定跑赢 SPY 基准、回撤发生在哪些时间段、以及这些时间段的湍流指数是什么水平。如果净值曲线拉得很漂亮但交易记录里买卖点稀稀拉拉说明模型大概率是在“靠运气躺赢”不是真的学会了择时。5. 避坑与排查四个真实翻车现场跑通这套代码不难难的是换数据、改参数之后还能保持稳定。下面四个问题是我自己跑这类 DRL 交易项目时踩过的坑每条按现象、原因、解决的顺序展开。5.1 训练损失不降反涨净值曲线长期趴在初始值现象训练日志里 episode reward 迟迟不涨甚至一路向下账户净值在初始值附近横跳。原因强化学习对奖励量纲和学习率的组合非常敏感。默认的对数收益在单步尺度上可能只有 1e-4 量级这么大的奖励信号喂给网络优势估计直接被数值噪声淹没再加上动作范围打开到做空模型在探索期频繁做空亏损形成负向循环。解决先把对数收益乘以一个缩放系数比如 1000 或 10000把奖励量纲拉回 0.1 到 10 的范围再把学习率降到 1e-4 左右重跑。如果还不行把动作 clip 到 [0, 1] 禁用做空让模型先学会做多再放开做空。5.2 回测曲线漂亮换一段行情直接穿仓现象训练集和验证集上表现很好一旦把回测区间换到没见过的年份收益曲线彻底失控。原因最常见的是数据泄漏——标准化或湍流统计量对全样本做了 fit验证段和回测段的特征里混入了未来信息。模型等于开卷考试分数自然高。解决严格按时间顺序切分训练、验证、回测三段标准化只从训练段拟合湍流阈值也只用训练期湍流序列的分位数来确定。代码里只要出现对全表做 StandardScaler().fit_transform 再切分基本可以断定有泄漏。提示检查泄漏最快的办法是把回测起始点之前的特征均值方差打印出来和训练段对比如果差很多说明预处理段可能偷看了未来。5.3 换了自己的股票数据一跑就报 KeyError现象把 CSV 换成自己的股票数据后环境报错说找不到 “close” 或某技术指标列。原因新数据的列名和预处理器的输出对不上比如有的数据源列名是小写有的带中文有的没有复权字段。环境拿到缺列的数据自然直接崩。解决先统一列名映射把数据规整成 date、open、high、low、close、volume 的标准形式清掉 NaN然后在 config.py 里同步 feature_cols 列表跑任何事情之前先 print(df.columns) 核对一遍。5.4 湍流阈值设得太“灵”模型全程空仓现象某段回测的净值曲线是一条平线交易记录几乎空白。原因湍流阈值设得低于训练期常见值把每一段行情都判定为高湍流风控闸门全程关闭策略被动休假。解决先画 dow30_turbulence_index.csv 的分布看训练期湍流的 90% 到 95% 分位数把阈值设在那里或者对湍流序列先做滚动平均平滑再比较避免单日极端值误触发闸门。5.5 排查顺序遇到异常先查哪一行按下面的顺序过一遍能省下大量瞎调参的时间数据切分和标准化有没有混入未来信息奖励量纲是否过小导致优势估计被噪声吞掉feature_cols 列名与预处理输出是否完全一致湍流阈值是否把策略误关在门外。6. 二次开发把奖励函数与动作空间改成自己的交易逻辑落到改造层面最值得动的就是奖励函数。默认的对数收益奖励只关心净值变化没有考虑换手成本也没有惩罚过度持仓实盘里很容易产生频繁买卖的高成本策略。可以替换成带换手惩罚的版本def rl_reward(prev_nav, curr_nav, prev_pos, curr_pos, fee_rate0.0005, pos_penalty0.01): log_return np.log(curr_nav / prev_nav) turnover abs(curr_pos - prev_pos) return log_return - fee_rate * turnover - pos_penalty * curr_pos ** 2这个函数有三项第一项是净值增长驱动模型赚钱第二项是换手惩罚仓位变化越大扣得越多模型会主动降低无效交易频率第三项是持仓惩罚惩罚长期满仓模型在震荡行情里会更愿意降仓位避险。接入方式很简单在环境 step 里把 reward 的计算替换成这个函数的输出就行。fee_rate 要按你自己的真实佣金加滑点来填A 股和港美股差别很大不要照抄默认值。动作空间也可以往多标的方向扩展。当前环境是单标的单动作如果你手里的策略要同时交易多只股票把 Box 的 shape 从 (1,) 改成 (n,)每个分量表示一只股票的仓位比例。多仓位分配记得做归一化用 softmax 而不是简单除以总和避免出现总仓位超过账户资金的安全问题。换数据是最常见的二次开发场景把你的 CSV 按标准 OHLCV 列名整理好塞进 data 目录改 config 里的路径重新跑一遍训练和回测。换完数据不要急着看收益曲线先跑一遍第 5 章开头那个排查顺序确认没有时间泄漏再谈策略。我当年换数据集时图省事直接对全表做了标准化再切分在 validation 环境上自欺欺人了一个月后来发现模型靠未来均值“作弊”那段血泪教训之后我每次换数据都强制走一遍泄漏检查清单再开始谈收益。希望这套源码能帮你从“能跑通”走到“敢改造”少走一点我当时的弯路。本文还有配套的精品资源点击获取