神经网络变得简单(第 81 部分):上下文引导运动分析(CCMR)·综合运用
🌊

神经网络变得简单(第 81 部分):上下文引导运动分析(CCMR)·综合运用

(3/3)· 从卷积横截面到注意力运动流,补齐环境动态量化这块最后拼图

案例拆解新手友好 第 3/3 篇
多数策略把环境状态当成某一根 K 线的横截面快照,忽略了指标随时间的迁移方向。CCMR 借计算机视觉里的光流思路,给状态变化配上定量向量,这类上下文引导在遮挡和大位移下更稳。

◍ 反向传播里的梯度回传链路

这段逻辑跑在 XCiT 类注意力层反向计算里,核心是把输出梯度沿 FF、LPI、QKV 三条通路往回送。每一层卷积或归一化梯度计算前都用 IsStopped() 探活,EA 被强制停止时立刻返回 false,避免 MT5 策略测试器里卡死。 权重偏移量随优化器切换:SGD 下 FF_Weights 步长取 4,Adam 类取 6;cLPI_Weights 则对应 5 与 7。这个差值是反传代码里最容易抄错的地方,直接决定梯度写进哪段缓冲区。 第一层(i==0)的输入与梯度不来自本层张量,而是取 prevLayer.getOutput() 和 prevLayer.getGradient(),说明网络栈是靠层间指针接力而非复制。外汇与贵金属模型用这套结构时,参数规模稍大就可能拖慢回测,属高风险实验范畴。 逐行看,ConvolutionInputGradients 先把 FF 侧梯度算进 temp,SumAndNormilize 以系数 1 做归一;随后梯度穿过 LPI 的卷积与 BatchNormInsideGradient,再回投到 AO_Tensors。最后 XCiTInsideGradients 与 DeConcat 把梯度拆回 Query、Key、Value,完成一步完整反传。

MQL5 / C++
FF_Tensors.At(i * class="num">4), FF_Tensors.At(i * class="num">4 + class="num">2), class="num">4 * iWindow, iWindow, None))
				 class="kw">return false;
			 CBufferFloat *temp = cLPI.At(i * class="num">6 + class="num">5);
			if(IsStopped() ||
!ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? class="num">4 : class="num">6)), FF_Tensors.At(i * class="num">4 + class="num">1),
cLPI.At(i * class="num">6 + class="num">2), temp, iWindow, class="num">4 * iWindow, LReLU))
				 class="kw">return false;
			class=class="str">"cmt">//--- Sum gradients
			if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false, class="num">0, class="num">0, class="num">0, class="num">1))
				 class="kw">return false;
			out_grad = temp;
			class=class="str">"cmt">//--- Passing gradient through LPI
			if(IsStopped() ||
!ConvolutionInputGradients(cLPI_Weights.At(i * (optimization == SGD ? class="num">5 : class="num">7) + class="num">2), temp,
cLPI.At(i * class="num">6 + class="num">1), cLPI.At(i * class="num">6 + class="num">4),  class="num">2 * iHeads, class="num">2, None, class="num">0, iHeads))
				 class="kw">return false;
			if(IsStopped() ||
!BatchNormInsideGradient(cLPI.At(i * class="num">6), cLPI.At(i * class="num">6 + class="num">3),
cLPI_Weights.At(i * (optimization == SGD ? class="num">5 : class="num">7) + class="num">1), cLPI.At(i * class="num">6 + class="num">1),  
cLPI.At(i * class="num">6 + class="num">4), LReLU))
				 class="kw">return false;
			if(IsStopped() ||
!ConvolutionInputGradients(cLPI_Weights.At(i * (optimization == SGD ? class="num">5 : class="num">7)), cLPI.At(i * class="num">6 + class="num">3),
AO_Tensors.At(i * class="num">2), AO_Tensors.At(i * class="num">2 + class="num">1),  iLPIWindow, iHeads, None, class="num">0,
iLPIStep))
				 class="kw">return false;
			temp = AO_Tensors.At(i * class="num">2 + class="num">1);
			class=class="str">"cmt">//--- Sum and normilize gradients
			if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false, class="num">0, class="num">0, class="num">0, class="num">1))
				 class="kw">return false;
			class=class="str">"cmt">//--- Passing gradient to query, key and value
			if(IsStopped() || !XCiTInsideGradients(cConcat.At(i * class="num">2), cConcat.At(i * class="num">2 + class="num">1), S_Tensors.At(i * class="num">2), temp))
				 class="kw">return false;
			if(IsStopped() ||
!DeConcat(QKV_Tensors.At(i * class="num">2 + class="num">1), cValue.At(i * class="num">2 + class="num">1), cConcat.At(i * class="num">2 + class="num">1), class="num">2 * iWindowKey * iHeads,
iWindowKey * iHeads))
				 class="kw">return false;
			class=class="str">"cmt">//---
			CBufferFloat *inp = NULL;
			if(i == class="num">0)
				{
				 inp = prevLayer.getOutput();
				 temp = prevLayer.getGradient();
				}

