神经网络变得简单(第 74 部分):自适应轨迹预测·综合运用
📘

神经网络变得简单(第 74 部分):自适应轨迹预测·综合运用

第 3/3 篇

◍ 权重回传与轨迹网络骨架搭建

这段逻辑先把上游模块的输出指针喂给各层权重更新接口:多头注意力输出 W0、注意力输出 AttentionOut 以及前馈层 FF[0]→FF[1] 的级联,任何一步返回 false 就直接中断,保证拓扑连接失败时不会带病训练。 CreateTrajNetDescriptions 负责把编码器、端点、概率三个对象数组初始化,若传入空指针则现场 new 出来,内存申请失败同样返回 false。随后 encoder 清空并逐层压入 CLayerDescription:输入层用 defNeuronBaseOCL,节点数 = HistoryBars * BarDescr,优化器选 ADAM;第二层 BatchNorm 的 batch 尺寸取 MathMax(1000, GPTBars),这是一个硬下限,样本少于 1000 也按 1000 跑。 Embedding 层把窗口写进 descr.windows,输出维度 window_out = EmbeddingSize,节点数变为 GPTBars;再叠一层 defNeuronPEOCL 做位置编码。最后用 for(l=0; l<Lenc; l++) 准备循环堆叠后续编码器层。 开 MT5 把 Lenc、GPTBars、EmbeddingSize 这几个宏打出来看一眼,若 GPTBars 小于 1000,BatchNorm 的实际 batch 会和你直觉不符,倾向引发训练初期梯度抖动。外汇与贵金属行情下用这类网络做轨迹预测属高风险,回测结论仅代表历史样本概率。

