利用 MQL5 向导和 Hlaiman EA 生成器创建神经网络 EA·进阶篇
📘

利用 MQL5 向导和 Hlaiman EA 生成器创建神经网络 EA·进阶篇

第 2/3 篇

HNN 信号类的骨架与可调参数

在 MT5 里做基于历史形态神经网络的信号模块,第一步是把类结构立起来。下面这段声明定义了隐藏层数量、每层神经元数、训练轮次、最近一次信号值,以及承载形态数值的 pattern 数组,还有是否对形态做归一化的开关。 hnn_layers、hnn_neurons、hnn_epoch 三个 int 变量直接决定网络容量与训练成本;pattern[] 的长度由 PatternBarsCount 动态设定,调用时会在原值上加 1,例如传入 10 则数组实际尺寸为 11。外汇与贵金属波动非线性强,这类网络仅提供概率倾向,实盘前务必在策略测试器跑过。 公开方法里,LayersCount / NeuronsCount / EpochCount / Normalize 都是一行赋值,真正干活的是 FillPattern(按时间准备形态)、TeachHNN(训练)、SaveFileHNN(落盘网络文件)。开 MT5 新建 EA 时,先把这几个 setter 接上外部输入参数,就能在属性框里实时调层数和神经元数。

MQL5 / C++
  class="type">int                hnn_layers;             class=class="str">"cmt">// layers count
  class="type">int                hnn_neurons;            class=class="str">"cmt">// neurons count
  class="type">int                hnn_epoch;              class=class="str">"cmt">// learn epoch
  class="type">class="kw">double             hnn_signal;             class=class="str">"cmt">// value of last signal
  class="type">class="kw">double             pattern[];              class=class="str">"cmt">// values of the pattern
  class="type">bool               hnn_norm;               class=class="str">"cmt">// normalize pattern
class="kw">public:
                      CSignalHNN(class="type">void);      class=class="str">"cmt">// class constructor
                      ~CSignalHNN(class="type">void);     class=class="str">"cmt">// class destructor
  class=class="str">"cmt">//--- methods of setting adjustable parameters
  class="type">void               PatternBarsCount(class="type">int value) { hnn_in = value; ArrayResize(pattern, value + class="num">1);  }
  class="type">void               LayersCount(class="type">int value)      { hnn_layers = value;  }
  class="type">void               NeuronsCount(class="type">int value)     { hnn_neurons = value; }
  class="type">void               EpochCount(class="type">int value)       { hnn_epoch = value;   }
  class="type">void               Normalize(class="type">bool value)       { hnn_norm = value;    }
  class=class="str">"cmt">//--- method of verification of settings
  class="kw">virtual class="type">bool       ValidationSettings(class="type">void);
  class=class="str">"cmt">//--- method of creating the indicator and timeseries
  class="kw">virtual class="type">bool       InitIndicators(CIndicators *indicators);
  class=class="str">"cmt">//--- methods of checking conditions of entering the market
  class="kw">virtual class="type">class="kw">double     Direction(class="type">void);
  class="type">bool               FillPattern(class="type">class="kw">datetime tim = class="num">0);        class=class="str">"cmt">// prepare pattern
  class="type">bool               AddPattern(class="type">class="kw">string name, class="type">int ptype);  class=class="str">"cmt">// add new pattern
  class="type">bool               TeachHNN(class="type">void);                      class=class="str">"cmt">// learn neural net
  class="type">bool               SaveFileHNN(class="type">void);                   class=class="str">"cmt">// neural net file

◍ HNN 信号类的初始化与句柄回收

在 MT5 里把外部神经网信号接进 EA,第一步是给 CSignalHNN 类挂好句柄。类内声明了 CalculateHNN() 负责算信号、InitHNN(bool openn) 做初始化、FreeHNN() 做反初始化,其中 FreeHNN 直接判断 m_hnn 非零且非 INVALID_HANDLE 才 FileClose,避免重复关句柄导致 4019 错误。 InitHNN 的实作里,先取品种名与周期:若类内 m_symbol 非空就用它,否则退回 _Symbol 与 _Period;周期分钟数由 PeriodSeconds(hnn_period)/60 算出,拼成文件名 hnn_fil 供后续定位神经网模型。

管道用 FileOpen(HLAIMAN_PIPE, FILE_READFILE_WRITEFILE_BIN) 打开,返回句柄写回 m_hnn。若 openn 为真且 hnn_fil 不存在,会依次在公共目录 TERMINAL_COMMONDATA_PATH 与本地 TERMINAL_DATA_PATH+MQL5+FILES 下补全路径——外汇与贵金属行情高波动,模型文件放错目录会让信号返回空值,开 MT5 后先确认文件存在再跑。