「反向传播里的梯度回传与权重更新断点」

这段逻辑处在 Cross-XCiT 注意力层反向计算的核心:每一层先取前馈张量的偏移位置,temp 落在 i*4-1、inp 落在 i*4-3,把上一层残差和当前查询向量接上。 若优化器走 SGD,权重索引跨 2 步;若换 Adam 类,则跨 3 步——这个 2 与 3 的差异直接决定 QKV_Weights.At() 取哪一组动量缓冲。 ConvolutionInputGradients 每次调用前都塞了 IsStopped() 检查,EA 在 MT5 里被手动停止时会立刻 return false,避免显卡端空跑整批梯度。 最后一层(i == iLayers-1)的 cV_Weights 梯度直接写回 Motion.getGradient();非末层则先丢进 TempBuffer,再过 SumAndNormilize 做层间归约,out_grad 才向后传。 updateInputWeights 里卷积权重更新同样按优化器分叉:SGD 用 At(l*2+1) 做增量,Adam 类则补 At(l*3+1) 与 At(l*3+2) 两组状态,iWindow 与 2*iWindowKey*iHeads 界定卷积核覆盖的键值头维度。

MQL5 / C++
else
  {
   temp = FF_Tensors.At(i * class="num">4 - class="num">1);
   inp = FF_Tensors.At(i * class="num">4 - class="num">3);
  }
if(IsStopped() ||
!ConvolutionInputGradients(QKV_Weights.At(i * (optimization == SGD ? class="num">2 : class="num">3)), QKV_Tensors.At(i * class="num">2 + class="num">1),
inp, temp, iWindow, class="num">2 * iWindowKey * iHeads, None))
   class="kw">return false;
if(i > class="num">0)
   out_grad = temp;
if(i == iLayers - class="num">1)
  {
   if(IsStopped() ||
!ConvolutionInputGradients(cV_Weights.At(i * (optimization == SGD ? class="num">2 : class="num">3)), cValue.At(i * class="num">2 + class="num">1),
Motion.getOutput(), Motion.getGradient(), iWindow, iWindowKey * iHeads, None))
     class="kw">return false;
  }
else
  {
   if(IsStopped() ||
!ConvolutionInputGradients(cV_Weights.At(i * (optimization == SGD ? class="num">2 : class="num">3)), cValue.At(i * class="num">2 + class="num">1),
Motion.getOutput(), GetPointer(TempBuffer), iWindow, iWindowKey * iHeads, None))
     class="kw">return false;
   if(IsStopped() ||
!SumAndNormilize(GetPointer(TempBuffer), Motion.getGradient(), Motion.getGradient(), iWindow2, false))
     class="kw">return false;
  }
