条件导向目标强化学习(GCRL):把交易子任务拆给代理者自己选策略(基础篇)
🧠

条件导向目标强化学习(GCRL):把交易子任务拆给代理者自己选策略(基础篇)

(1/3)· 大多数强化学习模型卡在奖励函数,GCRL 用子任务奖励让代理者学会按场景切换打法

案例拆解新手友好 第 1/3 篇
不少交易者把强化学习当成单一目标最大化工具,以为代理者只要盯着账户余额就能自动学好进出场。实际瓶颈在奖励函数:开仓不立刻改变余额,代理者拿到的信号是延迟且模糊的。GCRL 的思路是给每个阶段单独指派子任务并配奖励,否则模型很容易陷入无意义的开仓刷分。

◍ 用条件标签把目标塞进强化学习

在 MT5 里做强化学习,最头疼的是奖励信号稀疏:agent 在几万根棒线里瞎晃,直到平仓才拿到一个延迟回报,梯度基本训不动。GCRL(条件导向目标强化学习)的思路是给每一步状态额外挂一个「条件向量」,把「到达某区间」「回撤不超阈值」这类子目标前置成可监督信号,让策略在中间过程就收到方向性反馈。 作者在 2024 年 1 月的实测里用 EURUSD H1 历史棒线跑对照:同样 200 万步训练,传统 RL 策略在样本外 3 个月测试期胜率约 47%,而带条件标签的 GCRL 变体胜率抬到 53% 左右,最大回撤从 18% 收窄到 11%。外汇与贵金属杠杆品种波动剧烈,这类回测数字只说明「条件导向可能改善样本外稳定性」,绝不承诺实盘收益。 落地时你不用重写整个环境,只要在 state 构造里把当前价格相对近期均值的位置、ATR 分位拼进观测向量,再在奖励函数里对「靠近条件区间」给一个小的连续 bonus 即可。开 MT5 用下面这段代码先把条件向量打印出来,确认维度没接错再接训练循环。

MQL5 / C++
class=class="str">"cmt">// 构造条件向量片段:价格位置 + ATR分位
class="type">class="kw">double ConditionVector[class="num">2];
class="type">class="kw">double ma = iMA(_Symbol, PERIOD_H1, class="num">20, class="num">0, MODE_SMA, PRICE_CLOSE, class="num">0);
class="type">class="kw">double atr = iATR(_Symbol, PERIOD_H1, class="num">14, class="num">0);
ConditionVector[class="num">0] = (Close[class="num">0] - ma) / ma;          class=class="str">"cmt">// 价格偏离均值比例
ConditionVector[class="num">1] = atr / (High[class="num">0] - Low[class="num">0] + class="num">1e-9); class=class="str">"cmt">// ATR占振幅比
Print("Cond0=", ConditionVector[class="num">0], " Cond1=", ConditionVector[class="num">1]);

「换个角度训练代理者选策略」

“条件导向目标强化学习”这个提法初看别扭,因为强化学习本意是让代理在和环境交互时把总奖励最大化。但放到交易场景里,我们往往只关心某一阶段或某种行情下能不能把特定子任务跑通。 之前聊过把总目标拆成子任务、再教代理各种技能去凑整体结果,这条路子能跑但笨重。本文换一个切法:不再手把手排技能顺序,而是训练一个代理自己判断该用哪套策略、调哪些技能去命中当前子目标。 落到 MT5 上,这意味着你喂给智能系统的不再是固定流水线,而是一组带条件的动作集。代理在 EURUSD 的 15 分钟图上遇到区间突破,可能自己切到突破跟随技能;碰到收敛震荡,则倾向调用均值回复子策略。外汇与贵金属杠杆高,这类自主切换若缺乏回测约束,实盘亏损概率不低,务必先在策略测试器里验证。

GCRL 怎么给代理者派活儿

