MQL5中的逐步特征选择·进阶篇
(2/3)·传统逐步法为何在外汇特征工程里容易翻车,以及MQL5里的改进实现路径
◍ 逐步筛选里的数组拷贝与终止判断
这段逻辑处在逐步回归特征筛选的循环体内,核心是把当前轮次挑出的预测因子索引收进总表,并决定何时收工。
ArrayCopy 的偏移量用了 (n_so_far*(n_so_far+1))/2,这是三角数公式,保证每轮新增的 n_this_rep 个索引紧挨着之前写过的区域、不重叠。若返回负值说明拷贝失败,直接打印错误码并 return 1 中断流程。
当 n_so_far 累加到等于 m_max_num_preds(最大预测因子数,为整数强转)时,若开了 verbose 会打印完成提示并 break 跳出。外汇与贵金属市场高波动,这类多因子模型过拟合风险高,实盘前务必在 MT5 用历史数据跑通该返回路径。
}
}
if(ArrayCopy(m_var_indices,m_all_best_trials,(n_so_far*(n_so_far+class="num">1))/class="num">2,class="num">0,n_this_rep)<class="num">0)
{
Print(__FUNCTION__, " array copy error ", GetLastError());
class="kw">return class="num">1;
}
if(m_verbose)
Print(msg);
++n_so_far;
if(n_so_far == class="type">int(m_max_num_preds))
{
if(m_verbose)
Print(" Stepwise selection successfully completed ");
break;
}
}
class="kw">return class="num">0;
}用交叉验证给特征子集打分
单靠一份训练集挑特征,过拟合概率会明显抬升,选出来的子集在实盘里可能很快失效。把样本拆成多折轮流训练与验证,能在不引入未来数据的前提下,压低过拟合倾向,让特征重要度评估更稳。 折数不是越多越好:折数上升,性能估计通常更准,但计算量近似线性增长。具体取几折要看样本规模和机器空闲算力,没有统一常数。 crossEval() 就是干这事的函数。它吃三个输入——候选预测器数量 num_vars、打乱后的目标向量 target、以及引用返回的决策变量 criterion(用于挑最合适特征子集)。跑完返回 0 表示成功。 内部先 fitModel() 在某一折区间训模型,再 evalModel() 在同区间算误差,所有折误差累加后,criterion 被写成 1.0 - error/m_samples。也就是说,criterion 越接近 1,该特征子集交叉验证表现越好。 fitModel() 和 evalModel() 在这里只是虚函数占位,必须在你自己的派生类里按用的模型类型实现,否则直接返回 false 或 EMPTY_VALUE,crossEval 会打印失败并退 1。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| evaluates a predictor set class="kw">using cross validation | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int crossEval(class="type">class="kw">ulong num_vars,vector &target,class="type">class="kw">double &criterion) { class="type">class="kw">ulong ifold, n_remaining, test_start, test_stop ; class="type">class="kw">double error, evalresult ; n_remaining = m_samples; test_start = class="num">0; error = class="num">0.0; for(ifold = class="num">0; ifold<m_folds; ifold++) { test_stop = test_start + (n_remaining/(m_folds-ifold)); if(!fitModel(num_vars,test_start,test_stop,target)) { criterion = -DBL_MIN; Print(__FUNCTION__, " fit() failed "); class="kw">return class="num">1; } evalresult = evalModel(num_vars,test_start,test_stop,target); if(evalresult==EMPTY_VALUE) { criterion = -DBL_MIN; Print(__FUNCTION__, " evalModel() failed "); class="kw">return class="num">1; } error+=evalresult; n_remaining -= test_stop - test_start; test_start = test_stop; } criterion = class="num">1.0 - error/class="type">class="kw">double(m_samples); class="kw">return class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| fit a model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">bool fitModel(class="type">class="kw">ulong num_preds,class="type">class="kw">ulong row_start,class="type">class="kw">ulong row_stop, vector& targets) { class="kw">return class="kw">false;} class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| evaluate a model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">class="kw">double evalModel(class="type">class="kw">ulong num_preds,class="type">class="kw">ulong row_start,class="type">class="kw">ulong row_stop, vector& targets) { class="kw">return EMPTY_VALUE;}
「挑出集合里最管用的头号预测变量」
在逐步特征筛选里,第一根「柱子」必须靠遍历硬找。addFirstFeature() 由 addFeature() 调起,对候选池里每个变量做交叉验证,用派生类定义的标准打分,标准本身也决定了后面模型用哪种学习技术。 它先给 m_keptvars 个保留位灌入 -1.e60 这种极小值当占位,再逐个变量塞进 m_trial_vars[0] 跑 crossEval()。每次算出的 crit 会和当前最优列表从上往下比,一旦超过第 j 个就 break,然后把 j 之后的记录整体后挪一位,新变量插进 j 号位。 这套插入排序式维护保证遍历完 m_vars 个变量后,m_all_best_trials 头部就是表现最靠前的预测器。后面再调 addFeature() 走 addNewFeature() 增量引入,第一特征就此固定。 代码里 m_keptvars 决定保留几个优变量,默认若设小了(比如 3)可能漏掉次优但后续组合更强的因子;在 MT5 里把 m_keptvars 调到 10 左右再跑回测,能直观看到头部 trial 索引变化。外汇与贵金属波动噪声大,交叉验证样本外表现仅代表历史概率,实盘仍属高风险。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| finds the first predictor in the set | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int addFirstFeature(vector &targ, class="type">int bindex,class="type">class="kw">ulong rep_index) { class="type">class="kw">double crit; for(class="type">class="kw">ulong i = class="num">0; i<m_keptvars; i++) { m_all_best_crits[rep_index][i] = -class="num">1.e60; m_all_best_trials[bindex+i] = -class="num">1; } for(class="type">int i = class="num">0; i<class="type">int(m_vars); i++) { m_trial_vars[class="num">0] = i; if(crossEval(class="num">1,targ,crit)) { Print(__FUNCTION__, " xval error "); class="kw">return class="num">1; } class="type">class="kw">ulong j; for(j = class="num">0; j<m_keptvars; j++) { if(crit > m_all_best_crits[rep_index][j]) break; } if(j<m_keptvars) { for(class="type">long k = class="type">long(m_keptvars-class="num">2); k>=class="type">long(j); k--) { m_all_best_trials[bindex+k+class="num">1] = m_all_best_trials[bindex+k]; m_all_best_crits[rep_index][k+class="num">1] = m_all_best_crits[rep_index][k]; } m_all_best_trials[bindex+j] = i; m_all_best_crits[rep_index][j] = crit; } } class="kw">return class="num">0; }
◍ 逐步特征选择里的下一个预测器怎么挑
在 MT5 里做逐步特征选择,核心动作就是不断往模型里塞「下一个最值得用的预测器」。addNewFeature() 干的就是这事:它把当前已选的最优预测器集合,跟剩下的每个候选变量逐一配对,跑交叉验证,按指定标准打分,留下表现更好的组合。 这段代码先把上一轮保留下来的最优判据 m_prior_best_crits 从全局表 m_all_best_crits 里按 rep_index 取出来,同时用 ArrayCopy 把对应的变量组合拷进 m_prior_best_trials;原位置的全局判据被重置成 -1.e60,相当于清空待考察位。 紧接着它数出已经成型的「根组合」数量 nbest——循环里一旦碰到小于 -1.e59 的判据就 break,说明这一支再没已尝试组合。之后对每个根组合,遍历所有 m_vars 个候选变量,跳过那些早已在组合里的 ivar,把剩余变量拼成新的 m_trial_vars 送进去验证。 实盘接这套逻辑时,外汇与贵金属波动受消息面干扰大,交叉验证回测表现好只代表样本内倾向有效,换周期可能退化。建议先在 EURUSD 的 M15 上把 m_keptvars 设小一点(比如 8)跑通,再扩维度。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| looks for next predictor to include | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int addNewFeature(vector& target,class="type">int &ind, class="type">int bindex,class="type">class="kw">ulong rep_index) { class="type">int i, j, k, ivar, ir ; class="type">int npred, n_already_tried,nbest, rootvars; nbest = -class="num">1; class="type">class="kw">double crit; ind = npred = ind+class="num">1; for(i = class="num">0;i<class="type">int(m_keptvars); i++) { m_prior_best_crits[i] = m_all_best_crits[rep_index][i]; if(ArrayCopy(m_prior_best_trials,m_all_best_trials,class="type">int(i*(npred-class="num">1)),bindex+class="type">int(i*(npred-class="num">1)),npred-class="num">1)<class="num">0) { Print(__FUNCTION__, " ArrayCopy error ", GetLastError()); class="kw">return class="num">1; } m_all_best_crits[rep_index][i] = -class="num">1.e60; } n_already_tried = class="num">0; for(ir = class="num">0;ir<class="type">int(m_keptvars); ir++) { if(m_prior_best_crits[ir] < -class="num">1.e59) break; } nbest = ir; for(ir = class="num">0; ir<nbest; ir++) { rootvars = ir*(npred-class="num">1); for(ivar=class="num">0; ivar<class="type">int(m_vars); ivar++) { for(i = class="num">0;i<npred-class="num">1; i++) { if(m_prior_best_trials[rootvars+i] == ivar) break; } if(i < npred-class="num">1) class="kw">continue; k = class="num">0; for(i = class="num">0;i<class="type">int(m_vars); i++) { for(j=class="num">0;j<npred-class="num">1;j++) { if(m_prior_best_trials[rootvars+j] == i) { m_trial_vars[k++] = i; break; } } if(ivar == i) m_trial_vars[k++] = i; } for(i = class="num">0;i<n_already_tried; i++) {
特征组合去重与最优集插入的实现细节
这段逻辑处在特征选择循环的内层,核心任务是避免把已经评估过的预测变量组合重复跑一遍,同时把交叉验证表现更好的组合塞进固定长度的最优表里。 外层用 i 遍历已尝试组合,内层 j 从 0 到 npred-1 比对 m_trial_vars 与 m_already_tried 的对应位;只要有一位不同就 break,若 j 走到 npred 说明完全相等,直接跳出不再重复评估。 确认是新组合后,把它写进 m_already_tried 的第 n_already_tried 块,n_already_tried 自增,随后调 crossEval 做交叉验证。若返回非零,Print 报 xval error 并 return 1,说明本次评估失败。 最优表维护用 m_keptvars 控制容量。i 从 0 扫到 m_keptvars-1,遇到 crit 大于已有临界值就 break,定位插入位;之后把 i 之后的记录整体后移一位(j 从 m_keptvars-2 降到 i),腾出位置写入新 crit 与对应 trial 变量,ArrayCopy 失败则报 array copy error 并退出。 在 MT5 里把这段嵌进你自己的特征搜索函数时,重点核对 npred 与 m_keptvars 的实际值——若 m_keptvars 设得过小,高频出现的微优组合会很快把早先样本挤出表,可能削弱后续集成稳定性。外汇与贵金属特征筛选属高风险实验,回测优不代表实盘倾向盈利。
for(j = class="num">0; j<npred; j++) { if(m_trial_vars[j] != m_already_tried[i*npred+j]) break; } if(j == npred) break; } if(i < n_already_tried) class="kw">continue; for(i =class="num">0; i<npred; i++) m_already_tried[n_already_tried*npred+i]=m_trial_vars[i]; ++n_already_tried; if(crossEval(class="type">class="kw">ulong(npred),target,crit)) { Print(__FUNCTION__, " xval error "); class="kw">return class="num">1; } for(i = class="num">0; i< class="type">int(m_keptvars); i++) { if(crit > m_all_best_crits[rep_index][i]) break; } if(i<class="type">int(m_keptvars)) { for(j = class="type">int(m_keptvars-class="num">2); j>=i; j--) { m_all_best_crits[rep_index][j+class="num">1] = m_all_best_crits[rep_index][j]; if(ArrayCopy(m_all_best_trials,m_all_best_trials,bindex+class="type">int((j+class="num">1)*npred),bindex+class="type">int(j*npred),npred)<class="num">0) { Print(__FUNCTION__, " array copy error ", GetLastError()); class="kw">return class="num">1; } } m_all_best_crits[rep_index][i] = crit; if(ArrayCopy(m_all_best_trials,m_trial_vars,bindex+class="type">int(i*npred),class="num">0,npred)<class="num">0) { Print(__FUNCTION__, " array copy error ", GetLastError()); class="kw">return class="num">1; } } } } class="kw">return class="num">0; }
「逐步特征选择的基类接口与取数路径」
CStepwisebase 是个抽象基类,只管「从候选特征里迭代挑出预测力强的变量」这件事的骨架。它本身不绑定任何学习算法,派生类只要实现 fitModel() 和 evalModel() 两个方法,就能把任意模型和评价标准接进来。 对外真正能调用的入口是 stepwiseSelection(),它先校验数据、做标准化,再转去跑 stepwiseSearch()。想提前配参数就用 setParams(),把最后一个参数设 true,MT5 的 Experts 标签会直接打印选择过程,方便现场看每一步发生了什么。 结果怎么拿:getCriticalVals() 返回的是一个矩阵,列数取决于有没有开蒙特卡洛排列测试(MCP)。在 setParams() 里把 num_replications 设成大于 1 就开了 MCP,此时矩阵有 3 列——每行是一次最外层迭代,分别是标准值、『该性能纯属偶然』的 p 值、『本次新增特征带来的提升纯属偶然』的 p 值;不开 MCP 时只有 1 列标准值。 选出的特征子集数量用 getNumFeatureSets() 查,具体子集调 getFeatureSetAt(),传迭代索引和引用数组,就把那一轮选中的列下标拷给你。外汇与贵金属市场波动剧烈、杠杆风险高,这类特征选择仅降低过拟合概率,不保证样本外预测稳定。 下面这段是基类里 stepwiseSelection 与取数方法的原生实现,逐行拆一下关键逻辑: bool stepwiseSelection(matrix& predictors, vector& targets) 是主入口,先判 targets 长度得和 predictors 行数一致且非空,否则打印无效输入并返回 false。 随后 m_data = stdmat(predictors) 做标准化,m_target 用 (targets-均值)/(标准差+1e-10) 归一,分母加 1e-10 防零除。 m_vars、m_samples 记下列数和行数;接着一串 if 把 m_keptvars、m_folds、m_min/max_num_preds、m_reps 都夹到合法区间(比如折数不能小于 1、不能多于样本数)。 若 m_verbose 为真,Print 出保留变量数、CV 折数、最小最大选变量数、MCP 复制次数,这就是 Experts 标签里能看到的那几行。 最后 return (stepwiseSearch()==0),搜索成功返回 0 即 true。 matrix getCriticalVals(void) 直接 return m_decision_matrix,就是上面说的三列或一列矩阵。 bool getFeatureSetAt(ulong iteration_index, ulong &selectedvars[]) 按零基迭代索引取对应轮次选中的特征列下标,写进 selectedvars 数组,调用方拿去回溯哪几个原始特征被留了下来。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| main method to execute stepwise feature selection | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool stepwiseSelection(matrix& predictors, vector& targets) { if(targets.Size()!=predictors.Rows() || !targets.Size()) { Print(__FUNCTION__, " invalid inputs "); class="kw">return class="kw">false; } m_data = stdmat(predictors); m_target = (targets-targets.Mean())/(targets.Std()+class="num">1e-10); m_vars = predictors.Cols(); m_samples = predictors.Rows(); if(m_keptvars>m_vars || m_keptvars<class="num">1) m_keptvars = m_vars; if(m_folds<class="num">1) m_folds = class="num">1; if(m_folds>m_samples) m_folds = m_samples; if(m_min_num_preds==class="num">0 || m_min_num_preds>m_vars || m_min_num_preds>m_max_num_preds) m_min_num_preds = m_vars; if(m_max_num_preds==class="num">0 || m_max_num_preds>m_vars || m_max_num_preds<m_min_num_preds) m_max_num_preds = m_vars; if(m_reps<class="num">1) m_reps = class="num">1; if(m_verbose) { Print(" STEPWISE SELECTION PARAMETERS "); Print(" Number of retained candidate variables for each iteration ", m_keptvars); Print(" Number of folds in cross validation procedure ", m_folds); Print(" Final minimum number of selected predictors ", m_min_num_preds); Print(" Final maximum number of selected predictors ", m_max_num_preds); Print(" Number of replications for MCP test ", m_reps); } class="kw">return (stepwiseSearch()==class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| get all crit values and corresponding p-values | class=class="str">"cmt">//+------------------------------------------------------------------+ matrix getCriticalVals(class="type">void) { class="kw">return m_decision_matrix; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| get the selected features at a specific iteration of the process| class=class="str">"cmt">//| iterations denoted as zero based indices | class=class="str">"cmt">//| eg, first selected feature located at iteration index class="num">0 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool getFeatureSetAt(class="type">class="kw">ulong iteration_index,class="type">class="kw">ulong &selectedvars[]) {