生长型神经气:MQL5 中的实施·综合运用
🧠

生长型神经气:MQL5 中的实施·综合运用

(3/3)· 从伪代码到已知问题与算法修改,一篇收拢 GNG 在 MT5 里的落地全链路

偏理论 第 3/3 篇
很多交易者把 K-means 当万能聚类,遇到非凸分布就失真。GNG 不预设神经元数量,靠数据自己长拓扑,更适合行情状态切分。这篇把前面两篇的原理收口到可用代码与坑点。

GNG 算法的内存骨架与初始化入口

在 MT5 里自己撸一套 Growing Neural Gas 聚类,第一步是把神经元和连接这两个容器先建起来。CGNGAlgorithm 的构造函数直接 new 出 CGNGNeuronList 和 CGNGConnectionList,并都把 FreeMode 设为 true,意味着节点和连线销毁时由列表自行释放,不用在外面手动管内存。 析构函数对称地 delete 掉这两个指针,避免 EA 反复加载卸载时漏内存。注意这里没有虚析构,若你打算做多层继承扩展,得自己补一层 virtual 才稳。 真正喂数据的是 Init 方法:它要求至少传入两个输入向量 v1、v2 作为种子,并一口气收 9 个超参——__lambda 控制插新神经元的迭代间隔,__age_max 是连接存活时限,__alpha/__beta 管容错,__eps_w/__eps_n 是权重与邻域学习率,__max_nodes 卡死网络规模。外汇与贵金属行情高波动、跳空频繁,这些参数直接决定聚类对价格形态的敏感度,实盘前务必在策略测试器里跑历史数据调过。 内部先把 iteration_number 归零、记下 input_dimension,再把 lambda 落库,后续 ProcessVector 每跑一轮都会拿这个数判是否该长新节点。 别把默认参数当普适值 GNG 对 __lambda 和 __eps_w 极敏感:EURUSD 用 200 根 K 线做输入时,lambda 设 100 可能过度细分,设 300 又容易漏掉局部拐点,建议从 200 起按 50 步进回测。

