数据分组处理方法:在MQL5中实现多层迭代算法。·进阶篇
(2/3)· 接基础篇概念,这一篇把MIA的MQL5实现与调参坑位一次摊开
「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字段存训练集算出的系数向量
<span class="comment">class=class="str">"cmt">//+---------------------------------------------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| 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 { linear, linear_cov, quadratic }; <span class="comment">class=class="str">"cmt">//+-----------------------------------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Enum for specifying the QR decomposition method for linear equations solving in models. |</span> <span class="comment">class=class="str">"cmt">//+-----------------------------------------------------------------------------------------------+</span> <span class="keyword">enum</span> Solver { fast, accurate, balanced }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//|Enum for specifying the external criterion |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">enum</span> CriterionType { reg, symReg, stab, symStab, unbiasedOut, symUnbiasedOut, unbiasedCoef, absoluteNoiseImmun, symAbsoluteNoiseImmun }; <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 { <span class="keyword">vector</span> 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 时会因长度不一致截断。 外汇与贵金属杠杆高、滑点跳空频繁,这类离线容器只解决数据存储,实盘信号延迟风险仍在,参数维度务必和你的样本窗口对齐。
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,公开区才刚开始。外汇与贵金属波动大、杠杆高,用这类模型做信号前务必在策略测试器里用历史数据回测,实盘可能失效。
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,跑大规模组合搜索时内存重分配次数会明显下降,你可以直接对比策略测试器的日志时长验证。
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 打印出来对比一下就知道。
} 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 后是否按预期保留。
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; }