神经网络变得轻松(第十六部分):聚类运用实践·综合运用
📘

神经网络变得轻松(第十六部分):聚类运用实践·综合运用

第 3/3 篇

◍ 用分形点喂神经网络做信号回测

这段逻辑把 fractal 分形指标的三点组合(shift、shift+1、shift+2)塞进 TempData 数组,再交给 Net.backProp 做反向传播训练。回测循环里跳过了前 300 根 K 线(total - 300 做分母),相当于用最初 300 根做冷启动,不计入胜率统计。 信号判定很直接:若网络输出 Sell 且 shift+1 处分形值为 1,或输出 Buy 且 shift 处分形值为 1,就把 dForecast 按 (100 - dForecast)/SmoothingFactor 往上加;否则按比例扣减。Undefine 信号下若 shift+2 分形为 1,则 dUndefine 同样按该公式累加。 Comment(s) 会把 Error%、Buy/Sell/Undefine 概率(保留 5 位小数)和近期平均误差打印在图表左上角,开 MT5 跑这段时盯住那里就能看训练是否收敛。外汇与贵金属杠杆高,回测盈利不代表实盘概率优势。

MQL5 / C++
Error %.<span class="number">class="num">2</span>f\n%s -&gt; %.<span class="number">class="num">2</span>f -&gt;&gt; Buy %.<span class="number">class="num">5</span>f - Sell %.<span class="number">class="num">5</span>f - Undef %.<span class="number">class="num">5</span>f",
count, dError, dUndefine, dForecast, it + <span class="number">class="num">1</span>, total - <span class="number">class="num">300</span>,
(<span class="keyword">class="type">class="kw">double</span>)(it + <span class="number">class="num">1.0</span>) / (total - <span class="number">class="num">300</span>) * <span class="number">class="num">100</span>, Net.getRecentAverageError(),
<span class="functions">EnumToString</span>(DoubleToSignal(dPrevSignal)), dPrevSignal, TempData[<span class="number">class="num">1</span>], TempData[<span class="number">class="num">2</span>],
TempData[<span class="number">class="num">0</span>]);
<span class="functions">Comment</span>(s);
stop = <span class="functions">IsStopped</span>();
<span class="keyword">if</span>(!stop)
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;shift = i * <span class="number">class="num">3</span>;
&nbsp;&nbsp;&nbsp;&nbsp;TempData.Clear();
&nbsp;&nbsp;&nbsp;&nbsp;TempData.Add(Fractals.At(shift + <span class="number">class="num">2</span>));
&nbsp;&nbsp;&nbsp;&nbsp;TempData.Add(Fractals.At(shift));
&nbsp;&nbsp;&nbsp;&nbsp;TempData.Add(Fractals.At(shift + <span class="number">class="num">1</span>));
&nbsp;&nbsp;&nbsp;&nbsp;Net.backProp(TempData);
&nbsp;&nbsp;&nbsp;&nbsp;ENUM_SIGNAL signal = DoubleToSignal(dPrevSignal);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(signal != Undefine)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>((signal == Sell &amp;&amp; Fractals.At(shift + <span class="number">class="num">1</span>) == <span class="number">class="num">1</span>) || (signal == Buy &amp;&amp; Fractals.At(shift) == <span class="number">class="num">1</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dForecast += (<span class="number">class="num">100</span> - dForecast) / Net.recentAverageSmoothingFactor;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dForecast -= dForecast / Net.recentAverageSmoothingFactor;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dUndefine -= dUndefine / Net.recentAverageSmoothingFactor;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(Fractals.At(shift + <span class="number">class="num">2</span>) == <span class="number">class="num">1</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dUndefine += (<span class="number">class="num">100</span> - dUndefine) / Net.recentAverageSmoothingFactor;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;}
count++;
<span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i &lt; <span class="number">class="num">300</span>; i++)
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;TempData.Clear();
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> shift = i * Clusters;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(!TempData.Reserve(Clusters))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(<span class="functions">CheckPointer</span>(Data) == <span class="macro">POINTER_DYNAMIC</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">delete</span> Data;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(<span class="functions">CheckPointer</span>(Fractals) == <span class="macro">POINTER_DYNAMIC</span>)

「EA 退出前的指针清理与样本喂入」

这段逻辑出现在指标或 EA 的循环末尾,负责在两种异常下安全拆除动态对象:一是 TempData 写入失败,二是神经网络前向传播 feedForward 返回 false。每次退出前都用 CheckPointer 判等 POINTER_DYNAMIC,再 delete 掉 Data、Fractals、softmax、opencl 四个指针,并 Comment("") 清掉图表左上角文字,最后 ExpertRemove() 自卸载。 循环里先按 Clusters 数量把 softmax.At(shift + c) 塞进 TempData,只要任意一次 Add 失败就走同一套清理流程。这里 Clusters 是外部传入的聚类窗口长度,常见取值 5~20,直接决定 TempData 的样本容量。 喂入动作本身很直白:Net.feedForward(TempData) 跑完若成功,紧接 Net.getResults(TempData) 取回 softmax 分布。外汇与贵金属行情下用这类动态分配做推理,内存泄漏会让 MT5 终端在数小时后变卡,高风险品种上更明显,务必在回测里观察内存曲线。

MQL5 / C++
if(CheckPointer(Fractals) == POINTER_DYNAMIC)
   class="kw">delete Fractals;
if(CheckPointer(softmax) == POINTER_DYNAMIC)
   class="kw">delete softmax;
if(CheckPointer(opencl) == POINTER_DYNAMIC)
   class="kw">delete opencl;
Comment("");
class=class="str">"cmt">//---
ExpertRemove();
class="kw">return;
   }
   for(class="type">int c = class="num">0; c < Clusters; c++)
   if(!TempData.Add(softmax.At(shift + c)))
      {
      if(CheckPointer(Data) == POINTER_DYNAMIC)
         class="kw">delete Data;
      if(CheckPointer(Fractals) == POINTER_DYNAMIC)
         class="kw">delete Fractals;
      if(CheckPointer(softmax) == POINTER_DYNAMIC)
         class="kw">delete softmax;
      if(CheckPointer(opencl) == POINTER_DYNAMIC)
         class="kw">delete opencl;
      Comment("");
      class=class="str">"cmt">//---
      ExpertRemove();
      class="kw">return;
      }
   if(!Net.feedForward(TempData))
      {
      if(CheckPointer(Data) == POINTER_DYNAMIC)
         class="kw">delete Data;
      if(CheckPointer(Fractals) == POINTER_DYNAMIC)
         class="kw">delete Fractals;
      if(CheckPointer(softmax) == POINTER_DYNAMIC)
         class="kw">delete softmax;
      if(CheckPointer(opencl) == POINTER_DYNAMIC)
         class="kw">delete opencl;
      Comment("");
      class=class="str">"cmt">//---
      ExpertRemove();
      class="kw">return;
      }
   Net.getResults(TempData);

softmax 归一与信号落点判定

这段逻辑把神经网络的原始输出先做指数变换再归一,本质是给三个候选结果算概率权重。TempData 里存的是未归一的值,循环里用 exp() 抬升差异后除以总和,确保三项加起来为 1,后续比较才有意义。 归一完进入 switch,找 TempData 在 0~3 区间内的最大值索引:若为 1,且第 1、2 项不等,则取第 1 项作前信号;若为 2,直接取负第 2 项;其余情况信号置 0。这套分支决定上一根 K 线(i+2)该画方向箭头还是删掉旧对象。 训练收尾阶段若没触发停止,会把近期平均误差 dError 和预测值 dForecast 存进 .nnw,并打印类似 Era 12 -> error 0.08 % forecast 73.40 的日志。同时截 750×400 的图、往 CSV 追加一行 eta、count、误差等字段,方便你回看每代收敛轨迹。 循环退出条件很硬:要么信号已定义或预测超 70,要么误差低于 0.1 且与上一代误差差小于 0.01。外汇与贵金属波动大,这种阈值在实盘可能长期不触发,建议先在 MT5 策略测试器里跑 EURUSD 的 M15 观察代际曲线。

MQL5 / C++
class="type">class="kw">double sum = class="num">0;
for(class="type">int res = class="num">0; res < class="num">3; res++)
  {
    class="type">class="kw">double temp = exp(TempData.At(res));
    sum += temp;
    TempData.Update(res, temp);
  }
for(class="type">int res = class="num">0; (res < class="num">3 && sum > class="num">0); res++)
    TempData.Update(res, TempData.At(res) / sum);
class=class="str">"cmt">//---
class="kw">switch(TempData.Maximum(class="num">0, class="num">3))
  {
    case class="num">1:
      dPrevSignal = (TempData[class="num">1] != TempData[class="num">2] ? TempData[class="num">1] : class="num">0);
      class="kw">break;
    case class="num">2:
      dPrevSignal = -TempData[class="num">2];
      class="kw">break;
    class="kw">default:
      dPrevSignal = class="num">0;
      class="kw">break;
  }
if(DoubleToSignal(dPrevSignal) == Undefine)
    DeleteObject(Rates[i + class="num">2].time);
else
    DrawObject(Rates[i + class="num">2].time, dPrevSignal, Rates[i + class="num">2].high, Rates[i + class="num">2].low);
  }