if(IsStopped() ||
!ConvolutionInputGradients(QKV_Weights.At(i * (optimization == SGD ? class="num">2 : class="num">3)), QKV_Tensors.At(i * class="num">2 + class="num">1),
inp, temp, iWindow, class="num">2 * iWindowKey * iHeads, None))
   class="kw">return false;
class=class="str">"cmt">//--- Sum and normilize gradients
if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false, class="num">0, class="num">0, class="num">0, class="num">1))
   class="kw">return false;
if(i > class="num">0)
   out_grad = temp;
 }
class=class="str">"cmt">//---
  class="kw">return true;
 }
class="type">bool CNeuronCrossXCiTOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Motion)
  {
  if(CheckPointer(NeuronOCL) == POINTER_INVALID)
    class="kw">return false;
  CBufferFloat *inputs = NeuronOCL.getOutput();
  for(class="type">uint l = class="num">0; l < iLayers; l++)
   {
   if(IsStopped() ||
!ConvolutuionUpdateWeights(QKV_Weights.At(l * (optimization == SGD ? class="num">2 : class="num">3)), QKV_Tensors.At(l * class="num">2 + class="num">1),
inputs, (optimization == SGD ? QKV_Weights.At(l * class="num">2 + class="num">1) :
QKV_Weights.At(l * class="num">3 + class="num">1)), (optimization == SGD ? NULL :
QKV_Weights.At(l * class="num">3 + class="num">2)), iWindow, class="num">2 * iWindowKey * iHeads))
     class="kw">return false;

反向传播里的卷积权重更新分支

这段训练循环展示了在 CNN 类模型中,每一层 l 如何根据优化器类型切换权重张量的偏移步长。SGD 模式下卷积权重按 2 或 5 的倍数寻址,而带动量或自适应学习率的非 SGD 模式则扩展到 3 或 7,二者内存布局差异直接决定你拷贝代码时 At() 的索引不能写错。 IsStopped() 被放在每个更新调用前做短路判断,意味着用户在 MT5 策略测试器里点停止时,当前层若正在跑卷积反向传播会立即 return false,避免卡死在 OpenCL 队列。实际调试时若发现某层权重不更新,先确认不是被这个退出信号截断了。 下面这段是原始循环体的核心片段,逐行对应上述逻辑: 第1个 if 处理 cV 卷积层权重,SGD 时取 l*2 与 l*2+1 两组,非 SGD 取 l*3 起的三组; 第2个 if 处理 cLPI 卷积,窗口与头数由 iLPIWindow、iHeads 控制,步长 iLPIStep; 第3个 if 只做 BatchNorm 的权重更新,偏移量为 l*5+1 或 l*7+1; 第4个 if 再用 2*iHeads 窗口做二次卷积; 第5、6个 if 处理前馈 FF 张量,SGD 偏移步长 4,非 SGD 步长 6,卷积核宽由 iWindow 与 4*iWindow 决定。 类声明部分定义了 CNeuronCCMROCL 的网络骨架:FeatureExtractor 与 LocalContext 都是 CCMREncoder,GlobalContext 用跨通道注意力,MotionContext 做运动交叉,RecurentUnit 接 LSTM。外汇与贵金属行情高频跳变,这类结构在 EURUSD 15M 上过拟合概率偏高,实盘前务必用历史数据验证。

MQL5 / C++
if(IsStopped() ||
!ConvolutuionUpdateWeights(cV_Weights.At(l * (optimization == SGD ? class="num">2 : class="num">3)), cValue.At(l * class="num">2 + class="num">1), inputs,
(optimization == SGD ? cV_Weights.At(l * class="num">2 + class="num">1) : cV_Weights.At(l * class="num">3 + class="num">1)),
(optimization == SGD ? NULL : cV_Weights.At(l * class="num">3 + class="num">2)), iWindow,
iWindowKey * iHeads))
   class="kw">return false;
if(IsStopped() ||
!ConvolutuionUpdateWeights(cLPI_Weights.At(l * (optimization == SGD ? class="num">5 : class="num">7)), cLPI.At(l * class="num">6 + class="num">3),
AO_Tensors.At(l * class="num">2), (optimization == SGD ? cLPI_Weights.At(l * class="num">5 + class="num">3) :
cLPI_Weights.At(l * class="num">7 + class="num">3)), (optimization == SGD ? NULL :
cLPI_Weights.At(l * class="num">7 + class="num">5)), iLPIWindow, iHeads, iLPIStep))
   class="kw">return false;
if(IsStopped() ||
!BatchNormUpdateWeights(cLPI_Weights.At(l * (optimization == SGD ? class="num">5 : class="num">7) + class="num">1), cLPI.At(l * class="num">6 + class="num">4)))
   class="kw">return false;
if(IsStopped() ||
!ConvolutuionUpdateWeights(cLPI_Weights.At(l * (optimization == SGD ? class="num">5 : class="num">7) + class="num">2), cLPI.At(l * class="num">6 + class="num">5),
cLPI.At(l * class="num">6 + class="num">1), (optimization == SGD ? cLPI_Weights.At(l * class="num">5 + class="num">4) :
cLPI_Weights.At(l * class="num">7 + class="num">4)), (optimization == SGD ? NULL :
cLPI_Weights.At(l * class="num">7 + class="num">6)), class="num">2 * iHeads, class="num">2, iHeads))
   class="kw">return false;
if(IsStopped() ||
!ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? class="num">4 : class="num">6)), FF_Tensors.At(l * class="num">4 + class="num">2),
cLPI.At(l * class="num">6 + class="num">2), (optimization == SGD ? FF_Weights.At(l * class="num">4 + class="num">2) :
FF_Weights.At(l * class="num">6 + class="num">2)), (optimization == SGD ? NULL :
FF_Weights.At(l * class="num">6 + class="num">4)), iWindow, class="num">4 * iWindow))
   class="kw">return false;
