数据分组处理方法:在MQL5中实现多层迭代算法。·进阶篇
🧮

数据分组处理方法:在MQL5中实现多层迭代算法。·进阶篇

(2/3)· 接基础篇概念,这一篇把MIA的MQL5实现与调参坑位一次摊开

实战向 第 2/3 篇
不少人在MQL5里照抄伪代码跑MIA,结果迭代层数一多就爆内存或陷入死循环。问题往往出在候选模型池没设上限,以及停止准则直接套用样例阈值,和自己的样本量根本不匹配。

「MT5里落地GMDH要啃的三层头文件」

把C++版的GMDH模型搬进MT5,多线程训练和QR分解的备选方案得砍掉,但核心逻辑原样保留。程序拆成三个头文件,先读gmdh_internal.mqh,它定义所有自定义类型和基类工具,是后面两座头文件的地基。 文件开头三个枚举先把模型可调的口子定死。PolynomialType决定下一轮拿旧变量生成新变量用哪种多项式:linear就是v0+v1*x1+v2*x2,linear_cov多带一个交互项v3*x1*x2,quadratic再补上x1^2和x2^2两项。Solver在MT5里只留一个可选项(C++原版用Eigen做Householder变形QR),CriterionType列了9种外部停止准则,从reg常规SSE到symAbsoluteNoiseImmun对称噪声免疫,训练时按选中的规则筛候选模型。 四个结构体接着上:BufferValues存训练/测试集算出的系数和预测向量;PairDVXd绑一个标量和向量;PairMVXd用矩阵装输入、向量装输出,每行对应一个目标;SplittedData专门存切分后的训练集和测试集。类层面,Combination是单个候选模型,CVector和CVector2d是嵌套容器,Criterion实现9种准则的计算,末尾validateInputData()做入参校验,timeSeriesTransformation()按lags把单向量翻成输入+目标结构——lags就是拿几个先导值去推后面的值。 第二个头文件gmdh.mqh里,splitData()收矩阵+目标向量,testSize控测试比例,shuffle和randomSeed管不打乱、用啥种子。GmdhModel是基类, train/predict之外还有save/load,模型以JSON落盘到MT5终端共用目录。第三个mia.mqh的MIA类继承它,搞多层迭代,fit()有两个重载:一个只吃time_series+lags做纯时间序列,另一个吃vars矩阵+targets做带自变量建模。fit参数表里kBest决定每层留几个局部模型当下一层输入,limit设了外部准则最低提升阈值才继续训。 训完调predict()塞输入向量和预测数量,成功回预测向量,失败回零向量。外汇和贵金属行情高波动,这类模型在外盘实盘只作概率参考,别当确定性信号。下面这段代码是gmdh_internal.mqh里最前面的枚举定义,逐行看: enum PolynomialType 定义多项式类型枚举,控制新变量生成方式 linear 线性:v0+v1*x1+v2*x2 linear_cov 带交互项线性:多v3*x1*x2 quadratic 二次:补v4*x1^2+v5*x2^2 enum Solver 线性方程求解的QR分解方法枚举(MT5仅留一个选项) fast / accurate / balanced 三种标记 enum CriterionType 外部准则枚举,9个值对应上文停止规则 struct BufferValues 结构体,coeffsTrain字段存训练集算出的系数向量

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+---------------------------------------------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;Enumeration for specifying the polynomial type to be used to construct new variables from existing ones|</span>
<span class="comment">class=class="str">"cmt">//+---------------------------------------------------------------------------------------------------------+</span>
<span class="keyword">enum</span> PolynomialType
&nbsp;&nbsp;{
&nbsp;&nbsp; linear,
&nbsp;&nbsp; linear_cov,
&nbsp;&nbsp; quadratic
&nbsp;&nbsp;};
<span class="comment">class=class="str">"cmt">//+-----------------------------------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;Enum&nbsp;&nbsp;for specifying the QR decomposition method for linear equations solving in models.&nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">class=class="str">"cmt">//+-----------------------------------------------------------------------------------------------+</span>
<span class="keyword">enum</span> Solver
&nbsp;&nbsp;{
&nbsp;&nbsp; fast,
&nbsp;&nbsp; accurate,
&nbsp;&nbsp; balanced
&nbsp;&nbsp;};
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|Enum for specifying the external criterion&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">enum</span> CriterionType
&nbsp;&nbsp;{
&nbsp;&nbsp; reg,
&nbsp;&nbsp; symReg,
&nbsp;&nbsp; stab,
&nbsp;&nbsp; symStab,
&nbsp;&nbsp; unbiasedOut,
&nbsp;&nbsp; symUnbiasedOut,
&nbsp;&nbsp; unbiasedCoef,
&nbsp;&nbsp; absoluteNoiseImmun,
&nbsp;&nbsp; symAbsoluteNoiseImmun
&nbsp;&nbsp;};
<span class="comment">class=class="str">"cmt">//+-------------------------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Structure for storing coefficients and predicted values calculated in different ways|</span>
<span class="comment">class=class="str">"cmt">//+--------------------------------------------------------------------------------------+</span>
<span class="keyword">class="kw">struct</span> BufferValues
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">vector</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;coeffsTrain; <span class="comment">class=class="str">"cmt">// Coefficients vector calculated using training data</span>