MQL5 / C++
  if(!W0.UpdateInputWeights(GetPointer(MHAttentionOut)))
      class="kw">return false;
  if(!FF[class="num">0].UpdateInputWeights(GetPointer(AttentionOut)))
      class="kw">return false;
  if(!FF[class="num">1].UpdateInputWeights(GetPointer(FF[class="num">0])))
      class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CreateTrajNetDescriptions(CArrayObj *encoder, CArrayObj *endpoints, CArrayObj *probability)
  {
class=class="str">"cmt">//---
  CLayerDescription *descr;
class=class="str">"cmt">//---
  if(!encoder)
    {
      encoder = new CArrayObj();
      if(!encoder)
        class="kw">return false;
    }
  if(!endpoints)
    {
      endpoints = new CArrayObj();
      if(!endpoints)
        class="kw">return false;
    }
  if(!probability)
    {
      probability = new CArrayObj();
      if(!probability)
        class="kw">return false;
    }
class=class="str">"cmt">//--- Encoder
  encoder.Clear();
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronBaseOCL;
  class="type">int prev_count = descr.count = (HistoryBars * BarDescr);
  descr.activation = None;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">1
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronBatchNormOCL;
  descr.count = prev_count;
  descr.batch = MathMax(class="num">1000, GPTBars);
  descr.activation = None;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">2
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronEmbeddingOCL;
    {
      class="type">int temp[] = {prev_count};
      ArrayCopy(descr.windows, temp);
    }
  prev_count = descr.count = GPTBars;
  class="type">int prev_wout = descr.window_out = EmbeddingSize;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">3
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronPEOCL;
  descr.count = prev_count;
  descr.window = prev_wout;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
  for(class="type">int l = class="num">0; l < Lenc; l++)
    {
      class=class="str">"cmt">//--- layer class="num">4

「编码器后段的多头注意力堆叠」

这段逻辑在编码器后半段连续挂了 6 层描述对象,从转置层一路铺到多层多头注意力,靠 prev_count 与 prev_wout 两个上游变量在层间倒腾维度。 第 5 层用 defNeuronMH2AttentionOCL,把 window 设成 prev_count、count 设成 prev_wout,并显式写了 step=8、window_out=16、optimization=ADAM;第 6 层换成 defNeuronMLMHAttentionOCL,同样 step=8、window_out=16,但多了一个 layers=1 的字段。 第 8、9 层把 count 与 window 的取值对调回 prev_count / prev_wout,相当于把前面转置过的张量再喂给注意力层做一次特征交叉。 每次 new CLayerDescription() 失败或 encoder.Add 返回假,都直接 delete descr 并 return false,说明这套结构对内存泄漏零容忍,你在 MT5 里改层参数时若忘了配对释放,编译虽过但跑起来容易崩。

MQL5 / C++
if(!(descr = new CLayerDescription()))
   class="kw">return false;
descr.type = defNeuronTransposeOCL;
descr.count = prev_count;
descr.window = prev_wout;
if(!encoder.Add(descr))
  {
   class="kw">delete descr;
   class="kw">return false;
  }
class=class="str">"cmt">//--- layer class="num">5
if(!(descr = new CLayerDescription()))
   class="kw">return false;
descr.type = defNeuronMH2AttentionOCL;
descr.count = prev_wout;
descr.window = prev_count;
descr.step = class="num">8;
descr.window_out = class="num">16;
descr.optimization = ADAM;
if(!encoder.Add(descr))
  {
   class="kw">delete descr;
   class="kw">return false;
  }
class=class="str">"cmt">//--- layer class="num">6
if(!(descr = new CLayerDescription()))
   class="kw">return false;
descr.type = defNeuronMLMHAttentionOCL;
descr.count = prev_wout;
descr.window = prev_count;
descr.step = class="num">8;
descr.window_out = class="num">16;
descr.layers = class="num">1;
descr.optimization = ADAM;
if(!encoder.Add(descr))
  {
   class="kw">delete descr;
   class="kw">return false;
  }
class=class="str">"cmt">//--- layer class="num">7
if(!(descr = new CLayerDescription()))
   class="kw">return false;
descr.type = defNeuronTransposeOCL;
descr.count = prev_wout;
descr.window = prev_count;
if(!encoder.Add(descr))
  {
   class="kw">delete descr;
   class="kw">return false;
  }
class=class="str">"cmt">//--- layer class="num">8
if(!(descr = new CLayerDescription()))
   class="kw">return false;
descr.type = defNeuronMH2AttentionOCL;
descr.count = prev_count;
descr.window = prev_wout;
descr.step = class="num">8;
descr.window_out = class="num">16;
descr.layers = class="num">1;
descr.optimization = ADAM;
if(!encoder.Add(descr))
  {
   class="kw">delete descr;
   class="kw">return false;
  }
class=class="str">"cmt">//--- layer class="num">9
if(!(descr = new CLayerDescription()))
   class="kw">return false;
descr.type = defNeuronMLMHAttentionOCL;
descr.count = prev_count;
descr.window = prev_wout;
descr.step = class="num">8;

编码器与概率分支的层定义拆解

这段配置把 ADAPT 编码器和后续概率分支的层结构一次性铺开,核心常量先钉死:Lenc=3 表示编码器块数量,NForecast=5 表示预测步数,概率层最终输出维度是 3*NForecast=15。 编码器部分先设 window_out=16、layers=1、优化器 ADAM,Add 失败就删描述器并返回 false,避免悬空对象。端点层从输入层开始,prev_count 乘 prev_wout 决定输入神经元数,激活用 None;layer1 压到 LatentCount 且走 SIGMOID,layer2 用 SoftMax 且 step=1,layer3 直接拉到 15 个线性输出。 概率分支复用端点第 0 层作输入,再拼一个 defNeuronConcatenate:window=prev_count、step=15、激活 SIGMOID;layer2 用 LReLU 激活的 BaseOCL 层收口。外汇与贵金属行情下用这套结构做概率预测属高风险,过拟合可能导致样本外表现显著衰减。 让小布替你跑这套:把 Lenc 改 4、NForecast 改 8,看 MT5 内核日志里层维度是否按 3*8=24 重算,能快速验证 concat 的 step 参数是否真绑定 NForecast。

MQL5 / C++
descr.window_out = class="num">16;
descr.layers = class="num">1;
descr.optimization = ADAM;
if(!encoder.Add(descr))
  {
   class="kw">delete descr;
   class="kw">return false;
  }
 }
class="macro">#define      Lenc                 class="num">3        class=class="str">"cmt">//Number ADAPT Encoder blocks
class="macro">#define      NForecast            class="num">5        class=class="str">"cmt">//Number of forecast
class=class="str">"cmt">//--- Endpoints
  endpoints.Clear();
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
   class="kw">return false;
  descr.type = defNeuronBaseOCL;
  prev_count = descr.count = (prev_count * prev_wout);
  descr.activation = None;
  descr.optimization = ADAM;
  if(!endpoints.Add(descr))
   {
    class="kw">delete descr;
    class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">1
  if(!(descr = new CLayerDescription()))
   class="kw">return false;
  descr.type = defNeuronBaseOCL;
  descr.count = LatentCount;
  descr.activation = SIGMOID;
  descr.optimization = ADAM;
  if(!endpoints.Add(descr))
   {
    class="kw">delete descr;
    class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">2
  if(!(descr = new CLayerDescription()))
   class="kw">return false;
  descr.type = defNeuronSoftMaxOCL;
  descr.count = LatentCount;
  descr.step = class="num">1;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!endpoints.Add(descr))
   {
    class="kw">delete descr;
    class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">3
  if(!(descr = new CLayerDescription()))
   class="kw">return false;
  descr.type = defNeuronBaseOCL;
  descr.count = class="num">3 * NForecast;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!endpoints.Add(descr))
   {
    class="kw">delete descr;
    class="kw">return false;
   }
class=class="str">"cmt">//--- Probability
  probability.Clear();
class=class="str">"cmt">//--- Input layer
  if(!probability.Add(endpoints.At(class="num">0)))
   class="kw">return false;
class=class="str">"cmt">//--- layer class="num">1
  if(!(descr = new CLayerDescription()))
   class="kw">return false;
  descr.type = defNeuronConcatenate;
  descr.count = LatentCount;
  descr.window = prev_count;
  descr.step = class="num">3 * NForecast;
  descr.optimization = ADAM;
  descr.activation = SIGMOID;
  if(!probability.Add(descr))
   {
    class="kw">delete descr;
    class="kw">return false;
   }
class=class="str">"cmt">//--- layer class="num">2
  if(!(descr = new CLayerDescription()))
   class="kw">return false;
  descr.type = defNeuronBaseOCL;
  descr.count = LatentCount;
  descr.activation = LReLU;

◍ 概率头与双编码器的层描述拼装

这段逻辑在给一个基于 OpenCL 的神经网络框架填层描述。概率输出分支连续塞了三层:第三层用 defNeuronBaseOCL 做基础线性层,节点数等于 NForecast,无激活;第四层换 defNeuronSoftMaxOCL,count 同为 NForecast 且 step=1,负责把原始得分压成类别概率分布。 CreateDescriptions 里先兜底三个对象指针,actor、end_encoder、state_encoder 任一为空就 new 出来,失败直接返 false。end_encoder 输入端 descr.count = 3 * NForecast,也就是把三类端点特征沿预测窗口堆叠;下一层用 defNeuronConcatenate,window 设为 prev_count、step 为 NForecast,激活选 LReLU,把局部窗口信息压缩进 LatentCount 个隐节点。 state_encoder 的输入维度是 GPTBars * EmbeddingSize,对应一段序列嵌入的展开长度,同样走 defNeuronBaseOCL 且无激活。外汇与贵金属行情下用这类结构做概率预测属于高风险尝试,过拟合与样本外失效的概率偏高,建议先在 MT5 策略测试器用极小样本跑通内存与维度断言再放大。 逐行看 end_encoder 输入层那段:descr.type 指定基础层、prev_count 暂存 3*NForecast 供后续 concatenate 引用 window,optimization 全统一 ADAM——若你改 NForecast 宏,务必同步检查 SoftMax 层 count,否则 Add 可能静默失败返 false。

MQL5 / C++
  descr.optimization = ADAM;
  if(!probability.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">3
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronBaseOCL;
  descr.count = NForecast;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!probability.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">4
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronSoftMaxOCL;
  descr.count = NForecast;
  descr.step = class="num">1;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!probability.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CreateDescriptions(CArrayObj *actor, CArrayObj *end_encoder, CArrayObj *state_encoder)
  {
class=class="str">"cmt">//---
  CLayerDescription *descr;
class=class="str">"cmt">//---
  if(!actor)
    {
      actor = new CArrayObj();
      if(!actor)
        class="kw">return false;
    }
  if(!end_encoder)
    {
      end_encoder = new CArrayObj();
      if(!end_encoder)
        class="kw">return false;
    }
  if(!state_encoder)
    {
      state_encoder = new CArrayObj();
      if(!state_encoder)
        class="kw">return false;
    }
class=class="str">"cmt">//--- Endpoints Encoder
  end_encoder.Clear();
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronBaseOCL;
  class="type">int prev_count = descr.count = class="num">3 * NForecast;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!end_encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">1
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronConcatenate;
  descr.count = LatentCount;
  descr.window = prev_count;
  descr.step = NForecast;
  descr.optimization = ADAM;
  descr.activation = LReLU;
  if(!end_encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- State Encoder
  state_encoder.Clear();
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronBaseOCL;
  prev_count = descr.count = GPTBars * EmbeddingSize;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!state_encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">1

「编码器与 Actor 网络的层定义落点」

在 MT5 自建的强化学习推理框架里,状态编码器先挂一层 Concatenate 型神经元,窗口取上一层输出数 prev_count,步长绑定账户描述 AccountDescr,优化器统一用 ADAM,激活走 SIGMOID。若 Add 失败立即 delete 描述对象并返回 false,避免悬空指针。 Actor 网络从 Clear 起步,输入层用 defNeuronBaseOCL、激活 None,节点数 LatentCount 同时回写 prev_count。随后 layer 1 再拼一个 Concatenate,窗口=prev_count、步长=LatentCount、激活改 LReLU,这是隐空间里做特征交叉的关键一步。 往后 layer 2 回到 BaseOCL+SIGMOID,layer 3 节点数直接拉到 2*NActions 且去激活,layer 4 用 defNeuronVAEOCL 把维度压回 NActions。注意 layer 3 的 2*NActions 是动作均值与对数方差的拼接位,改 NActions 数值会直接改变这一层显存占用。 OnInit 里先 ResetLastError,LoadTotalBase 失败就 PrintFormat 报错码并返回 INIT_FAILED。三个 .nnw 模型文件(Enc / Endp / Prob)带 dtStudied 与 true 标志载入,任一失败就 new CArrayObj 准备兜底重建编码器——外汇与贵金属品种上跑这套网络,参数误配可能导致回测与实盘信号偏移,属高风险操作,建议先在策略测试器用历史数据验证层结构。

MQL5 / C++
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = prev_count;
   descr.step = AccountDescr;
   descr.optimization = ADAM;
   descr.activation = SIGMOID;
   if(!state_encoder.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- Actor
   actor.Clear();
class=class="str">"cmt">//--- Input layer
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = LatentCount;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">1
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = prev_count;
   descr.step = LatentCount;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">2
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">3
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   descr.count = class="num">2 * NActions;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">4
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronVAEOCL;
   descr.count = NActions;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   ResetLastError();
   if(!LoadTotalBase())
     {
      PrintFormat("Error of load study data: %d", GetLastError());
      class="kw">return INIT_FAILED;
     }
class=class="str">"cmt">//--- load models
   class="type">class="kw">float temp;
   if(!ADAPTEncoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true) ||
      !ADAPTEndpoints.Load(FileName + "Endp.nnw", temp, temp, temp, dtStudied, true) ||
      !ADAPTProbability.Load(FileName + "Prob.nnw", temp, temp, temp, dtStudied, true)
     )
     {
      CArrayObj *encoder = new CArrayObj();

初始化阶段神经网络装载与维度校验

EA 初始化时先尝试从磁盘载入三套网络权重:状态编码器(StEnc.nnw)、端点编码器(EndEnc.nnw)与执行网络(Act.nnw)。只要其中任意一份文件缺失或读取失败,代码会转而调用 CreateDescriptions 与对应 Create 方法现场搭默认结构,再释放临时 CArrayObj 容器,避免内存泄漏。 网络对象建好后,统一通过 GetOpenCL 把计算上下文派发给所有编码器与 ADAPT 系列网络。这一步决定了后续推理能否走 GPU 加速,在 MT5 的「专家属性—OpenCL」里没勾选设备时,这里拿到的句柄可能为空,CPU 回退会明显拖慢每根 K 线的推理耗时。 装载完成必须做维度对账。Actor 的输出节点数要等于 NActions,否则直接 INIT_FAILED 并打印『scope of the actor does not match the actions count』;ADAPTEndpoints 的输出总数必须是 3 * NForecast,因为每根预测端点含高/低/收三类值。若你改了 NForecast 参数却忘了重训网络,这里会卡死初始化而非 silently 跑偏。 ADAPTEncoder 首层输出维度还要再核一次:Result.Total() 应等于 HistoryBars * BarDescr。这套三重校验是排查『EA 加载即报错』的首选断点,直接在 MT5 终端日志看 PrintFormat 输出的实际数与期望值即可定位是哪层网络维度不对。

MQL5 / C++
CArrayObj *endpoint = new CArrayObj();
CArrayObj *prob = new CArrayObj();
if(!CreateTrajNetDescriptions(encoder, endpoint, prob))
  {
   class="kw">delete endpoint;
   class="kw">delete prob;
   class="kw">delete encoder;
   class="kw">return INIT_FAILED;
  }
if(!ADAPTEncoder.Create(encoder) ||
   !ADAPTEndpoints.Create(endpoint) ||
   !ADAPTProbability.Create(prob))
  {
   class="kw">delete endpoint;
   class="kw">delete prob;
   class="kw">delete encoder;
   class="kw">return INIT_FAILED;
  }
class="kw">delete endpoint;
class="kw">delete prob;
class="kw">delete encoder;
 }
 if(!StateEncoder.Load(FileName + "StEnc.nnw", temp, temp, temp, dtStudied, true) ||
   !EndpointEncoder.Load(FileName + "EndEnc.nnw", temp, temp, temp, dtStudied, true) ||
   !Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true))
  {
   CArrayObj *actor = new CArrayObj();
   CArrayObj *endpoint = new CArrayObj();
   CArrayObj *encoder = new CArrayObj();
   if(!CreateDescriptions(actor, endpoint, encoder))
     {
      class="kw">delete actor;
      class="kw">delete endpoint;
      class="kw">delete encoder;
      class="kw">return INIT_FAILED;
     }
   if(!Actor.Create(actor) ||
!StateEncoder.Create(encoder) ||
!EndpointEncoder.Create(endpoint))
     {
      class="kw">delete actor;
      class="kw">delete endpoint;
      class="kw">delete encoder;
      class="kw">return INIT_FAILED;
     }
   class="kw">delete actor;
   class="kw">delete endpoint;
   class="kw">delete encoder;
   class=class="str">"cmt">//---
   }
 OpenCL = Actor.GetOpenCL();
 StateEncoder.SetOpenCL(OpenCL);
 EndpointEncoder.SetOpenCL(OpenCL);
 ADAPTEncoder.SetOpenCL(OpenCL);
 ADAPTEndpoints.SetOpenCL(OpenCL);
 ADAPTProbability.SetOpenCL(OpenCL);
 Actor.getResults(Result);
 if(Result.Total() != NActions)
   {
    PrintFormat("The scope of the actor does not match the actions count(%d <> %d)",
NActions, Result.Total());
    class="kw">return INIT_FAILED;
   }
class=class="str">"cmt">//---
 ADAPTEndpoints.getResults(Result);
 if(Result.Total() != class="num">3 * NForecast)
   {
    PrintFormat("The scope of the Endpoints does not match forecast endpoints(%d <> %d)",
                                                            class="num">3 * NForecast, Result.Total());
    class="kw">return INIT_FAILED;
   }
class=class="str">"cmt">//---
 ADAPTEncoder.GetLayerOutput(class="num">0, Result);
 if(Result.Total() != (HistoryBars * BarDescr))
  {

◍ 训练循环里的轨迹采样与状态喂入

这段 MT5 自定义指标/EA 的初始化尾部与 Train 函数开头,暴露了基于概率轨迹的强化式训练骨架。初始化阶段若编码器输入尺寸与历史状态描述不符(Result.Total() 对比 HistoryBars*BarDescr),直接返回 INIT_FAILED,梯度缓冲创建或图表事件注册失败也同样终止,说明任何资源配置错位都会让后续训练根本跑不起来。 Train 函数先用 GetProbTrajectories(Buffer, 0.9) 拿到轨迹概率分布,阈值 0.9 意味着只取高置信轨迹参与迭代。主循环受 Iterations 上限、IsStopped() 与内部 Stop 三重控制,单次迭代先用 SampleTrajectory 抽一条轨迹,再算 batch = GPTBars + 48,可见每个训练窗口在 GPTBars 基础上固定外扩 48 根棒。 状态起点 state 用 MathRand() 平方归一化后乘可用区间,刻意压低小概率区被抽中的权重;若 state<=0 就 iter-- 并跳过,避免越界。之后清空 ADAPTEncoder,从 state 到 end(end 受 Buffer[tr].Total - PrecoderBars 限制)逐根把状态数组塞进编码器前向传播,任一 feedForward 失败立即置 Stop 跳出。 开 MT5 把这段接进自己的神经网络指标,先核对 HistoryBars*BarDescr 与实际编码器维度,否则初始化必挂;想加快收敛可把 0.9 调低到 0.7 观察样本多样性变化,外汇与贵金属品种波动结构差异大,这类训练对过拟合敏感,实盘前务必用历史数据回测验证。

MQL5 / C++
PrintFormat("Input size of Encoder doesn&class="macro">#x27;t match state description(%d <> %d)",
Result.Total(), (HistoryBars * BarDescr));
   class="kw">return INIT_FAILED;
   }
   if(!bGradient.BufferInit(MathMax(AccountDescr, NForecast), class="num">0) ||
      !bGradient.BufferCreate(OpenCL))
   {
      PrintFormat("Error of create buffers: %d", GetLastError());
      class="kw">return INIT_FAILED;
   }
   if(!EventChartCustom(ChartID(), class="num">1, class="num">0, class="num">0, "Init"))
   {
      PrintFormat("Error of create study event: %d", GetLastError());
      class="kw">return INIT_FAILED;
   }
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
   }
class="type">void Train(class="type">void)
  {
class=class="str">"cmt">//---
   vector<class="type">class="kw">float> probability = GetProbTrajectories(Buffer, class="num">0.9);
   vector<class="type">class="kw">float> result, target;
   matrix<class="type">class="kw">float> targets, temp_m;
   class="type">bool Stop = false;
class=class="str">"cmt">//---
   class="type">uint ticks = GetTickCount();
   for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !Stop); iter ++)
     {
      class="type">int tr = SampleTrajectory(probability);
      class="type">int batch = GPTBars + class="num">48;
      class="type">int state = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) *
(Buffer[tr].Total - class="num">2 - PrecoderBars - batch));
      if(state <= class="num">0)
        {
         iter--;
         class="kw">continue;
        }
      ADAPTEncoder.Clear();
      class="type">int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars);
      for(class="type">int i = state; i < end; i++)
        {
         bState.AssignArray(Buffer[tr].States[i].state);
         class=class="str">"cmt">//--- Trajectory
         if(!ADAPTEncoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, false,
(CBufferFloat*)NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }
         if(!ADAPTEndpoints.feedForward((CNet*)GetPointer(ADAPTEncoder), -class="num">1,
(CBufferFloat*)NULL))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }

「从网络输出里抠出极值段」

这段逻辑干的事,是把 ADAPT 网络前向跑完后的状态矩阵,压成未来 NForecast 根 K 线里的方向极值。先喂编码器与端点网络,一旦 feedForward 返回失败就打印函数名加行号、置 Stop 并 break,训练循环直接中断。 targets 初始化为 PrecoderBars×3 的零矩阵,逐行把 Buffer 里滞后 i+1 起的状态搬进来;若单行维度超过 BarDescr,就先转成临时矩阵、按 BarDescr 重塑再截到 3 列,只留末行。这样能把过长状态描述降维成三列候选。 对第 0 列做 CumSum 得到累计走势,再据此填第 1、2 列为加减对应列后的边界。若首值等于 0 则用第 1 行替补,按首值正负定 direct,顺向扫到符号翻转为止,extr 记下连续同向根数,targets 随之 Resize(extr,3)。 direct>=0 时取横向最大填 target 且第 2 列取最小;否则取横向最小且第 1 列取最大。最后从端点网络取 result,重塑为 1×result.Size() 再转 NForecast×3,完成预测边界抽取。外汇与贵金属波动剧烈,这类极值仅作概率参考,实盘须自担高风险。

MQL5 / C++
if(!ADAPTProbability.feedForward((CNet*)GetPointer(ADAPTEncoder), -class="num">1,
(CNet*)GetPointer(ADAPTEndpoints)))
  {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    Stop = true;
    break;
  }
 targets = matrix<class="type">class="kw">float>::Zeros(PrecoderBars, class="num">3);
 for(class="type">int t = class="num">0; t < PrecoderBars; t++)
  {
   target.Assign(Buffer[tr].States[i + class="num">1 + t].state);
   if(target.Size() > BarDescr)
     {
      matrix<class="type">class="kw">float> temp(class="num">1, target.Size());
      temp.Row(target, class="num">0);
      temp.Reshape(target.Size() / BarDescr, BarDescr);
      temp.Resize(temp.Rows(), class="num">3);
      target = temp.Row(temp.Rows() - class="num">1);
     }
   targets.Row(target, t);
  }
 target = targets.Col(class="num">0).CumSum();
 targets.Col(target, class="num">0);
 targets.Col(target + targets.Col(class="num">1), class="num">1);
 targets.Col(target + targets.Col(class="num">2), class="num">2);
 class="type">int extr = class="num">1;
 if(target[class="num">0] == class="num">0)
   target[class="num">0] = target[class="num">1];
 class="type">int direct = (target[class="num">0] > class="num">0 ? class="num">1 : -class="num">1);
 for(class="type">int i = class="num">1; i < PrecoderBars; i++)
  {
   if((target[i]*direct) < class="num">0)
     break;
   extr++;
  }
 targets.Resize(extr, class="num">3);
 if(direct >= class="num">0)
   {
    target = targets.Max(AXIS_HORZ);
    target[class="num">2] = targets.Col(class="num">2).Min();
   }
 else
   {
    target = targets.Min(AXIS_HORZ);
    target[class="num">1] = targets.Col(class="num">1).Max();
   }
 ADAPTEndpoints.getResults(result);
 targets.Reshape(class="num">1, result.Size());
 targets.Row(result, class="num">0);
 targets.Reshape(NForecast, class="num">3);

