PPO强化学习实战:A股投资组合调仓策略全解析
简介面向量化投资与强化学习交叉方向的中高阶开发者资源以Python为工具完整演示了PPO算法在A股市场构建投资组合的落地流程。资源共35个文件RAR压缩包约4.29MB8个py脚本覆盖数据读取、股票环境构建、经验回放、智能体训练等核心链路10个pth对应各训练阶段的模型权重2个npz和1个dataframe封装了A股股票基础数据另有收益率曲线图可直观评估策略累计表现整体覆盖从数据到结果的完整流程。内容包括PPO关键改进、15只股票的每日调仓决策、投资组合动态优化等核心模块能够帮助读者从理论走向实操快速复现强化学习选股与调仓的完整实验。已有1615人学习资源包代码结构清晰适合已具备Python与stable-baselines3基础的读者继续调参、替换股票池或扩展交易环境。1. 用 PPO 做 A股投资组合这东西到底靠不靠谱假设你手里有 20 只股票每天开盘前要决定每只买多少、卖多少。过去你可能靠均线金叉、MACD 背离这些单因子规则交易成本高还容易拿不住。现在你想直接上一套 Python 量化交易策略代码让强化学习自己学习调仓逻辑——这就是标题里 PPO 算法的位置。我直接说结论PPO 不是用来“预测明天涨跌”的它是用来学“在什么市场状态下怎么调整持仓权重”的决策模型。它把行情序列当作环境观测把调仓动作当作策略输出再用累积收益当作奖励信号。这套方案对有点 PyTorch 基础、想尝试深度强化学习的 A股量化从业者最合适。它能解决组合管理中“规则写不全、参数调不完”的问题代价是训练不稳定、回测容易过拟合后面每一步都是坑。2. A股行情数据清洗与特征工程给强化学习喂什么样的“原始观察”2.1 数据源选择与预处理为什么 A股要按股票代码拼接而不是按日期对齐做强化学习环境的第一步不是搭网络而是把行情数据整理成环境能消费的“状态序列”。A股数据有个容易翻车的点不同股票的交易日历不是完全对齐的——有的股票停牌几天有的股票上市晚有的股票被 ST 后涨跌停幅度变了。如果按“日期对齐”去构造面板数据会出现大量 NaN 和未来函数。常见做法是先按股票代码分组每只股票单独做时间序列处理再在环境内部按“当前日期 持仓股票列表”动态拼接状态向量。数据源方面我一般用 akshare 或 tushare 拉日线前复权数据。前复权必须做否则除权除息会让价格出现假跳空PPO 会把这种跳空当成真实的收益信号。预处理顺序是按代码分组 → 按日期排序 → 前复权 → 删除上市不足 60 天的次新股 → 剔除停牌超过 30 天的股票 → 对特征做 z-score 归一化。注意归一化要在训练窗口内做不能在整个数据集上做否则又是未来函数。一个实用的检查方法是每只股票单独看一眼复权后的价格序列除权日不应该有超过 10% 的价格跳空。A股的除权除息处理错后面无论是 reward 计算还是状态编码都会跟着错而且很难排查——你以为是模型没学好其实是数据里有前复权漏算。2.2 特征工程把“涨跌”变成可学习的马尔可夫状态强化学习的状态必须满足马尔可夫性当前状态要尽量携带决策所需的历史信息。A股行情原始数据只有 OHLCV这些不够。我一般会构造四类特征按顺序拼接成状态向量第一类是价格衍生特征比如 5 日、10 日、20 日收益率以及收益率的标准差波动率。第二类是成交量特征比如量比当日成交量 / 5 日均量、成交量 20 日分位数——量比用于捕捉资金异动。第三类是技术指标特征比如 RSI14 日、MACD 的 DIF 值、布林带宽度这些不是玄学而是给策略提供“超买超卖”的连续信号。第四类是市场整体特征比如沪深 300 指数 20 日收益率、全市场上涨家数占比——这能让策略感知大盘环境。注意不要把 60 个原始 OHLCV 直接扔进去。高维稀疏输入会让 PPO 的 critic 网络很难收敛训练步数直接翻倍。我用的是每只股票 18 维特征20 只股票就是 360 维状态向量加上账面上的现金比例和当前仓位总共不到 400 维。对 PPO 来说这个维度是可以接受的。特征构造完还要做一个关键操作把每个特征 z-score 标准化。均值 μ 和标准差 σ 用训练集计算验证集和测试集复用同一组参数。如果分开计算分布偏移会直接破坏 PPO 的在线学习稳定性。2.3 用 Python 写最小可运行的数据集构建代码下面给一份能直接改着用的数据预处理代码用 akshare 拉日线构造特征并归一化。你不需要照抄重点是理解顺序。import akshare as ak import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler def load_stock_data(stock_code: str, start: str, end: str) - pd.DataFrame: 拉取单只股票前复权日线构造基础特征 raw ak.stock_zh_a_hist( symbolstock_code, perioddaily, start_datestart, end_dateend, adjustqfq, # 前复权必须 ) df raw[[日期, 开盘, 收盘, 最高, 最低, 成交量]].copy() df.columns [date, open, close, high, low, volume] df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) return df def build_features(df: pd.DataFrame) - pd.DataFrame: 构造 18 维特征收益、波动、量比、RSI、MACD DIF out pd.DataFrame() out[date] df[date] out[ret_5] df[close].pct_change(5) out[ret_10] df[close].pct_change(10) out[ret_20] df[close].pct_change(20) out[vol_5] df[close].pct_change(5).rolling(10).std() out[vol_20] df[close].pct_change(20).rolling(20).std() out[volume_ratio] df[volume] / df[volume].rolling(5).mean() out[volume_pct] df[volume].rolling(20).rank(pctTrue) # RSI 14 delta df[close].diff() gain delta.clip(lower0).rolling(14).mean() loss (-delta.clip(upper0)).rolling(14).mean() rs gain / loss.replace(0, np.nan) out[rsi_14] 100 - 100 / (1 rs) # MACD DIF ema12 df[close].ewm(span12).mean() ema26 df[close].ewm(span26).mean() out[dif] ema12 - ema26 return out def prepare_state_matrix(stock_codes, start, end): 把所有股票的特征按代码纵向拼接返回 date 索引的宽表 all_feats [] for code in stock_codes: df load_stock_data(code, start, end) feats build_features(df) feats[stock_code] code all_feats.append(feats) concat_df pd.concat(all_feats, ignore_indexTrue) # 宽表行是日期列是 (stock_code, feature) 的 MultiIndex wide concat_df.pivot_table( indexdate, columnsstock_code, values[c for c in concat_df.columns if c not in [date, stock_code]] ) wide wide.sort_index() return wide代码逻辑说明load_stock_data负责拉数据和排序build_features构造 18 维基础特征prepare_state_matrix把多只股票拼成宽表。这里有个容易犯的错误——pct_change 会产生 NaNPPO 前向计算时 NaN 会直接毁掉整个 batch 的梯度。所以wide输出后要做两件事删除上市不足 60 天的股票前 60 行特征全 NaN然后用前向填充 0 填充处理剩余 NaN。参数说明start和end的格式是YYYYMMDDakshare 要求adjustqfq是前复权必填。build_features里所有 rolling 窗口都是 5、10、20 这种业务常用值你可以按自己的股票池换成 3、6、12。但要注意窗口长度不要超过训练数据长度的 20%否则有效样本太少。3. PPO 算法选型与最小实现从原理到 PyTorch 代码3.1 为什么是 PPO 而不是 DQN 或 SAC策略梯度在组合优化里的适用性投资组合动作是一个连续权重向量表示“每只股票分配多少仓位”。DQN 适合离散动作空间虽然可以强行把权重离散成 0、0.5、1 三档但 20 只股票就有 3 的 20 次方种组合Q 网络根本学不动。SAC 是 off-policy 的最大熵算法采样效率高但 SAC 对 reward 尺度极其敏感A股 reward 天然是负偏态的亏损交易日多于盈利交易日调温度系数能调到你怀疑人生。PPO 是 on-policy 的虽然采样效率低但每一步都基于当前策略分布做重要性采样稳定性最好而且 clip 机制对超参数不敏感——这正符合 A股数据里信噪比极低、短期波动剧烈的情况。另一个重要原因是 PPO 的截断目标天然适配“组合优化”中的约束。t 时刻的权重向量 w_t 限制在 [0,1] 并且总和为 1这是单纯形约束。你可以在动作头后面加一个 softmax 层把网络输出的 logits 映射成权重——这比 DQN 套离散组合动作干净得多。PPO 的 policy 网络输出层的 softmax 直接就是交易权重cash ratio 可以单独作为动作向量里预留的一维让模型自己学“空仓”这个动作。3.2 PPO-Clip 与 PPO-Penalty我一般选 Clip 版附参数表PPO 有两个公认变种。PPO-Penalty 在目标函数里加 KL 散度惩罚项把新旧策略的差异约束在 δ 范围内但 δ 的初始值和自适应调节容易让训练震荡。PPO-Clip 则是用 min(r_t * A_t, clip(r_t, 1-ε, 1ε) * A_t) 的截断目标直接限制每次更新步长实现简单在金融数据上表现更稳。我一般选 Clip 版下面的实现也都是基于它。核心参数表参数推荐值说明clip_range0.2新老策略比值的截断范围太大更新激进太小学习慢gae_lambda0.95广义优势估计的折扣因子控制偏差-方差权衡ent_coef0.01熵正则系数A股数据噪声大建议调低到 0.005-0.01learning_rate3e-4过高会导致策略崩坏过低会学不动batch_size256经验池采样 batch影响优势估计的方差n_steps2048每轮采集轨迹长度A股交易日有限够用即可ent_coef在 A股环境里要特别小心。熵系数会让策略保持“随机性”但金融数据噪声极大如果熵系数设成 0.05 或者更高策略会一直随机调仓换手率高到你想哭。我踩过这个坑后面第 5 章细说。3.3 最小 PPO 实现Actor-Critic 网络与 GAE 代码骨架下面是一份结构完整的 PPO-Clip 逻辑骨架用 PyTorch 实现。Actor 输出 softmax 权重Critic 输出状态价值。import torch import torch.nn as nn import torch.optim as optim import numpy as np class ActorCritic(nn.Module): Actor 输出组合权重Critic 估计状态价值 def __init__(self, state_dim, action_dim, hidden256): super().__init__() self.actor nn.Sequential( nn.Linear(state_dim, hidden), nn.Tanh(), nn.Linear(hidden, hidden), nn.Tanh(), nn.Linear(hidden, action_dim), nn.Softmax(dim-1) ) self.critic nn.Sequential( nn.Linear(state_dim, hidden), nn.Tanh(), nn.Linear(hidden, hidden), nn.Tanh(), nn.Linear(hidden, 1) ) def forward(self, state): probs self.actor(state) value self.critic(state) return probs, value def compute_gae(rewards, values, dones, gamma0.99, lam0.95): 广义优势估计A股日频数据 gamma 设 0.99 足够 advantages np.zeros_like(rewards) gae 0.0 for t in reversed(range(len(rewards))): if t len(rewards) - 1: next_value 0.0 else: next_value values[t 1] delta rewards[t] gamma * next_value * (1 - dones[t]) - values[t] gae delta gamma * lam * (1 - dones[t]) * gae advantages[t] gae returns advantages values return advantages, returns def ppo_update(agent, optimizer, states, actions, old_log_probs, returns, advantages, clip_range0.2): PPO-Clip 核心更新逻辑 states torch.FloatTensor(np.array(states)) actions torch.FloatTensor(np.array(actions)) old_log_probs torch.FloatTensor(old_log_probs) returns torch.FloatTensor(returns) advantages torch.FloatTensor(advantages) for _ in range(4): # 每个 batch 更新 4 轮 probs, values agent(states) dist torch.distributions.Categorical(probs) new_log_probs dist.log_prob(actions.argmax(dim-1)) # 注意此处仅示意 ratio torch.exp(new_log_probs - old_log_probs) clipped torch.clamp(ratio, 1 - clip_range, 1 clip_range) actor_loss -torch.min(ratio * advantages, clipped * advantages).mean() critic_loss nn.MSELoss()(values.squeeze(-1), returns) ent_loss -dist.entropy().mean() total_loss actor_loss 0.5 * critic_loss - 0.01 * ent_loss optimizer.zero_grad() total_loss.backward() nn.utils.clip_grad_norm_(agent.parameters(), 0.5) optimizer.step()逻辑说明ActorCritic网络把状态映射成权重分布和价值估计。compute_gae用逆序遍历计算优势值这样每条轨迹末尾的 reward 才会向开头传播。ppo_update里标了注释的部分——如果动作是连续权重向量而不是离散类别不能用Categorical要换MultivariateNormal或把动作定义为 Dirichlet 分布否则 softmax 输出的维度会被当成类别概率训练方向完全错。我在代码里特意留了这个口子就是为了提醒你动作空间决定了分布类型这是 PPO 落地中最容易被忽略的结构性错误。参数说明gamma0.99适合 A股日频数据因为交易日一年就 250 个左右不需要像游戏那样设 0.99 以上的长视距。lam0.95是 GAE 的默认值它控制偏差和方差lam 越小偏差越小lam 越大方差越小。clip_range0.2是经验推荐值我试过 0.1 和 0.30.1 收敛太慢0.3 策略会突变。clip_grad_norm_的 0.5 是为了防止 reward 分布极端时梯度爆炸——A股里连续跌停会带来极大的负 reward这一步不能省。4. 把 A股交易规则塞进环境构建投资组合模拟器4.1 交易成本、涨跌停、停牌环境里必须处理的三个 A股现实强化学习环境如果不管交易规则回测曲线会好看得离谱实盘一跑就现原形。A股和美股最大的差异是 T1 和涨跌停限制。T1 意味着当天买入的股票当天不能卖出环境必须在 step 函数里记录“今日买入的仓位不可交易”这个状态。涨跌停意味着某些股票在极端行情下无法以理想价格成交——如果环境允许跌停板上的股票被卖出策略会学到一个现实中不存在的“逃命能力”。我处理这三件事的方式是交易成本按双边千分之三佣金 印花税 滑点对每笔调仓金额扣费涨跌停价格基于前收盘价计算如果某只股票的当前价格触及涨停或跌停则禁止买入或卖出操作停牌股票直接冻结仓位既不允许交易也不参与 reward 计算。这三条规则加进去以后策略的换手率会显著下降拿仓周期变长这正是实盘需要的。4.2 状态空间与动作空间定义权重向量与手数约束的取舍状态空间我在第 2 章已经构造好了是 (date, feature) 的宽表。但要注意环境返回的状态必须是“当前时间点可见的”数据——也就是说t 日状态只能包含 t 日收盘前已公开的数据不能包含 t 日收盘后的数据。如果用了 t1 日的特征做 t 日决策就是典型的未来函数泄漏。动作空间有两个选择连续权重向量或离散档位。连续权重加 softmax 的优势是梯度平滑但实盘下单时不能按权重直接买还要换算成手数100 股为 1 手。我一般用连续权重向量在环境内部做手数舍入w_i * total_assets 除以 close_price * 100向下取整剩余资金留在现金。这个过程会引入 0.5% 左右的跟踪误差可以在 reward 里加一个持仓不匹配惩罚让模型自己学会不要提出 0.7 这种难以精确执行的权重。4.3 最小可运行环境代码用 gymnasium 封装一个 20 支股票的组合环境下面给一个简化版环境20 支股票连续权重动作带交易成本和涨跌停过滤。这个环境可以直接跑 PPO但你要按自己的数据格式改_get_state方法。import gymnasium as gym from gymnasium import spaces import numpy as np import pandas as pd class PortfolioEnv(gym.Env): A股投资组合环境输入特征宽表输出权重动作 def __init__(self, feature_df, stock_codes, costs0.003, max_hold1.0): super().__init__() self.features feature_df self.stock_codes stock_codes self.n_stocks len(stock_codes) self.costs costs self.max_hold max_hold # 状态空间特征 当前仓位 现金比例 self.feature_dim feature_df.shape[1] self.obs_dim self.feature_dim self.n_stocks 1 self.action_space spaces.Box(low0.0, high1.0, shape(self.n_stocks 1,)) self.observation_space spaces.Box(low-np.inf, highnp.inf, shape(self.obs_dim,)) self.current_step 0 self.weights np.zeros(self.n_stocks 1) self.weights[-1] 1.0 # 初始全仓现金 def _get_state(self): feat self.features.iloc[self.current_step].values return np.concatenate([feat, self.weights]) def step(self, action): # action 是权重向量最后一位是现金比例做归一化 action np.clip(action, 0.0, 1.0) action action / action.sum() # 获取当日收益下一日的实际收益作为 reward next_day_ret self.features.iloc[self.current_step 1].values # 简化假设特征里含收益列实际应单独传 old_value self.weights (1 next_day_ret) new_weights action # 交易成本调仓部分的绝对值乘以成本 turnover np.abs(new_weights - self.weights).sum() cost_penalty self.costs * turnover reward old_value - cost_penalty - 1.0 self.weights new_weights self.current_step 1 done self.current_step len(self.features) - 2 return self._get_state(), reward, done, False, {turnover: turnover} def reset(self, seedNone): self.current_step 0 self.weights np.zeros(self.n_stocks 1) self.weights[-1] 1.0 return self._get_state(), {}代码逻辑说明环境状态由“特征 当前权重”拼接而成动作是归一化后的权重向量。step里next_day_ret是简化写法——实际项目中需要单独传入下一日的收益率矩阵而不是从特征里取。turnover计算的是调仓导致的换手率用来扣交易成本。reward 是组合价值变化减去换手成本这是让策略“少交易、拿得住”的关键设计。参数说明costs0.003是双边费率A股佣金普遍万 2.5加上印花税卖出千 1双边合计大约 0.13% 到 0.2%。我设置 0.3% 是为了含滑点保守一点。max_hold1.0表示允许满仓。如果策略发现频繁调仓收益覆盖不了成本它会自然学会低换手——这正是我们要的行为。注意动作空间维数是n_stocks 1最后一维是现金比例这样策略可以自主选择空仓。5. PPO 训练避坑与调参实战从 5 个必调参数到 4 个典型翻车记录5.1 训练循环与日志记录让 PPO 在 1 万步内看到学习信号训练循环不复杂但日志记录很关键。你要记录三个信号policy loss、value loss、平均 reward 和平均换手率。平均 reward 反映策略是否在赚钱平均换手率反映策略有没有学坏。如果 reward 在下降但换手率却在上升那说明策略正在利用交易成本漏洞——比如频繁在小波动间套利而真实市场上这种机会根本不存在。一个重要的训练技巧是 warm-up。前 500 步固定随机策略让环境积累一批真实经验再开始 PPO 更新。原因是随机策略可以在特征空间里均匀采样避免初始分布偏差。另一个技巧是 reward 归一化把 reward 除以滚动 100 步的标准差。A股某些极端下跌行情会让 reward 变成 -0.2 甚至更低不归一化的话 critic 网络很容易被前几轮的大数值带偏。1 万步不是硬指标。我的经验是20 支股票、2000 天数据PPO 在 3 万步左右开始出现稳定的学习信号平均 reward 不再剧烈震荡、换手率降到 20% 以下。如果 5 万步还在原地打转问题基本不在模型而在数据或 reward 设计。5.2 5 个必调参数clip_range、gae_lambda、ent_coef、lr、batch_size这五个参数我单独拎出来因为它们在 A股环境里各有各的脾气。clip_range控制策略更新幅度。A股 reward 噪声大clip_range 设 0.2 时单条极端轨迹比如一个涨停板会把策略瞬间拉偏。我实际用 0.15 比较多代价是收敛慢一点但策略更稳。想省事就用 0.2但要配后面说的lr一起调。gae_lambda控制优势估计的时间尺度。lambda 接近 1 时策略会看到更长远的收益适合捕捉中期趋势lambda 接近 0 时策略只盯短期。A股日频数据我推荐 0.9 到 0.95 之间。太高会让策略在震荡市里学出“死扛”的毛病——因为远期收益方差太大。ent_coef是最容易翻车的参数。熵正则本来是鼓励探索的但 A股数据里噪声已经是天然的“探索源”。我见过有人把 ent_coef 设 0.01 以上结果策略每两天就全仓换一遍股票换手率 300% 以上回测曲线看着漂亮实盘佣金直接吞掉全部超额收益。我的建议是 0.005 起步如果训练熵降得太快再上调到 0.01。lr和batch_size要一起调。PPO 是 on-policybatch_size太大比如 2048会导致每个 batch 里包含太多旧策略的数据重要性采样比率失真太小比如 32则优势估计方差大。20 支股票的日频场景我一般用batch_size256lr3e-4。如果 loss 震荡剧烈先把 lr 降到 1e-4不要动 batch_size。5.3 4 个典型翻车记录现象 → 原因 → 解决第 1 条踩坑记录策略连续 20 天满仓不动但训练 loss 完全不变。现象训练 loss 曲线平得像一条直线reward 也不涨不跌。原因特征归一化时用了全局均值/方差而 A股不同股票的价格量级差异极大——比如茅台 1700 元农业银行 3 元把这两个价格直接拼到同一特征里softmax 输出几乎由高价股主导低价股的动作概率被压到 0。解决所有价格类特征统一转成收益率或对数收益率每股价格绝对值不进入特征。这个坑如果没有实盘背景光跑代码是根本想不到的。第 2 条踩坑记录回测收益率为 35%实盘亏 8%。现象回测曲线完美上扬年化 35%但模拟实盘跑了一个月就是不赚钱。原因reward 计算用了未来数据——我在第 4 章特意强调过“t 时刻用 t 日收盘后的特征做 t 日决策”实际写代码时很容易把当天的收益也算进 reward导致策略在 t 日“看到”了 t 日的涨跌结果这等于作弊。解决强制规定环境 step 中只能使用 t 日及之前的公开数据t1 日的收益统一在下一步返回写一个测试用例断言状态矩阵时间索引严格递增。第 3 条踩坑记录训练到一半突然 reward 崩盘一路负到底。现象训练前 5000 步都很正常到 6000 步时 reward 突然从 0.05 跌到 -0.3之后再也回不去。原因PPO 更新幅度过大策略坍缩到单一动作——比如全仓某只股票。A股单票仓位过重时一旦遇到连续跌停reward 会持续为大负数策略进一步疯狂“抄底”形成恶性循环。解决给权重动作加一个最大仓位限制比如单票不超过 30%并在 reward 里加一个 Carhart 风格的高换手惩罚项。此外把clip_range从 0.2 降到 0.15能显著减缓坍缩。第 4 条踩坑记录换手率爆炸增长但策略换手率越低回测反而越差。现象策略学到“尽量不交易”换手率降到 5% 以下但回测收益也从年化 25% 变成 8%。原因ent_coef 设成 0策略过早收敛到确定性策略失去了对市场风格切换的适应能力。比如市场从大盘价值切换到大盘成长时固定不交易的策略完全无法跟随。解决ent_coef不能设成 0而且要在训练中动态调整——前期 0.01 鼓励探索后期衰减到 0.001 让策略逐渐稳定。scheduler 用ent_coef * 0.99^epoch就可以。6. 从单次回测到滚动验证用组合换手率和最大回撤评估 PPO 策略6.1 滚动训练窗口与样本外测试协议单次回测根本不够。A股市场风格变化极快2019 年核心资产牛、2021 年新能源牛、2024 年红利股熊市你用 2019 到 2020 年数据训练的策略去回测 2023 年结果好得惊人但这毫无意义——策略没有见过 2021 年后的市场结构变化回测结果只是“运气”。常见做法是滚动窗口每 6 个月为一个训练期用训练期数据训练 PPO然后在下一个 3 个月做样本外测试。比如训练期是 2020 年 1 月到 6 月测试期是 2020 年 7 月到 9 月然后训练期滑动到 2020 年 4 月到 9 月测试期是 2020 年 10 月到 12 月以此类推。这样每一段测试数据都没有参与训练结果可信度大幅提升。滚动窗口的坑在于训练成本。PPO 每 6 个月重新训练一次每次 3 万步20 支股票的数据量大概花 40 分钟到一个小时如果做 8 个窗口总耗时 6 小时以上。我一般先把训练步数减半1.5 万步做一轮快速验证确认策略没有结构性错误再跑全量滚动。6.2 一个特例当策略连续 20 天空仓时先查哪个环节这个例子来自我自己的血泪经验。有一次训练出来的策略测试期前 20 天全空仓——reward 是 0换手率也是 0。我一开始以为是模型学会了避险但看净值曲线发现它错过了后面一波 15% 的上涨。问题不在模型在数据那段时间股票池里有 3 只股票因为连续涨停被停牌核查停牌股票的收益率为 0而我在构建 reward 时把停牌股票的收益按 0 计入这相当于给“持有”动作发了一个固定负收益策略自然学会用空仓来规避这种“僵尸仓位”。解决方式停牌股票的仓位在停牌期间既不贡献收益也不参与调仓但它的市值变动应该保留——停牌期间如果其它股票上涨组合仓位会因停牌股占比被动下降这应该是环境状态的一部分而不是 reward 的惩罚。正确做法是对停牌股单独设置一个 mask当期调仓时不允许买卖它reward 计算时使用已实现收益的加权平均。这个问题提醒我一个更通用的习惯强化学习策略翻车时先不要怀疑算法先画一张“动作分布 vs 特征分布”的时间序列图。如果动作分布长期落在某个边界比如全空仓或全满仓几乎都是环境或 reward 出了问题而不是 PPO 没调好。最后分享一个我个人保留的验证习惯训练完 PPO 后把它的调仓信号和最简单的等权重组合、固定 50% 仓位组合做个半年维度的对比。如果 PPO 的换手率是等权组合的 3 倍以上但年化收益只高出不到 5%那说明策略的 alpha 全靠交易成本换来的这种模型我不会上实盘。A股市场结构变化太快PPO 这样的深度强化学习模型最适合的是“学习调仓逻辑”而不是“预测市场方向”——想清楚这一点你会省下很多调参和翻车的功夫。希望帮到你。本文还有配套的精品资源点击获取