神经网络变得简单(第 71 部分):目标条件预测编码(GCPC)·综合运用
📘

神经网络变得简单(第 71 部分):目标条件预测编码(GCPC)·综合运用

第 3/3 篇

◍ 序列批处理里的误差与标准差追踪

这段逻辑跑在自编码类模型的训练循环里,每次取一批状态向量送进编码器再经解码器还原,核心不是预测价格,而是算重建误差的动态标准差。 end 用来限制本批处理边界,取 state+batch 与缓冲总量的较小值,避免越界读取 PrecoderBars 之外的数据;循环内先把第 i 个状态赋给 State,再走 LastEncoder 与 Encoder 的前向传播,任一步返回 false 就打印函数名与行号并 break。 目标矩阵 targets 先按状态尺寸铺成行,若 size 大于 BarDescr 就 reshape 成多行再补 PrecoderBars 行、列数截到 3,等于把未来若干根的状态也拼进同一样本。 误差用 target - result 得到向量,标准差按递推式 STD = sqrt((STD^2 * std_count + error^2)/(std_count+1)) 更新,且 std_count 被钳在 999 以内,意味着长期方差不会因样本无限累积而过度平滑。 check 向量拿绝对误差减 STD*STD_Multiplier,只要最大值大于 0 就触发注释处的分支——这步就是异常检测的门槛,STD_Multiplier 调大则漏报多、调小则外汇与贵金属这类高波动品种上误报概率明显上升。

MQL5 / C++
class="type">int end = MathMin(state + batch, Buffer[tr].Total - PrecoderBars);
for(class="type">int i = state; i < end; i++)
  {
   State.AssignArray(Buffer[tr].States[i].state);
   if(!LastEncoder.BufferWrite() || !Encoder.feedForward((CBufferFloat*)GetPointer(State), class="num">1, false,
(CBufferFloat*)GetPointer(LastEncoder)))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      break;
     }
   if(!Decoder.feedForward(GetPointer(Encoder), -class="num">1, (CBufferFloat*)NULL))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      break;
     }
   target.Assign(Buffer[tr].States[i].state);
   class="type">ulong size = target.Size();
   targets = matrix<class="type">float>::Zeros(class="num">1, size);
   targets.Row(target, class="num">0);
   if(size > BarDescr)
     targets.Reshape(size / BarDescr, BarDescr);
   class="type">ulong shift = targets.Rows();
   targets.Resize(shift + PrecoderBars, class="num">3);
   for(class="type">int t = class="num">0; t < PrecoderBars; t++)
     {
      target.Assign(Buffer[tr].States[i + t].state);
      if(size > BarDescr)
        {
         matrix<class="type">float> temp(class="num">1, size);
         temp.Row(target, class="num">0);
         temp.Reshape(size / BarDescr, BarDescr);
         temp.Resize(size / BarDescr, class="num">3);
         target = temp.Row(temp.Rows() - class="num">1);
        }
      targets.Row(target, shift + t);
     }
   targets.Reshape(class="num">1, targets.Rows()*targets.Cols());
   target = targets.Row(class="num">0);
   Decoder.getResults(result);
   vector<class="type">float> error = target - result;
   std_count = MathMin(std_count, class="num">999);
   STD = MathSqrt((MathPow(STD, class="num">2) * std_count + MathPow(error, class="num">2)) / (std_count + class="num">1));
   std_count++;
   vector<class="type">float> check = MathAbs(error) - STD * STD_Multiplier;
   if(check.Max() > class="num">0)
     {
      class=class="str">"cmt">//---

自编码训练循环里的断点保护与进度回显

这段逻辑处在自编码网络的训练主循环收尾段,核心动作是把解码器误差反向传播回去,并周期性把训练进度打到图表上。若 Decoder 或 Encoder 的 backProp 任一返回失败,立即打印函数名与行号并 break,避免脏权重继续写盘。 Result.AssignArray(CAGrad(error) + result);

if(!Decoder.backProp(Result, (CNet *)NULL)

!Encoder.backPropGradient(GetPointer(LastEncoder), GetPointer(Gradient))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); break; } 上面四行是误差回流的硬闸门:AssignArray 把当前梯度叠进结果容器,两个 backProp 调用任一失败就退出内层循环。 进度回显被限制在每 500 毫秒一次(GetTickCount() - ticks > 500),用 percent 算整体迭代进度,Comment 输出 Decoder 的近期平均误差,精度到 15.8f。这种节流能防止高频刷新拖慢 MT5 主线程。 循环结束后 ExpertRemove() 被调用,EA 自行卸载;OnDeinit 里除非是初始化失败或重编译,否则把 Encoder 和 Decoder 各存成 .nnw 文件,并 delete 掉 Result 与 OpenCL 对象,防止句柄泄漏。外汇与贵金属行情下用这类自编码做特征压缩,模型过拟合概率偏高,实盘前务必用历史数据交叉验证。