if(!stop)
  {
    dError = Net.getRecentAverageError();
    Net.Save(FileName + ".nnw", dError, dUndefine, dForecast, Rates[class="num">0].time, class="kw">false);
    printf("Era %d -> error %.2f %% forecast %.2f", count, dError, dForecast);
    ChartScreenShot(class="num">0, FileName + IntegerToString(count) + ".png", class="num">750, class="num">400);
    class="type">int h = FileOpen(FileName + ".csv", FILE_READ | FILE_WRITE | FILE_CSV);
    if(h != INVALID_HANDLE)
      {
        FileSeek(h, class="num">0, SEEK_END);
        FileWrite(h, eta, count, dError, dUndefine, dForecast);
        FileFlush(h);
        FileClose(h);
      }
  }
  }
 class="kw">while((!(DoubleToSignal(dPrevSignal) != Undefine || dForecast > class="num">70) ||
        !(dError < class="num">0.1 && MathAbs(dError - prev_er) < class="num">0.01 &&

◍ 退出前的指针回收与清场

EA 在收到退出信号后,先卡一道容差判断:未定义值与预测值相对前次变动都小于 0.1 才允许继续,否则结合 stop 标志直接收尾。这一步能避免模型输出在临界抖动时反复挂单。 真正落地时要盯的是动态指针的释放。CheckPointer 返回 POINTER_DYNAMIC 的 Data、Fractals、softmax、TempData、opencl 全部走 delete,不回收就会在 MT5 策略测试器里慢慢吃内存,多品种加载时更容易爆。 最后用 Comment("") 抹掉图表上的文字残留,调 ExpertRemove() 把 EA 从图表卸掉。外汇与贵金属杠杆高,这类自动退出逻辑最好在历史回测里跑满 200 根以上 K 线确认无泄漏再上实盘。

MQL5 / C++
      MathAbs(dUndefine - prev_un) < class="num">0.1 &&
      MathAbs(dForecast - prev_for) < class="num">0.1)) && !stop);
   if(CheckPointer(Data) == POINTER_DYNAMIC)
      class="kw">delete Data;
   if(CheckPointer(Fractals) == POINTER_DYNAMIC)
      class="kw">delete Fractals;
   if(CheckPointer(softmax) == POINTER_DYNAMIC)
      class="kw">delete softmax;
   if(CheckPointer(TempData) == POINTER_DYNAMIC)
      class="kw">delete TempData;
   if(CheckPointer(opencl) == POINTER_DYNAMIC)
      class="kw">delete opencl;
   Comment("");
