尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深度强化学习驯服A股:从数据到回测的自动交易智能体实战

深度强化学习驯服A股:从数据到回测的自动交易智能体实战 简介面向金融科技与深度强化学习交叉方向的课程设计、毕业设计场景这份源码包提供了一套基于PPO等算法训练A股市场自动交易智能体的完整工程。项目包含数据回测、策略对比、交易结果可视化与模型参数分析等环节适合有一定Python与机器学习基础、希望将强化学习落地到真实股票场景的开发者参考。压缩包共130个文件约20.92MB包含12个Python脚本、7个Jupyter Notebook、82张交易结果图表以及日志、配置文件等。Notebook按成分股表现、传统策略、PPO参数查看、交易结果绘图等主题拆分便于一步步复现实验zip内还附带常用Shell脚本、配置文件与Git忽略规则等。解压后可按目录顺序运行调试目前已有101人学习浏览代码经过调试对正在完成股票量化和智能交易类项目的学生具有较高借鉴价值。1. 深度强化学习智能体跑A股自动交易难在哪儿把深度强化学习接到A股上很多人第一反应是“能不能预测涨跌”。跑过一年的分钟数据之后你会意识到交易智能体的上限从来不是PPO、SAC哪个更强而是数据怎么对齐、成交假设离真实有多远、奖励函数到底在优化什么。这三个问题决定了智能体是在学交易还是在对着一堆历史K线背答案。这篇文章适合已经会写Python、有基础机器学习经验的工程师。你的目标不是复现某一张惊艳的收益曲线而是拿到一份“基于深度强化学习训练A股股票自动交易的智能体源码”之后能拆得开、补得全、改得动。我按“数据与交易环境 → MDP建模 → 训练调参 → 回测验证 → 源码改造”的路径把从离线数据到可上线交易智能体的完整链路讲清楚。先从不引人注意但决定成败的数据与撮合环境说起。2. 深度强化学习的A股数据地基分钟K线、复权与gym交易环境拿到那份源码.zip第一步不是看模型而是看它怎么把K线喂给环境。A股有涨跌停、停牌、除权除息也有T1和交割规则这些都得在数据层先处理掉。我一般把数据准备拆成三件事选数据源与复权方式、清洗停牌与异常bar、提供统一的gym接口。2.1 前复权还是后复权训练数据里最容易被忽略的bug复权直接决定价格序列是否连续。以后复权数据训练、以前复权数据回测是回测环境最典型的污染方式。训练时我统一用后复权价格后复权以上市首日为基准往后调整最新价格等于真实市价历史价格按除权因子放大或缩小不会引入未来的复权信息。前复权以最新价格为基准反向调整历史价格每次新增交易日都会改动整条历史序列等于让环境状态里藏了未来。停牌处理也直接影响训练质量。A股停牌期间成交量为0一些数据接口仍会补出时间戳但没有有效价格。常见做法是把停牌bar直接丢掉并在环境里记录“当前bar之后是否有下一根可用bar”没有就提前结束回合。我还会过滤上市不足60个交易日的次新股避免开板前后的极端波动把策略带偏。2.2 自定义gym环境收盘出信号、次日开盘成交的撮合逻辑训练智能体需要标准交互接口。下面是最小可跑的gym环境骨架撮合逻辑采用“本bar收盘出信号、下一bar开盘成交”这是回测里避免未来函数的底线import numpy as np import gymnasium as gym from gymnasium import spaces class AShareTradingEnv(gym.Env): def __init__(self, features: np.ndarray, opens: np.ndarray, closes: np.ndarray, cost: float 0.001): super().__init__() self.features features self.opens opens self.closes closes self.cost cost # 动作目标持仓比例的增量范围 [-1, 1] self.action_space spaces.Box(low-1.0, high1.0, shape(1,), dtypenp.float32) # 状态历史特征 当前持仓比例 self.observation_space spaces.Box( low-np.inf, highnp.inf, shape(features.shape[1] 1,), dtypenp.float32 ) self.idx 0 self.cash 1_000_000.0 self.shares 0.0 def reset(self, *, seedNone, optionsNone): super().reset(seedseed) self.idx 0 self.cash 1_000_000.0 self.shares 0.0 state np.append(self.features[self.idx], 0.0) return state.astype(np.float32), {} def step(self, action): action float(np.clip(action[0], -1.0, 1.0)) price self.opens[self.idx 1] # 成交价取下一根bar的开盘价 # 用成交价重估净资产作为调整仓前的基准 net_worth self.cash self.shares * price cur_ratio (self.shares * price) / net_worth target_ratio np.clip(cur_ratio action, -1.0, 1.0) target_value target_ratio * net_worth trade_value target_value - self.shares * price commission abs(trade_value) * self.cost # 先扣成本再更新持仓 self.cash - trade_value commission self.shares trade_value / price self.idx 1 # 按本bar真实收盘价结算净资产奖励取对数差分 net_worth_now self.cash self.shares * self.closes[self.idx] reward np.log(net_worth_now / net_worth) done self.idx len(self.closes) - 2 state np.append(self.features[self.idx], self.shares * self.closes[self.idx] / net_worth_now) return state.astype(np.float32), reward, done, False, {}代码里的核心逻辑是把动作解释为持仓比例的增量而不是目标仓位本身。这样智能体每次决策只做边际调整训练早期不容易出现一步满仓、一步清仓的疯狂节奏。成本在成交瞬间从现金里扣除而不是在期末一次性计提更贴近真实账户的资金流。奖励用对数收益天然支持多期复利数值上也比净收益更平滑。这版代码是演示用骨架普通账户不允许透支买入完整实现里还要把target_ratio限制在现金可覆盖的范围内真实源码包则通常会再补三个核心约束当日涨停时禁止买入、跌停时禁止卖出、T1限制当日买入的份额不能当日卖出。涨停不能买、跌停不能卖是A股撮合的真实规则不加进去智能体就会学到在跌停价上疯狂抄底的动作模式。2.3 成本模型参数佣金、印花税与滑点怎么给交易成本对深度强化学习结果的杀伤力远大于模型结构。我用的是一组常见默认值训练和回测必须统一成本项A股常见取值说明佣金万2.5双向不同券商差异大最低5元起收印花税卖出单边0.05%按卖出成交金额计过户费十万分之一双向沪市股票收取通常并入佣金估算滑点0.01%0.05%信号价与成交价的平均偏差冲击成本按成交额比例递增小资金可忽略单日换手率高于10%必须建模把单边成本直接放大到千分之三来训练是验证成本敏感度最有效的做法。智能体在模拟环境里学会频繁换手且还能盈利说明它对成本模型足够鲁棒反过来训练集上高换手、高收益把cost从0.0005改到0.003后收益曲线掉头向下那就是典型的成本过拟合。3. 把A股自动交易写成MDP状态、动作与奖励的取舍深度强化学习训练交易智能体的本质是把“每天怎么买卖”变成一个序贯决策问题环境给出行情特征智能体输出动作环境结算奖励。很多源码包最终跑不出结果往往不是强化学习实现有问题而是MDP的三个组件设计粗糙。下面拆开讲。3.1 状态空间别再把过去N天涨跌幅堆成一个向量状态是智能体观察世界的全部信息。我常用的最小特征分四组价格衍生过去5/10/20日收益率、最高最低价位置、当日振幅、成交量比、5日均量偏离度、换手率、技术指标RSI、MACD柱、布林带位置、账户状态当前持仓比例、现金比例、累计盈亏。特征不是越多越好。A股分钟数据通常只有量价强行构造几百维特征会拉低样本效率。我一般先用相关性过滤掉冗余维度保留20到50维然后做归一化。归一化的窗口选择很讲究用整段历史的均值和方差做标准化等于是把未来信息渗进状态正确做法是用截至当前bar的滚动统计量或者用一个固定的滑动窗口。这个细节决定训练集和验证集是不是被“连体”。源码包里常见的错误是全样本fit标准化器训练和回测共用同一组参数。虽然省事但上线后状态分布一变策略表现马上垮掉。把标准化器封装进env里每个bar实时计算训练和上线行为才一致。3.2 动作空间连续仓位调整与三档离散动作怎么选动作空间直接决定训练难度。三种最常用的设计类型形式适合场景常见问题连续仓位输出 [-1,1] 映射为目标持仓比例资金量大、追求精细调仓边界动作保守难触发加减仓离散三档加仓/减仓/不变交易频率低、手续费敏感长期退化成只减不增离散五档-1/-0.5/0/0.5/1 仓位变化表达力与收敛速度折中档位语义不清晰A股T1规则下当日买入的股票次日才能卖出所以动作集合要区分开仓和平仓的可行区间。环境内部必须维护“今日可卖数量”这个状态否则智能体可以偷偷学到无风险的日内“来回”操作回测收益越漂亮越要警惕这一点。常见实现是给每只持仓股记录一个冻结份额买入产生的份额标记为不可卖次日解锁。我一般选连续仓位增量动作a∈[-1,1]目标仓位当前仓位a再clip到[-1,1]。连续动作空间的策略梯度更平滑配合PPO和SAC都容易收敛。离散动作适合交易信号本身就很粗的场景比如只做日线级别的加减仓。3.3 奖励函数累计收益不是唯一选择夏普也能写进奖励奖励是智能体唯一的优化信号。直接用每日净资产的对数收益做奖励最简单reward_t log(V_t) - log(V_{t-1})问题在于它只鼓励“净值涨”完全不惩罚回撤。同样回测三年策略A年化30%但最大回撤40%策略B年化20%但最大回撤8%对数收益奖励会把A排在前面可实盘里你大概率拿不住A。一个常用的改法是差分夏普比率DSR把回报均值除以回报波动让智能体主动学习低波动路径并支持在线流式计算。一个最小实现是这样的def differential_sharpe_reward(returns: np.ndarray, eta: float 0.1) - np.ndarray: 差分夏普奖励用增量式矩估计近似滚动夏普适合在step里逐项更新。 A 0.0 # 收益的一阶矩EMA均值 B 0.0 # 收益的二阶矩EMA平方均值 rewards np.zeros_like(returns) for t, r in enumerate(returns): denom B - A * A if denom 1e-8: # 先用历史矩计算本步奖励再更新矩 rewards[t] (B * r - 0.5 * A * r * r) / np.power(denom, 1.5) A eta * (r - A) B eta * (r * r - B) return rewards注意循环里的顺序奖励用上一时刻的A、B计算然后才更新矩估计这保证当前步不会把“自己的收益”提前算进历史矩里。eta控制历史记忆长度eta越大对近期波动越敏感A股行情风格切换快我一般取0.05到0.1。把每个step的reward替换成这个输出PPO训练时策略自然会对收益和波动做平衡。风控上更重的话可以在log收益基础上直接扣回撤惩罚reward_t log_ret_t - lambda * max(0, dd_t)其中dd_t为当前净值相对历史峰值的回撤率。lambda取1.0时策略每承受1%回撤就需要1%的额外收益来弥补这比训练完成后再叠加风控规则干净得多。4. 用PPO训练A股自动交易智能体选型、代码与五组关键参数MDP定义好之后算法选型是下一个分水岭。A股自动交易智能体开发中绝大多数源码包以PPO为骨干这一章讲清楚为什么、怎么跑、什么参数值得先调。4.1 为什么选PPO而不是DQN或SACDQN对连续仓位不友好做离散动作又额外引入档位设计状态值函数在非平稳行情里容易震荡。SAC的最大熵框架在奖励尺度不稳定时需要更多调参A股奖励经常因为单日涨跌停出现极端值SAC要让温度系数追上奖励尺度变化训练过程很磨人。PPO用clip机制限制每次策略更新的幅度对奖励尺度漂移和非平稳环境的容忍度最高这是交易源码包里它出场率最高的直接原因。另一个考虑是样本组织方式。A股日线单只股票每年只有两百多个样本on-policy只能把多只股票拼接起来训练分钟数据样本量大PPO的on-policy策略反而稳定。我一般保持PPO为主力同时留一个SAC对照实验骨干代码复用同一套env和wrapper超参各跑一组就行。4.2 最小训练代码stable-baselines3并行环境与LSTM策略A股交易序列天然有时序依赖用RecurrentPPO能记住最近若干bar的价格演化过程。下面是可跑的最小例程数据准备好后直接在命令行执行from sb3_contrib import RecurrentPPO from stable_baselines3.common.vec_env import DummyVecEnv, VecNormalize def make_env(features, opens, closes): def _init(): return AShareTradingEnv(features, opens, closes, cost0.001) return _init # 多只股票拼接后的训练集在此传入每个env对应一只股票切片 env DummyVecEnv([make_env(train_feats, train_opens, train_closes)]) env VecNormalize(env, norm_obsTrue, norm_rewardFalse, clip_obs10.0) model RecurrentPPO( MlpLstmPolicy, # MLPLSTM兼顾当前特征与时序记忆 env, learning_rate3e-4, n_steps2048, # 每次收集2048步再更新 batch_size256, n_epochs10, gamma0.99, gae_lambda0.95, # 优势估计的长期依赖强度 clip_range0.2, ent_coef0.005, # 熵系数控制探索 verbose1, seed42, ) model.learn(total_timesteps200_000) model.save(ashare_ppo_agent.zip)VecNormalize负责对状态做实时标准化norm_rewardFalse是因为对数收益已经带有成本和仓位变化的物理尺度再做窗口归一化会在不同回测期之间引入奖励尺度漂移不利于观察成本对策略的约束。n_steps乘以并行环境数是每次更新的样本量单env下2048步大约覆盖8个交易日对分钟数据来说是几百根bar更新频率合理。总步数20万在分钟数据上能覆盖多段完整行情日线拼接数据稍显不够可把total_timesteps提到50万以上。4.3 决定智能体训练成败的五组参数训练中要盯的参数很多真正能决定成败的是下面五组我按排查顺序排列参数常见范围调参方向失败信号learning_rate1e-4 ~ 3e-4损失震荡就减半训练曲线抖动或NaNn_steps1024 ~ 4096样本方差大就加大更新过频收益剧烈摆动clip_range0.1 ~ 0.3策略乱跳就调小单次更新破坏旧策略ent_coef0 ~ 0.02熵跌到0就加大动作分布坍缩成单点gamma0.95 ~ 0.999想要更长周期就调大持仓黏住不换手排查顺序建议是先看训练过程的奖励滑动均值是否稳定上行再看熵保持是否健康最后才看回测收益。很多人上来就调学习率结果换手率骤降、动作固化问题其实在ent_coef设成了0。5. 回测与过拟合排查让智能体在A股策略里不发疯模型能跑通只完成了三分之一。A股源码包最常见的死法不是训练失败而是过拟合叠加未来函数后的虚假繁荣。这一章给出回测的硬性流程和四个过拟合信号。5.1 滚动回测与三段数据切分训练前先把时间序列切成训练、验证、测试三段不能随机打乱。常见比例是70%训练、15%验证、15%测试。验证集用来做early stopping和选超参测试集只能跑一次跑完不能再回头动参数。如果测试集失败唯一的出路是回去换假设而不是继续调参再测。更严格的玩法是滚动回测。把训练窗口向前滚动每训练一年就在接下来三个月回测回测结果并入训练集继续滚。这能暴露策略在风格切换下的适应性。A股风格切换很频繁2020年的核心资产、2021年的新能源、2024年的小微盘想用一套参数吃遍所有年份基本不可能。我习惯固定几个市场风格区间单独看每个区间的夏普与最大回撤而不是只盯整体数字。用简单的split配置控制流程SPLITS [ (2015-01-01, 2018-12-31), # 训练 (2019-01-01, 2019-12-31), # 验证 (2020-01-01, 2021-12-31), # 测试区间1 (2022-01-01, 2023-12-31), # 测试区间2 ] # 每个测试区间只允许使用之前所有区间的数据做标准化与特征工程 # 回测配置里一旦出现测试区间内的统计量直接判为失败5.2 交易智能体过拟合的四个典型信号回测报告里出现下面四种情况都要停下来排查训练集夏普很高验证集立刻掉头。这是最典型的过拟合说明特征或超参把噪声当成规律。处理办法是简化特征集并利用验证集做早停。换手率和收益同时暴涨。多半是动作在利用撮合漏洞或未来函数。把cost放大10倍重跑一次如果收益曲线明显塌掉策略就没有真正学会交易逻辑。最大回撤集中在某一段特定行情。策略可能对某类微观结构做过拟合。换一批训练期未出现过的股票池做外推测试能快速识别这类脆弱性。参数微小扰动导致结果剧烈变化。把随机种子从0改成1训练结果差异很大说明策略还没收敛或训练方差过高。一个稳定的交易智能体对seed和训练集切片都应该有可解释的稳健性。5.3 实盘部署的边界合规通道、半自动上线与复盘指标实盘前必须确认程序化交易的身份合法。个人和机构都通过券商提供的合规程序化交易通道执行通常需要提前开通对应权限订单频率和报单比例也受券商风控管理这是底线问题。上线第一版我强烈建议半自动智能体只输出目标仓位由人工在交易软件里执行。原因不是策略不行而是实盘状态永远比模拟滞后先跑两周模拟盘记录实际成交价与信号价的偏差。复盘的指标只有一个滑点偏差分布。如果偏差中位数超过训练时设定的滑点参数就把环境的滑点调到实际值以上重新训练。6. 源码落地的进阶改造延迟修复、奖励塑形与部署陷阱6.1 特征延迟紧急修复未来函数的最后一道防线即使环境撮合做了“次日开盘成交”如果特征里直接用了当天收盘价计算的技术指标智能体依然能在盘中“看到”未来。检查时不光看状态向量还要看特征生成的时间戳。一个直觉规则t时刻能用的特征只能是t-1及之前数据的函数。遇到shift(-1)或者直接引用close[t]的特征生成代码一律改成close[t-1]并在环境里加一个信号延迟参数。6.2 奖励塑形把回撤惩罚写进训练回撤惩罚最常见的做法是净值曲线最低点向下的距离。把该值所在bar截断只对“当前净值低于历史最高值”的部分惩罚。def reward_with_drawdown(log_ret: float, equity: float, peak: float, lam: float 1.0) - float: dd max(0.0, (peak - equity) / peak) return log_ret - lam * dd每一步rollout时维护peak并传进这个函数。lambda取1.0策略回撤时惩罚强度相当于用收益去对冲取0.3则更温和。比训练后再叠加风控更稳定因为策略的每个动作都感知到回撤成本。源码包里常把这段逻辑写在train里而不是env里导致分布式训练时奖励不一致。把它收敛进env的step函数所有worker的奖励口径才能保持统一。6.3 部署前把源码.zip过一遍自检清单拿到源码包后先跑通再谈改进自检顺序建议如下表检查项最简验证方法复权方式训练集与回测集统一用后复权未来函数特征只依赖t-1前数据成交使用次日开盘价T1约束环境里维护可卖持仓状态成本参数训练与回测共用同一套佣金/印花税/滑点过拟合换seed再训练验证集结果保持合理滑动窗口标准化不引入整段历史的统计量把这六项写进训练管线的入口函数任何一份新下载的交易智能体源码都要先过这关再投入算力去调参。这样源码包就不会只停留在“能跑”的阶段而是变成一套你看得懂、敢跑、能上线的A股自动交易智能体。本文还有配套的精品资源点击获取
返回列表