halo 的技术博客

返回

先说结论:PPO 在交易里最值钱的产出不是”预测得更准”,而是”仓位下得更聪明”。本文用纯 numpy 从零实现一个连续动作空间的 PPO 交易 agent,在均值回复的合成市场上训练 200 轮后,它自己学会了”涨多了做空、跌多了做多”——而且是平滑渐进的仓位函数,不是硬规则的 ±1 满仓跳变。样本外结果把这个差异量化得很清楚:PPO 的 Sharpe(1.57)与满仓硬规则(1.75)接近,但最大回撤只有硬规则的五分之一(-3.5% vs -17.2%),换手率也只有五分之一。同样的信号,不一样的仓位艺术。

为什么下单量是个强化学习问题#

监督学习范式下你训练一个分类器:输入特征,输出”明天涨/跌”。但从预测到下单之间隔着三个监督学习管不了的东西:

  1. 动作是连续的。不是”买或不买”,是”持仓 -1 到 +1 之间的哪一点”。0.3 仓和 0.9 仓在风险上是完全不同的决策。
  2. 决策有状态依赖。从 +0.8 调到 -0.8 要付双倍换手成本;当前持仓是决策的一部分,这是序贯决策而非独立预测。
  3. 优化目标是轨迹级的。你要的是整段时间的风险调整收益,不是每一步的预测准确率。一个准确率 55% 但重仓错误时刻的模型,比准确率 52% 但仓位控制得当的模型亏得多。

强化学习的建模刚好对上:状态 s(市场特征 + 当前持仓)、动作 a(目标仓位,连续标量)、奖励 r(仓位 × 次期收益 − 换手成本)。agent 的目标是最大化折现累积奖励——它直接优化你真正关心的东西

从策略梯度到 PPO:为什么需要 clip#

策略梯度方法直接参数化策略 π(a|s;θ),沿着”提高高优势动作概率”的方向更新 θ。裸的策略梯度(REINFORCE)有个致命弱点:步长极难调。步子小了训练慢如蜗牛;步子大了策略一次跳变,跳到一个烂区域后采出来的数据也是烂的,从此螺旋坠毁——策略梯度的数据是自己采的,坏策略产生坏数据,坏数据强化坏策略。

PPO 的解法优雅在于用概率比度量策略偏移:

r(θ)=πθ(as)πθold(as)r(\theta) = \frac{\pi_\theta(a|s)}{\pi_{\theta_{old}}(a|s)}

新旧策略对同一动作给出的概率之比。r=1 表示没变,r=1.5 表示新策略让这个动作概率提高了 50%。PPO 的目标函数:

LCLIP=E[min(r(θ)A, clip(r(θ),1ϵ,1+ϵ)A)]L^{CLIP} = \mathbb{E}\left[\min\big(r(\theta)A,\ \text{clip}(r(\theta), 1-\epsilon, 1+\epsilon)A\big)\right]

clip 目标函数

读这张图:优势 A>0(动作比平均好)时,目标随 r 上升,但r 超过 1+ε 后被削平——继续提高这个动作的概率不再有奖励,梯度归零。A<0 时对称:r 压到 1-ε 以下后不再有惩罚减免。效果是把每次更新钉死在旧策略附近的”信任域”里,用一个 min 和一个 clip 替代了 TRPO 的二阶约束优化,实现只要几行代码。

纯 numpy 实现:线性高斯策略#

为了把机制看透,我们不用任何 RL 框架。策略是线性高斯的:均值由特征线性组合过 tanh 得到(保证仓位在 ±1 内),标准差 σ 是可学习的全局参数:

def policy_mean(x, theta):
    return np.tanh(phi(x) @ theta)      # phi(x) = [特征, 1],带截距

# 采样动作:均值 + 高斯探索噪声
means    = np.tanh(P @ theta)           # P: (HORIZon, D+1) 特征矩阵
acts_raw = means + np.exp(log_std) * rng.normal(0, 1, HORIZON)
acts     = np.clip(acts_raw, -1, 1)     # 仓位物理约束
python