◍ 回测容器与配对结构的写法

做跨样本验证时,最忌把训练集和测试集的预测值混在同一个数组里。下面这段结构把 coeffsTest、coeffsAll 与四组 yPred(训练集被训练系数预测、训练集被测试系数预测、测试集被训练系数预测、测试集被测试系数预测)分开存,能直接对比过拟合程度。 BufferValues 的拷贝构造和赋值运算符把 7 个 vector 字段逐字段浅拷贝,MT5 里 vector 类型自带引用语义,复制成本可控。注意原文里 coeffsTrain 在声明段没列出,却在拷贝函数里出现,编译前得补上声明,否则会报未定义成员。 PairDVXd 则是「标量 + 向量」的配对容器,默认构造里 second 用 vector::Zeros(10) 初始化成长度 10 的零向量。如果你做特征维度不是 10 的回归,把 10 改成实际因子数,否则 Copy 时会因长度不一致截断。 外汇与贵金属杠杆高、滑点跳空频繁,这类离线容器只解决数据存储,实盘信号延迟风险仍在,参数维度务必和你的样本窗口对齐。

MQL5 / C++
vector coeffsTest; class=class="str">"cmt">// Coefficients vector calculated using testing data
vector coeffsAll; class=class="str">"cmt">// Coefficients vector calculated using learning data
vector yPredTrainByTrain; class=class="str">"cmt">// Predicted values for *training* data calculated using coefficients vector calculated on *training* data
vector yPredTrainByTest; class=class="str">"cmt">// Predicted values for *training* data calculated using coefficients vector calculated on *testing* data
vector yPredTestByTrain; class=class="str">"cmt">// Predicted values for *testing* data calculated using coefficients vector calculated on *training* data
vector yPredTestByTest; class=class="str">"cmt">//Predicted values for *testing* data calculated using coefficients vector calculated on *testing* data

BufferValues(class="type">void)
 {
 }

BufferValues(BufferValues &other)
 {
   coeffsTrain = other.coeffsTrain;
   coeffsTest = other.coeffsTest;
   coeffsAll = other.coeffsAll;
   yPredTrainByTrain = other.yPredTrainByTrain;
   yPredTrainByTest = other.yPredTrainByTest;
   yPredTestByTrain = other.yPredTestByTrain;
   yPredTestByTest = other.yPredTestByTest;
 }
 BufferValues class="kw">operator=(BufferValues &other)
 {
   coeffsTrain = other.coeffsTrain;
   coeffsTest = other.coeffsTest;
   coeffsAll = other.coeffsAll;
   yPredTrainByTrain = other.yPredTrainByTrain;
   yPredTrainByTest = other.yPredTrainByTest;
   yPredTestByTrain = other.yPredTestByTrain;
   yPredTestByTest = other.yPredTestByTest;
   class="kw">return this;
 }
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| class="kw">struct PairDV |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct PairDVXd
 {
  class="type">class="kw">double first;
  vector second;

  PairDVXd(class="type">void)
  {
   first = class="num">0.0;
   second = vector::Zeros(class="num">10);
  }

  PairDVXd(class="type">class="kw">double &_f, vector &_s)
  {
   first = _f;
   second.Copy(_s);
  }

  PairDVXd(PairDVXd &other)
  {
   first = other.first;
   second = other.second;
  }
  PairDVXd class="kw">operator=(PairDVXd& other)
  {
   first = other.first;
   second = other.second;
   class="kw">return this;
  }
 };

