深度强化学习股票交易策略:从MDP设计到回测避坑指南
发布时间:2026/9/28 3:05:52来源:尧图网络
简介一套基于深度强化学习的自动化股票交易策略设计源码面向量化研究者、金融分析师和具备Python基础的开发者解决人工交易受情绪影响、难以适应市场动态变化的问题。项目用PPO、A2C、DDPG三种Actor-Critic算法训练交易代理覆盖数据预处理、强化学习环境构建、模型训练、回测验证与结果可视化全流程可在真实行情数据上复现与扩展。压缩包共40个文件大小6.4MB。核心为14个Python脚本负责算法实现与交易环境封装4个CSV保存历史行情数据4个XML配置算法参数1个Jupyter Notebook提供交互式回测演示另有模型参数存档、训练图表和许可证说明目录结构清晰便于按模块学习。已有304人学习下载。通过阅读源码与Notebook可掌握深度强化学习在金融决策中的应用技巧理解策略网络调参与评估方法为自主设计自动化交易系统提供可运行的参考模板。1. 先泼盆冷水深度强化学习做自动交易解决的从来不是“预测涨跌”刚接触这个标题的人最容易产生一个误会以为这是一套用深度强化学习来预测股票明天涨跌、然后自动买入卖出的系统。真按这个思路去做结果通常很惨。原因在于预测模型的目标是“最小化预测误差”而交易系统的目标是“在有限本金和交易成本约束下最大化收益风险比”这两件事的优化方向并不一致。预测准了不代表赚钱赚到钱也不代表预测准。深度强化学习在这条链路里真正扮演的角色不是预报员而是“决策器”——它通过不断试错学习的是“在给定当前市场状态时该买、该卖、还是该持有”。如果你手里已经拿到一份“基于深度强化学习的自动化股票交易策略设计源码”你会发现它的骨架通常不是一堆预测模型而是一个强化学习环境加上一个策略网络再配上一套回测评估循环。这三块东西也是我们这篇笔记接下来要拆解的主线。这个方向适合谁适合那些已经熟悉股票数据、有一定的Python和PyTorch基础想把强化学习真正落到交易决策上的人。不适合指望拿到源码直接躺赢的人——这点我放在前面说清楚因为后面很多坑都是从“拿源码就直接跑”开始的。2. 策略设计第一步把交易问题改写成强化学习能懂的MDP2.1 为什么交易问题能套进强化学习框架以及MDP五元组怎么对应任何强化学习问题第一步都是把业务问题转成马尔可夫决策过程也就是MDP。股票交易天然适合做这个转化智能体是交易者环境是市场状态是当前可观测的行情和技术指标动作是仓位操作奖励是资金曲线的变化。五元组S, A, P, R, γ可以这样对应状态S当前时刻的持仓、现金、最新K线特征、均线、波动率等信息。注意这里有个前置假设市场满足马尔可夫性即当前状态已经包含了决策所需的全部历史信息。实际操作中我们不可能拿到所有信息所以尽量把有用的特征塞进状态向量里。动作A要么是离散的买入、卖出、持有要么是连续的仓位比例比如-1到1之间负代表做空。转移概率P交易环境里的转移是部分可观测且非平稳的所以通常不显式建模交给神经网络去逼近。奖励R单步收益或收益的某种变形比如对数收益率减去交易成本。折扣因子γ通常取0.99到0.999之间。γ太小会让策略变得短视只盯着眼前一两天的收益γ太大则训练难度上升回报方差变大。源码里最核心的“环境”类一般就是实现reset和step这两个函数。reset负责把账户状态清零、把数据指针拨到序列开头step输入动作根据当前K线数据计算成交、更新账户、返回新的状态和奖励。这个设计其实和OpenAI Gym的接口完全一致方便后续接上现成的强化学习算法库。2.2 状态空间怎么构造特征不是越多越好时机对齐才是最大坑写环境代码时我见过最多的问题出在状态构造上。常见的做法是取最近N根K线的“开盘、收盘、最高、最低、成交量”再拼上一些指标RSI、MACD、布林带最后做归一化。这里有两个非常容易翻车的细节。第一个是“未来数据泄漏”。如果归一化用的是全量数据的均值和标准差那么在回测时某个时间点的状态其实已经包含了未来信息。正确的做法是在序列上做滚动标准化——对每一个时间点只用该点之前的数据计算均值和方差。很多号称“回测很漂亮”的源码一上实盘就崩根因往往就在这里。第二个是“特征对齐”。假如你取最近30根日线作为状态那么在第t天做决策RNN或Transformer可以处理时序信息但如果是用MLP多层感知机你需要把序列展平成向量。展平之前必须确认向量最后一个位置对应的是最新一根K线而不是最早那根。这个顺序反了网络学出来的东西完全是噪声。踩过这个坑之后我习惯在环境里加一段断言专门检查状态张量最后一维的数据创建时间戳是不是递增排列的。2.3 奖励函数设计收益率、对数收益率、带交易成本三者差别巨大奖励函数决定了策略优化的方向。最简单的写法是reward (equity_now - equity_before) / equity_before即单步资金变化率。但直接用这个策略会倾向于“赌一把大的”——因为涨10%的一次收益可以覆盖前面20次小亏损。于是常见做法改成对数收益率reward log(equity_now) - log(equity_before)让收益趋向对称。再进一步必须在奖励里扣除交易成本否则策略会学到高频买卖每次动作只要产生细微的正预期收益就频繁进出。对A股而言双边费用大约在万几到千一之间取决于是否包含印花税。我一般会在环境里配置一个cost参数默认值设成0.001动作与上一动作不同时就扣除一次成本。这样策略学出来才更接近真实约束下的选择。下面是一段最小可运行的股票交易环境骨架按上述逻辑实现import numpy as np class StockTradingEnv: 最小化的股票交易环境只为说明MDP五要素的落地方案。 def __init__(self, prices, features, cost0.001, gamma0.99): self.prices prices # shape: [T] self.features features # shape: [T, feature_dim] self.cost cost self.gamma gamma self.reset() def reset(self): self.t 0 # 当前K线下标 self.cash 1.0 # 初始资金归一化为1 self.shares 0.0 # 持仓股数 self.prev_action 0 # 0:空仓, 1:满仓, 便于计算换手成本 return self._get_state() def _get_state(self): # 只使用t时刻之前的数据避免未来函数 end self.t 1 start max(0, end - 30) return self.features[start:end].reshape(-1) def step(self, action): # action: 0 清仓, 1 满仓买入 price self.prices[self.t] # 执行动作扣除手续费 if action 1 and self.prev_action 0: self.shares self.cash * (1 - self.cost) / price self.cash 0.0 elif action 0 and self.prev_action 1: self.cash self.shares * price * (1 - self.cost) self.shares 0.0 # 进入下一时刻计算账户总资产 self.t 1 if self.t len(self.prices): done True self.t len(self.prices) - 1 else: done False equity self.cash self.shares * self.prices[self.t] reward np.log(equity) - np.log(self.equity_before) self.equity_before equity self.prev_action action return self._get_state(), reward, done, {equity: equity}这段代码里有几个参数需要关注。cost是交易成本我在0.001到0.003之间调过成本越高策略换手率越低但过高的成本会让模型彻底不敢交易学出来的策略变成了“永远空仓”。gamma是折扣因子0.99适合日线级别如果是分钟级数据可以设到0.995以上因为分钟级决策更密集远期奖励的重要性更高。另外注意_get_state里强制只用当前时刻之前的数据这个结构能挡住未来数据泄漏不要为了方便把整个数据集放入状态。3. 算法选型与网络结构DQN、PPO、DDPG到底怎么选3.1 DQN处理离散动作适合三分类场景但别指望它做仓位控制许多源码里首选的是DQNDeep Q-Network因为实现简单、资料多、调参经验丰富。DQN的输出是一个Q值向量每个维度对应一个离散动作的价值对股票交易来说自然映射成“买入、卖出、持有”三分类。但实际跑下来DQN有两个明显短板。第一它学出来的策略倾向于“死守一个动作”因为Q值初始化接近训练初期随机探索会让网络固化了“持有最优”的偏见尤其当奖励里包含交易成本时模型宁可不动也不愿尝试。第二DQN的评估依赖Q值但股票数据是非平稳的Q值的分布漂移很严重训练过程中的target network更新不及时容易发散。如果你手里源码用的DQN建议你把重点放在探索策略上。把ε从1.0按指数衰减到0.05衰减步数设到总步数的60%以上并把replay buffer设大至少10万条以上打破相邻样本的相关性。这样还能让DQN跑出一点像样的结果。3.2 PPO与DDPG连续动作场景下的主力选手如果要做“仓位比例”这种连续动作DQN就不够用了。常见的有两类方案基于策略梯度的PPOProximal Policy Optimization和基于Actor-Critic的DDPGDeep Deterministic Policy Gradient。实际源码里这两类都很多我的选型经验是用日线、数量少单标的或十只以下、训练时间充裕选PPO。它天然适合离散或连续动作对超参不敏感收敛稳定是通用场景最稳的选择。用分钟线、高频交易、需要平滑的仓位变化选DDPG。DDPG输出确定性动作加上OU噪声或高斯噪声做探索交易动作的连续性更好但训练极不稳定需要花大量时间调target网络软更新系数。这两种算法在源码里通常都会预置网络骨架。PPO需要维护两个网络actor输出动作分布连续时是高斯分布的均值和标准差critic输出状态价值V(s)。DDPG则是四个网络actor在线网络、actor目标网络、critic在线网络、critic目标网络。新手建议先跑PPO因为它对随机种子和奖励尺度的敏感度低一些。3.3 Actor-Critic网络输入输出细节一篇代码骨架说明白无论选哪个算法网络输入输出都围绕“状态进入网络动作和值从不同头出来”这个结构。我经常用下面这段方式去检验一份源码的网络实现是否合理import torch import torch.nn as nn import torch.nn.functional as F class ActorCritic(nn.Module): 适用于离散/连续动作的共享骨干结构。 股票特征向量输入 - 共享层 - actor头 critic头。 def __init__(self, state_dim, num_actions, continuousFalse): super().__init__() self.continuous continuous self.shared nn.Sequential( nn.Linear(state_dim, 128), nn.ReLU(), nn.Linear(128, 64), nn.ReLU(), ) self.actor_discrete nn.Linear(64, num_actions) self.actor_continuous_mean nn.Linear(64, num_actions) self.actor_continuous_logstd nn.Parameter(torch.zeros(num_actions)) self.critic nn.Linear(64, 1) def forward(self, x): features self.shared(x) value self.critic(features) if self.continuous: mean self.actor_continuous_mean(features) logstd self.actor_continuous_logstd.expand_as(mean) std torch.exp(logstd) return mean, std, value else: logits self.actor_discrete(features) return logits, value这里有一个关键参数值得关注logstd在源码里经常被初始化成全零向量意思是初始标准差为1探索范围比较大。如果你发现训练到一半策略突然变成“乱开仓”把logstd初始值调到-1或-2可以缩小初始探索步长让策略在前期更稳定。共享骨干的维度128-64适合特征维度在几十这个量级如果你的状态里拼了300个以上的特征中间层可以加宽到256。critic输出的是标量价值估计不套激活函数因为价值可以是任意实数。4. 跑通一套最小实现数据准备、训练循环与参数调优4.1 数据怎么喂训练集验证集划分必须按时间块而不是随机划分股票数据最大的特点是时序依赖随机打乱训练集验证集会直接造成未来数据泄漏。常见做法是按时间顺序把数据切成三段前70%训练、中间15%验证、最后15%测试。训练时用滑动窗口方式在训练集内采样每个episode从随机时间点开始跑一段固定长度比如250根K线后结束。这样做的好处是每个episode能覆盖不同的市场状态模型不至于只见过单一行情。如果只有一份数据也至少要按时间留出最后一段做样本外验证。我见过不少源码的训练集和回测集是重叠的回测收益曲线看起来漂亮但要知道那只是模型在“背答案”不是真正的策略能力。下面是一段数据切分和采样的常规写法import numpy as np def split_data(features, prices, train_ratio0.7, val_ratio0.15): 按时间顺序切分训练/验证/测试集禁止随机打乱。 n len(prices) train_end int(n * train_ratio) val_end int(n * (train_ratio val_ratio)) train (features[:train_end], prices[:train_end]) val (features[train_end:val_end], prices[train_end:val_end]) test (features[val_end:], prices[val_end:]) return train, val, test def sample_episode(features, prices, seq_len250): 在训练集内随机采样一个episode的起始点。 total len(prices) - seq_len start np.random.randint(0, total) end start seq_len return features[start:end], prices[start:end]注意这里seq_len的选择直接影响训练效率。250对于日线来说大约是1年的交易日能覆盖较完整的趋势和回调设太短比如20会让奖励方差巨大模型学不到长期因果关系设太长超过1000则单次episode耗时太长训练很慢。实践中我习惯先用250跑通流程验证策略有效后再尝试500或750。4.2 训练循环里决定成败的3个参数buffer、batch、learning rate很多人调参只盯算法自带的学习率忽略了两个更基础的东西replay buffer容量和batch size。以PPO为例每轮迭代要收集一批轨迹然后用这个数据做多轮mini-batch更新。buffer太小时模型过拟合当前市场片段下一个episode换到不同行情就崩盘buffer太大时数据里包含了太多过时的市场状态策略更新方向被平均掉学得很慢。我给一组经验参考日线级别单episode长度250buffer存2000-5000条转移样本比较合理。学习率方面actor和critic建议分开设置actor用3e-4critic用1e-3。如果训练时发现loss震荡剧烈先把两个学习率都除以5再看训练曲线。DDPG里target网络的软更新系数τ我一般初始化0.005训练过程中如果发现目标Q值波动太大把τ降到0.001。4.3 回测指标不只是看累计收益Sharpe、最大回撤、换手率一起上训练完成后大多数源码会给出一张资金曲线图。如果只盯这条曲线很容易被“某一年翻了几倍”迷惑。真正要看的指标至少有三个年化Sharpe收益与波动的比值低于1的策略实盘意义就不大。最大回撤从峰值跌到谷底的最大幅度超过30%的策略遇到连续亏损时你很难拿得住。换手率日均换手高说明策略在频繁交易实盘里滑点和冲击成本会让收益大幅缩水。下面这段代码计算上述指标并判断策略是否值得继续调下去def evaluate_strategy(equity_curve, trades_per_day1.0): 输入每日账户净值序列输出核心绩效指标。 equity np.array(equity_curve, dtypefloat) daily_returns np.diff(equity) / equity[:-1] mean_ret np.mean(daily_returns) std_ret np.std(daily_returns) sharpe np.sqrt(252) * mean_ret / (std_ret 1e-8) peak np.maximum.accumulate(equity) drawdown (equity - peak) / peak max_drawdown drawdown.min() turnover trades_per_day # 还需要从动作记录里统计实际换手 return {sharpe: sharpe, max_drawdown: max_drawdown, turnover: turnover}很多源码在评估阶段没有扣手续费或滑点导致Sharpe虚高。建议你在写评估函数时把交易成本从收益里扣除后再计算这些指标这样一份策略有没有落地价值才看得出来。5. 回测不涨、训练崩溃、策略失效5个高频踩坑点与排查顺序5.1 训练loss在下降但收益曲线完全不动现象训练日志里actor的loss稳步下降critic的loss也在低位但生成的回测资金曲线是一条水平线几乎没有任何交易。原因这种情况十有八九是策略收敛到了“永远空仓”。原因可能是奖励函数里交易成本太高或者探索率下降太快模型在早期尝试几次交易亏损后发现空仓的期望收益最高于是再也不敢开仓。解决先调低交易成本把cost从0.003降到0.0005看是否会出现交易动作再把探索率固定在一个下限不要让它降到0比如PPO中entropy系数保底0.01保证策略一直有探索动力。最后确认奖励函数是否除以了某种尺度避免收益绝对值太小导致模型认为“做了和没做差不多”。5.2 训练阶段收益很高但样本外回测崩盘现象训练集上的累计收益曲线非常漂亮Sharpe能到3以上但切到样本外测试数据策略收益变成大幅亏损资金曲线直线下滑。原因这是最典型的过拟合和未来函数。未来函数的根源通常是数据预处理时使用了全量统计信息或者是状态特征里包含了未来时点的数据。过拟合则是因为模型容量太大、训练轮次太多把训练集里的噪声也“背”了下来。解决先排查未来函数把归一化改成滚动归一化。这一步通常能解决一半以上问题。如果仍然过拟合就加大训练集和验证集的间隔比如中间留出10%数据完全不用再把模型的隐藏层数减少或者增大L2正则项的系数。不要迷信“更大网络”在股票数据这个信噪比极低的场景下小网络往往更稳。5.3 训练到一半loss变成NaN或巨大正数现象训练跑到几千步之后loss突然变成NaN或者出现一个极大的正数之后程序报错退出。原因最常见的是梯度爆炸。股票特征的值域波动大比如某天价格突涨10%归一化后仍然是一个较大值传给网络的梯度会异常放大另一个常见原因是奖励reward的值域没控制住log收益在极端行情下会输给网络一个异常输入。解决给reward做clip限制在[-1, 1]区间给网络梯度做clipPyTorch里用torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm0.5)。同时检查状态特征是否做了对数变换价格类特征建议取log后再归一化能有效压值域。5.4 模型训练正常但策略总是过度交易现象回测结果显示日均换手率超过100%手续费和滑点吃掉大部分利润扣费后Sharpe只有0.2。原因奖励函数里没有足够的“动作平滑”约束模型发现每次微调仓位都能获得微小收益于是拼命高频操作。幅度上的微小优势被高频执行放大最终被真实交易成本抵消。解决在奖励函数里加动作惩罚项比如penalty -alpha * abs(action - prev_action)alpha从0.01开始调。这样模型每次改变仓位都要付出代价自然而然学会了低频决策。更彻底的做法是把决策周期拉长比如每5根K线才允许做一次动作从环境层面降低交易频率。5.5 多标的训练不收敛但单标的收敛得很好现象同一份代码在单只股票上训练能稳定盈利扩展到10只股票同时训练时loss震荡剧烈策略最终表现很差。原因多标的场景下每个episode采样到的股票不同状态分布差异大共享的神经网络需要同时拟合多个市场的规律。这种非平稳性会造成策略更新方向来回摆动模型学不到一个“通用”规律。解决一个稳妥的方案是“先单标的预训练再多标的微调”。先在1只股票上训练到策略稳定冻结一部分骨干参数再用其他股票数据做小学习率微调。另一种办法是保持多标的采样但每个episode固定使用同一只股票的数据让策略在更长的时间尺度上逐渐适应不同标的的风格。6. 怎么验证一份交易策略源码真的能跑最小行动路径与进阶建议拿到一份深度强化学习交易源码先别急着换算法、改网络。我建议你先做三件事第一用源码自带的环境随机策略跑200个episode记录奖励分布的均值和方差第二把训练集切小到原来的20%只训练500步确认整个训练流程能不出错地跑完第三把源码里的评估函数单独抠出来用一组手工构造的“永远持有一半仓位”的简单策略跑一遍回测确认评估指标的计算逻辑没有明显bug。这三步做完你对这份源码的工程质量心里就有底了。进阶方向上我优先级排序是先修环境层奖励、成本、数据切分再调算法层学习率、探索率最后才折腾网络结构。换网络结构往往是收益最低的改动因为股票交易的主要矛盾从来不是网络拟合能力不够而是目标函数和成本建模不够真实。另外提一个日常习惯训练过程中每隔一段时间保存一次模型权重同时把当时的回测指标写进日志。很多时候你回看训练曲线会发现最好的模型不是训练步数最多那个而是中间某个阶段出现过一次“明显跑赢基准”参数快照。有了日志还能反推是哪个阶段开始过拟合的。这个习惯救过我很多次也建议你在自己复现时坚持。希望这篇梳理能帮你在碰这一堆源码时少走点弯路。提示所有涉及实盘的参数先用仿真数据验证再考虑接入模拟盘小仓位跑。深度强化学习策略的价值来自回测和样本外验证的严谨程度而不是模型本身的复杂度。初学者优先把环境做扎实比堆叠算法更能提升最终收益的确定性。本文还有配套的精品资源点击获取
网站建设高端定制企业官网