生长型神经气:MQL5 中的实施·综合运用
(3/3)· 从伪代码到已知问题与算法修改,一篇收拢 GNG 在 MT5 里的落地全链路
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 步进回测。
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 线当常规样本会扭曲拓扑,属典型高风险坑。
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 倍,外汇与贵金属品种波动大,实盘前务必用历史数据验证过拟合风险。
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。外汇与贵金属数据噪声大,盲目不停止会过拟合,调参前先确认退出路径。
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 常需几万向量才稳定——但千次内已足够看明白密度适配的逻辑。外汇与贵金属波动剧烈,这类可视化仅作结构认知,不构成任何方向暗示。
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 的暗灰矩形,并挂了标签对象。外汇与贵金属波动剧烈,这类实验性聚类仅作形态参考,实盘信号概率性失效,务必用小资金验证。
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 删掉,否则图上会堆积上一轮的连接线。外汇与贵金属行情跳空频繁,这种隔柱采样在重大数据周可能漏掉突变段,实盘前建议在历史回放里核对样本分布。
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+,节点数突变往往对应行情结构切换。外汇与贵金属杠杆高,图形只是辅助,信号失效须止损。
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),把指定子窗口里的所有对象一次性删光,避免下次运行叠图。
}
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 始终是黑盒,只有当数据依赖完全未知时才优先拿来挖结构。 外汇与贵金属杠杆高、神经网过拟合风险大,真要上实盘先用策略测试器跑样本外数据看回撤,再决定要不要把这层黑盒塞进你的执行链。