class=class="str">"cmt">//---
if(IsStopped() ||
!ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? class="num">4 : class="num">6) + class="num">1), FF_Tensors.At(l * class="num">4 + class="num">3),
FF_Tensors.At(l * class="num">4), (optimization == SGD ? FF_Weights.At(l * class="num">4 + class="num">3) :
FF_Weights.At(l * class="num">6 + class="num">3)), (optimization == SGD ? NULL :
FF_Weights.At(l * class="num">6 + class="num">5)), class="num">4 * iWindow, iWindow))
   class="kw">return false;
inputs = FF_Tensors.At(l * class="num">4 + class="num">1);
}
class=class="str">"cmt">//---
class="kw">return true;
}
class CNeuronCCMROCL : class="kw">public CNeuronBaseOCL
{
class="kw">protected:
 CCCMREncoder FeatureExtractor;
 CNeuronBaseOCL PrevFeatures;
 CNeuronBaseOCL Motion;
 CNeuronBaseOCL Temp;
 CCCMREncoder LocalContext;
 CNeuronXCiTOCL GlobalContext;
 CNeuronCrossXCiTOCL MotionContext;
 CNeuronLSTMOCL RecurentUnit;

◍ CCMROCL 神经元的初始化链路

CNeuronCCMROCL 是 MT5 中跑在 OpenCL 上的卷积记忆神经元,它的类声明里先挂了一个 UpScale 的 CNeuronConvOCL 成员,用于上采样输出。其余接口覆盖了前向传播 feedForward、权重更新 updateInputWeights,以及文件存取的 Save/Load,类型标识固定返回 defNeuronCCMROCL。 Init 方法是验证这类神经元能否用的第一道关。它先调用基类 CNeuronBaseOCL::Init,传入的神经元尺寸是 window_out * count,而非原始 window,这说明内部做了维度重排。 紧接着连续初始化 5 个子模块:FeatureExtractor 用 window 和 16 个特征通道、PrevFeatures 与 Motion 都接 16*count 宽度的中间张量、Temp 保留 window*count 的原始上下文、LocalContext 再回到 window 与 16 通道的局部卷积。任意一步返回 false,整个 Init 就失败。 一个容易踩的坑是梯度索引。代码里判断若 Motion 的梯度索引与 FeatureExtractor 不一致,就强制把 Motion 的设成前者。开 MT5 自己写派生类时,若漏了这行,反向传播可能静默算出错误梯度。

MQL5 / C++
  CNeuronConvOCL      UpScale;
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool        feedForward(CNeuronBaseOCL *NeuronOCL);
  class="kw">virtual class="type">bool        updateInputWeights(CNeuronBaseOCL *NeuronOCL);
class="kw">public:
                    CNeuronCCMROCL(class="type">void) {};
                   ~CNeuronCCMROCL(class="type">void) {};
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool        Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                           class="type">uint window, class="type">uint window_out, class="type">uint count,
                           ENUM_OPTIMIZATION optimization_type,
                           class="type">uint batch);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">bool        calcInputGradients(CNeuronBaseOCL *prevLayer);
  class=class="str">"cmt">//---
  class="kw">virtual class="type">int         Type(class="type">void) const   { class="kw">return defNeuronCCMROCL;  }
  class=class="str">"cmt">//--- methods for working with files
  class="kw">virtual class="type">bool        Save(class="type">int const file_handle);
  class="kw">virtual class="type">bool        Load(class="type">int const file_handle);
  class="kw">virtual class="type">void        SetOpenCL(COpenCLMy *obj);
  class="kw">virtual class="type">void        TrainMode(class="type">bool flag);                    class=class="str">"cmt">///< Set Training Mode Flag
  class="kw">virtual class="type">bool        Clear(class="type">void);
  };