MQL5 / C++
Result.AssignArray(CAGrad(error) + result);
if(!Decoder.backProp(Result, (CNet *)NULL) ||
   !Encoder.backPropGradient(GetPointer(LastEncoder), GetPointer(Gradient)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   break;
  }
 }
 Encoder.getResults(result);
 LastEncoder.AssignArray(result);
 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", "Decoder", percent, Decoder.getRecentAverageError());
   Comment(str);
   ticks = GetTickCount();
  }
 }
 }
 Comment("");
class=class="str">"cmt">//---
 PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Decoder", Decoder.getRecentAverageError());
 ExpertRemove();
class=class="str">"cmt">//---
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
 {
class=class="str">"cmt">//---
  if(!(reason == REASON_INITFAILED || reason == REASON_RECOMPILE))
   {
    Encoder.Save(FileName + "Enc.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true);
    Decoder.Save(FileName + "Dec.nnw", Decoder.getRecentAverageError(), class="num">0, class="num">0, TimeCurrent(), true);
   }
  class="kw">delete Result;
  class="kw">delete OpenCL;
 }
CNet         Actor;
CNet         StateEncoder;
CNet         Encoder;
CNet         Goal;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
 {
class=class="str">"cmt">//---
  ResetLastError();
  if(!LoadTotalBase())
   {
    PrintFormat("Error of load study data: %d", GetLastError());

「EA 初始化时神经网络模型的加载与维度校验」

在 MT5 的 EA 初始化阶段,先尝试从磁盘加载四个神经网络文件:编码器(Enc.nnw)、状态编码器(StEnc.nnw)、目标网络(Goal.nnw)与行动者(Act.nnw)。若其中任一文件加载失败,终端会打印错误并返回 INIT_FAILED,EA 直接无法启动。 若模型文件缺失,代码会现场用 CArrayObj 构建 actor、goal、encoder 三套网络描述,调用 CreateDescriptions 与各自的 Create 方法重建结构;任一步失败同样释放内存并返回 INIT_FAILED。注意这里用 delete 主动析构临时数组,避免句柄泄漏。 加载或重建完成后,必须做维度对账。Actor 输出节点数要等于 NActions,Encoder 输出要等于 EmbeddingSize,Encoder 第 0 层输入要等于 HistoryBars * BarDescr,StateEncoder 第 0 层输入要等于 EmbeddingSize,Actor 第 0 层输入要等于 StateEncoder 的输出维数(latent_state)。任何一项不匹配都会触发 PrintFormat 并报 INIT_FAILED。 这类校验在外汇与贵金属这种高杠杆品种上尤其必要:模型维度错配若未被初始化拦截,实盘可能发出荒谬下单信号。开 MT5 把 NActions、EmbeddingSize、HistoryBars、BarDescr 几个宏打出来核对一遍,能省掉后面大半调试时间。

MQL5 / C++
   class="kw">return INIT_FAILED;
   }
class=class="str">"cmt">//--- load models
   class="type">float temp;
   if(!Encoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true))
   {
      Print("Cann&class="macro">#x27;t load Encoder model");
      class="kw">return INIT_FAILED;
   }
   if(!StateEncoder.Load(FileName + "StEnc.nnw", temp, temp, temp, dtStudied, true) ||
      !Goal.Load(FileName + "Goal.nnw", temp, temp, temp, dtStudied, true) ||
      !Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true))
   {
      CArrayObj *actor = new CArrayObj();
      CArrayObj *goal = new CArrayObj();
      CArrayObj *encoder = new CArrayObj();
      if(!CreateDescriptions(actor, goal, encoder))
         {
          class="kw">delete actor;
          class="kw">delete goal;
          class="kw">delete encoder;
          class="kw">return INIT_FAILED;
         }
      if(!Actor.Create(actor) || !StateEncoder.Create(encoder) || !Goal.Create(goal))
         {
          class="kw">delete actor;
          class="kw">delete goal;
          class="kw">delete encoder;
          class="kw">return INIT_FAILED;
         }
      class="kw">delete actor;
      class="kw">delete goal;
      class="kw">delete encoder;
      class=class="str">"cmt">//---
   }
   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;
   }
   Encoder.getResults(Result);
   if(Result.Total() != EmbeddingSize)
   {
      PrintFormat("The scope of the Encoder does not match the embedding size(%d <> %d)", EmbeddingSize, Result.Total());
      class="kw">return INIT_FAILED;
   }