反向传播里的账户状态归一化

这段逻辑处在自适应模型训练的反向传播阶段,先对目标矩阵做平方差并定位最小误差行,再把结果回灌给端点、编码器与概率三层网络。任何一层 backProp 返回失败就立即打印函数名与行号、置 Stop 并跳出,避免脏梯度继续污染权重。 账户特征向量 bAccount 的构造值得单独看:它用上一根状态(MathMax(i-1,0) 防越界)的余额、净值做除法差分,得到 8 个归一化分量,最后一项把时间戳相对 2023 全年毫秒跨度做正弦周期编码。注意 x 的分母是 D'2024.01.01' - D'2023.01.01',即约 31536000000 毫秒,若 time 为 0 则直接喂 0 而非 NaN。 这种把『权益回撤比』和『时间周期性』拼进同一特征向量的做法,在 MT5 里跑强化学习类 EA 时很常见。你可以直接把这段抄进自己的 CTrade 训练循环,改一下 account[2]~[6] 的语义(比如持仓数、保证金率)就能适配自己的风控信号。外汇与贵金属杠杆高,这类自适应模型过拟合后实盘可能快速回撤,上量前务必用历史分段时间验证。

MQL5 / C++
   temp_m = targets;
   for(class="type">int i = class="num">0; i < class="num">3; i++)
      temp_m.Col(temp_m.Col(i) - target[i], i);
   temp_m = MathPow(temp_m, class="num">2.0f);
   class="type">ulong pos = temp_m.Sum(AXIS_VERT).ArgMin();
   targets.Row(target, pos);
   Result.AssignArray(targets);
   class=class="str">"cmt">//---
   if(!ADAPTEndpoints.backProp(Result, (CBufferFloat*)NULL))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      Stop = true;
      break;
     }
   if(!ADAPTEncoder.backPropGradient((CBufferFloat*)NULL))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      Stop = true;
      break;
     }
   bProbs.AssignArray(vector<class="type">class="kw">float>::Zeros(NForecast));
   bProbs.Update((class="type">int)pos, class="num">1);
   bProbs.BufferWrite();
   if(!ADAPTProbability.backProp(GetPointer(bProbs), GetPointer(ADAPTEndpoints)))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      Stop = true;
      break;
     }
   class=class="str">"cmt">//--- Policy
   class="type">class="kw">float PrevBalance = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">0];
   class="type">class="kw">float PrevEquity = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">1];
   bAccount.Clear();
   bAccount.Add((Buffer[tr].States[i].account[class="num">0] - PrevBalance) / PrevBalance);
   bAccount.Add(Buffer[tr].States[i].account[class="num">1] / PrevBalance);
   bAccount.Add((Buffer[tr].States[i].account[class="num">1] - PrevEquity) / PrevEquity);
   bAccount.Add(Buffer[tr].States[i].account[class="num">2]);
   bAccount.Add(Buffer[tr].States[i].account[class="num">3]);
   bAccount.Add(Buffer[tr].States[i].account[class="num">4] / PrevBalance);
   bAccount.Add(Buffer[tr].States[i].account[class="num">5] / PrevBalance);
   bAccount.Add(Buffer[tr].States[i].account[class="num">6] / PrevBalance);
   class="type">class="kw">double time = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7];
   class="type">class="kw">double x = time / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;);
   bAccount.Add((class="type">class="kw">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));