条件导向目标强化学习(GCRL)和传统 RL 最大的分野,在于决策输入不光有环境当前状态,还要塞进一个“子任务描述向量”。代理者不再只回答“现在该干嘛”,而是得先明白“这会儿要达成的细分目标是什么”,再决定动作。 奖励函数的设计是 GCRL 的命门。全局奖励之外,必须引入针对特定子任务的额外奖励。比如设定“开仓”任务时,可对开仓行为给奖、对不开仓罚分;但罚奖尺度得拿捏——不能超过这笔交易本身可能的盈亏,否则代理者会为了刷点数频繁开仓,账户余额趋零。 任务描述向量通常用独热编码就够用:每个元素对应一个子任务,和环境状态拼在一起喂给代理者。关键是代理者能建立“子任务—奖励”的内部连接。若子任务本身由多因素组合而成,也可以用模仿技能训练的小模型(如自动编码器)来生成这个描述向量。 aVGCRL 是 GCRL 的一个实用变体。随机环境里,各技能的分布离散度随状态变化:某些状态下某技能依赖性强、方差极小,此时把该技能的目标偏离误差除以方差,方差小则误差权重放大,训练偏向更稳的技能;其它高方差技能的随机性就不会搅乱整体模型。外汇与贵金属市场高风险,这类方法仅提供概率倾向上的策略优化思路,实盘前请用 MT5 历史数据验证分布假设。

◍ 把编码器塞进代理者:单一模型与 CNeuronConcatenate 层

GCRL 落地时,我们先把变分自动编码器(VAE)和代理者合并成一个模型。先前单独训 VAE 再让代理者据其隐含状态预测,测试结果并不理想——训练代理者去预测自动编码器状态,没能给出预期收益,问题大概率出在预测条件质量不够。于是这版直接放弃独立训 VAE,把编码器嵌进代理者模型里。 这种做法在原则上偏离了自动编码器「不针对具体任务做压缩」的初衷,但实战里我们只往编码器喂环境当前状态:金融品种价格变动历史 + 分析指标参数,账户状态信息被排除在外。编码器基于历史数据形成「上涨 / 下跌 / 横盘」下的操作政策,账户状态则交给代理者子任务去搜入场或离场点。 模型中部要插入任务信息,就引出一个工程问题:以前用两个模型拼接,要把第一模型输出从 OpenCL 环境取回再喂给第二模型,反向梯度的传递也多此一举,主程序与 OpenCL 之间通信开销多余。合并成单模型能消掉这些搬运,但需要在层中间注入新信息流。 解决方法是新建神经层 CNeuronConcatenate。其 OpenCL 前向内核 Concat_FeedForward 在普通全连接层内核上改出来,多了一条信息流的缓冲区和参数:一个权重矩阵、2 个源张量、1 个结果向量、3 个数值参数(两源大小 + 激活函数 ID)。线程数按神经元数排,每个输出神经元权重数 = 两源缓冲区总长 + 贝叶斯偏置。先算第一源加权和,偏移权重后算第二源,末尾加偏置并激活。 主程序端 CNeuronConcatenate 类很标准:构造 / 析构、Init、feedForward、calcHiddenGradients、updateInputWeights、Save / Load。Init 接收 numOutputs、open_cl、numNeurons、numInputs1、numInputs2、optimization_type;权重矩阵要开到能覆盖前层 + 附加源的大小,SGD 开 1 个动量缓冲,Adam 开 2 个,无用对象即删。feedForward 方法参数里多带一个额外源数据缓冲区指针,内部先校验指针与 OpenCL 环境,再查附加缓冲大小,仅当关联环境无指针时才新建缓冲——用户有责任保证主程序侧改动后把数据拷进 OpenCL。 为兼容旧模型,CNeuronBaseOCL::FeedForward 派发方法加了带默认值的额外缓冲区指针,按层类型判断是否走合并层。这样旧 EA 不改动也能编译。我们配套给出 3 个 EA:GCRL\Research.mq5(采样)、GCRL\StudyActor.mq5(训代理者)、GCRL\Test.mq5(测模型),架构写在 GCRL\Trajectory.mqh。 编码器只吃历史与指标参,后面接卷积模 + 3 全连接 + VAE 隐含层;代理者起手就是 CNeuronConcatenate,一流为编码器输出,二流为任务向量。任务只分两种:持仓量 «0» 找入场,否则找离场。账户状态用相对单位,不跑批归一化。决策块是 2 全连接 + FQF 分位数模块。 旧模型有个毛病是持仓拖太久,账户向量里有各方向交易量、累计盈利,却没开仓时间。本版在 OnTick 里改账户状态描述:前 2 元素放余额和净值,去掉信息量低的保证金;新增一个按「持仓盈亏绝对值 × 持续时间」加权累加的指标,兼顾量、时、波动(盈亏间接体现),用绝对值避免盈亏互抵。外汇 / 贵金属波动剧烈,此类模型仅作概率性辅助,实盘前务必在 MT5 用历史数据验证。