class=class="str">"cmt">//---
   Encoder.GetLayerOutput(class="num">0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
   {
      PrintFormat("Input size of Encoder doesn&class="macro">#x27;t match state description(%d <> %d)", Result.Total(),
(HistoryBars * BarDescr));
      class="kw">return INIT_FAILED;
   }
class=class="str">"cmt">//---
   StateEncoder.GetLayerOutput(class="num">0, Result);
   if(Result.Total() != EmbeddingSize)
   {
      PrintFormat("Input size of State Encoder doesn&class="macro">#x27;t match Bottleneck(%d <> %d)", Result.Total(), EmbeddingSize);
      class="kw">return INIT_FAILED;
   }
class=class="str">"cmt">//---
   StateEncoder.getResults(Result);
   class="type">int latent_state = Result.Total();
   Actor.GetLayerOutput(class="num">0, Result);
   if(Result.Total() != latent_state)
   {
      PrintFormat("Input size of Actor doesn&class="macro">#x27;t match output State Encoder(%d <> %d)", Result.Total(), latent_state);
      class="kw">return INIT_FAILED;
   }
class=class="str">"cmt">//---

◍ 初始化与退出时的张量对齐校验

在 MT5 用 OpenCL 跑强化学习模型时,初始化阶段最容易被忽略的是维度一致性。下面这段逻辑先取 Goal 网络第 0 层输出总量作为 latent_state,再让 Encoder 输出结果,若两者 Total() 不相等就直接 INIT_FAILED,说明目标网络与编码器输入尺寸不匹配。 随后 Goal 网络自身输出总量还必须等于奖励数 NRewards,否则同样终止初始化并打印 'The scope of Goal doesn't match rewards count'。这两个判断能挡掉大部分因网络结构改了但参数没同步导致的静默崩溃。 缓冲区创建依赖 EmbeddingSize 与 AccountDescr 的最大值:bGradient 用 MathMax(EmbeddingSize, AccountDescr) 做长度,bLastEncoder 用 EmbeddingSize。任一处 BufferInit 或 BufferCreate 失败都会返回错误码并中止。 退出时若不是 INITFAILED 或 RECOMPILE,会把 Actor、StateEncoder、Goal 三个网络按文件名后缀 Act.nnw / StEnc.nnw / Goal.nnw 落盘,时间戳取 TimeCurrent()。注意正常重载指标也会触发保存,回测中途别手滑改文件名覆盖权重。 训练函数开头用 GetProbTrajectories(Buffer, 0.9) 取轨迹概率,0.9 是采样温度类参数,调低会更保守;同时把 STD_Actor、STD_Goal 初始化为零向量,长度分别锁死 NActions 与 NRewards,这一步决定了后续标准差累加的维度边界。

MQL5 / C++
  Goal.GetLayerOutput(class="num">0, Result);
  latent_state = Result.Total();
  Encoder.getResults(Result);
  if(Result.Total() != latent_state)
    {
      PrintFormat("Input size of Goal doesn&class="macro">#x27;t match output Encoder(%d <> %d)", Result.Total(), latent_state);
      class="kw">return INIT_FAILED;
    }
class=class="str">"cmt">//---
  Goal.getResults(Result);
  if(Result.Total() != NRewards)
    {
      PrintFormat("The scope of Goal doesn&class="macro">#x27;t match rewards count(%d <> %d)", Result.Total(), NRewards);
      class="kw">return INIT_FAILED;
    }
  if(!bLastEncoder.BufferInit(EmbeddingSize, class="num">0) ||
      !bGradient.BufferInit(MathMax(EmbeddingSize, AccountDescr), class="num">0) ||
      !bLastEncoder.BufferCreate(OpenCL) ||
      !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=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//---
  if(!(reason == REASON_INITFAILED || reason == REASON_RECOMPILE))
    {
      Actor.Save(FileName + "Act.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true);
      StateEncoder.Save(FileName + "StEnc.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true);
      Goal.Save(FileName + "Goal.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true);
    }
  class="kw">delete Result;
  class="kw">delete OpenCL;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Train function                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Train(class="type">void)
  {
class=class="str">"cmt">//---
  vector<class="type">float> probability = GetProbTrajectories(Buffer, class="num">0.9);
  vector<class="type">float> result, target;
  matrix<class="type">float> targets;
  STD_Actor = vector<class="type">float>::Zeros(NActions);
  STD_Goal = vector<class="type">float>::Zeros(NRewards);
  class="type">int std_count = class="num">0;
  class="type">bool Stop = false;
class=class="str">"cmt">//---

轨迹采样里的编码器喂数与账户特征构造

这段逻辑跑在蒙特卡洛式回放循环里,外层用 GetTickCount 记时,只要没触发 IsStopped 或自定义 Stop,就按 Iterations 次数反复抽样本轨迹。每次先由 SampleTrajectory(probability) 抽一条轨迹编号 tr,再算 batch = GPTBars + 50,作为单次前向传播的窗口长度。 state 的取法很刁:用两次 MathRand 相乘再除以 32767 平方,把均匀分布压成偏向小值的正态近似,乘上可用区间 (Buffer[tr].Total - 2 - PrecoderBars - batch)。若 state <= 0 就 iter-- 并重抽,避免越界。 窗口内逐根 bar 喂给 Encoder:bState 取当前状态向量,经 bLastEncoder 缓冲后做 feedForward。任意一步写缓冲或前向失败就打印函数与行号、置 Stop=true 并 break,方便在 MT5 Experts 日志里定位哪一根断的。 账户侧特征不是裸值而是比值:余额变化率 (cur-prev)/prev、权益/余额、权益变化率,以及保证金相关字段除以 PrevBalance。时间特征把 2023→2024 的秒差当年周期做 sin,月线和周线则分别用 PeriodSeconds(PERIOD_MN1/W1) 做 cos,把日历节律编码进向量。外汇与贵金属回测请记住:这种合成特征仅反映历史样本分布,实盘大概率偏离,杠杆品种风险极高。

MQL5 / C++
  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">50;
      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;
        }
      Encoder.Clear();
      bLastEncoder.BufferInit(EmbeddingSize, class="num">0);
      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">//---
         if(!bLastEncoder.BufferWrite() ||
            !Encoder.feedForward((CBufferFloat*)GetPointer(bState), class="num">1, false, (CBufferFloat*)GetPointer(bLastEncoder)))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            Stop = true;
            break;
           }
         class="type">float PrevBalance = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">0];
         class="type">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">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_MN1);
         bAccount.Add((class="type">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);

「账户态编码与折扣奖励的目标构建」

这段逻辑把账户特征按日线周期做了正弦映射,再塞进编码器做状态嵌入。注意 x = time / PeriodSeconds(PERIOD_D1) 把时间戳换算成「天」为单位的浮点值,MathSin 用 2*M_PI*x 生成周期信号,x 为 0 时直接给 0 避免除零噪声。 奖励目标用 PrecoderBars 长度滚动计算:先取相邻两步 rewards 之差乘 DiscFactor 写成初值,再用 MathPow(DiscFactor, t) 做前向累加,最终 Sum(1) 按行求和、ArgMax 挑最大期望行。外汇与贵金属波动剧烈,这套折扣因子敏感性高,实盘前务必在 MT5 策略测试器里改 DiscFactor 看回测曲线漂移。 Actor 前向跑完拿 getResults,error = target - result 后做滚动标准差 STD_Actor:公式用 std_count 上限 999 做滑动更新,MathSqrt 内是旧方差加权加新误差平方再平均。check 用 MathAbs(error) 减 STD_Actor*STD_Multiplier 做异常截断,STD_Multiplier 调大可能让训练更鲁棒但也更迟钝。

MQL5 / C++
bAccount.Add((class="type">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">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(Encoder), -class="num">1, (CBufferFloat*)GetPointer(bAccount)))
   {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
   }
targets = matrix<class="type">float>::Zeros(PrecoderBars, NRewards);
result.Assign(Buffer[tr].States[i + class="num">1].rewards);
for(class="type">int t = class="num">0; t < PrecoderBars; t++)
   {
   target = result;
   result.Assign(Buffer[tr].States[i + t + class="num">2].rewards);
   target = target - result * DiscFactor;
   targets.Row(target, t);
   }
for(class="type">int t = class="num">1; t < PrecoderBars; t++)
   {
   target = targets.Row(t - class="num">1) + targets.Row(t) * MathPow(DiscFactor, t);
   targets.Row(target, t);
   }
result = targets.Sum(class="num">1);
class="type">ulong row = result.ArgMax();
target = targets.Row(row);
bGoal.AssignArray(target);
class=class="str">"cmt">//--- Actor
if(!Actor.feedForward((CNet *)GetPointer(StateEncoder), -class="num">1, (CBufferFloat*)GetPointer(bGoal)))
   {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
   }
target.Assign(Buffer[tr].States[i].action);
target.Clip(class="num">0, class="num">1);
Actor.getResults(result);
vector<class="type">float> error = target - result;
std_count = MathMin(std_count, class="num">999);
STD_Actor = MathSqrt((MathPow(STD_Actor, class="num">2) * std_count + MathPow(error, class="num">2)) / (std_count + class="num">1));
check = MathAbs(error) - STD_Actor * STD_Multiplier;

◍ 目标网络里的误差标准差截断

在强化学习式的 EA 训练循环里,Goal 网络的前向与反向传播靠一段条件判断来避免被异常误差带偏。当 check.Max() 大于 0 时,说明当前误差绝对值超过了滚动标准差乘以 STD_Multiplier 的阈值,才会把 CAGrad(error)+result 写回并触发 Actor 与 StateEncoder 的 backProp。 STD_Goal 的更新用了在线方差近似:STD_Goal = sqrt((STD_Goal^2 * std_count + error^2) / (std_count + 1)),且 std_count 被 MathMin 限制在 999 以内,意味着长期训练后旧样本权重会被压得很低,对近期突变更敏感。 下面这段是原文里 Goal 支路的核心代码片段,注意 feedForward 失败时直接置 Stop 并 break,训练循环就此中断而非重试: 如果 GetTickCount() 与 ticks 差值超过 500 毫秒,循环会顺手算一个 percent 进度量,用来在外层做耗时监控。外汇与贵金属行情下这类在线训练若放在实盘 tick 里跑,高波动可能让 STD_Goal 频繁越界,触发大量反向传播,务必先在 MT5 策略测试器里用历史数据验证计算开销。

MQL5 / C++
if(check.Max() > class="num">0)
  {
  Result.AssignArray(CAGrad(error) + result);
  if(!Actor.backProp(Result, (CBufferFloat *)GetPointer(bGoal), (CBufferFloat *)GetPointer(bGradient)) ||
     !StateEncoder.backPropGradient(GetPointer(bAccount), (CBufferFloat *)GetPointer(bGradient)))
    {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    Stop = true;
    break;
    }
  }
class=class="str">"cmt">//--- Goal
if(!Goal.feedForward((CNet *)GetPointer(Encoder), -class="num">1, (CBufferFloat*)NULL))
  {
  PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
  Stop = true;
  break;
  }
target=targets.Row(row);
result = target / (MathAbs(target) + FLT_EPSILON);
result = MathPow(vector<class="type">float>::Full(NRewards, class="num">2), result);
target = target * result;
Goal.getResults(result);
error = target - result;
std_count = MathMin(std_count, class="num">999);
STD_Goal = MathSqrt((MathPow(STD_Goal, class="num">2) * std_count + MathPow(error, class="num">2)) / (std_count + class="num">1));
std_count++;
check = MathAbs(error) - STD_Goal * STD_Multiplier;
if(check.Max() > class="num">0)
  {
  Result.AssignArray(CAGrad(error) + result);
  if(!Goal.backProp(Result, (CBufferFloat *)NULL, (CBufferFloat *)NULL))
    {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    Stop = true;
    break;
    }
  }
Encoder.getResults(result);
bLastEncoder.AssignArray(result);
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);

把双网络误差打印到图表与日志

这段片段负责在训练或推理循环里把 Actor 与 Goal 两个对象的近期平均误差输出出来,方便在 MT5 图表上直接盯进度。 先用 StringFormat 拼两行文本,%-14s 左对齐占 14 字符宽放名字("Actor"/"Goal"),%6.2f%% 把 percent 显示成保留两位小数带百分号,%15.8f 留 15 位宽、8 位小数放 getRecentAverageError() 的返回值。两行累加进 str 后由 Comment(str) 推到图表左上角。 随后 ticks = GetTickCount() 记录时刻,配合外层节流逻辑控制刷新频率,避免每 tick 都重绘。 循环结束后先 Comment("") 清掉图表文字,再用 PrintFormat 把函数名(__FUNCTION__)、行号(__LINE__)、对象名和误差以 %10.7f 七位小数打进专家日志;最后 ExpertRemove() 卸载 EA。外汇与贵金属市场高风险,这类误差数值仅反映模型拟合状态,不代表任何实盘收益可能。

MQL5 / C++
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", "Goal", percent, Goal.getRecentAverageError());
Comment(str);
ticks = GetTickCount();
}
}
}
Comment("");
class=class="str">"cmt">//---
PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Actor", Actor.getRecentAverageError());
PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Goal", Goal.getRecentAverageError());
ExpertRemove();
class=class="str">"cmt">//---
}

「EURUSD H1 上的训练与测试切分」

模型在 EURUSD、H1 周期上完成训练与验证。训练数据取自 2023 年前 7 个月历史,测试数据直接接在后面用 2023 年 8 月行情,避免未来函数泄漏。 训练分两段走。第一段按 Real-ORL 思路把真实信号验算写进训练集,再用 Research.mq5 配合随机策略补充样本;第二段用 StudyEncoder.mq5 训掩码自动编码器,验算结构一致,无需反复重构数据集,跑到达标即可停。 第二段训智能体策略与目标模型时采用迭代法:先训模型,再回灌新数据重训。作者原话提到过程比预期稳,训练与测试期都跑出了盈利策略——但外汇高杠杆品种,样本外 8 月单月盈利不代表稳健,复制前请在 MT5 用同周期重跑确认。

◍ 演示程序别直接丢进实盘

这套把训练拆成轨迹学习加独立政策学习两段的思路,重心放在把已观测趋势投射到未来状态的概率上,确实能抬高喂给决策模块的信息密度。我们在 MT5 里用 MQL5 把流程跑通,验证了方法在离线数据上的可落地性。 但必须点明:原文里所有脚本都只是技术演示,没有经过真实外汇或贵金属行情的压力测试。这类品种杠杆高、跳空频繁,直接挂实盘可能迅速放大回撤。 真要验证,先在策略测试器里用历史 Tick 跑一遍 CFPI 的离线回放,确认信号延迟和重构误差在你的容忍带内,再谈下一步。

收束

这套 LSTM 预测工程在 MT5 里落地时,实际拆成了 8 个程序文件:6 个 EA 与类库负责样本收集、策略训练、编码器训练和模型测试,另有两个 OpenCL 相关文件承担神经网络底层运算。从 Research.mq5 抓样本到 Test.mq5 跑验证,整条链路不依赖第三方框架,全在 MQL5 环境内闭环。 外汇与贵金属市场高杠杆、高波动,直接挂训练好的模型实盘前,建议先用策略测试器跑至少三年 tick 数据,重点看 Drawdown 与样本外衰减。ZIP 包 757.07 KB,解压后改一个品种参数就能复现基础实验。 写这套系统的作者公开了全部源码结构,但平台不对策略收益做任何担保。真要上手,先开 MT5 把 Trajectory.mqh 里的状态结构读一遍,比盲目调神经层数更有用。

常见问题

在批处理循环里每 N 步打印当前 batch 的均方误差与标准差,若标准差连续放大超过初始值 3 倍建议停训检查学习率。
在训练循环加断点保护:每完成一个 epoch 写本地权重快照并回显百分比,重启后从最近快照续训即可。
小布可接入你的训练日志,实时显示误差曲线与 epoch 进度,并在异常断开时提醒你从最近快照恢复。
在 OnInit 里读取模型元信息,校验输入神经元数、时间窗长度与账户特征维度,不一致直接返回 INIT_FAILED。
把余额、净值、持仓比归一化到 [0,1],与价格回报拼接成定长向量,再按模型输入维度补零或截断。