◍ 把时间周期塞进神经网络的编码细节

这段代码在做一件事:把月、周、日三个周期的时间坐标转成三角函数特征,喂给账户状态缓冲区 bAccount。x = time / PeriodSeconds(PERIOD_MN1) 算出当前时间相当于多少个月,再乘 2π 取余弦;周线和日线同理取正弦,周期越短振荡频率越高。 若 bAccount.GetIndex() >= 0 才执行 BufferWrite(),说明缓冲区必须攒够至少一条样本才会落盘,否则前面的特征白算。三个编码器 StateEncoder、EndpointEncoder 和 Actor 是串联的:任一个 feedForward 返回 false 就打印函数名加行号、置 Stop=true 并 break,训练循环直接停。 最后那段是动作空间与风控的对接:result 初始化成 NActions 维零向量,value 取 SYMBOL_TRADE_TICK_VALUE_LOSS(每点亏损金额),risk 固定为净值的 1%(AccountInfoDouble(ACCOUNT_EQUITY)*0.01)。direct>0 时把 target[1]/_Point/MaxTP 缩放到 [0,1] 写进 result[1] 当止盈权重,止损 sl 取 target[1]/3 与 -target[2] 的较大值换算成点数,且不低于 MaxSL/10。外汇和贵金属杠杆高,1% 净值风险也可能在跳空时超额亏损,参数请先在策略测试器跑通再上实盘。