把数据集拆进结构体才方便跑模型

在 MT5 里做 GMDH 类算法,第一步不是急着写训练循环,而是先把输入输出和切分结果用结构体收口。下面两个结构体就是干这个的:PairMVXd 存「矩阵 + 向量」成对数据,SplittedData 存训练集与测试集的四块内容。 PairMVXd 默认构造里 first 和 second 都初始化成 10×10 零矩阵和 10 维零向量(matrix::Zeros(10,10) 与 vector::Zeros(10)),说明作者默认最小可用维度是 10。如果你拿 EURUSD 的 10 根 K 线做特征,这个尺寸刚好能跑通,但样本量再小就会越界。 SplittedData 把 xTrain、xTest、yTrain、yTest 全塞进一个对象,默认同样是 10 维零矩阵/向量。拷贝构造和赋值运算符都做了浅层成员复制,意味着你传引用时不会丢数据,但注意它没做深拷贝保护,多线程下共享同一实例可能互相踩。 Combination 类开头挂了 _combination、_bestCoeffs 两个 vector 和 _evaluation 一个 double,公开区才刚开始。外汇与贵金属波动大、杠杆高,用这类模型做信号前务必在策略测试器里用历史数据回测,实盘可能失效。

MQL5 / C++
class=class="str">"cmt">//| structure PairMVXd                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct PairMVXd
  {
   matrix                first;
   vector                second;
                        PairMVXd(class="type">void)
     {
      first = matrix::Zeros(class="num">10,class="num">10);
      second = vector::Zeros(class="num">10);
     }
                        PairMVXd(matrix &_f,  vector& _s)
     {
      first = _f;
      second = _s;
     }
                        PairMVXd(PairMVXd &other)
     {
      first = other.first;
      second = other.second;
     }
   PairMVXd              class="kw">operator=(PairMVXd &other)
     {
      first = other.first;
      second = other.second;
      class="kw">return this;
     }
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  Structure for storing parts of a split dataset                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct SplittedData
  {
   matrix                xTrain;
   matrix                xTest;
   vector                yTrain;
   vector                yTest;
                        SplittedData(class="type">void)
     {
      xTrain = matrix::Zeros(class="num">10,class="num">10);
      xTest = matrix::Zeros(class="num">10,class="num">10);
      yTrain = vector::Zeros(class="num">10);
      yTest = vector::Zeros(class="num">10);
     }
                        SplittedData(SplittedData &other)
     {
      xTrain = other.xTrain;
      xTest =  other.xTest;
      yTrain = other.yTrain;
      yTest =  other.yTest;
     }
   SplittedData          class="kw">operator=(SplittedData &other)
     {
      xTrain = other.xTrain;
      xTest =  other.xTest;
      yTrain = other.yTrain;
      yTest =  other.yTest;
      class="kw">return this;
     }
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Сlass representing the candidate model of the GMDH algorithm       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class Combination
  {
   vector                _combination,_bestCoeffs;
   class="type">class="kw">double                _evaluation;
class="kw">public:

「组合类与容器类的骨架拆解」

下面这段 MQL5 类定义给出了遗传/组合优化里的两个核心结构:Combination 负责存一组权重向量与评分,CVector 则充当这些实例的动态容器。 Combination 默认构造里把 _combination 初始化成长度为 10 的零向量,_bestCoeffs 直接拷贝该零向量,_evaluation 置为 DBL_MAX,意味着初始态视作最差评分。带 vector 引用的构造支持从外部传入组合或系数,尺寸由传入向量决定,不再写死 10。 重载的 operator< 只比较 _evaluation,这让 CVector 后续做排序时天然按误差升序排,方便保留精英个体。CVector 的默认构造把 m_reserve 设成 1000,即预留 1000 个元素的数组内存,减少频繁扩容;参数化构造允许指定 size 与 mem_reserve,并立刻 ArrayResize 到位。 在 MT5 里新建 EA 把这两段类贴进头文件,改 m_reserve 从 1000 调到 5000,跑大规模组合搜索时内存重分配次数会明显下降,你可以直接对比策略测试器的日志时长验证。

MQL5 / C++
Combination(class="type">void) { _combination = vector::Zeros(class="num">10); _bestCoeffs.Copy(_combination); _evaluation = DBL_MAX; }
Combination(vector &comb) : _combination(comb) { _bestCoeffs=vector::Zeros(_combination.Size()); _evaluation = DBL_MAX;}
Combination(vector &comb, vector &coeffs) : _combination(comb),_bestCoeffs(coeffs) { _evaluation = DBL_MAX; }
Combination(Combination &other) { _combination = other.combination(); _bestCoeffs=other.bestCoeffs(); _evaluation = other.evaluation();}
 vector combination(class="type">void) { class="kw">return _combination;}
 vector bestCoeffs(class="type">void) { class="kw">return _bestCoeffs; }
 class="type">class="kw">double evaluation(class="type">void) { class="kw">return _evaluation; }
 class="type">void setCombination(vector &combination) { _combination = combination; }
 class="type">void setBestCoeffs(vector &bestcoeffs) { _bestCoeffs = bestcoeffs; }
 class="type">void setEvaluation(class="type">class="kw">double evaluation) { _evaluation = evaluation; }
 class="type">bool class="kw">operator<(Combination &combi) { class="kw">return _evaluation<combi.evaluation();}
 Combination class="kw">operator=(Combination &combi)
  {
   _combination = combi.combination();
   _bestCoeffs = combi.bestCoeffs();
   _evaluation = combi.evaluation();
   class="kw">return this;
  }
 };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| collection of Combination instances                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CVector
 {
class="kw">protected:
  Combination    m_array[];
  class="type">int            m_size;
  class="type">int            m_reserve;
class="kw">public:
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//| class="kw">default constructor                                              |
  class=class="str">"cmt">//+------------------------------------------------------------------+
         CVector(class="type">void) :m_size(class="num">0),m_reserve(class="num">1000) { }
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//| parametric constructor specifying initial size                   |
  class=class="str">"cmt">//+------------------------------------------------------------------+
         CVector(class="type">int size, class="type">int mem_reserve = class="num">1000) :m_size(size),m_reserve(mem_reserve)
  {
   ArrayResize(m_array,m_size,m_reserve);

◍ 向量容器的拷贝与增改实现

在 MT5 自建组合类容器时,拷贝构造函数决定了对象传参和赋值的成本。下面这段代码把源对象的 size 与 reserve 一并继承,用 ArrayResize 按原预留量扩容,再逐元素拷贝,避免反复重分配。 push_back 每次调用前先 ResetLastError,以 ArrayResize(m_array, Size()+1, m_reserve) 追加一位;若返回长度小于预期则打印错误并返回 false,否则 m_size 自增后写入。注意 m_reserve 作为第三参能在批量压入时降低重分配次数。 setAt 做边界检查,index 越界直接 Print 并返回 false,不抛异常;合法则原地覆盖。这种显式错误返回比依赖运行时崩溃更适合 EA 长时间跑盘。 别把 reserve 当摆设 很多抄来就用的向量类漏了把 other.reserve() 传给 ArrayResize,结果拷贝后预留量为 0,后续 push_back 每次都重分配,回测里 10 万次插入可能多耗数秒。开 MT5 把 m_reserve 打印出来对比一下就知道。

MQL5 / C++
             }
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//| Copy constructor                                                 |
  class=class="str">"cmt">//+------------------------------------------------------------------+
                     CVector(CVector &other)
    {
      m_size = other.size();
      m_reserve = other.reserve();
      ArrayResize(m_array,m_size,m_reserve);
      for(class="type">int i=class="num">0; i<m_size; ++i)
        m_array[i]=other[i];
    }
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//| destructor                                                       |
  class=class="str">"cmt">//+------------------------------------------------------------------+
                     ~CVector(class="type">void)
    {
    }
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//| Add element to end of array                                      |
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class="type">bool              push_back(Combination &value)
    {
      ResetLastError();
      if(ArrayResize(m_array,class="type">int(m_array.Size()+class="num">1),m_reserve)<m_size+class="num">1)
        {
         Print(__FUNCTION__," Critical error: failed to resize underlying array ", GetLastError());
         class="kw">return false;
        }
      m_array[m_size++]=value;
      class="kw">return true;
    }
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//| set value at specified index                                     |
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class="type">bool              setAt(class="type">int index, Combination &value)
    {
      ResetLastError();
      if(index < class="num">0 || index >= m_size)
        {
         Print(__FUNCTION__," index out of bounds ");
         class="kw">return false;
        }
      m_array[index]=value;
      class="kw">return true;
    }
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//|access by index                                                   |
  class=class="str">"cmt">//+------------------------------------------------------------------+

向量容器的访问与内存预留接口

CVector 类在组合生成逻辑之外,还重载了下标与赋值运算符,并暴露了首尾元素访问和容量查询方法。下标运算符返回的是 GetPointer 包装的对象指针,索引先转成 uint 再取 m_array,避免负索引直接越界。 赋值运算符先把自身 clear(),随后拷贝对方的 size 与 reserve,用 ArrayResize 按目标尺寸重分配,再逐元素浅拷贝。注意这里返回的是 this 指针,支持连续赋值,但元素若为指针类型需自行管理深拷贝。 back() 与 front() 分别返回 m_array[m_size-1] 和 m_array[0] 的指针,调用前必须确认 size 大于 0,否则取 -1 下标会触发运行时错误。size() 直接走 ArraySize,reserve() 读成员变量,而带参 reserve(int) 仅在 new_reserve>0 时才生效,负值会被静默忽略。 开 MT5 把这段贴进自定义类,跑一个 3 元素向量的赋值与 back 调用,就能验证 m_reserve 在 resize 后是否按预期保留。

MQL5 / C++
 Combination*      class="kw">operator[](class="type">int index)
    {
      class="kw">return GetPointer(m_array[class="type">uint(index)]);
    }
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//|overload assignment class="kw">operator                                        |
  class=class="str">"cmt">//+------------------------------------------------------------------+
  CVector            class="kw">operator=(CVector &other)
    {
      clear();
      m_size = other.size();
      m_reserve = other.reserve();
      ArrayResize(m_array,m_size,m_reserve);
      for(class="type">int i=class="num">0; i<m_size; ++i)
         m_array[i]= other[i];
      class="kw">return this;
    }
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//|access last element                                                 |
  class=class="str">"cmt">//+------------------------------------------------------------------+
  Combination*      back(class="type">void)
    {
      class="kw">return GetPointer(m_array[m_size-class="num">1]);
    }
  class=class="str">"cmt">//+-------------------------------------------------------------------+
  class=class="str">"cmt">//|access by first index                                               |
  class=class="str">"cmt">//+------------------------------------------------------------------+
  Combination*      front(class="type">void)
    {
      class="kw">return GetPointer(m_array[class="num">0]);
    }
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//| Get current size of collection ,the number of elements             |
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class="type">int               size(class="type">void)
    {
      class="kw">return ArraySize(m_array);
    }
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//|Get the reserved memory size                                        |
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class="type">int               reserve(class="type">void)
    {
      class="kw">return m_reserve;
    }
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//|set the reserved memory size                                        |
  class=class="str">"cmt">//+------------------------------------------------------------------+
  class="type">void              reserve(class="type">int new_reserve)
    {
      if(new_reserve > class="num">0)
         m_reserve = new_reserve;
    }
把模型评估交给小布盯盘
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到回测曲线与过拟合预警,你只管调结构。把重复劳动交给小布,你专注决策。

常见问题

MIA每层只做两两变量组合并外样本筛选,不反向传播;MQL5里要用文件或数组缓存每层胜出模型,避免重复计算。
倾向在验证误差首次回升前停手;层数过多可能过拟合,概率上反而掉预测力,需用持有样本卡一道。
目前小布盯盘内置的是诊断与可视化,MIA建模仍需你在本机MQL5编译执行,但导出结果可贴回小布做比对。
周末跳空与流动性断裂会让Volterra项失真,建议先切分时段再分组,贵金属同样属高风险品种需严控杠杆。