神经网络: 智能交易系统自我优化·综合运用
(3/3)·从算法到ALGLIB落地,两个数学任务验证MLP能否撑起EA的自动重优化闭环
◍ 从文件复原多层感知机并取网络响应
这段逻辑负责把已训练好的神经网络参数从二进制文件读回,并封装成可复用的推理接口。读取时按层遍历:末层(k==numCapas-1)单独处理输出缩放,用 FileReadDouble 取 media 与 sigma 后交给 MLPSetOutputScaling;其余层则逐神经元设 funcTipo、umbral,并把跨层权重写回 MLPSetWeight。 文件指针关闭前若任何一步失败,exito 标记会置否,调用方据此判断网络加载是否成功。实盘里若 exito 返回 false,说明模型文件版本或结构不匹配,直接拿残参跑预测会出野值。 respuestaRed 是推理入口:MLPProcess 算完先把结果塞进 arSalidas。当 desnorm=true 时,按 maxAbsSalida / minAbsSalida 与 arMaxMinRelSals 记录的区间做反归一化,否则输出仍是压缩空间的数值。外汇与贵金属行情高波动,用未反归一化的输出直接下单可能严重偏离真实价格区间,须确认 desnorm 开关。 最终函数只返回 arSalidas[0] 作为网络响应。若你的网络是多输出头(如同时预测方向和波动率),这里要改下标或返回数组,否则只剩首通道有效。
} else if(k==numCapas-class="num">1) { media= FileReadDouble(puntFichRed); sigma= FileReadDouble(puntFichRed); CAlglib::MLPSetOutputScaling(objRed, i, media, sigma); } funcTipo= (class="type">int)FileReadDouble(puntFichRed); umbral= FileReadDouble(puntFichRed); CAlglib::MLPSetNeuronInfo(objRed, k, i, funcTipo, umbral); for(j= class="num">0; k<(numCapas-class="num">1) && j<arNeurCapa[k+class="num">1]; j++) { peso= FileReadDouble(puntFichRed); CAlglib::MLPSetWeight(objRed, k, i, k+class="num">1, j, peso); } } } } FileClose(puntFichRed); class="kw">return(exito); } class=class="str">"cmt">//--------------------------------------- 请求网络响应 --------------------------------- class="type">class="kw">double respuestaRed(CMultilayerPerceptronShell &ObjRed, class="type">class="kw">double &arEntradas[], class="type">class="kw">double &arSalidas[], class="type">bool desnorm= false) { class="type">class="kw">double resp= class="num">0, nNeuron= class="num">0; CAlglib::MLPProcess(ObjRed, arEntradas, arSalidas); if(desnorm) class=class="str">"cmt">//如果输出数据规范化应改变 { nNeuron= ArraySize(arSalidas); for(class="type">int k= class="num">0; k<nNeuron; k++) arSalidas[k]= desNormValor(arSalidas[k], maxAbsSalida, minAbsSalida, arMaxMinRelSals[class="num">1], arMaxMinRelSals[class="num">0]); } resp= arSalidas[class="num">0]; class="kw">return(resp); }
双图表分工让优化不误事
MT5 允许同一终端里每个图表各挂一个 EA 并行跑。把第一张图设成执行模式管策略,第二张图开优化模式只训练神经网络,执行端每隔一段时间去读优化端写出的网络文本文件,行情监视就不会因算力被吃掉而断档。 原单跑优化约耗 4–6 分钟,拆成双图表后总处理拉到 8–15 分钟,具体看亚洲还是欧洲时段活跃度,但策略端从不停止。外汇与贵金属杠杆高,这种并行只解决算力调度,不削减品种本身风险。 用两个枚举把节奏和角色交给用户:mis_PLAZO_OPTIM 决定每天 / 隔天 / 周末重算,mis_TIPO_EAred 切 _OPTIMIZA 或 _EJECUTA。horaOptim 默认 3 点(本地),对应欧盘清晨低波动;想启动即算就开 optimInicio。 OnTimer 由 OnInit 里 EventSetTimer(tmp) 拉起,tmp 至少每小时触发一次。优化端到点查是否重算,执行端查网络文件是否被更新。实测每晚 3 点用前 3 个月 1H 数据重训:输入 35、隐层 45 与 8、输出 2 神经元,耗时 35–45 分钟。
enum mis_PLAZO_OPTIM {_DIARIO, _DIA_ALTERNO, _FIN_SEMANA}; enum mis_TIPO_EAred {_OPTIMIZA, _EJECUTA}; class="kw">input mis_TIPO_EAred tipoEAred = _OPTIMIZA; class=class="str">"cmt">//执行任务类型 class="kw">input mis_PLAZO_OPTIM plazoOptim = _DIARIO; class=class="str">"cmt">//网络优化的时间间隔 class="kw">input class="type">int horaOptim = class="num">3; class=class="str">"cmt">//网络优化的本地时间 class="kw">input class="type">bool optimInicio = true; class=class="str">"cmt">//当 EA 启动时优化神经网络 class="type">class="kw">double fechaUltLectura; class="type">bool reOptimizada= false; /---------------------------------- 定时器 -------------------------------------- class="type">void OnTimer() { class="type">bool existe= false; class="type">class="kw">string fichRed= ""; if(tipoEAred==_OPTIMIZA) class=class="str">"cmt">//EA 工作于 "优化器" 模式 { class="type">bool optimizar= false; class="type">int codS= class="num">0, hora= infoFechaHora(TimeLocal(), _HORA); class=class="str">"cmt">//接收完整的当前时间 if(!redOptimizada) optimizar= horaOptim==hora && permReoptimDia(); fichRed= "copiaSegurRed_"+Symbol()+".red"; class=class="str">"cmt">//定义神经网络文件名 existe= buscaFich(fichRed, "*.red"); class=class="str">"cmt">//搜索神经网络文件保存的磁盘位置 if(!existe || optimizar) redOptimizada= gestionRed(objRed, simb, intervEntrada!="", intervSalida!="", imprDatosEntrena, barajaDatos); if(hora>(horaOptim+class="num">6)) redOptimizada= false; class=class="str">"cmt">//评估时间 class="num">6 个小时以上, 实际优化的网络应认定过时 guardaVarGlobal(redOptimizada); class=class="str">"cmt">//在磁盘上保存 "reoptimizada" (重新优化) 的值 } else if(tipoEAred==_EJECUTA) class=class="str">"cmt">//EA 工作于 "致动器" 模式 { class="type">class="kw">datetime fechaUltOpt= class="num">0; fichRed= "copiaSegurRed_"+Symbol()+".red"; class=class="str">"cmt">//定义神经网络文件名 existe= buscaFich(fichRed, "*.red"); class=class="str">"cmt">//搜索神经网络文件保存的磁盘位置 if(existe) { fechaUltOpt= fechaModifFich(class="num">0, fichRed); class=class="str">"cmt">//定义最后优化日期 (网络文件修改)
「神经网络文件的重读与重优化闸门」
EA 在运行时不会盲目重载模型。它先比对优化日期与最后一次读取日期:若 fechaUltOpt 大于 fechaUltLectura,说明磁盘上的 .red 文件来自更新的训练结果,这时才调用 recuperaRedFich 重建网络,并把当前 TimeCurrent() 写回全局变量落盘。 下面这段逻辑直接决定「哪天允许重优化」。plazoOptim 设为 _DIARIO 时,周二到周六(diaSemana 不为 6 和 0)均可;_DIA_ALTERNO 只在奇数星期几(周二、四、六)放行;_FIN_SEMANA 仅周六(diaSemana==5)开启。改这几个宏,就能控制实盘重训练频率。 文件存在性检查交给 buscaFich:用 FileFindFirst 拿句柄,循环 FileFindNext 比对文件名,命中即返回 true。注意 carpeta 默认 FILE_COMMON,意味着跨终端共享目录——若你手动挪了 .red 文件到本地数据夹,这个函数会一直返回 false 并触发 Alert。外汇与贵金属波动剧烈,自动重优化可能放大过拟合风险,上线前建议在策略测试器跑至少 3 个月 tick 数据验证。
if(fechaUltOpt>fechaUltLectura) class=class="str">"cmt">//是否优化日期晚于最后一次读取 { recuperaRedFich(objRed, fichRed); class=class="str">"cmt">//读取并生成新的神经网络 fechaUltLectura= (class="type">class="kw">double)TimeCurrent(); guardaVarGlobal(fechaUltLectura); class=class="str">"cmt">//在磁盘上保存新的读取日期 Print("Network restored after optimization... "+simb); class=class="str">"cmt">//在屏幕上显示消息 } } else Alert("tipoEAred==_EJECUTA --> 神经网络文件未发现: "+fichRed+".red"); } class="kw">return; } class=class="str">"cmt">//--------------------------------- 启用重新优化 --------------------------------- class="type">bool permReoptimDia() { class="type">int diaSemana= infoFechaHora(TimeLocal(), _DSEM); class="type">bool permiso= (plazoOptim==_DIARIO && diaSemana!=class="num">6 && diaSemana!=class="num">0) || class=class="str">"cmt">//优化 [从周二到周六的每天] (plazoOptim==_DIA_ALTERNO && diaSemana%class="num">2==class="num">1) || class=class="str">"cmt">//优化 [周二, 周四和周六] (plazoOptim==_FIN_SEMANA && diaSemana==class="num">5); class=class="str">"cmt">//优化 [周六] class="kw">return(permiso); } class=class="str">"cmt">//-------------------------------------- 搜索文件 -------------------------------------------- class="type">bool buscaFich(class="type">class="kw">string fichBusca, class="type">class="kw">string filtro= "*.*", class="type">int carpeta= FILE_COMMON) { class="type">bool existe= false; class="type">class="kw">string fichActual= ""; class="type">long puntBusca= FileFindFirst(filtro, fichActual, carpeta); if(puntBusca!=INVALID_HANDLE) { ResetLastError(); class="kw">while(!existe) { FileFindNext(puntBusca, fichActual); existe= fichActual==fichBusca; } FileFindClose(puntBusca); } else Print("文件未发现!"); infoError(_LastError, __FUNCTION__); class="kw">return(existe); }
◍ 拿二进制转十进制给网络做压力测试
想验证一套神经网络系统靠不靠谱,最省事的办法是先拿一个有标准答案的任务喂给它,再把它的输出和已知结果比对。二进制到十进制的映射就是这种任务:规则明确、答案确定,适合用来探网络的底。 脚本用前 800 个自然数的 10 位二进制串(10 个输入神经元、1 个输出神经元、无隐藏层)训练网络,再用 801 到 1000 的 200 个数做评估。比如输入 1100110100(即十进制 820),网络应当输出接近 820 的值,第三列标记 'true' 表示预测与期望一致。 以「10 输入、0 隐藏、1 输出、线性激活」跑完,Terminal\Common\Files 下的 ScriptBinDec-infRN.csv 显示 200 个评估样本全部命中,结果堪称优秀。 换结构就翻车了:若改成「10 输入、隐藏层 20+8 神经元、1 输出」,评估列里大量非 'true',属于不可接受误差。这暴露出神经网络最麻烦的一点——层数、每层神经元数、激活函数怎么配,没有理论公式直接给答案。 别把结构当成随便叠积木 实际只能靠经验趟坑:自己跑几千次组合,或先用快速挖掘类统计软件在进 MQL5 前筛掉明显低效的结构,再上 MT5 实测。外汇与贵金属行情受宏观事件驱动,用 NN 做此类映射验证时同样要意识到过拟合与样本外失效的高风险。
class="macro">#class="kw">property script_show_confirm class="macro">#define FUNC_CAPA_OCULTA class="num">1 class="macro">#define FUNC_SALIDA -class="num">5 class=class="str">"cmt">//class="num">1= 双曲正切; class="num">2= e^(-x^class="num">2); class="num">3= x>=class="num">0 raizC(class="num">1+x^class="num">2) x<class="num">0 e^x; class="num">4= S 形函数; class=class="str">"cmt">//class="num">5= 二项式 x>class="num">0.5?class="num">1: class="num">0; -class="num">5= 线性函数 class="macro">#include <Math\Alglib\alglib.mqh> enum mis_PROPIEDADES_RED {N_CAPAS, N_NEURONAS, N_ENTRADAS, N_SALIDAS, N_PESOS}; class=class="str">"cmt">//--------------------------------- 输入 --------------------- sinput class="type">int nNeuronEntra= class="num">10; class=class="str">"cmt">//输入层神经元数量 class=class="str">"cmt">//class="num">2^class="num">8= class="num">256 class="num">2^class="num">9= class="num">512; class="num">2^class="num">10= class="num">1024; class="num">2^class="num">12= class="num">4096; class="num">2^class="num">14= class="num">16384 sinput class="type">int nNeuronCapa1= class="num">0; class=class="str">"cmt">//第一个隐藏层的神经元数量 (不能 <class="num">1) sinput class="type">int nNeuronCapa2= class="num">0; class=class="str">"cmt">//第二个隐藏层的神经元数量 (不能 <class="num">1) //class="num">2^class="num">8= class="num">256 class="num">2^class="num">9= class="num">512; class="num">2^class="num">10= class="num">1024; class="num">2^class="num">12= class="num">4096; class="num">2^class="num">14= class="num">16384 sinput class="type">int nNeuronSal= class="num">1; class=class="str">"cmt">//输出层的神经元数量 sinput class="type">int historialEntrena= class="num">800; class=class="str">"cmt">//训练历史 sinput class="type">int historialEvalua= class="num">200; class=class="str">"cmt">//评估历史 sinput class="type">int ciclosEntrena= class="num">2; class=class="str">"cmt">//训练循环 sinput class="type">class="kw">double tasaAprende= class="num">0.001; class=class="str">"cmt">//网络训练级别 sinput class="type">class="kw">string intervEntrada= ""; class=class="str">"cmt">//输入规范化: 期望的最小值和最大值 max(空= 无需优化) sinput class="type">class="kw">string intervSalida= ""; class=class="str">"cmt">//输出优化: 期望最小值和最大值 (空= 无需优化) sinput class="type">bool imprEntrena= true; class=class="str">"cmt">//显示训练/评估数据 class=class="str">"cmt">// ------------------------------ 全局变量 ----------------------------- class="type">int puntFichTexto= class="num">0; class="type">class="kw">ulong contFlush= class="num">0; CMultilayerPerceptronShell redNeuronal; CMatrixDouble arDatosAprende(class="num">0, class="num">0); CMatrixDouble arDatosEval(class="num">0, class="num">0); class="type">class="kw">double minAbsSalida= class="num">0, maxAbsSalida= class="num">0;
用脚本把二进制塞进神经网络验精度
这段 MT5 脚本把「二进制转十进制」当成一个极小监督任务,用来跑通你自建的神经网络函数簇。它不交易,只验证 redNeuronal 在已知映射上的拟合能力,外汇与贵金属行情用这类模型前务必自测,杠杆品种误判即放大亏损。 脚本先以 creaRedNeuronal 建网,再用 preparaDatosEntra 与 normalizaDatosRed 把训练集 arDatosAprende 投喂进去;entrenaEvalRed 返回的 errorMedioEntren 就是本次平均训练误差,直接落盘到 csv 方便你横向比参数。 评估段从 inicio 到 fin 遍历整数,dec_A_baseNumerica 把 k 编成 2 进制串喂网,respuestaRed 吐出小数后用 MathRound 取整。correcto 标记网络输出是否等于原十进制数,全部写进『numBinEntra;numDecSalidaRed;correcto』三列——你开 MT5 跑完搜 csv,数 correcto=false 的行就能算出误判率。 别把正态当圣经:normEntrada / normSalida 只是开关,intervEntrada 为空时网络拿原始 0/1 训练,误差曲线可能更抖,但小样本下未必更差。
class="type">class="kw">string nombreEA= "ScriptBinDec"; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() class=class="str">"cmt">//二进制 - 十进制转换器 { class="type">class="kw">string mensIni= "脚本转换器 二进制 - 十进制", mens= "", cadNumBin= "", cadNumRed= ""; class="type">int contAciertos= class="num">0, arNumBin[], inicio= historialEntrena+class="num">1, fin= historialEntrena+historialEvalua; class="type">class="kw">double arSalRed[], arNumEntra[], salida= class="num">0, umbral= class="num">0, peso= class="num">0; class="type">class="kw">double errorMedioEntren= class="num">0; class="type">bool normEntrada= intervEntrada!="", normSalida= intervSalida!="", correcto= false, creada= creaRedNeuronal(redNeuronal); if(creada) { iniFichImprime(puntFichTexto, nombreEA+"-infRN", ".csv",mensIni); preparaDatosEntra(redNeuronal, arDatosAprende, intervEntrada!="", intervSalida!=""); normalizaDatosRed(redNeuronal, arDatosAprende, normEntrada, normSalida); errorMedioEntren= entrenaEvalRed(redNeuronal, arDatosAprende); escrTexto("-------------------------", puntFichTexto); escrTexto("RESPUESTA RED------------", puntFichTexto); escrTexto("-------------------------", puntFichTexto); escrTexto("numBinEntra;numDecSalidaRed;correcto", puntFichTexto); for(class="type">int k= inicio; k<=fin; k++) { cadNumBin= dec_A_baseNumerica(k, arNumBin, class="num">2, nNeuronEntra); ArrayCopy(arNumEntra, arNumBin); salida= respuestaRed(redNeuronal, arNumEntra, arSalRed); salida= MathRound(salida); correcto= k==(class="type">int)salida; escrTexto(cadNumBin+";"+IntegerToString((class="type">int)salida)+";"+correcto, puntFichTexto); cadNumRed= ""; } } deIniFichImprime(puntFichTexto); class="kw">return; }
「6. 任务 2: 质数检测器」
我们来研究一个类似的任务。这次, NN 将定义它是否为质数。训练矩阵将包含 10 列, 其中高达 800 的自然数均变换为 10 个字符的二进制形式, 一列指示它是 ("1") 或不是 ("0") 素数。换言之, 我们将有 800 行和 11 列。接下来, 我们要让 NN 分析接下来 200 个自然数 (从 801 到 1000) 的二进制形式, 并定义哪个数是素数, 哪个不是。由于这个任务更加困难, 让我们打印出得到匹配的统计数据。 脚本依照指定参数 (NN 没有隐藏层, 10 个输入神经元, 第一个隐藏层 20 个神经元, 以及输出层 1 个神经元) 执行之后, 结果比前一个任务还糟糕。生成的 ScriptNumPrimo-infRN.csv 文件位于 Terminal\Common\Files 目录, 提供了以下数据: 在此我们可以看到 800 (809) 之后的第一个素数没有被网络检测到 (true = 非 true)。以下是统计摘要: 报告表明, NN 在评估区间 (801 到 1000) 上的 200 个数字中猜测出 78%。然而, 在区间内出现的 29 个素数中, 它只检测到 13 个 (44.83%)。 如果我们使用以下网络结构进行测试: "10 个输入层神经元, 第一隐藏层 35 个神经元, 第二隐藏层 10 个神经元, 和输出层 1 个神经元", 脚本在执行期间显示以下数据: 正如我们在下面的图片中可以看到的, 结果恶化, 时间 0.53 分钟, 而平均训练误差等于 0.04208383。 因此, 我们再次反思: 如何以最好的方式定义网络内部结构? [CODE] <span class="preprocessor">#include </span><Math\Alglib\alglib.mqh> <span class="keyword">enum</span> mis_PROPIEDADES_RED {N_CAPAS, N_NEURONAS, N_ENTRADAS, N_SALIDAS, N_PESOS}; <span class="comment">//--------------------------------- 输入 ----------------------- ---------------------</span> <span class="keyword">sinput</span> <span class="keyword">int</span> nNeuronEntra= <span class="number">10</span>; <span class="comment">//输入层神经元数量</span>
◍ 神经网络训练后的质数判别回测
这段逻辑跑在 MT5 的 EA 主流程里,前提是 creaRedNeuronal 返回 true,也就是网络结构已经建好。随后它把学习集做归一化,调用 entrenaEvalRed 拿到平均训练误差 errorMedioEntren,再把 0/1 编码的十进制数逐个送进网络做前向推理。 推理部分用 dec_A_baseNumerica 把整数 k 转成二进制串塞进输入层,esPrimo 算真实标签,respuestaRed 出网络预测值,MathRound 后和标签比对得到 correcto。每一条样本都会写进 CSV:十进制、二进制、是否质数、网络判质数、是否判对。 收尾统计是硬指标:contAciertos 除以 historialEvalua 得到总准确率百分比,aciertoPrimo 除以 totalPrimos 得到质数识别准确率。比如若 historialEvalua=1000、contAciertos=985,则总成功率打印为 98.50 %;若 totalPrimos=168、aciertoPrimo=160,则质数命中率约 95.24 %。外汇与贵金属交易接入此类模型时,信号误判会带来高风险,须用历史样本先验证命中率再上实盘。 下面把核心回测代码拆开看,每一行都在决定你最后 CSV 里那几个百分点。
class="type">class="kw">double errorMedioEntren= class="num">0; class="type">bool correcto= false, esNumPrimo= false, creada= creaRedNeuronal(redNeuronal); if(creada) { iniFichImprime(puntFichTexto, nombreEA+"-infRN", ".csv",mensIni); preparaDatosEntra(redNeuronal, arDatosAprende, intervEntrada!="", intervSalida!=""); normalizaDatosRed(redNeuronal, arDatosAprende, normEntrada, normSalida); errorMedioEntren= entrenaEvalRed(redNeuronal, arDatosAprende); escrTexto("-------------------------", puntFichTexto); escrTexto("RESPUESTA RED------------", puntFichTexto); escrTexto("-------------------------", puntFichTexto); escrTexto("numDec;numBin;numPrimo;numPrimoRed;correcto", puntFichTexto); for(class="type">int k= inicio; k<=fin; k++) { cadNumBin= dec_A_baseNumerica(k, arNumBin, class="num">2, nNeuronEntra); esNumPrimo= esPrimo(k); ArrayCopy(arNumEntra, arNumBin); numPrimoRed= respuestaRed(redNeuronal, arNumEntra, arSalRed); numPrimoRed= MathRound(numPrimoRed); correcto= esNumPrimo==(class="type">int)numPrimoRed; if(esNumPrimo) { totalPrimos++; if(correcto) aciertoPrimo++; } if(correcto) contAciertos++; linea= IntegerToString(k)+";"+cadNumBin+";"+esNumPrimo+";"+(numPrimoRed==class="num">0? "false": "true")+";"+correcto; escrTexto(linea, puntFichTexto); } } escrTexto("成功 / 总数;"+DoubleToString((class="type">class="kw">double)contAciertos/(class="type">class="kw">double)historialEvalua*class="num">100, class="num">2)+" %", puntFichTexto); escrTexto("发现质数;"+IntegerToString(aciertoPrimo)+";"+"总质数;"+IntegerToString(totalPrimos), puntFichTexto); escrTexto("成功 / 总质数;"+DoubleToString((class="type">class="kw">double)aciertoPrimo/(class="type">class="kw">double)totalPrimos*class="num">100, class="num">2)+" %", puntFichTexto); deIniFichImprime(puntFichTexto); class="kw">return;
最后说句实在话
前面几节把 ALGLIB 接进 MQL5 跑神经网络自优化的路子已经铺开:二进制-十进制互转、质数判定、按网络内部结构回查结果这三段脚本(Script_redNeuronal_BIN-DEC.mq5 28.2 KB、Script_redNeuronal_NUM-PRIMOS.mq5 30.21 KB)是直接能拖进 MT5 编译运行的。 有用户在 2023 年 9 月反馈,MetaTrader 更新后 EA 停止学习,根因是旧库写法 arDatos[fila].Set(colum,valor) 在新库要改成 arDatos.Set(fila,colum,valor),改完即恢复——这种底层 API 变动在外盘贵金属/外汇高杠杆环境下会直接断掉策略闭环,动手前先核对你本地 ALGLIB 版本。 至于这套思路能否落地成稳定盈利系统,目前没有回测胜率或样本外数据支撑,只能说是个可验证的起点;开 MT5 把两个脚本跑一遍,比空谈架构更有用。