神经网络变得轻松(第二部分):网络训练和测试·进阶篇
MT5 里搭神经网络预测分形的初始化套路
做分形预测的第一道坎不是模型,而是把 MT5 的类库和全局环境先立起来。除了自己写的 NeuroNet.mqh,还要挂上 SymbolInfo、TimeSeries、Volumes、Oscilators 这几个标准库,分别管品种属性、序列、成交量、振荡器。参数层建议直接写成 input:例如 StudyPeriod=10(年)、HistoryBars=20(历史深度)、RSIPeriod=14,这样不用改代码就能换品种和周期。 类的实例化全挤在 OnInit 里。CSymbolInfo 先建实例并刷新图表品种数据;时间序列和指标实例每次 new 完必须判空并初始化,失败就 return INIT_FAILED。本例用 VOLUME_TICK(即时报价量),若想用真实成交量,把 Volumes.Create 的参数字串换成 VOLUME_REAL 即可。 神经网络本身用 CNet,构造时把结构数组的引用传进去。训练极吃 CPU,重启后网络不会保留,所以我的做法是:先不填结构声明实例,试从本地文件加载已训网络(文件名走 #define);加载失败才打印错误码并现场建未训练网络。结构数组用 CArrayInt,元素个数=层数,数值=该层神经元数:输入层 12×HistoryBars(本例 12×20=240),首个隐藏层 1000 个神经元,之后每层递减 70% 但保底 20,输出层回归填 1、分类填 3。 统计初值要手动清零:dError(误差标准差)、dUndefine(未定义分形占比)、dForecast(正确预测占比)、dtStudied(最后训练烛条时间)。MQL5 没有异步调用,若在 OnInit 直接跑 Train,终端会一直卡在初始化。变通办法是发自定义事件,在 OnChartEvent 里用 lparam 带训练起始日期来触发 Train,OnInit 末尾建好 CArrayDouble 交换缓冲区就正常返回。OnDeinit 记得释放内存。 梯度爆炸我曾踩过坑:隐藏层权重失控增长,超出变量上限直接瘫痪网络。根因是后层索要的输出超出激活函数范围。解法是对神经元目标值做标准化,附带的改进梯度代码能把这种情况压住。外汇和贵金属波动无序性高,这类模型仅作概率参考,实盘前务必用历史数据自测。
class="macro">#include "NeuroNet.mqh" class="macro">#include <Trade\SymbolInfo.mqh> class="macro">#include <Indicators\TimeSeries.mqh> class="macro">#include <Indicators\Volumes.mqh> class="macro">#include <Indicators\Oscilators.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| class="kw">input parameters | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">input class="type">int StudyPeriod = class="num">10; class=class="str">"cmt">//Study period, years class="kw">input class="type">uint HistoryBars = class="num">20; class=class="str">"cmt">//Depth of history ENUM_TIMEFRAMES TimeFrame = PERIOD_CURRENT; class=class="str">"cmt">//--- class="kw">input group "---- RSI ----" class="kw">input class="type">int RSIPeriod = class="num">14; class=class="str">"cmt">//Period class="kw">input ENUM_APPLIED_PRICE RSIPrice = PRICE_CLOSE; class=class="str">"cmt">//Applied price class=class="str">"cmt">//--- class="kw">input group "---- CCI ----"
◍ 多指标输入的声明与对象初始化
把 CCI、ATR、MACD 三类指标同时塞进一个 EA,第一步就是参数分组声明。下面这段代码把 CCI 周期固定为 14、典型价计算,ATR 同为 14 周期,MACD 则按 12/26/9 加收盘价,三组 input 用 group 字符串在 MT5 属性框里自然分层,调参时不用来回翻。 指标句柄不是直接用 iCCI 那种函数式,而是用 CiCCI、CiATR、CiMACD 等类指针。OnInit 里逐个 new 出来,并用 CheckPointer 判空,任一失败直接返回 INIT_FAILED,避免后续 Tick 里空指针崩 EA。 外汇与贵金属杠杆高,这类多指标共振逻辑只是过滤噪音,实际信号触发仍可能连续止损,上真仓前务必在策略测试器跑至少三年 Tick 数据。 别把默认周期当最优解 14/12/26/9 是平台给的出厂值,欧澳和美日的波动结构不同,直接套同一组参数容易在震荡段频繁假突破,建议先改 CCIPeriod 到 20 观察信号稀疏度。
class="kw">input class="type">int CCIPeriod = class="num">14; class=class="str">"cmt">//Period class="kw">input ENUM_APPLIED_PRICE CCIPrice = PRICE_TYPICAL; class=class="str">"cmt">//Applied price class=class="str">"cmt">//--- class="kw">input group "---- ATR ----" class="kw">input class="type">int ATRPeriod = class="num">14; class=class="str">"cmt">//Period class=class="str">"cmt">//--- class="kw">input group "---- MACD ----" class="kw">input class="type">int FastPeriod = class="num">12; class=class="str">"cmt">//Fast class="kw">input class="type">int SlowPeriod = class="num">26; class=class="str">"cmt">//Slow class="kw">input class="type">int SignalPeriod= class="num">9; class=class="str">"cmt">//Signal class="kw">input ENUM_APPLIED_PRICE MACDPrice = PRICE_CLOSE; class=class="str">"cmt">//Applied price CSymbolInfo *Symb; CiOpen *Open; CiClose *Close; CiHigh *High; CiLow *Low; CiVolumes *Volumes; CiTime *Time; CNet *Net; CArrayDouble *TempData; CiRSI *RSI; CiCCI *CCI; CiATR *ATR; CiMACD *MACD; class=class="str">"cmt">//--- class="type">class="kw">double dError; class="type">class="kw">double dUndefine; class="type">class="kw">double dForecast; class="type">class="kw">double dPrevSignal; class="type">class="kw">datetime dtStudied; class="type">bool bEventStudy; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Symb=new CSymbolInfo(); if(CheckPointer(Symb)==POINTER_INVALID || !Symb.Name(_Symbol)) class="kw">return INIT_FAILED; Symb.Refresh(); Open=new CiOpen(); if(CheckPointer(Open)==POINTER_INVALID || !Open.Create(Symb.Name(),TimeFrame)) class="kw">return INIT_FAILED; class=class="str">"cmt">//---
「指标容器与神经网络的初始化绑定」
在 EA 的 Initialize() 里,先把标准时序数据用 Ci 系列类逐个挂到当前品种和周期上。Close、High、Low、Time 只吃 Symb.Name() 和 TimeFrame 两个参数;Volumes 多传一个 VOLUME_TICK 指明按 tick 成交量聚合;RSI、CCI、ATR、MACD 则各自带周期与价格字段,任一个 Create 失败就直接 return INIT_FAILED,避免半初始化状态跑实盘。 下面这段是逐行拆解,照着抄就能在 MT5 里复现绑定逻辑: Close=new CiClose();
| if(CheckPointer(Close)==POINTER_INVALID | !Close.Create(Symb.Name(),TimeFrame)) |
|---|
return INIT_FAILED; // 新建收盘价容器,指针无效或绑定品种周期失败则初始化中止 High=new CiHigh();
| if(CheckPointer(High)==POINTER_INVALID | !High.Create(Symb.Name(),TimeFrame)) |
|---|
return INIT_FAILED; // 最高价同理 Low=new CiLow();
| if(CheckPointer(Low)==POINTER_INVALID | !Low.Create(Symb.Name(),TimeFrame)) |
|---|
return INIT_FAILED; // 最低价同理 Volumes=new CiVolumes();
| if(CheckPointer(Volumes)==POINTER_INVALID | !Volumes.Create(Symb.Name(),TimeFrame,VOLUME_TICK)) |
|---|
return INIT_FAILED; // 成交量容器,指定 VOLUME_TICK 按逐笔量 Time=new CiTime();
| if(CheckPointer(Time)==POINTER_INVALID | !Time.Create(Symb.Name(),TimeFrame)) |
|---|
return INIT_FAILED; // 时间轴容器 RSI=new CiRSI();
| if(CheckPointer(RSI)==POINTER_INVALID | !RSI.Create(Symb.Name(),TimeFrame,RSIPeriod,RSIPrice)) |
|---|
return INIT_FAILED; // RSI 带周期与计算价 CCI=new CiCCI();
| if(CheckPointer(CCI)==POINTER_INVALID | !CCI.Create(Symb.Name(),TimeFrame,CCIPeriod,CCIPrice)) |
|---|
return INIT_FAILED; // CCI 同上 ATR=new CiATR();
| if(CheckPointer(ATR)==POINTER_INVALID | !ATR.Create(Symb.Name(),TimeFrame,ATRPeriod)) |
|---|
return INIT_FAILED; // ATR 只带周期 MACD=new CiMACD();
| if(CheckPointer(MACD)==POINTER_INVALID | !MACD.Create(Symb.Name(),TimeFrame,FastPeriod,SlowPeriod,SignalPeriod,MACDPrice)) |
|---|
return INIT_FAILED; // MACD 吃三周期加价格 #define FileName Symb.Name()+ "_" +EnumToString((ENUM_TIMEFRAMES)Period())+ "_" +IntegerToString(HistoryBars,3)+ "fr_ea" // 拼出神经网络文件命名:品种_周期_历史根数(3位补零)fr_ea Net=new CNet(NULL); ResetLastError();
| if(CheckPointer(Net)==POINTER_INVALID | !Net.Load(FileName+".nnw",dError,dUndefine,dForecast,dtStudied,false)) |
|---|
{ printf("%s - %d -> Error of read %s prev Net %d",__FUNCTION__,__LINE__,FileName+".nnw",GetLastError()); CArrayInt *Topology=new CArrayInt(); if(CheckPointer(Topology)==POINTER_INVALID) return INIT_FAILED; if(!Topology.Add(HistoryBars*12)) return INIT_FAILED; int n=1000; bool result=true; for(int i=0;(i<4 && result);i++) { // 网络载入失败则建拓扑:输入节点数=HistoryBars*12,最多重试 4 次、上限 1000 隐节点 文件名宏把 HistoryBars 用 IntegerToString 补成 3 位,比如 HistoryBars=200 就落成 '200fr_ea',这样不同回看长度的网络权重文件不会互相覆盖。 若 Net.Load 读不到 .nnw,代码会先打错误日志,再 new 一个 CArrayInt 拓扑、塞入 HistoryBars*12 个输入节点,然后跑最多 4 轮、隐层上限 1000 的构建循环——外汇与贵金属波动剧烈、杠杆高,这类自构网络在 live 前务必用策略测试器跑足样本,否则过拟合概率偏高。
Close=new CiClose(); if(CheckPointer(Close)==POINTER_INVALID || !Close.Create(Symb.Name(),TimeFrame)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- High=new CiHigh(); if(CheckPointer(High)==POINTER_INVALID || !High.Create(Symb.Name(),TimeFrame)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- Low=new CiLow(); if(CheckPointer(Low)==POINTER_INVALID || !Low.Create(Symb.Name(),TimeFrame)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- Volumes=new CiVolumes(); if(CheckPointer(Volumes)==POINTER_INVALID || !Volumes.Create(Symb.Name(),TimeFrame,VOLUME_TICK)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- Time=new CiTime(); if(CheckPointer(Time)==POINTER_INVALID || !Time.Create(Symb.Name(),TimeFrame)) class="kw">return INIT_FAILED; RSI=new CiRSI(); if(CheckPointer(RSI)==POINTER_INVALID || !RSI.Create(Symb.Name(),TimeFrame,RSIPeriod,RSIPrice)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- CCI=new CiCCI(); if(CheckPointer(CCI)==POINTER_INVALID || !CCI.Create(Symb.Name(),TimeFrame,CCIPeriod,CCIPrice)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- ATR=new CiATR(); if(CheckPointer(ATR)==POINTER_INVALID || !ATR.Create(Symb.Name(),TimeFrame,ATRPeriod)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- MACD=new CiMACD(); if(CheckPointer(MACD)==POINTER_INVALID || !MACD.Create(Symb.Name(),TimeFrame,FastPeriod,SlowPeriod,SignalPeriod,MACDPrice)) class="kw">return INIT_FAILED; class="macro">#define FileName Symb.Name()+"_"+EnumToString((ENUM_TIMEFRAMES)Period())+"_"+IntegerToString(HistoryBars,class="num">3)+"fr_ea" Net=new CNet(NULL); ResetLastError(); if(CheckPointer(Net)==POINTER_INVALID || !Net.Load(FileName+".nnw",dError,dUndefine,dForecast,dtStudied,class="kw">false)) { printf("%s - %d -> Error of read %s prev Net %d",__FUNCTION__,__LINE__,FileName+".nnw",GetLastError()); CArrayInt *Topology=new CArrayInt(); if(CheckPointer(Topology)==POINTER_INVALID) class="kw">return INIT_FAILED; if(!Topology.Add(HistoryBars*class="num">12)) class="kw">return INIT_FAILED; class="type">int n=class="num">1000; class="type">bool result=true; for(class="type">int i=class="num">0;(i<class="num">4 && result);i++) {
神经网络初始化与图表事件训练的衔接
这段逻辑处在 EA 初始化的收尾阶段,核心是先把拓扑结构跑通,再挂上自定义图表事件来触发训练。若 Topology 任意一层 Add 失败,直接 delete 并返回 INIT_FAILED,避免后续空指针崩在 Net 构造里。 n 每次循环乘 0.3 并保底 20,意味着隐层节点数按指数衰减收敛;最后单独 Add(1) 作为输出层单神经元,适配标量预测(如价格方向概率)。 EventChartCustom 的触发时间用 iTime 算:当 dForecast>=70 时系数取 1,否则取 10,再乘 100*recentAverageSmoothingFactor 并夹在 [0, dtStudied] 内。也就是说预测值越高,重训间隔越短,可能让模型在趋势明朗时更跟手。 OnChartEvent 只认 id==1001,收到后调 Train(lparam) 并把 bEventStudy 置 false,随后手动 OnTick() 刷新一次信号。OnDeinit 则逐个 CheckPointer 后 delete 掉 Symb / Open / Close,防止 MT5 卸载时残留句柄。 开 MT5 把 recentAverageSmoothingFactor 从默认改到 0.05 和 0.2 各跑一轮,观察 1001 事件触发频率的差值,能直接验证上面的时间公式。外汇与贵金属杠杆高,此类模型仅作辅助,实盘前务必在策略测试器跑足样本。
result=(Topology.Add(n) && result); n=(class="type">int)MathMax(n*class="num">0.3,class="num">20); } if(!result) { class="kw">delete Topology; class="kw">return INIT_FAILED; } if(!Topology.Add(class="num">1)) class="kw">return INIT_FAILED; class="kw">delete Net; Net=new CNet(Topology); class="kw">delete Topology; if(CheckPointer(Net)==POINTER_INVALID) class="kw">return INIT_FAILED; dError=-class="num">1; dUndefine=class="num">0; dForecast=class="num">0; dtStudied=class="num">0; } TempData=new CArrayDouble(); if(CheckPointer(TempData)==POINTER_INVALID) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- bEventStudy=EventChartCustom(ChartID(),class="num">1,(class="type">long)MathMax(class="num">0,MathMin(iTime(Symb.Name(),PERIOD_CURRENT,(class="type">int)(class="num">100*Net.recentAverageSmoothingFactor*(dForecast>=class="num">70 ? class="num">1 : class="num">10))),dtStudied)),class="num">0,"Init"); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class=class="str">"cmt">//--- if(id==class="num">1001) { Train(lparam); bEventStudy=class="kw">false; OnTick(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- if(CheckPointer(Symb)!=POINTER_INVALID) class="kw">delete Symb; class=class="str">"cmt">//--- if(CheckPointer(Open)!=POINTER_INVALID) class="kw">delete Open; class=class="str">"cmt">//--- if(CheckPointer(Close)!=POINTER_INVALID) class="kw">delete Close;
◍ 指针回收与回测窗口的初始化落点
在 MT5 的 EA 析构逻辑里,凡是经 CheckPointer 分配过的指标/数据对象(High、Low、Time、Volumes、RSI、CCI、ATR、MACD、Net、TempData 等),都得先判 POINTER_INVALID 再 delete,否则在脚本被强制终止时可能留下内存幽灵,下次加载同品种同周期会读到脏缓冲。 Train() 的启动以当前时间倒推 StudyPeriod 年作为回测起点:start_time.year-=StudyPeriod 后若溢出则夹到 1900,再用 StructToTime 转成 datetime。实盘里 StudyPeriod=10 意味着系统会试图拉取十年 Tick 重构,EURUSD 在 M15 下可能触发 Bars() 返回超 3 万根,内存峰值要提前预留。 do-while 首轮先按 Bars() 与 HistoryBars 的较小值做 BufferResize,十个缓冲(开高低收、时间、量、四个指标)任一失败就 break。注意 Refresh(OBJ_ALL_PERIODS) 是同步阻塞调用,在外汇高波动时段可能卡掉主线程 200ms 以上,贵金属 XAUUSD 跳空时更明显,属高风险操作环境。 缓冲区就绪后 prev_up/prev_for/prev_er 置 -1,bar 信号默认 Undefine,add_loop 先 false——这代表每一轮重训都从“无先验”开始,避免上一段行情的权重污染新样本。
class=class="str">"cmt">//--- if(CheckPointer(High)!=POINTER_INVALID) class="kw">delete High; class=class="str">"cmt">//--- if(CheckPointer(Low)!=POINTER_INVALID) class="kw">delete Low; class=class="str">"cmt">//--- if(CheckPointer(Time)!=POINTER_INVALID) class="kw">delete Time; class=class="str">"cmt">//--- if(CheckPointer(Volumes)!=POINTER_INVALID) class="kw">delete Volumes; class=class="str">"cmt">//--- if(CheckPointer(RSI)!=POINTER_INVALID) class="kw">delete RSI; class=class="str">"cmt">//--- if(CheckPointer(CCI)!=POINTER_INVALID) class="kw">delete CCI; class=class="str">"cmt">//--- if(CheckPointer(ATR)!=POINTER_INVALID) class="kw">delete ATR; class=class="str">"cmt">//--- if(CheckPointer(MACD)!=POINTER_INVALID) class="kw">delete MACD; class=class="str">"cmt">//--- if(CheckPointer(Net)!=POINTER_INVALID) class="kw">delete Net; if(CheckPointer(TempData)!=POINTER_INVALID) class="kw">delete TempData; } class="type">void Train(class="type">class="kw">datetime StartTrainBar=class="num">0) class="type">int count=class="num">0; class="type">class="kw">double prev_up=-class="num">1; class="type">class="kw">double prev_for=-class="num">1; class="type">class="kw">double prev_er=-class="num">1; class="type">class="kw">datetime bar_time=class="num">0; class="type">bool stop=IsStopped(); class="type">MqlDateTime sTime; class="type">MqlDateTime start_time; TimeCurrent(start_time); start_time.year-=StudyPeriod; if(start_time.year<=class="num">0) start_time.year=class="num">1900; class="type">class="kw">datetime st_time=StructToTime(start_time); dtStudied=MathMax(StartTrainBar,st_time); do { class="type">int bars=(class="type">int)MathMin(Bars(Symb.Name(),TimeFrame,dtStudied,TimeCurrent())+HistoryBars,Bars(Symb.Name(),TimeFrame)); prev_un=dUndefine; prev_for=dForecast; prev_er=dError; ENUM_SIGNAL bar=Undefine; if(!Open.BufferResize(bars) || !Close.BufferResize(bars) || !High.BufferResize(bars) || !Low.BufferResize(bars) || !Time.BufferResize(bars) || !RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars) || !Volumes.BufferResize(bars)) class="kw">break; Open.Refresh(OBJ_ALL_PERIODS); Close.Refresh(OBJ_ALL_PERIODS); High.Refresh(OBJ_ALL_PERIODS); Low.Refresh(OBJ_ALL_PERIODS); Volumes.Refresh(OBJ_ALL_PERIODS); Time.Refresh(OBJ_ALL_PERIODS); RSI.Refresh(OBJ_ALL_PERIODS); CCI.Refresh(OBJ_ALL_PERIODS); ATR.Refresh(OBJ_ALL_PERIODS); MACD.Refresh(OBJ_ALL_PERIODS); stop=IsStopped(); class="type">bool add_loop=class="kw">false;