下面这段是类内反初始化与初始化核心,逐行拆一下 FreeHNN: if(m_hnn!=0 && m_hnn!=INVALID_HANDLE) // 句柄有效才动手 FileClose(m_hnn); // 关管道释放系统资源 m_hnn=0; // 置零防止野指针 初始化入口 bool CSignalHNN::InitHNN(bool openn) 中,m_hnn=FileOpen(...) 拿到跨进程管道,后面用 CON_OPENN+CON_TRUE 通知外部应用开启会话。

MQL5 / C++
  class="type">class="kw">double              CalculateHNN(class="type">void);                                        class=class="str">"cmt">// calc neural signal
                                                                                   class=class="str">"cmt">//class="kw">protected:
  class=class="str">"cmt">//--- method of initialization of the Hlaiman Application
   class="type">bool                InitHNN(class="type">bool openn);                                        class=class="str">"cmt">// Hlaiman App Init
   class="type">void                FreeHNN(class="type">void)
    {                                                                               class=class="str">"cmt">// Hlaiman App Deinit
     if(m_hnn!=class="num">0 && m_hnn!=INVALID_HANDLE)
      {
       FileClose(m_hnn);
       m_hnn=class="num">0;
      }
    };
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Initialize HNN                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSignalHNN::InitHNN(class="type">bool openn)
  {
class=class="str">"cmt">//--- initialize Hlaiman Application
   class="type">int num=class="num">0;
   class="type">ulong res=class="num">0;
   if(m_symbol!=NULL)
    {
     hnn_sym=m_symbol.Name();
     hnn_period=m_period;
      } else {
     hnn_sym=_Symbol;
     hnn_period=_Period;
    }
   hnn_per = class="type">class="kw">string(PeriodSeconds(hnn_period) / class="num">60);
   hnn_fil = hnn_nam + NAME_DELIM + hnn_sym + hnn_per + NAME_DELIM + class="type">class="kw">string(hnn_index) + TYPE_NEURO;
   if(m_hnn== class="num">0|| m_hnn == INVALID_HANDLE)
     m_hnn=FileOpen(HLAIMAN_PIPE,FILE_READ|FILE_WRITE|FILE_BIN);
   if(m_hnn!=class="num">0 && m_hnn!=INVALID_HANDLE)
    {
     class="type">class="kw">string source,result="";
     if(openn==true)
      {
       result=CON_OPENN+CON_TRUE;
       if(!FileIsExist(hnn_fil,FILE_READ))
        {
         if(FileIsExist(hnn_fil,FILE_READ|FILE_COMMON))
           hnn_fil=TerminalInfoString(TERMINAL_COMMONDATA_PATH)+PATH_FILES+hnn_fil;
         else
          {
           class=class="str">"cmt">//                hnn_fil = hnn_path + PATH_MQL5 + PATH_FILES + hnn_fil;
           hnn_fil=TerminalInfoString(TERMINAL_DATA_PATH)+PATH_MQL5+PATH_FILES+hnn_fil;
          }
       }

「用 Pascal 脚本在 MT5 里热加载神经网络模型」

这段脚本负责在 EA 初始化阶段,把训练好的神经网络参数文件挂到对应品种和周期对象上。路径拼接依赖 TerminalInfoString(TERMINAL_DATA_PATH) 拿到终端数据目录,再叠加 MQL5\Files 子目录,文件找不到时 result 会带上 CON_OPENN+CON_FALSE 标记。 核心逻辑写在一段 Pascal 源码字符串里:先 Open('mt45.dll') 调外部库,再按 hnn_path 打开终端对象,用 ObjectOfName 找专家对象(不存在就 AddObject('TMT45Expert'))。品种对象按 '符号+周期' 命名,例如 'EURUSD15' 这种拼接值,避免多周期混淆。 如果 openn=true,走 sym.Open(hnn_fil) 直接读盘上模型文件,成功则返回 sym.TeachInput 作为层数输入;否则走分支手动写 TeachInput / TeachOutput / TeachLayer / TeachNeurons 四个整数参数。开 MT5 后把 hnn_fil 指向你自己的 .txt 权重文件,就能验证加载是否返回非零 TeachInput。 注意外汇与贵金属杠杆高、滑点随机,神经网络历史拟合不代表实盘概率优势,参数误配可能直接返回 0 导致 EA 静默不训。

MQL5 / C++
else hnn_fil=TerminalInfoString(TERMINAL_DATA_PATH)+PATH_MQL5+PATH_FILES+hnn_fil;
 } else {
 result=CON_OPENN+CON_FALSE;
 hnn_fil=TerminalInfoString(TERMINAL_DATA_PATH)+PATH_MQL5+PATH_FILES+hnn_fil;
}
 InitHNN; Interface "+result+" var libr, term, exp, sym: TObject;"
 " Implementation function main: integer;\n\r" class=class="str">"cmt">// Line #class="num">1
 " begin"
 " Result := class="num">0;"
 " libr := Open(&class="macro">#x27;mt45.dll&class="macro">#x27;);\n\r" class=class="str">"cmt">// Line #class="num">2
 " if (libr <> nil) then"
 " begin"
 " term := Open(&class="macro">#x27;"+hnn_path+"&class="macro">#x27;);\n\r" class=class="str">"cmt">// Line #class="num">3
 " if (term <> nil) then"
 " begin"
 " exp := term.ObjectOfName(&class="macro">#x27;"+hnn_nam+"&class="macro">#x27;);"
 " if (exp = nil) then exp := term.AddObject(&class="macro">#x27;TMT45Expert&class="macro">#x27;);\n\r" class=class="str">"cmt">// Line #class="num">5
 " if (exp <> nil) then"
 " begin"
 " if (exp.Name <> &class="macro">#x27;"+hnn_nam+"&class="macro">#x27;) then exp.Name := &class="macro">#x27;"+hnn_nam+"&class="macro">#x27;;\n\r" class=class="str">"cmt">// Line #class="num">6
 " sym := exp.ObjectOfName(&class="macro">#x27;"+hnn_sym+hnn_per+"&class="macro">#x27;);"
 " if (sym = nil) then sym := exp.AddObject(&class="macro">#x27;TMT45Symbol&class="macro">#x27;);"
 " if (sym <> nil) then"
 " begin"
 " sym.Log.Add(&class="macro">#x27;"+hnn_sym+hnn_per+"&class="macro">#x27;);\n\r"
 " if (sym.Name <> &class="macro">#x27;"+hnn_sym+hnn_per+"&class="macro">#x27;) then sym.Name := &class="macro">#x27;"+hnn_sym+hnn_per+"&class="macro">#x27;;"
 " if (sym.Period <> "+hnn_per+") then sym.Period := "+hnn_per+";"
 " if (openn = true) then"
 " begin"
 class=class="str">"cmt">//                 " sym.Log.Add(&class="macro">#x27;" + hnn_fil + "&class="macro">#x27;);"
 " if (sym.Open(&class="macro">#x27;"+hnn_fil+"&class="macro">#x27;)) then Result := sym.TeachInput;\n\r" class=class="str">"cmt">// ret input Line #class="num">8
 " end else"
 " begin"
 " sym.TeachInput := "+IntegerToString(hnn_in)+";"
 " sym.TeachOutput := "+IntegerToString(hnn_out)+";"
 " sym.TeachLayer := "+IntegerToString(hnn_layers)+";"
 " sym.TeachNeurons := "+IntegerToString(hnn_neurons)+";"

HNN 管道通信与信号计算的代码骨架

这段 MQL5 片段展示了层级神经网络(HNN)指标如何通过命名管道与外部 HLAIMAN 应用交换数据并完成初始化。写入源码字符串后调用 FileWriteString 与 FileFlush,把 TeachEpoch、FileName 等参数推给服务端,随后进入轮询等待返回文件尺寸大于 0。 轮询上限由 WAIT_TIMES 与 SLEEP_TIM 控制;若在回测环境(MQL5InfoInteger(MQL5_TESTER) 为真)则不受 WAIT_TIMES 约束,实盘下最多空转 WAIT_TIMES 次。读到内容后取前半长度字符串转整数,若返回值大于 RES_OK 才视为「初始化成功」,并据此 ArrayResize 出 pattern 数组。 CalculateHNN 开头先判管道句柄 m_hnn 是否为 0 或 INVALID_HANDLE,异常直接回 0.0;通过 FillPattern(0) 拉到数据才继续拼 CON_START 指令。外汇与贵金属波动剧烈,这类外部依赖型信号在断管或文件损坏时可能直接失效,开 MT5 跑前务必确认 HLAIMAN 应用已起。

MQL5 / C++
  " sym.TeachEpoch := "+IntegerToString(hnn_epoch)+";"
  " sym.FileName := &class="macro">#x27;"+hnn_fil+"&class="macro">#x27;;"
  " Result := sym.TeachInput;\n\r" class=class="str">"cmt">// ret input Line #class="num">9
  " end;"
  " end;"
  " end;"
  " end;"
  " end;"
  " end; end.";
  FileWriteString(m_hnn,source,StringLen);
  FileFlush(m_hnn);
  while(res<=class="num">0 && (MQL5InfoInteger(MQL5_TESTER) || num<WAIT_TIMES))
    {
     Sleep(SLEEP_TIM);
     res=FileSize(m_hnn);
     num++;
     }
  if(res>class="num">0)
    {
     result=FileReadString(m_hnn,class="type">int(res/class="num">2));
     res=StringToInteger(result);
     if(res<=RES_OK)
       printf(__FUNCTION__+": Error! Initialization data(possible reason: FILE NOT EXIST OR CORRUPTED "+hnn_fil);
     else
       {
       printf(__FUNCTION__+": Initialization successful! NEURAL PATTERN "+class="type">class="kw">string(res));
       ArrayResize(pattern,class="type">int(res+class="num">1));
       class="kw">return(true);
       }
    }
  else
    printf(__FUNCTION__+": Error! pipe server not responding(possible elimination: RESTART HLAIMAN APPLICATION)");
   }
  else
    printf(__FUNCTION__+": Error! initializing pipe server(possible reason: HLAIMAN APPLICATION IS NOT RUNNING!)");
class=class="str">"cmt">//--- ok
  class="kw">return(false);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculate HNN signal                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CSignalHNN::CalculateHNN(class="type">void)
  {
   if(m_hnn==class="num">0 || m_hnn==INVALID_HANDLE) class="kw">return(class="num">0.0);
   class="type">int num = class="num">0;
   class="type">ulong siz = class="num">0;
   class="type">class="kw">double res=class="num">0.0;
   class="type">class="kw">string source,result="";
   if(FillPattern(class="num">0)==true)
     {
     result=CON_START;

◍ 把形态数组拼进神经网推理脚本

这段逻辑干一件事:把前面提取出的价格形态 pattern 数组,拼成一段可调用的神经网络计算单元源码字符串。循环从下标 1 跑到数组倒数第二位,用 DoubleToString 把每个数值转成文本,中间以 CON_ADD 连接;末位单独追加 CON_END,避免尾部多出一个分隔符。 拼完的 result 被塞进一段 Pascal 风格的计算单元骨架里:先声明接口与变量,main 函数里加载 mt45.dll,再打开指定路径的神经网络术语文件,逐层取到对应品种周期的 exp 对象。若 lst 行数不少于网络输入数,就把每行字符串写进 NetInputs 的 NetValue,触发 Computed 后返回 NetOutputs 第一个节点的 NetValue。 在 MT5 里验证时,重点看 Line #6 附近:lst.Count 必须 ≥ sym.NetInputs.Count,否则整段推理被跳过、Result 恒为 0.0。外汇与贵金属市场波动剧烈、杠杆风险高,这套推理输出仅作概率参考,不能直接当作下单依据。

MQL5 / C++
for(class="type">int i=class="num">1; i<(ArraySize(pattern)-class="num">1); i++)
     result= result+DoubleToString(pattern[i])+CON_ADD;
   result = result + DoubleToString(pattern[ArraySize(pattern) - class="num">1]) + CON_END;
   source = "unit CalcHNN; Interface " + result + " var i: integer; libr, term, exp, sym, lst: TObject;"
         " Implementation function main: class="type">class="kw">double;\n\r" class=class="str">"cmt">// Line #class="num">1
         " begin"
         " Result := class="num">0.0;"
         " libr := Open(&class="macro">#x27;mt45.dll&class="macro">#x27;);\n\r" class=class="str">"cmt">// Line #class="num">2
         " if (libr <> nil) then"
         " begin"
         " term := Open(&class="macro">#x27;"+hnn_path+"&class="macro">#x27;);\n\r" class=class="str">"cmt">// Line #class="num">3
         " if (term <> nil) then"
         " begin"
         " exp := term.ObjectOfName(&class="macro">#x27;"+hnn_nam+"&class="macro">#x27;);\n\r" class=class="str">"cmt">// Line #class="num">4
         " if (exp <> nil) then"
         " begin"
         " sym := exp.ObjectOfName(&class="macro">#x27;"+hnn_sym+hnn_per+"&class="macro">#x27;);\n\r" class=class="str">"cmt">// Line #class="num">5
         " if (sym <> nil) then"
         " begin"
         " lst := TStringList.Create;"
         " if (lst <> nil) then"
         " begin"
         " lst.Text := cons;"
         " if (lst.Count >= sym.NetInputs.Count) then"
         " begin"
         " for i := class="num">0 to sym.NetInputs.Count - class="num">1 do"
         " begin"
         " sym.NetInputs.Objects[i].NetValue := StrToFloat(lst[i]);\n\r" class=class="str">"cmt">// Line #class="num">6
         class=class="str">"cmt">//                  " sym.Log.Add(&class="macro">#x27;Input &class="macro">#x27; + IntToStr(i) + &class="macro">#x27; = &class="macro">#x27; + lst[i]);"
         " end;"
         " sym.Computed := true;"
         " Result := sym.NetOutputs.Objects[class="num">0].NetValue;\n\r" class=class="str">"cmt">// ret input Line #class="num">7
         " end;"
         " lst.Free;"
         " end;"
         " end;"
         " end;"
         " end;"

「把形态写进神经网推理脚本」

上一节解决了从外部模块取数的阻塞等待,这一节看 AddPattern 如何把 K 线形态拼装成可执行的推理脚本。它先以 ptype 填入 pattern[0],再遍历数组用 CON_ADD 连接各分量,尾部用 CON_END 封口,最终拼出一段 Pascal 风格字符串。 这段字符串不是给人读的,是喂给 mt45.dll 加载的神经网络终端用的。源码里硬编码了 'unit AddPatternHNN' 与 'libr := Open(\'mt45.dll\')',说明实盘或回测时本机必须存在该 DLL,否则 libr 为 nil,整条推理链直接断掉。 注意第 3 行 term := Open('"+hnn_path+"') 与第 4 行 exp := term.ObjectOfName('"+hnn_nam+"'),路径和实例名都是外部变量。你在 MT5 里复用这段代码前,得先确认 hnn_path 指向的终端文件、hnn_nam 对应的网络对象真实存在,否则 exp 恒为 nil,返回结果永远为 0。外汇与贵金属品种波动剧烈,这类外部依赖一旦失效,信号可能瞬间消失,属高风险操作。

MQL5 / C++
class="type">bool CSignalHNN::AddPattern(class="type">class="kw">string name,class="type">int ptype)
  {
   class="type">int num=class="num">0;
   class="type">long res=class="num">0;
   class="type">ulong siz=class="num">0;
   class="type">class="kw">string result,source,nam=name;
   if(m_hnn!=class="num">0 || m_hnn!=INVALID_HANDLE)
     {
     pattern[class="num">0]=ptype;
     result=CON_START;
     for(class="type">int i=class="num">0; i<(ArraySize(pattern)-class="num">1); i++)
       result= result+DoubleToString(pattern[i])+CON_ADD;
     result = result + DoubleToString(pattern[ArraySize(pattern) - class="num">1]) + CON_END;
     source = "unit AddPatternHNN; Interface " + result + " Implementation function main: integer;"
            " var i: integer; out: class="type">class="kw">double; onam: class="type">class="kw">string;"
            " libr, term, exp, sym, ord, tck, lst: TObject;\n\r" class=class="str">"cmt">// Line #class="num">1
            " begin"
            " Result := class="num">0;"
            " libr := Open(&class="macro">#x27;mt45.dll&class="macro">#x27;);\n\r" class=class="str">"cmt">// Line #class="num">2
            " if (libr <> nil) then"
            " begin"
            " term := Open(&class="macro">#x27;"+hnn_path+"&class="macro">#x27;);\n\r" class=class="str">"cmt">// Line #class="num">3
            " if (term <> nil) then"
            " begin"
            " exp := term.ObjectOfName(&class="macro">#x27;"+hnn_nam+"&class="macro">#x27;);\n\r" class=class="str">"cmt">// Line #class="num">4
            " if (exp <> nil) then"
            " begin"

常见问题

主要看信号类构造时的窗口长度、形态维度、阈值与模型路径这几个字段,改完重编译即可生效。
先确认模型文件已放到指定目录且权限可读,再检查初始化顺序是否在OnInit里最早执行,失败要显式释放已开句柄。
可以,把EA源码或信号类片段贴给小布,它能标出句柄泄漏、参数越界和初始化顺序问题,省去手动排查。
用Pascal脚本触发管道命令让EA重新读取模型文件,确认返回码为0即代表热替换成功。
保证数组长度与网络输入层一致、归一化方式和训练时相同,否则推理输出会偏移甚至报错。