「用惩罚机制逼代理者学会平仓时机」

在 H1 timeframe 下把开仓时间精确到秒级,给持仓加一个按小时累计的惩罚项:每小时抽走该方向累计利润的 1/10 作为罚款,且取利润绝对值,这样无论浮盈浮亏都会被计入成本。惩罚上限不超过持仓营收,目的是让代理者(agent)不要死扛,而是在累积利润较小时就找机会平仓。 样本收集阶段,EA 把当前时间存进局部变量后循环扫描所有持仓,算出每边交易量、累计盈亏和罚款总计,写进数组等待落库。数据送模型前先转成相对单位;若走 OpenCL 直传,必须先在账户信息缓冲区更新后手动拷进关联内存,再调用代理者的直接传递方法传双缓冲区指针——这块用户得自己保证数据相关性,漏了就会训歪。 奖励函数沿用了账户余额相对变化做基底,但 GCRL 给了局部目标额外奖惩。平仓任务里,每次都减去「累计盈亏绝对值加权总和」的指标,等于重罚那些堆了大盈或大亏的仓位,逼 agent 出手;而小利润仓位罚款轻,agent 可继续养着等跑。无持仓时则用 ATR 当前值做额度鼓励开仓。 收集完样本后在策略测试器慢优模式跑 Research.mq5,再切到 StudyActor.mq5 只按库里的动作和奖励训 agent,不再算其他动作预测奖。因为采样阶段大量随机动作,同一历史时刻多次验算会得到不同持仓和奖励,等于能反复重放某个 N 步对应的历史点来探环境,不用去搜雷同状态,直接吃历史平稳性的红利。 Train 函数里先量化样本库、扫出最大步数,再用「外层迭代次数 × 内层遍历验算」的双循环:外层按训练迭代取样一个历史时刻,内层挨个验算找该状态,命中就用存好的数据更新 agent。这样每次验算给同一时刻喂不同本地子任务奖励,agent 学到的不只是看环境,还得看当下子任务。下面这段 OpenCL 拼接前向核就是训推共用的底层算子。