class="type">bool CNeuronCCMROCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_out,
                          class="type">uint count, ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
  if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_out * count, optimization_type, batch))
    class="kw">return false;
  if(!FeatureExtractor.Init(class="num">0, class="num">0, OpenCL, window, class="num">16, count, optimization, iBatch))
    class="kw">return false;
  if(!PrevFeatures.Init(class="num">0, class="num">1, OpenCL, class="num">16 * count, optimization, iBatch))
    class="kw">return false;
  if(!Motion.Init(class="num">0, class="num">2, OpenCL, class="num">16 * count, optimization, iBatch))
    class="kw">return false;
  if(Motion.getGradientIndex() != FeatureExtractor.getGradientIndex())
    Motion.SetGradientIndex(FeatureExtractor.getGradientIndex());
  if(!Temp.Init(class="num">0, class="num">3, OpenCL, window * count, optimization, iBatch))
    class="kw">return false;
  if(!LocalContext.Init(class="num">0, class="num">4, OpenCL, window, class="num">16, count, optimization, iBatch))
    class="kw">return false;

「CCMROCL 网络的初始化与前向传播链」

这段代码实现了 CNeuronCCMROCL 类里 init、feedForward 与 calcInputGradients 三个核心方法,把全局/局部上下文、运动上下文、循环单元和上采样层串成一条 OpenCL 加速的推理管道。 初始化阶段依次拉起 GlobalContext(设备号0、核号5、OpenCL、16/3/4 等参数)、MotionContext(核号6) 与 RecurentUnit(核号7,输入维度 16*count)。任何一步 Init 返回 false 就直接 return false,说明层间依赖是硬绑定,缺一层整个网络不可用。 feedForward 里先跑 SumAndNormilize 做特征差分:第一次偏移量 -0.5f,第二次 1.0f,把 PrevFeatures 与 Motion 输出做归一化融合,再依次推 LocalContext → GlobalContext → MotionContext → RecurentUnit → UpScale。这种级联顺序决定了梯度必须反向从 UpScale 回传。 calcInputGradients 严格逆序调用:UpScale 算 RecurentUnit 的梯度,一路退回到 LocalContext 算 Temp 的梯度。在 MT5 里改 window_out 或 count 后,若报 getGradientIndex 不匹配,优先检查 UpScale.Init 的实参而不是前面几层。外汇与贵金属行情下用此类模型做信号生成,过拟合与滑点风险偏高,实盘前务必用历史数据回测验证。

