神经网络实验(第 3 部分):实际应用(基础篇)
从离线模型到 MT5 实时信号
把训练好的神经网络接到 MetaTrader 5,核心不是模型多深,而是特征怎么随 tick 更新。多数失败案例源于用日线收盘做训练、却拿分钟 tick 去推理,分布漂移直接让输出失真。 实盘里建议用 OnTick 触发特征重算,而不是依赖定时器。以下片段演示了在每次报价到达时读取最近 50 根收盘价并做 z-score 标准化,供后续网络前向使用。外汇与贵金属杠杆高,这类信号仅作概率参考,不代表方向必现。 回测中若直接拿历史全体样本训练、再在同区间测试,EA 在 2022 年英镑闪崩日仍会给出错误多头倾向——说明样本外验证必须切分时间窗。
class="type">void OnTick() { class="type">class="kw">double closes[class="num">50]; CopyClose(_Symbol, PERIOD_M1, class="num">0, class="num">50, closes); class=class="str">"cmt">// 取 M1 最近 class="num">50 根收盘价 class="type">class="kw">double mean = class="num">0, var = class="num">0; for(class="type">int i=class="num">0;i<class="num">50;i++) mean += closes[i]; mean /= class="num">50; class=class="str">"cmt">// 算均值 for(class="type">int i=class="num">0;i<class="num">50;i++) var += (closes[i]-mean)*(closes[i]-mean); var /= class="num">49; class=class="str">"cmt">// 样本方差 class="type">class="kw">double z = (closes[class="num">0]-mean)/MathSqrt(var); class=class="str">"cmt">// 当前价 z-score class=class="str">"cmt">// 将 z 送入已加载网络输入节点 }
「为什么前面只做实验没谈落地」
前几篇里我反复折腾神经网络,做了重访几何的实验,也用智能优化把生成的 EA 调了一遍,但基本没碰一个现实问题:这些跑出来的东西到底怎么用。这个缺口一直悬着,本文就是来补的。 只靠 MetaTrader 5 自带工具,不挂任何第三方库,我会把前面得到的结果接进实盘逻辑,并顺手给 EA 加一套能扩功能的算法。写法上偏向一步一步跟着做,尽量少绕弯子,方便你开 MT5 直接复现。
◍ 把分散的优化结果塞进一个 EA
前两轮优化里,我分别拿到了两个系统的盈利因子、复杂准则最优值,以及感知机与神经网络的一组权重。对这批结果的离线测试显示,指标处于可容忍区间,没有明显过拟合崩坏。 顺着这条线往下想,下一步不是继续单跑,而是把全部优化产出并装进同一个 EA,让它们并行决策。手动挂 10 张图、跑 10 个独立 EA 既不方便也难统览,合并后能在同一上下文里同时吃进 10–20 个参数,用扩展视角看信号叠加效果。 外汇与贵金属杠杆高、滑点跳空频繁,多模型并行只是提高信息覆盖,不意味胜率必然抬升,实盘前务必在 MT5 策略测试器用真实点差回测。
EURUSD 一小时框架下的 TEMA 双线回测配置
这套 EA 只在 EURUSD 的 H1 周期跑,指标层用两条 TEMA:周期 1 与周期 24。作者多次对比后弃用普通 MA,理由是 TEMA 在反复测试中表现更稳定,平滑滞后也更小。 止损设在 600 点、止盈 60 点,属于典型的大止损小止盈的突破反手逻辑,配合剥头皮式出场。优化与测试统一用“仅开盘价”加“最复杂准则”模式,实测比“最大盈利”模式的结果更平滑、更少过拟合。 优化窗口取 3 年:2018.12.09 至 2021.12.09;前向验证切到其后 1 年(2021.12.09–2022.12.09)。前向测试同时纳入遗传算法跑出的 20 个最优参数组合,避免单组侥幸。 底仓 10,000 美元、杠杆 1:500,EA 优化走“快速遗传算法”感知器,并调用 DeepNeuralNetwork.mqh 的慢速完整算法库。外汇与贵金属杠杆交易高风险,3 年样本不代表未来概率,参数窗口建议你自己换区间重跑一遍。
「感知器权重砍到 20 就够用」
实测下来,带感知器的 EA 根本不需要 200 层的权重深度,20 层已经足够覆盖多数行情状态。优化循环因此改成从 0 以 1 为步长扫到 20,代码体积和调参时间都明显降下来了。 新加的 Param 参数控制感知器向正侧或负侧回撤的触发深度。把它调大,成交次数会往下掉,但单笔信号准确率倾向于往上走——这是个典型的频率换质量旋钮。 每套系统跑两个 EA:一个专职离线优化,另一个挂实盘。订单用注释字段做来源标记,序列号直接取自样本存档,MaxSeries 则卡住同屏并发单数。外汇和贵金属波动大、滑点跳空频繁,这种并发限制能降低小资金账户被瞬时冲击的概率。 下面这段是入场激活核心。循环按样本数组六分之一切分,取出四维特征 x1~x4 和 Param;perceptron1() 输出小于 -Param 且快线低于慢线时尝试卖,大于 Param 且快线高于慢线时尝试买,同时受 MaxSeries 与最大点差约束。 感知器函数本身把特征减 10 后,与两个指标在 6 柱、11 柱窗口的归一化斜率做点积。权重从样本来,输入是价格结构,输出是一个带符号的激活值,用来和 Param 比大小。
for(class="type">int i=class="num">0; i<=(ArraySize(EURUSD)/class="num">6)-class="num">1; i++){ comm=IntegerToString(i); x1=(class="type">int)StringToInteger(EURUSD[i][class="num">0]); x2=(class="type">int)StringToInteger(EURUSD[i][class="num">1]); x3=(class="type">int)StringToInteger(EURUSD[i][class="num">2]); x4=(class="type">int)StringToInteger(EURUSD[i][class="num">3]); Param=(class="type">int)StringToInteger(EURUSD[i][class="num">4]); class=class="str">"cmt">//SELL++++++++++++++++++++++++++++++++++++++++++++++++ if (CalculateSeries(Magic)<MaxSeries && (perceptron1()<-Param) && (CalculatePositions(symbolS1.Name(), Magic, POSITION_TYPE_SELL, EAComment+" En_"+comm)==class="num">0) && (ind_In1[class="num">1]>ind_In2[class="num">1]) && (SpreadS1<=MaxSpread)){ OpenSell(symbolS1.Name(), LotsXSell, TakeProfit, StopLoss, EAComment+" En_"+comm); } class=class="str">"cmt">//BUY++++++++++++++++++++++++++++++++++++++++++++++++ if (CalculateSeries(Magic)<MaxSeries && (perceptron1()>Param) && (CalculatePositions(symbolS1.Name(), Magic, POSITION_TYPE_BUY, EAComment+" En_"+comm)==class="num">0) && (ind_In1[class="num">1]<ind_In2[class="num">1]) && (SpreadS1<=MaxSpread)){ OpenBuy(symbolS1.Name(), LotsXBuy, TakeProfit, StopLoss, EAComment+" En_"+comm); } } class="type">class="kw">double perceptron1() { class="type">class="kw">double w1 = x1 - class="num">10.0; class="type">class="kw">double w2 = x2 - class="num">10.0; class="type">class="kw">double w3 = x3 - class="num">10.0; class="type">class="kw">double w4 = x4 - class="num">10.0; class="type">class="kw">double a1 = (((ind_In1[class="num">1]-ind_In1[class="num">6])/Point())/class="num">6); class="type">class="kw">double a2 = (((ind_In1[class="num">1]-ind_In1[class="num">11])/Point())/class="num">11); class="type">class="kw">double a3 = (((ind_In2[class="num">1]-ind_In2[class="num">6])/Point())/class="num">6); class="type">class="kw">double a4 = (((ind_In2[class="num">1]-ind_In2[class="num">11])/Point())/class="num">11); class="kw">return (w1 * a1 + w2 * a2 + w3 * a3 + w4 * a4); } class="type">class="kw">double perceptron1() { class="type">class="kw">double w1 = x1 - class="num">10.0; class="type">class="kw">double w2 = x2 - class="num">10.0; class="type">class="kw">double w3 = x3 - class="num">10.0; class="type">class="kw">double w4 = x4 - class="num">10.0; class="type">class="kw">double v1 = y1 - class="num">10.0; class="type">class="kw">double v2 = y2 - class="num">10.0; class="type">class="kw">double v3 = y3 - class="num">10.0; class="type">class="kw">double v4 = y4 - class="num">10.0; class="type">class="kw">double a1 = (((ind_In1[class="num">1]-ind_In1[class="num">6])/Point())/class="num">6); class="type">class="kw">double a2 = (((ind_In1[class="num">1]-ind_In1[class="num">11])/Point())/class="num">11); class="type">class="kw">double a3 = (((ind_In2[class="num">1]-ind_In2[class="num">6])/Point())/class="num">6); class="type">class="kw">double a4 = (((ind_In2[class="num">1]-ind_In2[class="num">11])/Point())/class="num">11);
◍ 感知机输出与下单触发的具体写法
两段 perceptron 函数把两个输入序列 ind_In1、ind_In2 在 6 柱与 11 柱窗口的斜率做了归一化,权重 w、v 各减 10.0 再乘进去,返回加权求和。perceptron1 用 4 个 a 项(含 6/11 两档),perceptron2 用 4 个 b 项(全 11 档),两者结构对称但采样长度不同,回测时改 6 或 11 会直接挪动斜率敏感度。 触发逻辑只看 perceptron1() 与阈值 Param 的关系:小于 -Param 且当前无同魔法码卖单、ind_In1[1] 高于 ind_In2[1]、点差不超 MaxSpread,才 OpenSell;大于 Param 且 ind_In1[1] 低于 ind_In2[1] 则 OpenBuy。外汇与贵金属杠杆高,这类条件单在跳空时可能滑点到不可控。 原文里 SELL/BUY 的 if 块出现了两次完全一致的片段,属于冗余复制,MT5 编译能过但实盘会重复检查;真要跑先把第二对重复判断删掉,只留一组即可。
class="type">class="kw">double b1 = (((ind_In1[class="num">1]-ind_In1[class="num">11])/Point())/class="num">11); class="type">class="kw">double b2 = (((ind_In2[class="num">1]-ind_In1[class="num">11])/Point())/class="num">11); class="type">class="kw">double b3 = (((ind_In1[class="num">1]-ind_In2[class="num">11])/Point())/class="num">11); class="type">class="kw">double b4 = (((ind_In2[class="num">1]-ind_In2[class="num">11])/Point())/class="num">11); class="kw">return (w1 * a1 + w2 * a2 + w3 * a3 + w4 * a4 + v1 * b1 + v2 * b2 + v3 * b3 + v4 * b4); } class="type">class="kw">double perceptron1() { class="type">class="kw">double w1 = x1 - class="num">10.0; class="type">class="kw">double w2 = x2 - class="num">10.0; class="type">class="kw">double w3 = x3 - class="num">10.0; class="type">class="kw">double w4 = x4 - class="num">10.0; class="type">class="kw">double a1 = (((ind_In1[class="num">1]-ind_In1[class="num">6])/Point())/class="num">6); class="type">class="kw">double a2 = (((ind_In1[class="num">1]-ind_In1[class="num">11])/Point())/class="num">11); class="type">class="kw">double a3 = (((ind_In2[class="num">1]-ind_In2[class="num">6])/Point())/class="num">6); class="type">class="kw">double a4 = (((ind_In2[class="num">1]-ind_In2[class="num">11])/Point())/class="num">11); class="kw">return (w1 * a1 + w2 * a2 + w3 * a3 + w4 * a4); } class="type">class="kw">double perceptron2() { class="type">class="kw">double v1 = y1 - class="num">10.0; class="type">class="kw">double v2 = y2 - class="num">10.0; class="type">class="kw">double v3 = y3 - class="num">10.0; class="type">class="kw">double v4 = y4 - class="num">10.0; class="type">class="kw">double b1 = (((ind_In1[class="num">1]-ind_In1[class="num">11])/Point())/class="num">11); class="type">class="kw">double b2 = (((ind_In2[class="num">1]-ind_In1[class="num">11])/Point())/class="num">11); class="type">class="kw">double b3 = (((ind_In1[class="num">1]-ind_In2[class="num">11])/Point())/class="num">11); class="type">class="kw">double b4 = (((ind_In2[class="num">1]-ind_In2[class="num">11])/Point())/class="num">11); class="kw">return (v1 * b1 + v2 * b2 + v3 * b3 + v4 * b4); } class=class="str">"cmt">//SELL++++++++++++++++++++++++++++++++++++++++++++++++ if ((perceptron1()<-Param) && (CalculatePositions(symbolS1.Name(), Magic, POSITION_TYPE_SELL, EAComment)==class="num">0) && (ind_In1[class="num">1]>ind_In2[class="num">1]) && (SpreadS1<=MaxSpread)){ OpenSell(symbolS1.Name(), LotsXSell, TakeProfit, StopLoss, EAComment); } class=class="str">"cmt">//BUY++++++++++++++++++++++++++++++++++++++++++++++++ if ((perceptron1()>Param) && (CalculatePositions(symbolS1.Name(), Magic, POSITION_TYPE_BUY, EAComment)==class="num">0) && (ind_In1[class="num">1]<ind_In2[class="num">1]) && (SpreadS1<=MaxSpread)){ OpenBuy(symbolS1.Name(), LotsXBuy, TakeProfit, StopLoss, EAComment); } class=class="str">"cmt">//SELL++++++++++++++++++++++++++++++++++++++++++++++++ if ((perceptron1()<-Param) && (CalculatePositions(symbolS1.Name(), Magic, POSITION_TYPE_SELL, EAComment)==class="num">0) && (ind_In1[class="num">1]>ind_In2[class="num">1]) && (SpreadS1<=MaxSpread)){ OpenSell(symbolS1.Name(), LotsXSell, TakeProfit, StopLoss, EAComment); } class=class="str">"cmt">//BUY++++++++++++++++++++++++++++++++++++++++++++++++ if ((perceptron1()>Param) && (CalculatePositions(symbolS1.Name(), Magic, POSITION_TYPE_BUY, EAComment)==class="num">0) && (ind_In1[class="num">1]<ind_In2[class="num">1]) && (SpreadS1<=MaxSpread)){ OpenBuy(symbolS1.Name(), LotsXBuy, TakeProfit, StopLoss, EAComment); } class=class="str">"cmt">//SELL++++++++++++++++++++++++++++++++++++++++++++++++