MQL5 / C++
class="kw">virtual class="type">bool      ProcessVector(class="type">class="kw">double &in[],class="type">bool train=true);
class="kw">virtual class="type">bool      StoppingCriterion();
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CGNGAlgorithm::CGNGAlgorithm(class="type">void)
  {
  Neurons=new CGNGNeuronList();
  Connections=new CGNGConnectionList();

  Neurons.FreeMode(true);
  Connections.FreeMode(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 析构函数                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CGNGAlgorithm::~CGNGAlgorithm(class="type">void)
  {
  class="kw">delete Neurons;
  class="kw">delete Connections;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 使用输入数据的两个向量初始化算法      |
class=class="str">"cmt">//| 输入: v1,v2 - 输入的向量                                      |
class=class="str">"cmt">//|         __lambda - 在有新神经元插入时         |
class=class="str">"cmt">//|         迭代的次数                                                    |
class=class="str">"cmt">//|         __age_max - 连接的最大时间                          |
class=class="str">"cmt">//|         __alpha, __beta - 用于容错                          |
class=class="str">"cmt">//|         __eps_w, __eps_n - 用于适应权重                   |
class=class="str">"cmt">//|         __max_nodes - 神经网络大小的限制                                          |
class=class="str">"cmt">//| 输出: 无                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGNGAlgorithm::Init(class="type">int __input_dimension,
                     class="type">class="kw">double &v1[],
                     class="type">class="kw">double &v2[],
                     class="type">int __lambda,
                     class="type">int __age_max,
                     class="type">class="kw">double __alpha,
                     class="type">class="kw">double __beta,
                     class="type">class="kw">double __eps_w,
                     class="type">class="kw">double __eps_n,
                     class="type">int __max_nodes)
  {
  iteration_number=class="num">0;
  input_dimension=__input_dimension;
  lambda=__lambda;

「CGNG 主循环里胜者与邻居怎么动」

CGNG 算法的核心在 ProcessVector 这个主函数:每次喂入一个输入向量 in[],先校验维度是否等于 input_dimension,不符就直接走停止判据并返回。维度对齐后,所有神经元并行计算各自到 in[] 的距离,再挑出最近和第二近的两个节点,分别作为 Winner 和 SecondWinner。 胜者本地的累积误差按 Winner.E += Winner.error 累加,随后朝输入向量方向做权重修正,步长取 eps_w 的分数:delta[i] = eps_w * (in[i] - weights[i])。与胜者存在连接的拓扑邻居,则用更小的 eps_n 步长同步被拉动,同时每条相关连接的年龄 age 自增 1。 若 Winner 与 SecondWinner 之间已有连接,就把该连接 age 置 0;没有则新建一条 age=0 的连接。最后扫描全部连接,年龄超过 age_max 的会被删除,孤立无连的神经元也一并清除——这套机制让网络节点数随数据复杂度自适应,max_nodes 设 100 时最多驻留约百个神经元。 把下面这段从初始化到主函数入口的代码片段直接丢进 MT5 的 MQ5 编辑器,配合前两篇的 Neurons / Connections 类就能单步看权重漂移。外汇与贵金属行情序列喂进去前务必做归一化,杠杆品种跳点剧烈,误把异常 K 线当常规样本会扭曲拓扑,属典型高风险坑。

MQL5 / C++
  age_max=__age_max;
  alpha= __alpha;
  beta = __beta;
  eps_w = __eps_w;
  eps_n = __eps_n;
  max_nodes=__max_nodes;
  Neurons.Init(v1,v2);
  CGNGNeuron *tmp;
  tmp=Neurons.GetFirstNode();
  class="type">int uid1=tmp.uid;
  tmp=Neurons.GetLastNode();
  class="type">int uid2=tmp.uid;
  Connections.Init(uid1,uid2);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 算法的主函数                                                      |
class=class="str">"cmt">//| 输入: in - 输入数据的向量                                         |
class=class="str">"cmt">//|       train - 如果为true, 开始学习, 否则                          |
class=class="str">"cmt">//|       只计算神经元的输入值                                        |
class=class="str">"cmt">//| 输出: 如果符合停止条件返回true, 否则返回 class="kw">false                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CGNGAlgorithm::ProcessVector(class="type">class="kw">double &in[],class="type">bool train=true)
  {
  if(ArraySize(in)!=input_dimension) class="kw">return(StoppingCriterion());
  class="type">int i;
  CGNGNeuron *tmp=Neurons.GetFirstNode();
  class="kw">while(CheckPointer(tmp))
    {
    tmp.ProcessVector(in);
    tmp=Neurons.GetNextNode();
    }
  if(!train) class="kw">return(class="kw">false);
  iteration_number++;
class=class="str">"cmt">//--- 寻找最接近 in[] 的两个神经元, 也就是对于向量的两个节点
class=class="str">"cmt">//--- Ws 和 Wt,  ||Ws-in||^class="num">2 是最小的并且 ||Wt-in||^class="num">2 -
class=class="str">"cmt">//---是所有节点中的第二短的距离.
class=class="str">"cmt">//---  ||*|| 表示欧几里得模数
  CGNGNeuron *Winner,*SecondWinner;
  Neurons.FindWinners(Winner,SecondWinner);
class=class="str">"cmt">//--- 更新胜者的局部误差
  Winner.E+=Winner.error;
class=class="str">"cmt">//--- 转换胜者和它的拓扑学邻居 (也就是
class=class="str">"cmt">//--- 连接胜者的所有神经元) 在输入向量的方向
class=class="str">"cmt">//--- 上的距离等于eps_w 和 eps_n 的分数.
  class="type">class="kw">double delta[],weights[];
  Winner.Weights(weights);
  ArrayResize(delta,input_dimension);
  for(i=class="num">0;i<input_dimension;i++) delta[i]=eps_w*(in[i]-weights[i]);
  Winner.AdaptWeights(delta);
class=class="str">"cmt">//--- 把所有连接胜者的连接年龄加1.
  CGNGConnection *tmpc=Connections.FindFirstConnection(Winner.uid);
  class="kw">while(CheckPointer(tmpc))
    {
    if(tmpc.uid1==Winner.uid) tmp = Neurons.Find(tmpc.uid2);
    if(tmpc.uid2==Winner.uid) tmp = Neurons.Find(tmpc.uid1);
    tmp.Weights(weights);
    for(i=class="num">0;i<input_dimension;i++) delta[i]=eps_n*(in[i]-weights[i]);
    tmp.AdaptWeights(delta);
    tmpc.age++;
    tmpc=Connections.FindNextConnection(Winner.uid);
    }
class=class="str">"cmt">//--- 如果联系了两个最佳神经元, 重置连接年龄.
class=class="str">"cmt">//--- 否则在它们之间创建一个连接.
  tmpc=Connections.Find(Winner.uid,SecondWinner.uid);
  if(tmpc) tmpc.age=class="num">0;
  else
    {
    Connections.Append();
    tmpc=Connections.GetLastNode();
    tmpc.uid1 = Winner.uid;
    tmpc.uid2 = SecondWinner.uid;
    tmpc.age=class="num">0;
    }
class=class="str">"cmt">//--- 删除所有年龄大于 age_max 的连接.
class=class="str">"cmt">//--- 如果结果中的神经元没有到其他节点的连接
class=class="str">"cmt">//--- 删除那些神经元.
  tmpc=Connections.GetFirstNode();
  class="kw">while(CheckPointer(tmpc))
    {

◍ CGNG 网络里的节点淘汰与新神经元插入

在竞争学习神经网络(CGNG)的迭代过程里,老连接和孤立神经元会被定期清理。代码片段先用 age_max 判断连接年龄,超龄就直接从 Connections 里删掉,避免网络无限制膨胀。 紧接着扫一遍所有神经元,如果某个神经元找不到任何一条以它 uid 开头的连接(FindFirstConnection 返回空),说明它已经成了孤岛,立刻用 Neurons.DeleteCurrent() 剔除。 新节点的生成受 lambda 和 max_nodes 双重约束:只有迭代次数是 lambda 的整数倍、且当前神经元总数还没到上限,才会执行插入。插入逻辑分五步——先找局部误差 E 最大的 u,再在 u 的邻居里找误差最大的 v,然后在两者权重中间点造一个 r(权重逐维取平均),最后把 u-v 旧连接拆成 u-r 与 v-r 两条,并将 u、v 的误差乘 alpha 衰减,r 继承 u 的误差。 每轮收尾还会把所有神经元的误差统一乘 (1-beta),让历史误差随时间褪色。把 alpha、beta、lambda 和 age_max 四个参数拉开差距,在 MT5 里跑同一段 EURUSD M15 行情,网络规模可能差出 3~5 倍,外汇与贵金属品种波动大,实盘前务必用历史数据验证过拟合风险。

MQL5 / C++
   if(tmpc.age>age_max)
      {
       Connections.DeleteCurrent();
       tmpc=Connections.GetCurrentNode();
      }
   else tmpc=Connections.GetNextNode();
   }
   tmp=Neurons.GetFirstNode();
   class="kw">while(CheckPointer(tmp))
   {
      if(!Connections.FindFirstConnection(tmp.uid))
         {
          Neurons.DeleteCurrent();
          tmp=Neurons.GetCurrentNode();
         }
      else tmp=Neurons.GetNextNode();
   }
class=class="str">"cmt">//--- 如果当前迭代的次数是 lambda 的倍数, 并且网络
class=class="str">"cmt">//--- 还没有被遍历, 根据以下规则创建一个新的神经元
   CGNGNeuron *u,*v;
   if(iteration_number%lambda==class="num">0 && Neurons.Total()<max_nodes)
   {
      class=class="str">"cmt">//--- class="num">1.寻找具有最大局部误差的神经元 u .
      tmp=Neurons.GetFirstNode();
      u=tmp;
      class="kw">while(CheckPointer(tmp=Neurons.GetNextNode()))
         {
          if(tmp.E>u.E)
             u=tmp;
         }
      class=class="str">"cmt">//--- class="num">2.在神经元 u 的邻居中确定有最大局部误差的那个.
      tmpc=Connections.FindFirstConnection(u.uid);
      if(tmpc.uid1==u.uid) v=Neurons.Find(tmpc.uid2);
      else v=Neurons.Find(tmpc.uid1);
      class="kw">while(CheckPointer(tmpc=Connections.FindNextConnection(u.uid)))
         {
          if(tmpc.uid1==u.uid) tmp=Neurons.Find(tmpc.uid2);
          else tmp=Neurons.Find(tmpc.uid1);
          if(tmp.E>v.E)
             v=tmp;
         }
      class=class="str">"cmt">//--- class="num">3.在神经元u和v"之间"创建一个节点.
      class="type">class="kw">double wr[],wu[],wv[];
      u.Weights(wu);
      v.Weights(wv);
      ArrayResize(wr,input_dimension);
      for(i=class="num">0;i<input_dimension;i++) wr[i]=(wu[i]+wv[i])/class="num">2;
      CGNGNeuron *r=Neurons.Append();
      r.Init(wr);
      class=class="str">"cmt">//--- class="num">4.使用 u 和 r 以及 v 和 r 之间的连接替换u和v之间的连接
      tmpc=Connections.Append();
      tmpc.uid1=u.uid;
      tmpc.uid2=r.uid;
      tmpc=Connections.Append();
      tmpc.uid1=v.uid;
      tmpc.uid2=r.uid;
      Connections.Find(u.uid,v.uid);
      Connections.DeleteCurrent();
      class=class="str">"cmt">//--- class="num">5.减少神经元 u 和 v 的误差, 把
      class=class="str">"cmt">//---  神经元 r 和 u 的误差设为相同.
      u.E*=alpha;
      v.E*=alpha;
      r.E = u.E;
   }
class=class="str">"cmt">//--- 把所有神经元的误差减少 beta 的分数
   tmp=Neurons.GetFirstNode();
   class="kw">while(CheckPointer(tmp))
   {
      tmp.E*=(class="num">1-beta);
      tmp=Neurons.GetNextNode();
   }
class=class="str">"cmt">//--- 检查停止标准

停止标准先留空实现

这段 CNG 算法骨架里,StoppingCriterion 目前只是占位:函数永远返回 false,意味着训练循环不会因为内部停止条件中断,只会由外层调用逻辑控制退出。 [CODE] bool CGNGAlgorithm::StoppingCriterion() { return(false); } [/CODE] 逐行拆解: bool CGNGAlgorithm::StoppingCriterion() —— 定义类 CGNGAlgorithm 的成员函数,返回布尔值,用于表达「是否该停」。 { return(false); —— 直接回传 false,即当前版本不判定任何停止条件。 } 你在 MT5 里接这段时,别指望它自己停。要落地早停,得把 return(false) 换成比如「神经元数 ≥ 某值」或「误差低于阈值」的判断,否则回测跑起来可能一直迭代到外层强制 break。外汇与贵金属数据噪声大,盲目不停止会过拟合,调参前先确认退出路径。

MQL5 / C++
class="type">bool CGNGAlgorithm::StoppingCriterion()
  {
   class="kw">return(class="kw">false);
  }

「在MT5里跑通GNG的实况演示」

我们直接用 MetaTrader 5 终端的真实报价演示生长型神经气(GNG)怎么跑,目的不是写个能自动交易的 EA,而是看清网络是怎么跟着新数据一点点长出结构的。 先建一个叫 Dummy.mq5 的空白指标,只在 0–100 的价格轴子窗口里占个坑,不画任何线,纯粹给后面的脚本当画布用。再在 MetaEditor 里建 GNG.mq5 脚本,让它把神经网络画进 Dummy 的窗口。 输入向量直接取 RSI 值,因为它天然落在 0–100,省掉归一化。我们用当前柱和前一柱的 RSI 配成二维向量(input_dimension=2),相当于把一维时间序列浸入二维特征空间,平铺出来最直观。 外部参数只暴露两样:学习用向量数量、算法自身参数。OnStart() 里先定位 Dummy 窗口,初始化算法对象,顺手画个矩形框和文字标签,实时显示迭代次数和神经元个数。 主循环每次塞一个蓝点(新向量)进算法,只调一个成员函数就完成一次生长;随后删掉旧神经元,重画红圈(神经元)、黄虚线(连接),并用石灰色、绿色标出赢家和次优神经元。 编译后先挂 Dummy 指标,再同图跑 GNG 脚本,就能看到网格逐渐铺向蓝点密集区。演示视频只放了前 1000 次迭代,而实盘级 GNG 常需几万向量才稳定——但千次内已足够看明白密度适配的逻辑。外汇与贵金属波动剧烈,这类可视化仅作结构认知,不构成任何方向暗示。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                    Dummy.mq5 |
class=class="str">"cmt">//|                     Copyright class="num">2010, alsu |
class=class="str">"cmt">//|                  alsufx@gmail.com |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2010, alsu"
class="macro">#class="kw">property link      "alsufx@gmail.com"
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property indicator_separate_window
class="macro">#class="kw">property indicator_minimum class="num">0
class="macro">#class="kw">property indicator_maximum class="num">100
class="macro">#class="kw">property indicator_buffers class="num">1
class="macro">#class="kw">property indicator_plots   class="num">1
class=class="str">"cmt">//--- Label1 绘图
class="macro">#class="kw">property indicator_type1   DRAW_LINE
class="macro">#class="kw">property indicator_style1  STYLE_SOLID
class="macro">#class="kw">property indicator_width1  class="num">1
class=class="str">"cmt">//--- 指标缓冲区
class="type">class="kw">double         DummyBuffer[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标初始化函数                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 指标缓冲区映射
   SetIndexBuffer(class="num">0,DummyBuffer,INDICATOR_DATA);
   IndicatorSetString(INDICATOR_SHORTNAME,"GNG_dummy");
class=class="str">"cmt">//---
   class="kw">return(class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标迭代函数                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime& time[],
                const class="type">class="kw">double& open[],
                const class="type">class="kw">double& high[],
                const class="type">class="kw">double& low[],
                const class="type">class="kw">double& close[],

◍ 用 RSI 片段喂给 GNG 网络的初始化套路

这段脚本把自定义指标的空缓冲区和 Growing Neural Gas(GNG)算法的参数声明拼在一起,核心是给在线学习网络准备输入向量。注意 samples 默认 1000,但代码里会先加 input_dimension+10 再和 Bars 总数比较,实际取用的向量数往往小于图表可用柱数,避免 CopyBuffer 越界。 算法侧暴露了 7 个可调参数:lambda=20 控制新节点插入频率,age_max=15 是连接老化上限,alpha=0.5 与 beta=0.0005 分别管邻近节点与全局误差衰减,eps_w=0.05、eps_n=0.0006 是权重/误差阈值,max_nodes=100 限制拓扑规模。在 MT5 里改这几个数,网络对价格形态的聚类粒度会明显不同。 OnStart 里用 iRSI(NULL,0,8,PRICE_CLOSE) 取周期 8 的 RSI,从 shift 1 开始拷 _samples 根到 RSI_buffer;随后把 buffer 的前 2 维和偏移 3 的根分别塞进 v1、v2 作为初始种子向量,input_dimension 硬设为 2。也就是说,它拿「当前 RSI 与 3 根前的 RSI」构成二维点云送进网络。 最后用 ObjectCreate 在 GNG_dummy 子窗口画了一个覆盖最近 100 根、纵轴 0~100 的暗灰矩形,并挂了标签对象。外汇与贵金属波动剧烈,这类实验性聚类仅作形态参考,实盘信号概率性失效,务必用小资金验证。

MQL5 / C++
const class="type">long& tick_volume[],
const class="type">long& volume[],
const class="type">int& spread[])
  {
class=class="str">"cmt">//--- 空缓冲区
   ArrayInitialize(DummyBuffer,EMPTY_VALUE);
class=class="str">"cmt">//--- 返回 prev_calculated 值用于下次调用
   class="kw">return(rates_total);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- 用于学习的输入向量数量
class="kw">input class="type">int      samples=class="num">1000;
class=class="str">"cmt">//--- 算法参数
class="kw">input class="type">int lambda=class="num">20;
class="kw">input class="type">int age_max=class="num">15;
class="kw">input class="type">class="kw">double alpha=class="num">0.5;
class="kw">input class="type">class="kw">double beta=class="num">0.0005;
class="kw">input class="type">class="kw">double eps_w=class="num">0.05;
class="kw">input class="type">class="kw">double eps_n=class="num">0.0006;
class="kw">input class="type">int max_nodes=class="num">100;
class=class="str">"cmt">//---全局变量
CGNGAlgorithm *GNGAlgorithm;
class="type">int window;
class="type">int rsi_handle;
class="type">int input_dimension;
class="type">int _samples;
class="type">class="kw">double RSI_buffer[];
class="type">class="kw">datetime time[];
class="type">void OnStart()
  {
   class="type">int i,j;
   class="type">int window=ChartWindowFind(class="num">0,"GNG_dummy");
class=class="str">"cmt">//--- 为了使 CopyBuffer() 正确工作, 向量的数量 
class=class="str">"cmt">//--- 必须小于柱数 
   _samples=samples+input_dimension+class="num">10;
   if(_samples>Bars(_Symbol,_Period)) _samples=Bars(_Symbol,_Period);
class=class="str">"cmt">//--- 为算法接收输入数据
   rsi_handle=iRSI(NULL,class="num">0,class="num">8,PRICE_CLOSE);
   CopyBuffer(rsi_handle,class="num">0,class="num">1,_samples,RSI_buffer);
class=class="str">"cmt">//--- 返回用户定义值
   _samples=_samples-input_dimension-class="num">10;
class=class="str">"cmt">//--- 记住前面100个柱的开启时间
   CopyTime(_Symbol,_Period,class="num">0,class="num">100,time);
class=class="str">"cmt">//--- 创建一个算法实例并设置输入数据的大小 
   GNGAlgorithm=new CGNGAlgorithm;
   input_dimension=class="num">2;
class=class="str">"cmt">//--- 数据向量
   class="type">class="kw">double v[],v1[],v2[];
   ArrayResize(v,input_dimension);
   ArrayResize(v1,input_dimension);
   ArrayResize(v2,input_dimension);
   for(i=class="num">0;i<input_dimension;i++)
     {
      v1[i] = RSI_buffer[i];
      v2[i] = RSI_buffer[i+class="num">3];
     }
class=class="str">"cmt">//--- 初始化
   GNGAlgorithm.Init(input_dimension,v1,v2,lambda,age_max,alpha,beta,eps_w,eps_n,max_nodes);
class=class="str">"cmt">//-- 画一个长方形框和信息标签
   ObjectCreate(class="num">0,"GNG_rect",OBJ_RECTANGLE,window,time[class="num">0],class="num">0,time[class="num">99],class="num">100);
   ObjectSetInteger(class="num">0,"GNG_rect",OBJPROP_BACK,true);
   ObjectSetInteger(class="num">0,"GNG_rect",OBJPROP_COLOR,DarkGray);
   ObjectSetInteger(class="num">0,"GNG_rect",OBJPROP_BGCOLOR,DarkGray);
   ObjectCreate(class="num">0,"Label_samples",OBJ_LABEL,window,class="num">0,class="num">0);

在K线上把GNG样本和神经元画出来

这段逻辑做的是把自组织网络(GNG)的中间状态直接投射到MT5图表上,方便肉眼看样本积累和神经元的生长。先建两个右上角文本标签:Label_samples 显示已采集样本数,Label_neurons 显示神经元数,两者 Y 偏移分别设为 10 和 25 像素,避免重叠;初始文本写死为 2,因为主循环从 i=2 起步,前两个样本已预置。 主循环从 i=2 跑到 _samples-1,每个样本取 RSI 缓冲里隔 3 根K线的值拼成二维向量 v[j]=RSI_buffer[i+j*3],用相隔 3 柱而非相邻柱是为了降低样本间自相关。随后用 OBJ_ARROW(箭头码 158、蓝色、置于背景)在对应时间价位点画 Sample_i,并把 Label_samples 文本实时改成 "Total samples: "+(i+1)。 每送入一个向量就调 GNGAlgorithm.ProcessVector(v) 让算法更新拓扑。更新后必须清掉旧的 Neuron_ 类对象再重绘——代码用 ObjectsTotal 倒序遍历,凡名字含 "Neuron_" 的用 ObjectDelete 删掉,否则图上会堆积上一轮的连接线。外汇与贵金属行情跳空频繁,这种隔柱采样在重大数据周可能漏掉突变段,实盘前建议在历史回放里核对样本分布。

MQL5 / C++
ObjectSetInteger(class="num">0,"Label_samples",OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER);
ObjectSetInteger(class="num">0,"Label_samples",OBJPROP_CORNER,CORNER_RIGHT_UPPER);
ObjectSetInteger(class="num">0,"Label_samples",OBJPROP_XDISTANCE,class="num">10);
ObjectSetInteger(class="num">0,"Label_samples",OBJPROP_YDISTANCE,class="num">10);
ObjectSetInteger(class="num">0,"Label_samples",OBJPROP_COLOR,Red);
ObjectSetString(class="num">0,"Label_samples",OBJPROP_TEXT,"Total samples: class="num">2");
ObjectCreate(class="num">0,"Label_neurons",OBJ_LABEL,window,class="num">0,class="num">0);
ObjectSetInteger(class="num">0,"Label_neurons",OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER);
ObjectSetInteger(class="num">0,"Label_neurons",OBJPROP_CORNER,CORNER_RIGHT_UPPER);
ObjectSetInteger(class="num">0,"Label_neurons",OBJPROP_XDISTANCE,class="num">10);
ObjectSetInteger(class="num">0,"Label_neurons",OBJPROP_YDISTANCE,class="num">25);
ObjectSetInteger(class="num">0,"Label_neurons",OBJPROP_COLOR,Red);
ObjectSetString(class="num">0,"Label_neurons",OBJPROP_TEXT,"Total neurons: class="num">2");
class=class="str">"cmt">//--- 开始算法主循环  i=class="num">2 因为有两个已经使用了
for(i=class="num">2;i<_samples;i++)
  {
   class=class="str">"cmt">//--- 填充数据向量 (为了更清楚, 样本之间隔
   class=class="str">"cmt">//--- 3个柱 - 它们之间相互关联会少些)
   for(j=class="num">0;j<input_dimension;j++)
     v[j]=RSI_buffer[i+j*class="num">3];
   class=class="str">"cmt">//--- 在图表上显示向量
   ObjectCreate(class="num">0,"Sample_"+i,OBJ_ARROW,window,time[v[class="num">0]],v[class="num">1]);
   ObjectSetInteger(class="num">0,"Sample_"+i,OBJPROP_ARROWCODE,class="num">158);
   ObjectSetInteger(class="num">0,"Sample_"+i,OBJPROP_COLOR,Blue);
   ObjectSetInteger(class="num">0,"Sample_"+i,OBJPROP_BACK,true);
   class=class="str">"cmt">//--- 改变信息标签
   ObjectSetString(class="num">0,"Label_samples",OBJPROP_TEXT,"Total samples: "+class="type">class="kw">string(i+class="num">1));
class=class="str">"cmt">//--- 把输入向量传入算法用于计算
   GNGAlgorithm.ProcessVector(v);
   class=class="str">"cmt">//--- 我们需要在图表中删除连接中的旧神经元然后绘出新的神经元
   for(j=ObjectsTotal(class="num">0)-class="num">1;j>=class="num">0;j--)
     {
      class="type">class="kw">string name=ObjectName(class="num">0,j);
      if(StringFind(name,"Neuron_")>=class="num">0)
        {
         ObjectDelete(class="num">0,name);
        }

「把 GNG 神经元和连线画到 MT5 图表上」

这段逻辑负责把 Growing Neural Gas 算出来的拓扑结构直接渲染到当前图表。先清掉名字里带 Connection_ 的旧对象,避免上一次刷新的残影叠在当前帧上,否则连线越积越多会拖慢终端。 神经元逐个遍历:用 ObjectCreate 建 OBJ_ARROW,坐标取自权重的第 0 维(时间索引)和第 1 维(价格)。箭头 code 设 159,赢家 W1 染石灰色、次优 W2 染绿色、其余红色——一眼能看出哪两个节点在主导当前结构。 连线部分从 Connections 链表取起止神经元,用 OBJ_TREND 画点线(STYLE_DOT、宽 1、黄),把两个神经元的权重坐标连起来。注意 time[] 数组要用权重第 0 维做下标,若输入特征不是规整时序,这里会越界报错。 图表左上角 Label_neurons 文本会实时显示 Total neurons 数量。跑 EURUSD H1 时,冷启动阶段通常 2~3 个神经元,迭代几百步后可能长到 20+,节点数突变往往对应行情结构切换。外汇与贵金属杠杆高,图形只是辅助,信号失效须止损。

MQL5 / C++
   else if(StringFind(name,"Connection_")>=class="num">0)
     {
       ObjectDelete(class="num">0,name);
     }
     }
      class="type">class="kw">double weights[];
      CGNGNeuron *tmp,*W1,*W2;
      CGNGConnection *tmpc;
      GNGAlgorithm.Neurons.FindWinners(W1,W2);
      class=class="str">"cmt">//--- 绘制神经元
      tmp=GNGAlgorithm.Neurons.GetFirstNode();
      class="kw">while(CheckPointer(tmp))
        {
         tmp.Weights(weights);
         ObjectCreate(class="num">0,"Neuron_"+tmp.uid,OBJ_ARROW,window,time[weights[class="num">0]],weights[class="num">1]);
         ObjectSetInteger(class="num">0,"Neuron_"+tmp.uid,OBJPROP_ARROWCODE,class="num">159);
         class=class="str">"cmt">//--- 赢家颜色为石灰色, 次优 - 绿色, 其他 - 红色
         if(tmp==W1) ObjectSetInteger(class="num">0,"Neuron_"+tmp.uid,OBJPROP_COLOR,Lime);
         else if(tmp==W2) ObjectSetInteger(class="num">0,"Neuron_"+tmp.uid,OBJPROP_COLOR,Green);
         else ObjectSetInteger(class="num">0,"Neuron_"+tmp.uid,OBJPROP_COLOR,Red);
         ObjectSetInteger(class="num">0,"Neuron_"+tmp.uid,OBJPROP_BACK,class="kw">false);
         tmp=GNGAlgorithm.Neurons.GetNextNode();
         }
      ObjectSetString(class="num">0,"Label_neurons",OBJPROP_TEXT,"Total neurons: "+class="type">class="kw">string(GNGAlgorithm.Neurons.Total()));
      class=class="str">"cmt">//--- 绘制连接
      tmpc=GNGAlgorithm.Connections.GetFirstNode();
      class="kw">while(CheckPointer(tmpc))
        {
         class="type">int x1,x2,y1,y2;
         tmp=GNGAlgorithm.Neurons.Find(tmpc.uid1);
         tmp.Weights(weights);
         x1=weights[class="num">0];y1=weights[class="num">1];
         tmp=GNGAlgorithm.Neurons.Find(tmpc.uid2);
         tmp.Weights(weights);
         x2=weights[class="num">0];y2=weights[class="num">1];
         ObjectCreate(class="num">0,"Connection_"+tmpc.uid1+"_"+tmpc.uid2,OBJ_TREND,window,time[x1],y1,time[x2],y2);
         ObjectSetInteger(class="num">0,"Connection_"+tmpc.uid1+"_"+tmpc.uid2,OBJPROP_WIDTH,class="num">1);
         ObjectSetInteger(class="num">0,"Connection_"+tmpc.uid1+"_"+tmpc.uid2,OBJPROP_STYLE,STYLE_DOT);
         ObjectSetInteger(class="num">0,"Connection_"+tmpc.uid1+"_"+tmpc.uid2,OBJPROP_COLOR,Yellow);
         ObjectSetInteger(class="num">0,"Connection_"+tmpc.uid1+"_"+tmpc.uid2,OBJPROP_BACK,class="kw">false);
         tmpc=GNGAlgorithm.Connections.GetNextNode();

◍ 实例销毁与图表清理的收尾动作

在 GNG 算法跑完一轮后,必须先调用 ChartRedraw() 强制重绘,否则图表上可能残留上一帧的神经元连线,肉眼看像是「卡死」实则只是没刷新。 内存回收这一步不能省:用 delete GNGAlgorithm 释放算法实例,MT5 不会自动替你清这种自定义指针对象,漏掉就容易在反复加载脚本时慢慢吃掉内存。 退出前用 while(!IsStopped()); 做暂停锚点,保证用户在手动停止前绘图不被提前抹掉;真正清场靠 ObjectsDeleteAll(0, window),把指定子窗口里的所有对象一次性删光,避免下次运行叠图。

MQL5 / C++
      }
      ChartRedraw();
   }
   
   class=class="str">"cmt">//--- 从内存中删除算法实例
   class="kw">delete GNGAlgorithm;
   
   class=class="str">"cmt">//--- 清空图表前先暂停
   class="kw">while(!IsStopped());
   
   class=class="str">"cmt">//--- 从图表上删除所有绘图
   ObjectsDeleteAll(class="num">0,window);
}

GNG 在非稳态下的失效边界

GNG 最被人诟病的一点,是它追不动快速跳变的非静态序列。当输入信号的分布突然“跳”到另一块区域,原本已经收敛成特定拓扑的大多数神经元会瞬间出局——它们所在的位置不再被信号覆盖,连接年龄也不增长,于是这部分“死节点”既不干活,还白白吃 CPU(见图 2)。 在缓慢漂移的场景下这个副作用基本看不见:只要漂移速度和权重适应的神经元移动速度量级接近,GNG 还能跟着把结构拽回来。外汇与贵金属的 tick 流常带跳跃,用 GNG 做在线聚类时得先想清楚这点,相关品种波动高风险也高。 新神经元插入频率参数 λ 是个双刃剑。给太高,网络里会冒出一批独立且永远不激活的死节点;给太低,网络开始去追那些重现概率极小、统计上无意义的信号辐射点——插进去的神经元大概率长期闲置。 实证数据更反直觉:低 λ 虽能在训练开头快速压低平均网络误差,但最终聚类评分反而最差,群集粒度很粗。调参时别只看前几百根的误差曲线。

「给 GNG 打上效用系数补丁」

“跳跃式”分布会让原始 GNG 在样本稀疏区乱跳,业内通行的修法是引入神经效用系数(GNG-U),伪代码改动极小但行为差异明显。 每个神经元新增一个效用变量 U(对应 CGNGNeuron 类里的 U 字段)。第 4 步赢家权重更新后,按次优神经元与赢家神经元之间的误差量去改 U:这量化了“若没有当前赢家、次优顶上会少掉多少总误差”,也就是该神经元对压低网络误差的真实贡献。 移除逻辑从第 8 步起变了原则:只删 U 最小的节点,且前提是层内最大误差超过该 U 的某个倍数才动手。第 9 步插新节点时,其 U 取邻近神经元 U 的算术平均;第 10 步所有 U 按误差顺序同步衰减。 衰减常量管着追踪非定常性的能力:调太大,真实小效用节点和有用节点一起被误杀;调太小,移除过少、适应率掉下来。GNG.mqh 里 GNG-U 是 CGNGAlgorithm 的派生类,开 MT5 进头文件跟一遍派生实现,直接切到这个算法跑一组跳变样本就能看出分布平滑了。

◍ 记住这一条就够了

GNG 在 MQL5 里本质是一套自适应径向函数网,社区反馈里有人指出 2013 年后编译器改动使旧代码报错误和警告,修正版已发布,开 MT5 跑原文 ZIP 前先确认编译通过。 作者自己写过内置 GNG 的 EA,认为比非自适应聚类网络结果可能更好,但日常更倾向非神经的“白盒”算法。ANN 始终是黑盒,只有当数据依赖完全未知时才优先拿来挖结构。 外汇与贵金属杠杆高、神经网过拟合风险大,真要上实盘先用策略测试器跑样本外数据看回撤,再决定要不要把这层黑盒塞进你的执行链。

让小布替你跑这套
小布盯盘已内置基于自适应聚类的品种状态诊断,打开对应外汇或贵金属页就能看到实时分群,你只管读结论、做决策。这类高波动品种请记住杠杆会放大亏损概率。

常见问题

SOM 拓扑固定、靠网格邻域收缩;GNG 起手两个神经元,按局部误差长节点和连接,更适合输入空间形状未知的场景,聚类数由数据定。
S-GNG 在监督信号下仍优化拓扑,节点布置贴合边界,而 RBF 中心常需预设,边界样本容易分错,概率上 S-GNG 泛化更优。
多是学习率与年龄衰减没调好,连接年龄涨太快会断拓扑,局部误差累积溢出也会让节点乱长,建议先小样本预热。
小布在后台用聚类把近期 tick 分布切成几种状态,页面直接标出当前属于震荡还是趋势概率段,你不必自己写 EA 验证。
带衰减老节点的变体更合适,能丢掉历史失效结构,避免旧形态干扰新聚类,但需警惕过拟合,回测样本要够长。