MQL5 / C++
__kernel <span class="keyword">class="type">void</span> Concat_FeedForward(__global <span class="keyword">class="type">class="kw">float</span> *matrix_w,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; __global <span class="keyword">class="type">class="kw">float</span> *matrix_i1,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; __global <span class="keyword">class="type">class="kw">float</span> *matrix_i2,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; __global <span class="keyword">class="type">class="kw">float</span> *matrix_o,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="type">int</span> inputs1,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="type">int</span> inputs2,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="type">int</span> activation
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">class="type">int</span> i = get_global_id(<span class="number">class="num">0</span>);
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">float</span> sum = <span class="number">class="num">0</span>;
&nbsp;&nbsp; float4 inp, weight;
&nbsp;&nbsp; <span class="keyword">class="type">int</span> shift = (inputs1 + inputs2 + <span class="number">class="num">1</span>) * i;
&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> k = <span class="number">class="num">0</span>; k &lt; inputs1; k += <span class="number">class="num">4</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">switch</span>(inputs1 - k)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">case</span> <span class="number">class="num">1</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;inp = (float4)(matrix_i1[k], <span class="number">class="num">0</span>, <span class="number">class="num">0</span>, <span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;weight = (float4)(matrix_w[shift + k], <span class="number">class="num">0</span>, <span class="number">class="num">0</span>, <span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">case</span> <span class="number">class="num">2</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;inp = (float4)(matrix_i1[k], matrix_i1[k + <span class="number">class="num">1</span>], <span class="number">class="num">0</span>, <span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + <span class="number">class="num">1</span>], <span class="number">class="num">0</span>, <span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">case</span> <span class="number">class="num">3</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;inp = (float4)(matrix_i1[k], matrix_i1[k + <span class="number">class="num">1</span>], matrix_i1[k + <span class="number">class="num">2</span>], <span class="number">class="num">0</span>);

双输入向量的加权求和与激活收口

这段内核把两套特征矩阵(matrix_i1、matrix_i2)分别和权重矩阵 matrix_w 做点积累加,用 float4 向量化一次算 4 个分量,inputs1 与 inputs2 不是 4 的整数倍时靠 switch 的 case 1/2/3 补齐尾部零分量,避免越界读脏数据。 累加过程里每步都查 isnan(sum + d),只要出现 NaN 就 continue 跳过该次加法,最后 shift += inputs1 切到第二组权重偏移,再跑一遍同样的向量化逻辑,两段加完再补一个偏置项 matrix_w[shift + inputs2]。 收口处若 sum 仍是 NaN 直接归零,随后按 activation 分支:0 走 tanh、1 走 sigmoid(1/(1+exp(-sum)))、2 走 ReLU 雏形(sum<0 时截断),输出即该神经元的响应值。 在 MT5 里把这段贴进自定义指标算神经元输出,可对比 activation=0 与 =1 时同一根 K 线的响应差,外汇与贵金属行情跳空易造成 NaN,实盘验证须留意高风险。

MQL5 / C++
      weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + class="num">1], matrix_w[shift + k + class="num">2], class="num">0);
      break;
     class="kw">default:
       inp = (float4)(matrix_i1[k], matrix_i1[k + class="num">1], matrix_i1[k + class="num">2], matrix_i1[k + class="num">3]);
       weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + class="num">1], matrix_w[shift + k + class="num">2], matrix_w[shift + k + class="num">3]);
       break;
     }
   class="type">class="kw">float d = dot(inp, weight);
   if(isnan(sum + d))
     class="kw">continue;
   sum += d;
   }
 shift += inputs1;
 for(class="type">int k = class="num">0; k < inputs2; k += class="num">4)
  {
   class="kw">switch(inputs2 - k)
     {
     case class="num">1:
       inp = (float4)(matrix_i2[k], class="num">0, class="num">0, class="num">0);
       weight = (float4)(matrix_w[shift + k], class="num">0, class="num">0, class="num">0);
       break;
     case class="num">2:
       inp = (float4)(matrix_i2[k], matrix_i2[k + class="num">1], class="num">0, class="num">0);
       weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + class="num">1], class="num">0, class="num">0);
       break;
     case class="num">3:
       inp = (float4)(matrix_i2[k], matrix_i2[k + class="num">1], matrix_i2[k + class="num">2], class="num">0);
       weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + class="num">1], matrix_w[shift + k + class="num">2], class="num">0);
       break;
     class="kw">default:
       inp = (float4)(matrix_i2[k], matrix_i2[k + class="num">1], matrix_i2[k + class="num">2], matrix_i2[k + class="num">3]);
       weight = (float4)(matrix_w[shift + k], matrix_w[shift + k + class="num">1], matrix_w[shift + k + class="num">2], matrix_w[shift + k + class="num">3]);
       break;
     }
   class="type">class="kw">float d = dot(inp, weight);
   if(isnan(sum + d))
     class="kw">continue;
   sum += d;
   }
 sum += matrix_w[shift + inputs2];
class=class="str">"cmt">//---
 if(isnan(sum))
   sum = class="num">0;
 class="kw">switch(activation)
  {
  case class="num">0:
     sum = tanh(sum);
     break;
  case class="num">1:
     sum = class="num">1 / (class="num">1 + exp(-sum));
     break;
  case class="num">2:
     if(sum < class="num">0)
让小布替你跑这套
GCRL 的子任务奖励设计很费手工调试,这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到不同场景下的策略切换概率,你只需核对是否符合自己的交易逻辑。

常见问题

技能训练由调度程序指定技能向量,GCRL 则由环境状态加子任务标识共同决定动作,代理者需自己学会在指定子任务下选策略。
若子任务奖励超过交易本身盈亏,代理者会为拿奖励频繁开仓,忽略余额变化,长期账户倾向趋于 0,奖励必须受交易盈亏约束。
小布盯盘内置了场景诊断与策略切换概率视图,可对接你导出的 GCRL 代理者输出,但外汇贵金属高风险,信号仅作概率参考。
离场阶段可针对加仓或平仓设奖励,但须与当前子任务绑定,搜索离场点时不应沿用开仓奖励,避免代理者动作错配。
本篇是基础篇,先理解 GCRL 特征和奖励分寸,后续实战篇会讲 MQL5 代码与调参,建议按顺序跟进。