MQL5 / C++
 x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);
 bAccount.Add((class="type">class="kw">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
 x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
 bAccount.Add((class="type">class="kw">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
 x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);
 bAccount.Add((class="type">class="kw">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
 if(bAccount.GetIndex() >= class="num">0)
   bAccount.BufferWrite();
 class=class="str">"cmt">//--- State embedding
 if(!StateEncoder.feedForward((CNet *)GetPointer(ADAPTEncoder), -class="num">1,
(CBufferFloat*)GetPointer(bAccount)))
   {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
   }
 class=class="str">"cmt">//--- Endpoint embedding
 if(!EndpointEncoder.feedForward(Result, -class="num">1, false, (CBufferFloat*)GetPointer(bProbs)))
   {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
   }
 class=class="str">"cmt">//--- Actor
 if(!Actor.feedForward((CNet *)GetPointer(StateEncoder), -class="num">1,
(CNet*)GetPointer(EndpointEncoder)))
   {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
   }
 result = vector<class="type">class="kw">float>::Zeros(NActions);
 class="type">class="kw">double value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE_LOSS);
 class="type">class="kw">double risk = AccountInfoDouble(ACCOUNT_EQUITY) * class="num">0.01;
 if(direct > class="num">0)
   {
   class="type">class="kw">float tp = class="type">class="kw">float(target[class="num">1] / _Point / MaxTP);
   result[class="num">1] = tp;
   class="type">int sl = class="type">int(MathMax(MathMax(target[class="num">1] / class="num">3, -target[class="num">2]) / _Point, MaxSL/class="num">10));

「反向传播与训练进度回显的实现细节」

这段逻辑处在强化学习训练循环的内层,负责把网络输出误差沿 Actor、StateEncoder、EndpointEncoder 与 ADAPTEncoder 逐级回传。任意一步 backProp 或 backPropGradient 返回失败,就立即置 Stop=true 并 break,避免脏梯度继续污染权重。 多头与空头分支里,result[2]/result[5] 把止损点数除以 MaxSL 做归一化,result[0]/result[3] 则用 risk/(value*sl) 与 0.01 取大值再加 FLT_EPSILON,保证仓位比例下限不为 0 且数值稳定。 进度显示受 tick 节流控制:仅当 GetTickCount()-ticks > 500 毫秒才刷新 Comment,把 Actor、Endpoints、Probability 三组网络的近期平均误差按百分比与 15.8f 精度打印出来。你在 MT5 策略测试器里跑同类训练,若 500ms 内无输出属正常节流,不必疑为卡死。 循环结束后统一调 Comment("") 清屏,防止图表左上角残留最后一帧训练文本。外汇与贵金属品种波动剧烈,这类自适应训练结果仅代表历史样本拟合,实盘部署需警惕过拟合与滑点侵蚀。

MQL5 / C++
result[class="num">2] = class="type">class="kw">float(sl) / MaxSL;
result[class="num">0] = class="type">class="kw">float(MathMax(risk / (value * sl), class="num">0.01))+FLT_EPSILON;
 }
 else
  {
  class="type">class="kw">float tp = class="type">class="kw">float((-target[class="num">2]) / _Point / MaxTP);
  result[class="num">4] = tp;
  class="type">int sl = class="type">int(MathMax(MathMax((-target[class="num">2]) / class="num">3, target[class="num">1]) / _Point, MaxSL/class="num">10));
  result[class="num">5] = class="type">class="kw">float(sl) / MaxSL;
  result[class="num">3] = class="type">class="kw">float(MathMax(risk / (value * sl), class="num">0.01))+FLT_EPSILON;
  }
 Result.AssignArray(result);
 if(!Actor.backProp(Result, (CNet *)GetPointer(EndpointEncoder)) ||
  !StateEncoder.backPropGradient(GetPointer(bAccount),
(CBufferFloat *)GetPointer(bGradient)) ||
  !EndpointEncoder.backPropGradient(GetPointer(bProbs),
(CBufferFloat *)GetPointer(bGradient))
  )
  {
  PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
  Stop = true;
  break;
  }
 if(!ADAPTEncoder.backPropGradient((CBufferFloat*)NULL))
  {
  PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
  Stop = true;
  break;
  }
 class=class="str">"cmt">//---
 if(GetTickCount() - ticks > class="num">500)
  {
  class="type">class="kw">double percent = (class="type">class="kw">double(i - state) / ((end - state)) + iter) * class="num">100.0 / (Iterations);
  class="type">class="kw">string str = StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Actor", percent,
Actor.getRecentAverageError());
  str += StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Endpoints", percent,
ADAPTEndpoints.getRecentAverageError());
  str += StringFormat("%-14s %class="num">6.2f%% -> Error %class="num">15.8f\n", "Probability", percent,
ADAPTProbability.getRecentAverageError());
  Comment(str);
  ticks = GetTickCount();
  }
  }
  }
 Comment("");
