在 MQL5 中提升数值预测的集成方法·综合运用
(3/3)·单一模型总在噪点前翻车?用纯MQL5把多个次优预测揉成更抗过拟合的集成
共识模型的拟合与预测入口
CGenReg 类用一个工作向量 m_work 承接多个子模型的输出,长度等于模型数组的大小。训练时先把因变量 train_targets 单列化塞进 matrix targ,再逐行跑每个模型的 forecast,把所有预测值铺进 preds 矩阵,最后交给 grnn 做拟合。 预测阶段更轻量:predict 只把各模型对当前 inputs 的 forecast 结果写进 m_work,再让 grnn 吐出最终值。注意这里返回的是 output[0],说明 grnn 输出是向量但只取首元素。 下面这段是核心实现,可直接贴进 MT5 的 .mqh 里验证编译。外汇与贵金属行情受杠杆与跳空影响,模型预测仅反映历史样本关系,实盘信号失效概率不低,务必用小资金或模拟盘先跑。
vector m_work ; class=class="str">"cmt">// Work vector nmodels class="type">long vector m_targs; matrix m_vars; } ; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Fit consensus model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CGenReg::fit(matrix & train_vars, vector &train_targets,IModel* &models[]) { m_targs = train_targets; m_vars = train_vars; m_work = vector::Zeros(models.Size()); matrix targ = matrix::Zeros(train_targets.Size(),class="num">1); if(!targ.Col(train_targets,class="num">0)) { Print(__FUNCSIG__, " error adding column ", GetLastError()); class="kw">return false; } matrix preds(m_vars.Rows(),models.Size()); for(class="type">ulong i = class="num">0; i<m_vars.Rows(); i++) { vector ins = m_vars.Row(i); for(class="type">uint j = class="num">0; j< models.Size(); j++) { preds[i][j] = models[j].forecast(ins); } } class="kw">return grnn.fit(preds,targ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Make a prediction | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CGenReg::predict(vector &inputs,IModel* &models[]) { vector output; for(class="type">uint i = class="num">0; i<models.Size(); i++) m_work[i] = models[i].forecast(inputs); output = grnn.predict(m_work); class="kw">return output[class="num">0]; }
◍ 用脚本实测几种集成模型的优劣边界
Ensemble_Demo.mq5 这套脚本在 MT5 里直接生成三组合成数据:干净数据训出的叫“好模型”,掺噪数据训出的叫“坏模型”,相对好模型有偏移的叫“有偏模型”。脚本允许你指定好/坏/有偏模型的数量,以及每组的样本量,还能通过 TrainCombinedModelsOnCleanData 参数切换——true 用干净数据训集成模型,false 用带噪数据训。 底层网络由 mlffnn.mqh 里的 FFNN 类承载,本质是个多层感知机。它用 m_trained 标记训练状态,m_weights 存层间权重矩阵,m_outputs 存各隐藏层输出,m_epochs 控制迭代次数,m_learn_rate 管权重更新步长,m_act_fn 选激活函数。私有方法 create 分配内存、calculate 做前向传播、backprop 反传调权;公共方法里 fit 训网络、predict 出预测。 默认参数跑出来的结果和切到带噪数据训共识模型的结果对比显示:集成方法多数情况下比单一模型表现好,但不存在万能最优。无约束回归容易过拟合,尤其样本小、噪声大时;GRNN 靠平滑在小样本高噪数据上稳,但大样本高质量数据会丢拟合力;简单平均和方差加权在噪声不确定时最省心。 外汇与贵金属市场高风险,这类合成实验结论只说明概率倾向,实盘前务必在 MT5 用验证集复测。把脚本里模型数量从默认改到 20 个好模型 + 10 个坏模型,看样本量变化时回测曲线怎么走,比看文章直观。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class for a basic feed-forward neural network | class=class="str">"cmt">//+------------------------------------------------------------------+ class FFNN { class="kw">protected: class="type">bool m_trained; class=class="str">"cmt">// flag noting if neural net successfully trained matrix m_weights[]; class=class="str">"cmt">// layer weights matrix m_outputs[]; class=class="str">"cmt">// hidden layer outputs matrix m_result; class=class="str">"cmt">// training result class="type">uint m_epochs; class=class="str">"cmt">// number of epochs class="type">ulong m_num_inputs; class=class="str">"cmt">// number of input variables for nn class="type">ulong m_layers; class=class="str">"cmt">// number of layers of neural net
「神经网络结构的初始化校验与权重矩阵落地」
在 MT5 里手搓前馈网络,第一道坎不是训练而是结构自检。create() 方法开头就卡了一条硬规则:总层数 m_layers 减去隐藏层数 m_hidden_layers 必须等于 1,也就是只允许有单个输出节点。若配置成多输出却没改这里,终端会直接 Print 出 "Network structure misconfiguration" 并返回 false,EA 连加载都过不了。 权重矩阵的维度按层递推:输入层权重 Init 接收 (m_num_inputs+1) 行 × m_hidden_layer_size[0] 列,那个 +1 是偏置项;中间隐藏层用上一层节点数 +1 去初始化当前层;最后一层输出层固定为 (m_hidden_layer_size[末层-1]+1) × 1。任何一次 Init 失败都会带 __LINE__ 和 GetLastError() 打印,方便你定位是哪一层维度算错。 建议把 m_hidden_layer_size[] 先写死成 {10, 10} 做双隐层验证,m_num_inputs 设 5 时首层权重应是 6×10。外汇与贵金属杠杆交易高风险,这类自定义网络仅作信号辅助,实盘前务必在策略测试器跑足历史样本。
class="type">ulong m_hidden_layers; class=class="str">"cmt">// number of hidden layers class="type">ulong m_hidden_layer_size[]; class=class="str">"cmt">// node config for layers class="type">class="kw">double m_learn_rate; class=class="str">"cmt">// learning rate ENUM_ACTIVATION_FUNCTION m_act_fn; class=class="str">"cmt">// activation function class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Initialize the neural network structure | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">bool create(class="type">void) { if(m_layers - m_hidden_layers != class="num">1) { Print(__FUNCTION__," Network structure misconfiguration "); class="kw">return false; } for(class="type">ulong i = class="num">0; i<m_layers; i++) { if(i==class="num">0) { if(!m_weights[i].Init(m_num_inputs+class="num">1,m_hidden_layer_size[i])) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); class="kw">return false; } } else if(i == m_layers-class="num">1) { if(!m_weights[i].Init(m_hidden_layer_size[i-class="num">1]+class="num">1,class="num">1)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); class="kw">return false; } } else { if(!m_weights[i].Init(m_hidden_layer_size[i-class="num">1]+class="num">1,m_hidden_layer_size[i])) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); class="kw">return false; } } }
前向传播与误差回传的矩阵实现
这段神经网络类的核心计算落在 calculate() 与 backprop() 两个虚函数里。calculate() 先校验输入矩阵的列数是否等于首层权重的行数减 1,不匹配就打印兼容错误并返回 0×0 矩阵,这是防止维度错配导致 MT5 EA 崩溃的第一道闸。 前向过程用循环把各隐藏层的偏置列(全 1 向量)拼到 temp 右侧,再走 MatMul 乘权重、Activation 走激活函数,最终输出层直接返回矩阵乘法结果,不套激活。 backprop() 开头就卡死 targets 与 result 的行列必须一致,否则打印「invalid function parameters」退出。外汇与贵金属市场波动剧烈、杠杆风险高,这类自研神经网络信号仅作概率参考,实盘前务必在 MT5 策略测试器用历史数据跑通维度逻辑。 下面只截取 calculate() 主体,方便你直接对照逐行改参:
class="kw">virtual matrix calculate(matrix &data) { if(data.Cols() != m_weights[class="num">0].Rows()-class="num">1) { Print(__FUNCTION__," input data not compatible with network structure "); class="kw">return matrix::Zeros(class="num">0,class="num">0); } matrix temp = data; for(class="type">ulong i = class="num">0; i<m_hidden_layers; i++) { if(!temp.Resize(temp.Rows(), m_weights[i].Rows()) || !temp.Col(vector::Ones(temp.Rows()), m_weights[i].Rows() - class="num">1)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); matrix::Zeros(class="num">0,class="num">0); } m_outputs[i]=temp.MatMul(m_weights[i]); if(!m_outputs[i].Activation(temp, m_act_fn)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); class="kw">return matrix::Zeros(class="num">0,class="num">0); } } if(!temp.Resize(temp.Rows(), m_weights[m_hidden_layers].Rows()) || !temp.Col(vector::Ones(temp.Rows()), m_weights[m_hidden_layers].Rows() - class="num">1)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); class="kw">return matrix::Zeros(class="num">0,class="num">0); } class="kw">return temp.MatMul(m_weights[m_hidden_layers]); }
◍ 反向传播里的权重更新细节
这段逻辑跑在神经网络的反向传播尾部,核心是把输出误差一层层往回传,并同步修正各层权重矩阵。 最外层循环从最后一个隐藏层索引 m_hidden_layers-1 向下走到 0,每一步先取该层输出的激活值与导数,再做维度对齐(补一列偏置的 1)。若 Activation / Derivative / Resize 任一步返回失败,立即 Print 出错行号并 return false,方便在 MT5 Experts 日志里定位是哪一层矩阵尺寸不匹配。 权重修正公式为 m_weights[i+1] = m_weights[i+1] + temp.Transpose().MatMul(loss) * m_learn_rate,学习率 m_learn_rate 直接乘在梯度外,意味着调大它单步权重变化更激进、调小则收敛更慢但可能更稳。 循环结束后,用输入数据 data 补偏置列,再对输入层权重 m_weights[0] 做最后一次更新后 return true。外汇与贵金属行情噪声大,这类自研网络在实盘前务必用历史 tick 做离线回测,过拟合概率不低。
class="kw">return false; } matrix loss = (targets - result) * class="num">2; matrix gradient = loss.MatMul(m_weights[m_hidden_layers].Transpose()); matrix temp; for(class="type">long i = class="type">long(m_hidden_layers-class="num">1); i>-class="num">1; i--) { if(!m_outputs[i].Activation(temp, m_act_fn)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); class="kw">return false; } if(!temp.Resize(temp.Rows(), m_weights[i+class="num">1].Rows()) || !temp.Col(vector::Ones(temp.Rows()), m_weights[i+class="num">1].Rows() - class="num">1)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); class="kw">return false; } m_weights[i+class="num">1] = m_weights[i+class="num">1] + temp.Transpose().MatMul(loss) * m_learn_rate; if(!m_outputs[i].Derivative(temp, m_act_fn)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); class="kw">return false; } if(!gradient.Resize(gradient.Rows(), gradient.Cols() - class="num">1)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); class="kw">return false; } loss = gradient * temp; gradient = (i>class="num">0)?loss.MatMul(m_weights[i].Transpose()):gradient; } temp = data; if(!temp.Resize(temp.Rows(), m_weights[class="num">0].Rows()) || !temp.Col(vector::Ones(temp.Rows()), m_weights[class="num">0].Rows() - class="num">1)) { Print(__FUNCTION__," ",__LINE__," ", GetLastError()); class="kw">return false; } m_weights[class="num">0] = m_weights[class="num">0] + temp.Transpose().MatMul(loss) * m_learn_rate; class="kw">return true; } class="kw">public: class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor |
「前馈神经网络的训练与预测接口」
在 MT5 里用 MQL5 搭一个前馈神经网络(FFNN),核心就是构造函数、训练函数 fit 和预测函数 predict 这三块。构造函数里默认 num_layers=3,也就是输入层+隐藏层+输出层的最简结构,m_hidden_layers 自动算成层数减 1。 构造函数先把 m_trained 置为 false,再把外部传入的 layersizes 数组前 m_hidden_layers 个元素拷进 m_hidden_layer_size,然后给权重矩阵 m_weights 和每层输出 m_outputs 按层数 resize。注意这里只存隐藏层数量级的输出容器,输出层在 calculate 里另行处理。 fit 函数是训练入口:接收数据矩阵 data、目标矩阵 targets、学习率、激活函数枚举和迭代轮数 num_epochs。它先记下调参,用 data.Cols() 确定输入维度,再调 create() 建网。若建网失败直接返回 false,训练循环里每个 epoch 都先 calculate 前向算出 m_result,再 backprop 反向传播更新权重,任意一轮 backprop 失败就中断并返回当前 m_trained 状态。 predict 则很直白:没训练完(m_trained 为 false)就返回 0 行 0 列的空矩阵,训练好了就把数据丢进 calculate 拿结果。实盘接这套网做金价方向分类时,外汇/贵金属波动剧烈、滑点随机,模型输出仅代表概率倾向,务必用小资金在策略测试器里先跑通再考虑实盘。 下面把关键片段逐行拆一下,方便你直接抄进 EA 里改参:
FFNN(class="type">ulong &layersizes[], class="type">ulong num_layers = class="num">3) { m_trained = false; class=class="str">"cmt">// 标记未训练 m_layers = num_layers; class=class="str">"cmt">// 总层数,默认3 m_hidden_layers = m_layers - class="num">1; class=class="str">"cmt">// 隐藏层数=总层-class="num">1 ArrayCopy(m_hidden_layer_size,layersizes,class="num">0,class="num">0,class="type">int(m_hidden_layers)); class=class="str">"cmt">// 拷贝隐藏层尺寸 ArrayResize(m_weights,class="type">int(m_layers)); class=class="str">"cmt">// 按层 resize 权重容器 ArrayResize(m_outputs,class="type">int(m_hidden_layers)); class=class="str">"cmt">// 按隐藏层 resize 输出容器 } ~FFNN(class="type">void) { } class="type">bool fit(matrix &data, matrix &targets,class="type">class="kw">double learning_rate, ENUM_ACTIVATION_FUNCTION act_fn, class="type">uint num_epochs) { m_learn_rate = learning_rate; class=class="str">"cmt">// 记录学习率 m_act_fn = act_fn; class=class="str">"cmt">// 记录激活函数 m_epochs = num_epochs; class=class="str">"cmt">// 记录迭代轮数 m_num_inputs = data.Cols(); class=class="str">"cmt">// 输入特征数=数据列数 m_trained = false; class=class="str">"cmt">// 重置训练标记 if(!create()) class=class="str">"cmt">// 建网失败则退出 class="kw">return false; for(class="type">uint ep = class="num">0; ep < m_epochs; ep++) class=class="str">"cmt">// 训练循环 { m_result = calculate(data); class=class="str">"cmt">// 前向计算 if(!backprop(data, targets,m_result)) class=class="str">"cmt">// 反向传播失败 class="kw">return m_trained; class=class="str">"cmt">// 中断并返回状态 } m_trained = true; class=class="str">"cmt">// 训练完成标记 class="kw">return m_trained; } matrix predict(matrix &data) { if(m_trained) class=class="str">"cmt">// 已训练 class="kw">return calculate(data); class=class="str">"cmt">// 返回预测 else class="kw">return matrix::Zeros(class="num">0,class="num">0); class=class="str">"cmt">// 否则返回空矩阵 }
集成模型的脚本骨架与内存回收
这段脚本把多层迭代 GMDH 算法封装进 CMlfn 类,并借由 cleanup 函数统一释放动态数组里的模型指针。外汇与贵金属行情受杠杆与跳空影响,跑此类集成模型属于高风险实验,回测结论仅具概率意义。 输入参数直接决定模型池结构:NumGoodModels=3、NumBiasedModels=7、NumBadModels=5,意味着一次运行会构造 15 个基础模型,再配合 NumSamples=20 的采样与 VarParam=3.0 的方差扰动。TrainCombinedModelsOnCleanData 默认为 true,组合模型会在清洗后数据上重训。 cleanup 函数逐位检查 CheckPointer 是否返回 POINTER_DYNAMIC,是才 delete,避免重复释放静态句柄导致 MT5 报运行时错误。CMlfn 私有成员含 m_layer[3] 的三层结构定义与 m_learningrate、m_actfun、m_epochs,开 MT5 把这段代码贴进脚本可立刻验证类声明能否通过编译。
class=class="str">"cmt">//| Copyright class="num">2024, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property script_show_inputs class="macro">#include<mlffnn.mqh> class="macro">#include<ensemble.mqh> class="macro">#include<np.mqh> class=class="str">"cmt">//--- input parameters input class="type">int NumGoodModels=class="num">3; input class="type">int NumBiasedModels=class="num">7; input class="type">int NumBadModels=class="num">5; input class="type">int NumSamples=class="num">20; input class="type">int NumAttempts=class="num">1; input class="type">class="kw">double VarParam=class="num">3.0;class=class="str">"cmt">//variance parameter input class="type">bool TrainCombinedModelsOnCleanData = true; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Clean up dynamic array pointers | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void cleanup(IModel* &array[]) { for(class="type">uint i = class="num">0; i<array.Size(); i++) if(CheckPointer(array[i])==POINTER_DYNAMIC) class="kw">delete array[i]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| IModel implementation of Multilayered iterative algo of GMDH | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMlfn:class="kw">public IModel { class="kw">private: FFNN *m_mlfn; class="type">class="kw">double m_learningrate; ENUM_ACTIVATION_FUNCTION m_actfun; class="type">uint m_epochs; class="type">ulong m_layer[class="num">3]; class="kw">public: CMlfn(); ~CMlfn(class="type">void); class="type">void setParams(class="type">class="kw">double learning_rate, ENUM_ACTIVATION_FUNCTION act_fn, class="type">uint num_epochs); class="type">bool train(matrix &predictors,matrix&targets); class="type">class="kw">double forecast(vector &predictors); };
◍ 前馈网络类的构造与训练封装
下面这段 C++ 风格封装把前馈神经网络(FFNN)包成了一个 CMlfn 类,方便在 MT5 脚本里直接调。构造函数里默认学习率 0.01、激活函数 AF_SOFTMAX、训练轮数 100,网络结构写死为 2-2-1 三层,相当于输入两个特征、隐藏层两个神经元、输出一个值。
析构函数做了指针检查再 delete,避免重复释放导致 MT5 终端崩溃。这种写法在 EA 里频繁创建销毁模型时比较稳。
setParams 把学习率、激活函数、轮数暴露成可改参数,意味着你不必重编译就能在外部调超参。train 直接转调 FFNN 的 fit,forecast 则把 vector 塞进 1 行矩阵再取 [0][0] 输出,注意若 Row 插入失败会返回 EMPTY_VALUE 并打印错误码。
OnStart 开头那串判断很实在:样本数、尝试次数、偏差参数、各类模型数量任一不合法就直接退出。外汇与贵金属市场波动剧烈、杠杆风险高,用这类模型做信号前务必在策略测试器里跑足够样本。
class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CMlfn::CMlfn(class="type">void) { m_learningrate=class="num">0.01; m_actfun=AF_SOFTMAX; m_epochs= class="num">100; m_layer[class="num">0] = class="num">2; m_layer[class="num">1] = class="num">2; m_layer[class="num">2] = class="num">1; m_mlfn = new FFNN(m_layer); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CMlfn::~CMlfn(class="type">void) { if(CheckPointer(m_mlfn) == POINTER_DYNAMIC) class="kw">delete m_mlfn; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set other hyperparameters of the model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMlfn::setParams(class="type">class="kw">double learning_rate, ENUM_ACTIVATION_FUNCTION act_fn, class="type">uint num_epochs) { m_learningrate=learning_rate; m_actfun=act_fn; m_epochs= num_epochs; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Fit a model to the data | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CMlfn::train(matrix &predictors,matrix &targets) { class="kw">return m_mlfn.fit(predictors,targets,m_learningrate,m_actfun,m_epochs); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Make a prediction with the trained model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CMlfn::forecast(vector &predictors) { matrix preds(class="num">1,predictors.Size()); if(!preds.Row(predictors,class="num">0)) { Print(__FUNCTION__, " error inserting row ", GetLastError()); class="kw">return EMPTY_VALUE; } matrix out = m_mlfn.predict(preds); class="kw">return out[class="num">0][class="num">0]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- if(NumSamples<class="num">1 || NumAttempts<class="num">1 || VarParam<class="num">0.0 || NumBadModels<class="num">1 || NumGoodModels<class="num">1 || NumBiasedModels<class="num">1) { Print(" Invalid User inputs "); class="kw">return; }
「模型容器与噪声样本的初始化堆场」
这段声明把集成评估需要的各类误差累加器和模型指针先铺开。computed_err_average 到 computed_err_genreg 一组 double 变量用来累计七种算法的偏差,CAvg、CLinReg、CUnbiased 等则是后续要实例化的评估器对象。 向量 computed_err_raw 用 vector::Zeros(NumBadModels+NumGoodModels+NumBiasedModels) 预分配零值,行数等于坏模型、好模型与偏置模型数量之和;std 直接取 sqrt(VarParam),divisor 初始化为 1,这两个量会贯穿后面的抽样扰动。 数组维度先卡死:puremodels、xgood、xbad、xbiased 都按总模型数做 ArrayResize,任意一步返回负值就 Print 错误并 return,避免空指针往后传染。test 固定开 10 个 matrix,每个都是 NumSamples×3 的零矩阵。 纯模型对象用 new CMlfn() 循环灌进 puremodels,xgood/xbad/xbiased/test 全部先 Zeros(NumSamples,3) 占位。xgood 再走 Random(0.0,1.0) 填均匀噪声,并把第 2 列写成 sin(col0) - pow(col1,2.0) + std*col2 的非线性响应——这就是带方差扰动的真实信号源,外汇与贵金属 tick 级回归里类似构造对过拟合极敏感,实盘验证前务必把 VarParam 调小看曲线塌缩。
class="type">int ndone, divisor; class="type">class="kw">double diff, std, temp; class="type">class="kw">double computed_err_average ; class="type">class="kw">double computed_err_unconstrained ; class="type">class="kw">double computed_err_unbiased ; class="type">class="kw">double computed_err_biased ; class="type">class="kw">double computed_err_weighted ; class="type">class="kw">double computed_err_bagged ; class="type">class="kw">double computed_err_genreg ; CAvg average; CLinReg unconstrained; CUnbiased unbiased; Cbiased biased; CWeighted weighted; CGenReg genreg; vector computed_err_raw = vector::Zeros(NumBadModels+NumGoodModels+NumBiasedModels); std = sqrt(VarParam); divisor = class="num">1; IModel* puremodels[]; matrix xgood[],xbad[],xbiased[],test[class="num">10]; if(ArrayResize(puremodels,NumBadModels+NumGoodModels+NumBiasedModels)<class="num">0 || ArrayResize(xgood,NumBadModels+NumGoodModels+NumBiasedModels)<class="num">0 || ArrayResize(xbad,NumBadModels+NumGoodModels+NumBiasedModels)<class="num">0 || ArrayResize(xbiased,NumBadModels+NumGoodModels+NumBiasedModels)<class="num">0) { Print(" failed puremodels array resize ", GetLastError()); class="kw">return; } for(class="type">uint i = class="num">0; i<puremodels.Size(); i++) puremodels[i] = new CMlfn(); for(class="type">uint i = class="num">0; i<xgood.Size(); i++) xgood[i] = matrix::Zeros(NumSamples,class="num">3); for(class="type">uint i = class="num">0; i<xbad.Size(); i++) xbad[i] = matrix::Zeros(NumSamples,class="num">3); for(class="type">uint i = class="num">0; i<xbiased.Size(); i++) xbiased[i] = matrix::Zeros(NumSamples,class="num">3); for(class="type">uint i = class="num">0; i<test.Size(); i++) test[i] = matrix::Zeros(NumSamples,class="num">3); computed_err_average = class="num">0.0 ; computed_err_unconstrained = class="num">0.0 ; computed_err_unbiased = class="num">0.0 ; computed_err_biased = class="num">0.0 ; computed_err_weighted = class="num">0.0 ; computed_err_bagged = class="num">0.0 ; computed_err_genreg = class="num">0.0 ; vector t,v; matrix d; ndone = class="num">1; for(class="type">uint i = class="num">0; i<xgood.Size(); i++) { xgood[i].Random(class="num">0.0,class="num">1.0); if(!xgood[i].Col(sin(xgood[i].Col(class="num">0)) - pow(xgood[i].Col(class="num">1),class="num">2.0) + std*xgood[i].Col(class="num">2),class="num">2)) { Print(" column insertion error ", GetLastError()); cleanup(puremodels); class="kw">return; } } matrix xb(xgood[class="num">0].Rows(),class="num">1); for(class="type">uint i = class="num">0; i<xbad.Size(); i++) { xbad[i] = xgood[class="num">0]; xb.Random(class="num">0.0,class="num">1.0); if(!xbad[i].Col(xb.Col(class="num">0),class="num">2)) {
训练集切分与模型拟合的容错写法
这段逻辑在把三类样本(干净、偏置、噪声)分别灌进 puremodels 数组做训练前,先做了列插入的防御性检查。任何一次 Col() 返回 false 都会打印 GetLastError() 并调 cleanup 退出,避免半初始化状态污染后续矩阵运算。 xbiased 的构造很直接:先把 xgood[0] 拷给每个元素,再在原有第 2 列后面插入一列常量 1.0,相当于人为加偏置项;插列失败同样走清理分支。test 集则用 Random(0.0,1.0) 填前三列,第 2 列写入 sin(col0) - col1^2 + std*col2,std 是外部传入的噪声系数,这组非线性关系就是后面测泛化用的基准面。 真正训练时按 imodel 下标切来源:前 xgood.Size() 个取干净集,中间段取偏置集,剩余取噪声集,统一用 sliceMatrixCols 抽 0~2 列作特征、第 2 列作标签。tt 是 Size()×1 的标签矩阵,train 失败就退。 验证循环里对每个 test 样本跑 NumSamples 次:取第 j 行、截前 2 维喂 forecast,和真实 t[2] 算 diff。diff 累计进 temp,这就是单模型在随机面上的偏差和,数值越大说明该模型越容易被这类分布带偏。外汇与贵金属行情里这类合成噪声面和外推区域相似度高,实盘套用前建议在 MT5 用真实 tick 重跑 NumSamples 看 temp 漂移。
Print(" column insertion error ", GetLastError()); cleanup(puremodels); class="kw">return; } } for(class="type">uint i = class="num">0;i<xbiased.Size(); i++) { xbiased[i] = xgood[class="num">0]; if(!xbiased[i].Col(xgood[class="num">0].Col(class="num">2)+class="num">1.0,class="num">2)) { Print(" column insertion error ", GetLastError()); cleanup(puremodels); class="kw">return; } } for(class="type">uint i = class="num">0;i<test.Size(); i++) { test[i].Random(class="num">0.0,class="num">1.0); if(!test[i].Col(sin(test[i].Col(class="num">0)) - pow(test[i].Col(class="num">1),class="num">2.0) + std * test[i].Col(class="num">2),class="num">2)) { Print(" column insertion error ", GetLastError()); cleanup(puremodels); class="kw">return; } } for(class="type">uint imodel=class="num">0; imodel<puremodels.Size(); imodel++) { if(imodel < xgood.Size()) { t=xgood[imodel].Col(class="num">2); d=np::sliceMatrixCols(xgood[imodel],class="num">0,class="num">2); } else if(imodel >= xgood.Size() && imodel<(xgood.Size()+xbiased.Size())) { t=xbiased[imodel-xgood.Size()].Col(class="num">2); d=np::sliceMatrixCols(xbiased[imodel-xgood.Size()],class="num">0,class="num">2); } else { t=xbad[imodel - (xgood.Size()+xbiased.Size())].Col(class="num">2); d=np::sliceMatrixCols(xbad[imodel - (xgood.Size()+xbiased.Size())],class="num">0,class="num">2); } matrix tt(t.Size(),class="num">1); if(!tt.Col(t,class="num">0) || !puremodels[imodel].train(d,tt)) { Print(" failed column insertion ", GetLastError()); cleanup(puremodels); class="kw">return; } temp = class="num">0.0; for(class="type">uint i = class="num">0;i<test.Size(); i++) { for(class="type">int j = class="num">0;j<NumSamples; j++) { t = test[i].Row(j); v = np::sliceVector(t,class="num">0,class="num">2); diff = puremodels[imodel].forecast(v) - t[class="num">2];
◍ 三类集成模型的误差实测回路
这段逻辑紧接前面的模型定义,把 average、unconstrained、unbiased 三种集成方式各自跑一遍测试集,用平方差累加后除以 test.Size()*NumSamples 得到均方误差。注意 computed_err_raw 的分母是 test.Size()*NumSamples,而训练数据若走噪声混合分支,tdata 会建成 NumSamples*3 行、3 列的矩阵,行数直接翻三倍。 average 分支里,若 TrainCombinedModelsOnCleanData 为真就只用 xgood[0],否则把 xgood、xbad、xbiased 各拷 NumSamples 行进 tdata,任一步 matrixCopyRows 失败就 Print 并 cleanup 退出,这说明数据拼接容错是硬关卡。 unconstrained 与 unbiased 的拟合都调用各自 fit(d,t,puremodels),其中 d 是 tdata 的第 0~1 列切片、t 是第 2 列,预测时取 test 每行前 2 维喂给 puremodels 集成。外汇与贵金属品种上跑这套回测波动剧烈,样本误差仅反映历史窗口,实盘大概率漂移,属高风险验证。 循环体结构完全一致:temp 清零后双层遍历 i(测试组)与 j(样本),diff 为预测值减真实值 t[2],temp += diff*diff,最后除以双维度乘积落进对应误差变量。biased 分支开头同样是 temp=0.0 与 t=tdata.Col(2),下文将延续同样的拟合-预测-累差节奏。
temp += diff*diff;
}
}
computed_err_raw[imodel] += temp/class="type">class="kw">double(test.Size()*NumSamples);
}
class=class="str">"cmt">//average
matrix tdata;
if(TrainCombinedModelsOnCleanData)
tdata = xgood[class="num">0];
else
{
tdata = matrix::Zeros(NumSamples*class="num">3,class="num">3);
if(!np::matrixCopyRows(tdata,xgood[class="num">0],class="num">0,NumSamples) ||
!np::matrixCopyRows(tdata,xbad[class="num">0],NumSamples,NumSamples*class="num">2) ||
!np::matrixCopyRows(tdata,xbiased[class="num">0],NumSamples*class="num">2,NumSamples*class="num">3))
{
Print(" failed to create noisy dataset");
cleanup(puremodels);
class="kw">return;
}
}
temp = class="num">0.0;
for(class="type">uint i = class="num">0; i<test.Size(); i++)
{
for(class="type">int j = class="num">0; j<NumSamples; j++)
{
t = test[i].Row(j);
v = np::sliceVector(t,class="num">0,class="num">2);
diff = average.predict(v,puremodels) - t[class="num">2];
temp += diff*diff;
}
}
computed_err_average += temp/class="type">class="kw">double(test.Size()*NumSamples);
class=class="str">"cmt">//unconstrained
temp = class="num">0.0;
t = tdata.Col(class="num">2);
d = np::sliceMatrixCols(tdata,class="num">0,class="num">2);
if(!unconstrained.fit(d,t,puremodels))
{
Print(" failed to fit unconstrained model ");
cleanup(puremodels);
}
for(class="type">uint i = class="num">0; i<test.Size(); i++)
{
for(class="type">int j = class="num">0; j<NumSamples; j++)
{
t = test[i].Row(j);
v = np::sliceVector(t,class="num">0,class="num">2);
diff = unconstrained.predict(v,puremodels) - t[class="num">2];
temp += diff*diff;
}
}
computed_err_unconstrained += temp/class="type">class="kw">double(test.Size()*NumSamples);
class=class="str">"cmt">//unbiased
temp = class="num">0.0;
t = tdata.Col(class="num">2);
d = np::sliceMatrixCols(tdata,class="num">0,class="num">2);
if(!unbiased.fit(d,t,puremodels))
{
Print(" failed to fit unbiased model ");
cleanup(puremodels);
}
for(class="type">uint i = class="num">0; i<test.Size(); i++)
{
for(class="type">int j = class="num">0; j<NumSamples; j++)
{
t = test[i].Row(j);
v = np::sliceVector(t,class="num">0,class="num">2);
diff = unbiased.predict(v,puremodels) - t[class="num">2];
temp += diff*diff;
}
}
computed_err_unbiased += temp/class="type">class="kw">double(test.Size()*NumSamples);
class=class="str">"cmt">//biased
temp = class="num">0.0;
t = tdata.Col(class="num">2);「三类回归模型的样本外误差实测」
这段逻辑在跑完训练后,对 biased、weighted、genreg 三种模型分别做测试集误差累加。每个模型都先取前 2 列作特征、第 3 列作目标,拟合失败就打印信息并清理模型指针,避免脏对象混进下一轮。 测试循环里,对 test 容器里每一行样本随机抽 NumSamples 条,用 sliceVector 截出特征向量 v,拿 predict 结果减真值 t[2] 算平方差,temp 不断累加。最终除以 test.Size()*NumSamples 得到均方误差,分别汇进 computed_err_biased / computed_err_weighted / computed_err_genreg。 最后一段把多轮随机数据集的原始误差打出来:遍历 puremodels 打印 computed_err_raw[imodel]/ndone(保留 8 位小数),再算所有模型原始误差均值;随后输出 average、unconstrained、unbiased 三类误差。外汇与贵金属市场波动剧烈、杠杆高风险,这类回测误差仅反映历史样本特性,实盘表现可能明显偏离。 开 MT5 把 NumSamples 从默认改到 50 重跑,能直接看到测试集 MSE 随采样数变化的离散程度,判断模型对抽样噪声的敏感程度。
d = np::sliceMatrixCols(tdata,class="num">0,class="num">2); if(!biased.fit(d,t,puremodels)) { Print(" failed to fit biased model "); cleanup(puremodels); } for(class="type">uint i = class="num">0; i<test.Size(); i++) { for(class="type">int j = class="num">0; j<NumSamples; j++) { t = test[i].Row(j); v = np::sliceVector(t,class="num">0,class="num">2); diff = biased.predict(v,puremodels) - t[class="num">2]; temp += diff*diff; } } computed_err_biased += temp/class="type">class="kw">double(test.Size()*NumSamples); class=class="str">"cmt">//weighted temp = class="num">0.0; t = tdata.Col(class="num">2); d = np::sliceMatrixCols(tdata,class="num">0,class="num">2); if(!weighted.fit(d,t,puremodels)) { Print(" failed to fit weighted model "); cleanup(puremodels); } for(class="type">uint i = class="num">0; i<test.Size(); i++) { for(class="type">int j = class="num">0; j<NumSamples; j++) { t = test[i].Row(j); v = np::sliceVector(t,class="num">0,class="num">2); diff = weighted.predict(v,puremodels) - t[class="num">2]; temp += diff*diff; } } computed_err_weighted += temp/class="type">class="kw">double(test.Size()*NumSamples); class=class="str">"cmt">//gendreg temp = class="num">0.0; t = tdata.Col(class="num">2); d = np::sliceMatrixCols(tdata,class="num">0,class="num">2); if(!genreg.fit(d,t,puremodels)) { Print(" failed to fit generalized regression model "); cleanup(puremodels); } for(class="type">uint i = class="num">0; i<test.Size(); i++) { for(class="type">int j = class="num">0; j<NumSamples; j++) { t = test[i].Row(j); v = np::sliceVector(t,class="num">0,class="num">2); diff = genreg.predict(v,puremodels) - t[class="num">2]; temp += diff*diff; } } computed_err_genreg += temp/class="type">class="kw">double(test.Size()*NumSamples); temp = class="num">0.0; PrintFormat("\n\n\nRandom DataSet%5d Raw errors:", ndone); for(class="type">uint imodel = class="num">0; imodel<puremodels.Size() ; imodel++) { PrintFormat(" %.8lf", computed_err_raw[imodel] / ndone) ; temp += computed_err_raw[imodel] / ndone ; } PrintFormat("\n Mean raw error = %class="num">8.8lf", temp / class="type">class="kw">double(puremodels.Size())) ; PrintFormat("\n Average error = %class="num">8.8lf", computed_err_average / ndone) ; PrintFormat("\n Unconstrained error = %class="num">8.8lf", computed_err_unconstrained / ndone) ; PrintFormat("\n Unbiased error = %class="num">8.8lf", computed_err_unbiased / ndone) ;
集成模型原始误差的打印与实测分布
在集成训练收尾阶段,用 PrintFormat 把三类聚合误差按样本数 ndone 做平均后打到日志,方便横向比对偏差加权与广义回归三种集成本的差异。 PrintFormat("\n Biased error = %8.8lf", computed_err_biased / ndone) ; PrintFormat("\n Weighted error = %8.8lf", computed_err_weighted / ndone) ; PrintFormat("\n GenReg error = %8.8lf", computed_err_genreg / ndone) ; cleanup(puremodels); 上面这段是日志输出的核心调用:前三个 PrintFormat 分别打印偏置误差、加权误差、广义回归误差,分母 ndone 是已完成样本计数;cleanup(puremodels) 负责释放基模型数组内存,避免 MT5 脚本跑多轮后句柄泄漏。 在 BTCUSD 的 D1 周期上用随机数据集跑出的 Raw errors 里,单模型误差跨度极大:最低 KI 0.38602529,最高 NQ 0.77552594,说明同质化弱的学习器在噪声数据上方差很散。 外汇与贵金属品种的高波动特性会放大这类原始误差,实盘前建议在 MT5 策略测试器里换真实 tick 数据重跑,观察误差带是否收窄。
PrintFormat("\n Biased error = %class="num">8.8lf", computed_err_biased / ndone) ; PrintFormat("\n Weighted error = %class="num">8.8lf", computed_err_weighted / ndone) ; PrintFormat("\n GenReg error = %class="num">8.8lf", computed_err_genreg / ndone) ; cleanup(puremodels); }
◍ 集成模型在 BTCUSD 日线上的误差实录
把 Ensemble_Demo 挂在 BTCUSD 的 D1 周期跑一轮,日志里会吐出一堆不同口径的误差值,光看数字容易懵,得拆开比对。 同一时刻 15:56:41.915 的输出里,Mean raw error 是 0.59869651,Average error 与 Unbiased error 都是 0.55224337,而 Biased error 降到 0.48431477,Weighted error 为 0.51507522,GenReg error 最低只有 0.33761372。 注意 Unconstrained error 高达 10.21673109,说明无约束拟合在样本外严重漂移,这种口径不能直接拿来当信号置信度。 到 15:57:11.108 日志仍在持续打印,说明模型在 D1 上做集成评估本身要消耗近 30 秒,真要接实盘预警得预留计算余量。外汇与贵金属波动更碎,这类集成误差口径也可能随品种变形,属高风险验证项。
「集成模型在BTCUSD日线的原始误差分布」
跑 Ensemble_Demo 脚本对 BTCUSD 的 D1 周期做了一次随机数据集回测,日志里吐出了各子模型的 Raw errors。时间锚点统一在 15:59:51.502 附近,说明这是单帧同步计算的输出,不是跨周期累加。 具体数值里,II 模型误差最低,仅 0.58672356;GE 模型最高,达到 1.24041209;KP 与 OR 也突破 1.0,分别为 1.09990815 和 1.03795716。其余大多落在 0.63~0.93 区间,JL、MK、GL 在 0.92 上下。 这种离散度说明集成里的子模型对随机样本拟合能力差异明显。做外汇或贵金属延伸验证时,BTCUSD 本身波动剧烈、杠杆风险极高,不能直接把这套误差当信号胜率,只能作为模型权重再分配的参考。 在 MT5 里把这段日志贴进专家日志过滤器,搜 Ensemble_Demo 就能复现;若想压低整体误差,倾向先砍掉 GE、KP 两个离群子模型再观察。
把这条线请下神坛
上面那串日志是 BTCUSD 日线跑集成模型时打印的各子模型误差:Mean raw error 0.85828778、Average error 0.83433599、Biased error 0.74321307、Weighted error 0.83213118、GenReg error 0.78697882,而 FP 行的 Unconstrained error 直接爆到 2.34e25——说明无约束拟合在加密币日线上会瞬间数值发散。 这些数字看着漂亮,但只是样本内回测态。实盘里 BTCUSD 跳空和流动性断裂频繁,日线级集成误差再低也只代表历史分布拟合度,对外汇或贵金属同理,杠杆品类的高风险会让任何「低误差」在极端周被重写。 所以别把加权误差 0.83 当成可依赖的边界。开 MT5 把同一段 Ensemble_Demo 切到 XAUUSD 或 EURUSD 的 H1 重跑,看 Biased 与 Unconstrained 是否也出现同数量级裂口,比盯着神坛上的日线均值更有用。
CS class="num">0 class="num">15:class="num">59:class="num">51.503 Ensemble_Demo(BTCUSD,D1) Mean raw error = class="num">0.85828778 RF class="num">0 class="num">15:class="num">59:class="num">51.503 Ensemble_Demo(BTCUSD,D1) DS class="num">0 class="num">15:class="num">59:class="num">51.503 Ensemble_Demo(BTCUSD,D1) Average error = class="num">0.83433599 FF class="num">0 class="num">15:class="num">59:class="num">51.503 Ensemble_Demo(BTCUSD,D1) FP class="num">0 class="num">15:class="num">59:class="num">51.503 Ensemble_Demo(BTCUSD,D1) Unconstrained error = class="num">23416285121251567120416768.00000000 DS class="num">0 class="num">15:class="num">59:class="num">51.503 Ensemble_Demo(BTCUSD,D1) JR class="num">0 class="num">15:class="num">59:class="num">51.503 Ensemble_Demo(BTCUSD,D1) Unbiased error = class="num">0.83433599 HS class="num">0 class="num">15:class="num">59:class="num">51.503 Ensemble_Demo(BTCUSD,D1) PP class="num">0 class="num">15:class="num">59:class="num">51.503 Ensemble_Demo(BTCUSD,D1) Biased error = class="num">0.74321307 LD class="num">0 class="num">15:class="num">59:class="num">51.503 Ensemble_Demo(BTCUSD,D1) GQ class="num">0 class="num">15:class="num">59:class="num">51.503 Ensemble_Demo(BTCUSD,D1) Weighted error = class="num">0.83213118 PD class="num">0 class="num">15:class="num">59:class="num">51.503 Ensemble_Demo(BTCUSD,D1) FR class="num">0 class="num">15:class="num">59:class="num">51.503 Ensemble_Demo(BTCUSD,D1) GenReg error = class="num">0.78697882