MQL5 / C++
  if(!GlobalContext.Init(class="num">0, class="num">5, OpenCL, class="num">16, class="num">3, class="num">4, count, class="num">4, optimization, iBatch))
      class="kw">return false;
  if(!MotionContext.Init(class="num">0, class="num">6, OpenCL, class="num">16, class="num">16, class="num">3, class="num">4, count, class="num">4, optimization, iBatch))
      class="kw">return false;
  if(!RecurentUnit.Init(class="num">0, class="num">7, OpenCL, class="num">16 * count, optimization, iBatch) || !RecurentUnit.SetInputs(class="num">16 * count))
      class="kw">return false;
  if(!UpScale.Init(class="num">0, class="num">8, OpenCL, class="num">16, class="num">16, window_out, count, optimization, iBatch))
      class="kw">return false;
  if(UpScale.getGradientIndex() != getGradientIndex())
      SetGradientIndex(UpScale.getGradientIndex());
  if(UpScale.getOutputIndex() != getOutputIndex())
      Output.BufferSet(UpScale.getOutputIndex());
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronCCMROCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
class=class="str">"cmt">//--- Delta Features
  if(!SumAndNormilize(FeatureExtractor.getOutput(), FeatureExtractor.getOutput(), PrevFeatures.getOutput(),
                                                                                   class="num">1, false, class="num">0, class="num">0, class="num">0, -class="num">0.5f))
      class="kw">return false;
  if(!FeatureExtractor.FeedForward(NeuronOCL))
      class="kw">return false;
  if(!SumAndNormilize(FeatureExtractor.getOutput(), PrevFeatures.getOutput(), Motion.getOutput(),
                                                                               class="num">1, false, class="num">0, class="num">0, class="num">0, class="num">1.0f))
      class="kw">return false;
  if(!LocalContext.FeedForward(NeuronOCL))
      class="kw">return false;
  if(!GlobalContext.FeedForward(GetPointer(LocalContext)))
      class="kw">return false;
  if(!MotionContext.FeedForward(GetPointer(GlobalContext), Motion.getOutput()))
      class="kw">return false;
class=class="str">"cmt">//--- Flow
  if(!RecurentUnit.FeedForward(GetPointer(MotionContext)))
      class="kw">return false;
  if(!UpScale.FeedForward(GetPointer(RecurentUnit)))
      class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronCCMROCL::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
  if(!UpScale.calcInputGradients(GetPointer(RecurentUnit)))
      class="kw">return false;
  if(!RecurentUnit.calcInputGradients(GetPointer(MotionContext)))
      class="kw">return false;
  if(!MotionContext.calcInputGradients(GetPointer(GlobalContext), GetPointer(Motion)))
      class="kw">return false;
  if(!GlobalContext.calcInputGradients(GetPointer(LocalContext)))
      class="kw">return false;
  if(!LocalContext.calcInputGradients(GetPointer(Temp)))

CCMROCL 神经元的权重回传与结构装配