class=class="str">"cmt">//---

把三层网络的均方误差打进日志

调试自适应神经网络 EA 时,最怕训练跑完却不知道每层误差收敛到什么程度。下面这段直接在 OnTester 收尾处把 Actor、Endpoints、Probability 三层的近期平均误差用 PrintFormat 按统一格式吐出来,精度锁在 10.7f,也就是小数点后 7 位,足够看清 0.0001 量级的抖动。 %s 填 __FUNCTION__ 显示函数名,%d 填 __LINE__ 显示行号,%-15s 左对齐留 15 字符给层名,%10.7f 右对齐输出浮点误差。三层依次打印后调 ExpertRemove() 卸载 EA,避免下一 tick 继续跑。 实盘接 MT5 时,把这段代码贴进自己的神经网络 EA 尾段,回测完打开「专家」标签,若 Probability 层误差长期高于 0.05 而其他两层低于 0.01,说明输出映射层可能欠拟合,外汇与贵金属杠杆高、价格噪声大,这类结构缺陷会显著放大信号失真风险。

MQL5 / C++
  PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__,
                                                       "Actor", Actor.getRecentAverageError());
  PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__,
                                          "Endpoints", ADAPTEndpoints.getRecentAverageError());
  PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__,
                                      "Probability", ADAPTProbability.getRecentAverageError());
  ExpertRemove();
