☰
深度强化学习做量化交易:PPO、A2C与DDPG集成策略实战
2026/10/19 6:58:56 网站建设 项目流程

简介:这是一份基于深度强化学习的自动化股票交易策略设计源码,面向金融科技研究者、量化交易入门者及对智能投顾感兴趣的开发者。项目采用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_col="close"): # 对数收益率比普通涨跌幅更适合做强化学习特征 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,做长周期可调 20
TURBULENCE_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_size=10, turbulence_threshold=None, initial_cash=1e6): self.df = df.reset_index(drop=True).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, high=1.0, shape=(1,), dtype=np.float32) # 观测空间:窗口天数 * 特征数,展平成一维向量 self.observation_space = Box( low=-np.inf, high=np.inf, shape=(window_size * len(feature_cols),), dtype=np.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, weights=None): # agents: [ppo_agent, a2c_agent, ddpg_agent] votes = [] for agent in agents: # 推理时统一走确定性输出,避免概率采样带来噪声 action = agent.predict(obs, deterministic=True) 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 排查顺序:遇到异常先查哪一行

按下面的顺序过一遍,能省下大量瞎调参的时间:

  1. 数据切分和标准化有没有混入未来信息;
  2. 奖励量纲是否过小,导致优势估计被噪声吞掉;
  3. feature_cols 列名与预处理输出是否完全一致;
  4. 湍流阈值是否把策略误关在门外。

6. 二次开发:把奖励函数与动作空间改成自己的交易逻辑

落到改造层面,最值得动的就是奖励函数。默认的对数收益奖励只关心净值变化,没有考虑换手成本,也没有惩罚过度持仓,实盘里很容易产生频繁买卖的高成本策略。可以替换成带换手惩罚的版本:

def rl_reward(prev_nav, curr_nav, prev_pos, curr_pos, fee_rate=0.0005, pos_penalty=0.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 环境上自欺欺人了一个月,后来发现模型靠未来均值“作弊”,那段血泪教训之后,我每次换数据都强制走一遍泄漏检查清单,再开始谈收益。希望这套源码能帮你从“能跑通”走到“敢改造”,少走一点我当时的弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询