CNeuronCCMROCL 的反向传播链是分层的:calcInputGradients 里先让 FeatureExtractor 算梯度,再用 SumAndNormilize 把上一层的梯度、Temp 梯度按系数 1.0f 归一合并;任一步返回 false 就整体中止,保证梯度不污染。 updateInputWeights 展示了从特征提取到递归单元的六级串联:FeatureExtractor → LocalContext → GlobalContext(吃 LocalContext 指针)→ MotionContext(吃 GlobalContext 输出)→ RecurentUnit → UpScale。每一级失败立即返回 false,这种硬短路写法在 MT5 的 OpenCL 神经网里能避免半更新导致的显存状态不一致。 Clear 方法除了清 RecurentUnit,还把 FeatureExtractor 的输出缓冲按 Total() 长度置 0 并写回;若 BufferWrite 失败同样返回 false,说明复位不是单纯置空指针,而是连设备端缓冲一起清。 CreateDescriptions 负责搭 encoder / actor / critic 三套结构。输入层用 defNeuronBaseOCL,节点数 = HistoryBars * BarDescr,优化器 ADAM,激活 None;第一隐藏层用 defNeuronBatchNormOCL,batch 取 MathMax(1000, GPTBars)。在 MT5 里把 GPTBars 调小于 1000 时,batch 仍锁在 1000,这是显存占用的硬下限,调参时要注意。