σ 就是探索的旋钮:训练早期 σ 大,agent 广泛试探各种仓位;随着策略变好,继续乱试的动作拿到负优势,σ 的梯度自然把它压小。探索强度不用人排程,它是学出来的。

奖励函数是全部金融语义的注入点:

reward[i] = act[i] * ret[t+1] * 100 - COST * abs(act[i] - pos_prev) * 100
#           仓位 × 次期收益        换手成本(惩罚仓位跳变)
python

注意 ret[t+1]——决策在 t 日收盘,吃 t+1 日收益,这是回测里”信号日不吃收益”纪律在 RL 里的等价物。换手成本项让 agent 学会”没必要就别动仓位”。

优势估计用 GAE(广义优势估计),本质是”这个动作比价值函数的预期好多少”的指数加权版本;再加一个线性价值头做 baseline 回归。PPO 更新循环的核心:

logp_old = gaussian_logp(acts_raw, means, std)        # 冻结旧策略概率
for epoch in range(4):                                 # 同批数据重用 4 次
    means_new = np.tanh(P @ theta)
    ratio     = np.exp(gaussian_logp(acts_raw, means_new, std) - logp_old)
    clipped   = np.clip(ratio, 1 - 0.2, 1 + 0.2)
    use_clip  = ratio * adv > clipped * adv            # 被 clip 的样本
    # 只对未触发 clip 的样本回传梯度(clip 区梯度为零)
    coef    = np.where(~use_clip, ratio * adv * dlogp_dmu * dmu_dz, 0.0)
    theta  += LR * P.T @ coef / HORIZON
python

use_clip 这行就是 PPO 的全部秘密:触发 clip 的样本梯度直接归零,它们对更新没有发言权。这也是为什么同一批数据敢重用 4 个 epoch——偏移一大就被自动断电。

实验:OU 均值回复市场#

合成市场用 Ornstein-Uhlenbeck 过程(持续系数 0.95)叠加轻微下行漂移和噪声——一个”涨多必跌、跌多必涨”但带噪声的均值回复环境。特征 4 维:3 日累计收益、20 日累计收益、20 日波动、当日 z-score。训练段 4000 天,测试段 1000 天(不同随机种子生成,完全样本外)。双边换手成本 2bp。

训练曲线

训练 200 轮,每轮采 512 步:episode 总奖励从前 10 轮均值 -8.9 爬到后 10 轮 +7.0。右图两条辅助曲线是 PPO 的健康监测仪:clip 触发比例维持低位(更新一直待在信任域内,学习率没开过头),σ 从 0.61 缓慢下降(探索在收敛,但仍保留随机性)。

学到的参数本身就可读——20 日累计收益特征的权重是 -0.947,一家独大且为负:过去 20 天涨得越多,仓位越空。agent 从零知识自己发现了市场的均值回复结构。

学到的策略长什么样#

学到的仓位函数

蓝线是学到的确定性策略 μ(s) 沿”20 日累计收益”特征的切面:一条平滑的递减 S 曲线。近期涨幅深度负值(超跌)→ 仓位接近 +0.9;涨幅温和 → 仓位温和;深度超涨 → 接近 -0.9。绿点是测试集上 1000 个实际交易日的仓位散布,贴着曲线走。

这条平滑曲线正是连续动作空间的价值所在。对比硬规则”20 日收益为正就满空、为负就满多”:信号方向完全一样,但硬规则只有 ±1 两档,信号强度信息全部丢失——刚刚转正的弱信号和极度超涨的强信号,下的都是同一个满仓。

样本外:Sharpe 打平,回撤只有五分之一#

样本外净值对比

1000 天样本外测试段(买入持有亏 -29.8% 的下行市):