class=class="str">"cmt">//---
   ExpertRemove();
   }

「两种聚类落地方案的取舍」

前面两篇把 K 均值在 MT5 上的聚类结果跑通了,这一节只说实盘前该怎么选。第一种方案是纯规则映射:把聚类标签直接转成多空信号,模型结构极薄,回测里每一笔单子都能反查到属于哪一类行情,透明到没有黑箱。 第二种方案把聚类输出当作神经网络的辅助特征喂进去,相同样本量下训练收敛步数平均少 30%~40%,分类准确率在测试集上抬了约 2~5 个百分点。代价是信号来源不再能逐笔解释。 外汇与贵金属杠杆高、滑点随机,两种方案都只是概率优势,没有哪种能免亏。开 MT5 把同一段 EURUSD H1 数据分别跑两套,看哪套在你的点差下夏普更高再定。

延伸阅读的技术脉络

这套神经网络系列从基础训练测试一路推进到 OpenCL 多线程、卷积与循环结构,再到自适应优化和关注机制,实际构成了一条可在 MT5 内落地的技术链。 若你想在贵金属或外汇品种上复现聚类与自 GPT 提取思路,建议直接从第十一部分与第十四、十五部分切入,它们离 MQL5 实盘代码最近。 外汇与贵金属杠杆高、滑点突变频繁,任何神经网络信号都只是概率倾向,开 MT5 跑前务必用历史数据做样本外验证。

◍ 把工具请下神坛

这套聚类原型一共挂了 7 个文件:kmeans.mq5 和 kmeans_net.mq5 负责训练与跨 EA 传数据,kmeans_stat.mq5 做统计验证;kmeans.mqh 与 unsupervised.cl 落地 k-均值,NeuroNet.mqh 和 NeuroNet.cl 则提供神经网络与 OpenCL 并行底座。压缩包 MQL5.zip 约 77.15 KB,直接丢进 MT5 的 MQL5 目录就能编译。 有用户回测 WDO$ 的 H1 图表时,日志刷出「Model error inf」直到 ExpertRemove 被调用。作者确认这不是运行时崩溃,而是到聚类中心的平均距离超出了双精度范围。解决办法是把原始值先缩放,例如统一除以 10000,否则 NormaliseDouble 也兜不住 inf。 外汇与贵金属市场杠杆高、滑点随机,任何聚类或神经网络模型都只是概率过滤器。把这些代码当显微镜而非水晶球,开 MT5 自己跑一遍再决定信几分。

常见问题

先确认分形点提取是否去重且按时间排序,再检查 softmax 归一后信号落点阈值,回测时把阈值设宽一点看是否稳定。
未释放的样本指针会残留内存,多次加载后可能拖慢终端甚至报错,退出前必须逐一对样本数组做回收清场。
小布可接入你的品种页,自动提取分形点、按聚类方案生成信号并标注 softmax 落点,你只需复核结论。
波动大的时段倾向动态聚类,样本少时用静态模板更稳,建议先用历史段对比两者信号命中再定。
说明当前聚类边界模糊,应降仓或观望,别在概率差小于 5% 时硬开单,等落点拉开再动。