MQL5 / C++
  class="kw">return false;
  if(!FeatureExtractor.calcInputGradients(prevLayer))
    class="kw">return false;
  if(!SumAndNormilize(prevLayer.getGradient(), Temp.getGradient(), prevLayer.getGradient(),
class="num">1, false, class="num">0, class="num">0, class="num">0, class="num">1.0f))
    class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronCCMROCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
  if(!FeatureExtractor.UpdateInputWeights(NeuronOCL))
    class="kw">return false;
  if(!LocalContext.UpdateInputWeights(NeuronOCL))
    class="kw">return false;
  if(!GlobalContext.UpdateInputWeights(GetPointer(LocalContext)))
    class="kw">return false;
  if(!MotionContext.UpdateInputWeights(GetPointer(GlobalContext), Motion.getOutput()))
    class="kw">return false;
  if(!RecurentUnit.UpdateInputWeights(GetPointer(MotionContext)))
    class="kw">return false;
  if(!UpScale.UpdateInputWeights(GetPointer(RecurentUnit)))
    class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNeuronCCMROCL::Clear(class="type">void)
  {
  if(!RecurentUnit.Clear())
    class="kw">return false;
class=class="str">"cmt">//---
  CBufferFloat *temp = FeatureExtractor.getOutput();
  temp.BufferInit(temp.Total(), class="num">0);
  if(!temp.BufferWrite())
    class="kw">return false;
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CreateDescriptions(CArrayObj *encoder, CArrayObj *actor, CArrayObj *critic)
  {
class=class="str">"cmt">//---
  CLayerDescription *descr;
class=class="str">"cmt">//---
  if(!encoder)
   {
     encoder = new CArrayObj();
     if(!encoder)
       class="kw">return false;
   }
  if(!actor)
   {
     actor = new CArrayObj();
     if(!actor)
       class="kw">return false;
   }
  if(!critic)
   {
     critic = new CArrayObj();
     if(!critic)
       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;

◍ 编码器逐层堆栈的失败回退逻辑

在 MT5 里搭一个基于 OpenCL 的行情编码器,不是一次性建好就完事,而是逐层往 encoder 对象里 Add 描述符。每一层 new 出来之后若 Add 返回 false,必须立刻 delete 掉 descr 并 return false,否则会漏内存且后续层拿到的是脏指针。 第二层先建 CLayerDescription,类型置为 defNeuronEmbeddingOCL,用临时数组 temp 装上一层神经元数 prev_count,再 ArrayCopy 进 descr.windows。随后把 prev_count 更新为 GPTBars,window_out 算成 EmbeddingSize/2,这一步直接决定嵌入层输出维度。 第三层到第五层分别是 Conv、PE、CCMR 类型,逻辑同构:new 失败就返 false;成功则接力上一层的 prev_count 与 prev_wout,改 window / window_out 后再 Add。CCMR 层把 window_out 重新拉回 EmbeddingSize,意味着多头注意力前的表征维度被规整回嵌入尺寸。 开 MT5 把 EmbeddingSize 改成 64、GPTBars 改成 120 跑一遍,能看到第三层 step 与 window 同步变成 32,若显存报错优先查第二层 temp 数组长度是否和 prev_count 对齐。外汇与贵金属行情用此类模型做特征提取属高风险实验,过拟合概率偏高。

MQL5 / C++
  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 / class="num">2;
  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 = defNeuronConvOCL;
  descr.count = prev_count;
  descr.step = descr.window = prev_wout;
  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">4
  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;
    }
class=class="str">"cmt">//--- layer class="num">5
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronCCMROCL;
  descr.count = prev_count;
  descr.window = prev_wout;
  descr.window_out = EmbeddingSize;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }

「EURUSD H1 上跑一轮真实回测」

把前几节用 MQL5 写好的 CCMR 模型放进 MT5 策略测试器,训练窗口取 2023 年 1—7 月 EURUSD H1 历史,测试窗口单独切到 2023 年 8 月,避免前视偏差。训练集上先调出能盈利的权重,再原样丢进测试集,不改动任何参数。 测试月内模型共触发 21 笔交易,胜率 52.3%,盈利单的最大与平均值均高于亏损单对应值,最终盈利因子 1.22。外汇与贵金属属高风险品种,该结果仅代表特定样本期,换时间段可能显著衰减,需自行在 MT5 重跑验证。 想复现的话,直接把训练好的模型文件挂进策略测试器、周期设为 H1、品种 EURUSD、日期框死 2023.08.01—2023.08.31 即可,重点看盈利因子是否掉到 1 以下。

在 MT5 里跑通 CCMR 的边界

CCMR 把上下文运动聚合和多尺度粗略到精细结合起来,即使在遮挡区也能产出较细的流向图。作者用两阶段分组:先算全局上下文特征,再在所有尺度上迭代引导运动特性,让基于 XCiT 的算法跨尺度保留各自内容。 我们在 MetaTrader 5 策略测试器里用真实行情数据训练并测试了模型,结果指向该方法在受阻区域仍具可用性,但样本外的泛化表现仍需你自己换品种复测。 需要明确:文中所有 MQL5 程序仅作方法演示,不构成任何交易信号。外汇与贵金属杠杆高、滑点突变更易击穿演示假设,实盘前务必在策略测试器用近 3 个月 tick 数据重跑一遍。

◍ 随包附带的七个程序文件

这套 LSTM 多元时间序列预测方案不是只给思路,作者把可跑的代码全打进了 MQL5.zip(1029.16 KB),下载解压就能在 MT5 里直接调。压缩包里分三层:Research.mq5、ResearchRealORL.mq5、Study.mq5、Test.mq5 四个 EA 分别管样本收集、Real-ORL 样本收集、模型训练和模型测试;Trajectory.mqh 与 NeuroNet.mqh 两个类库定义了系统状态结构和建网逻辑;NeuroNet.cl 则是 OpenCL 端跑神经网络的内核代码。 外汇与贵金属市场高杠杆、高波动,直接用这些 EA 前务必先在策略测试器用历史数据验证逻辑,实盘前用小资金过一遍。把类库路径include 进你自己的工程时,注意 NeuroNet.cl 的编译选项要开 OpenCL 支持,否则训练 EA 会直接报错退出。

交给小布盯盘看动态流
这些多尺度运动特征的诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到上下文引导的偏移提示,把重复劳动交给小布,你专注决策。

常见问题

CNN 感知场固定,难推断遮挡或大位移;GC 特征靠 XCiT 类注意力跨所有尺度迭代引导,视野不局限于局部窗口。
它把上一尺度的流场当更细尺度的匹配初值,减少迭代次数,同时保持凸约束下的亚像素一致性。
大概率会增加计算,但多尺度 GRU 更新步数可控,离线训练后轻量前向在常见品种页能跑通。
小布在对应品种页直接渲染上下文引导的偏移带,你只需设定阈值,不必自己写特征提取器。
高频噪声会干扰细尺度流,倾向先用平滑特征提取再做多尺度匹配,贵金属与外汇均属高风险,需样本外验证。