策略总收益Sharpe最大回撤日均换手
PPO 连续仓位+30.8%1.57-3.5%0.053
硬规则 ±1 满仓反转+278.7%1.75-17.2%0.269
买入持有-29.8%-0.33-35.4%

读数要诚实:硬规则的总收益远高——满仓押注在信号确实有效的市场里杠杆拉满,赚得自然多。但看风险调整后的全貌:Sharpe 几乎打平(1.57 vs 1.75),而 PPO 的最大回撤只有硬规则的 1/5、换手只有 1/5。PPO 在信号弱的时候只下小仓——错的时候亏得少;换手成本敏感的真实环境里,1/5 的换手意味着 1/5 的摩擦损耗。

**PPO 学到的不是更强的信号,而是信号强度到仓位大小的正确映射。**这件事监督学习给不了你——分类器输出的 0.55 概率和 0.85 概率,最后都被你手写的规则拍成同一个仓位。

四个必须拆穿的陷阱#

陷阱一:reward = 收益率就完事了。 奖励函数是 RL 里唯一的”价值观注入点”,裸收益率奖励教出来的 agent 是个赌徒——它对波动无感,会学出在高波动时刻满仓的策略。本文加了换手成本项;生产级实现还应考虑风险惩罚(如 reward 减 λ·|仓位|·波动率)。你在 reward 里没写的东西,agent 一定不在乎。

陷阱二:训练曲线涨 = 真学会了。 训练奖励爬升只说明 agent 适应了训练段。金融数据的残酷之处在于 regime 会变:在趋势市里训练的 agent 学会追涨,切到均值回复市就是绞肉机。本文测试段与训练段同分布(同一 OU 参数、不同种子),这是教学上限设定;真实市场里没人保证分布不漂移,必须做滚动样本外和跨 regime 测试。

陷阱三:RL 免疫过拟合。 恰恰相反——RL 的过拟合更隐蔽。监督学习过拟合看训练/验证 loss 分叉就能发现;RL 的 agent 可能记住了训练段的具体路径模式(“第 1200 天附近有个大反弹”),而你只看到奖励在涨。本文用线性策略(5 个参数)天然限制了记忆容量——这是刻意的。**换成神经网络策略前,先问自己有没有对应的正则化和验证机制。**参数越多的策略网络,越需要把”多种子重训 + 跨段测试”当成标配。

陷阱四:σ 收敛到很小 = 策略成熟。 σ 塌缩到接近零意味着 agent 停止探索,此后它对没见过的状态只会外推。如果 σ 在训练早期就塌缩(学习率过大或奖励尺度不对),agent 会锁死在第一个凑合的局部解上。健康的训练里 σ 应该缓慢下降且保有余量——本文从 0.61 降到 0.58 的缓坡就是这种形态。σ 的轨迹和 clip 触发比例一样,是必须监控的生命体征。

收尾#

PPO 把交易决策还原成它本来的样子:一个带成本、带状态、带风险偏好的连续控制问题。本文的极简实现展示了完整闭环——clip 目标守住更新步长、GAE 估计动作优势、可学习 σ 自动调节探索,200 轮训练让 agent 从零知识学出均值回复市场的平滑仓位函数;样本外 Sharpe 1.57 的同时把回撤和换手都压到硬规则的五分之一。

但请记住实验的边界:合成市场、同分布测试、5 参数线性策略——每放宽一项,陷阱三和陷阱二的威胁就放大一分。RL 交易的工程难度不在算法(PPO 的代码就这几十行),在于奖励设计、分布漂移防御和过拟合验证这三件算法之外的事。

(本文为合成数据教学演示,不构成投资建议;实盘应用还需处理滑点、容量、执行延迟与 regime 漂移。)

PPO 强化学习交易:用近端策略优化学会在连续动作空间下单
https://blog.halo26812.eu.org/blog/ppo-trading-agent
Author halo
Published at 2026年7月25日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