class=class="str">"cmt">//---
  }

◍ EURUSD H1 上的训练与测试切分

ADAPT 思路在 MQL5 里落地后,先拿 2023 年前 7 个月的 EURUSD H1 历史做训练,所有指标走默认参数,不做额外调参。测试阶段只换时间窗,直接跑 2023 年 8 月同品种同周期数据,模型结构与训练设定完全一致。 环境交互采集的数据结构没变,所以没补新训练样本,而是复用之前训练时攒下的验算结果。这种用已有验算反哺训练空间的做法,省掉了额外验算开销,但训练时仍应尽量喂入账户状态、持仓等多样化信息。 从结果看,轻量模型在训练集和 8 月测试集上都跑出了利润,说明这套相关关系分析思路在外汇高风险场景下可能具备一定有效性。建议开 MT5 策略测试器,用 EURUSD H1 复现 7 月训练、8 月测试的分段来验证。

「演示模型离实盘还差什么」

前面用 MQL5 把 ADAPT 跑通了,真实历史数据上训练和测试都出了结果,说明多智能体轨迹预测的思路在金融序列里不是空谈。自适应头脑在不扩模型体积的前提下抬了能力上限,动态权重学习也让个体适配更贴。 但必须说清:这套程序只是技术演示,没有接实时行情、没做滑点和点差建模,外汇和贵金属又是高杠杆高风险品种,直接拿来下单大概率被摩擦成本吃掉。 真要验证,自己开 MT5 把历史数据换成本平台 XAUUSD 的 M1,重跑训练脚本看回测曲线是否塌方,比看任何结论都实在。

记住这一条就够了

这套 LSTM 预测工程在 MT5 里拆成了 7 个部件:5 个 mq5 程序负责样本收集、Real-ORL 采集、训练与测试,另外 2 个 mqh 加 1 个 cl 文件管状态结构和 OpenCL 神经网络内核,整套压缩包 3615.14 KB。 想复现的人先注意,原文评论区已有人指出测试结论难落地,尤其没 GPU 或非 Mac 环境跑 Test.mq5 会很吃力,外汇与贵金属行情高波动,模型输出只作概率参考。 真要动手,从 Research.mq5 接真实 tick 起步,别一上来跳到 Trajectory.mqh 改结构,否则状态对齐会直接崩。

常见问题

先在初始化阶段做维度校验,确认编码器输出与概率分支输入形状一致;装载网络时打印各层 tensor 尺寸,错位处多数出在多头注意力堆叠后的拼接层。
原文用 2~3 层做堆叠,层数再多训练波动明显变大;建议先从 2 层起回测,观察验证集轨迹误差再加减。
可以。把网络结构描述发给小布,它能对照维度校验逻辑标出概率头与 Actor 网络落点不一致的地方,省去手动核对。
易漏的是概率分支最后的激活与采样层;原文强调要在层描述里显式写出分布参数输出,否则回传权重会静默失效。
大概率。Actor 网络层定义应接在编码器输出之后、动作映射之前;若插进概率分支中间会触发维度校验异常,调整落点即可。