神经网络变得轻松(第二部分):网络训练和测试·进阶篇
📘

神经网络变得轻松(第二部分):网络训练和测试·进阶篇

第 2/3 篇

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 记得释放内存。 梯度爆炸我曾踩过坑:隐藏层权重失控增长,超出变量上限直接瘫痪网络。根因是后层索要的输出超出激活函数范围。解法是对神经元目标值做标准化,附带的改进梯度代码能把这种情况压住。外汇和贵金属波动无序性高,这类模型仅作概率参考,实盘前务必用历史数据自测。

MQL5 / C++
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 观察信号稀疏度。

MQL5 / C++
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 前务必用策略测试器跑足样本,否则过拟合概率偏高。

MQL5 / C++
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 事件触发频率的差值,能直接验证上面的时间公式。外汇与贵金属杠杆高,此类模型仅作辅助,实盘前务必在策略测试器跑足样本。

MQL5 / C++
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——这代表每一轮重训都从“无先验”开始,避免上一段行情的权重污染新样本。

MQL5 / C++
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;

常见问题

按输入维度逐个建指标容器数组,在初始化函数里用iCustom或iMA等拿到句柄后绑到容器,回测前先Refresh一次避免空值。
检查ChartEvent里是否带了正确ID,且网络对象指针要在OnInit完成绑定,事件里只调训练方法不重复new对象。
可以,小布能读取你贴的初始化代码,指出指标句柄未刷新、指针未释放等常见坑,并给修改建议。
把指针回收和窗口初始化放在OnInit末尾并判空,回测首tick前用Bars()充足性校验再喂数据。
常见从3到7个指标起步,先小维度跑通再扩,过高维度在样本少时容易过拟合,贵金属波动大需控风险。