将互信息作为渐进特征选择的准则·综合运用
(3/3)·用互信息跑通最大依赖最小冗余的特征筛选,避开离散化与Parzen窗的估计陷阱
MRMR特征选择类的调用骨架
在 mutualinfo.mqh 里,Cmrmr 类把最大相关性最小冗余(MRMR)算法封装好了。实例化时四个参数直接决定算法行为:num_reps 是蒙特卡洛置换检验的重复次数,设成 ≤1 就跳过检验只跑一遍;max_preds 限制最终选出的特征数;chisquare_thresh 控制互信息自适应分区的卡方阈值;m_verbose 打开后过程信息会打印出来。 真正干活的是 StepWise(matrix &predictors, vector &targets),前者塞入所有候选特征矩阵,后者是目标变量向量,返回布尔值表示成败。函数里先用 Capm 实例算每个候选变量与目标的互信息,存进 relevance;首轮选互信息最高的特征,之后每加一个特征就用 sum_redundancy 数组累加它和剩余候选的平均互信息,再用「相关性减平均冗余」的 MRMR 准则挑下一个。 初始非置换轮会打一张互信息排名表,后续置换轮拿它当基准做组级和单独显著性计数。跑完 StepWise 后,GetSelectedVars() 取回被选列索引,GetCriticalValues() 取回含 p 值的明细矩阵——这两句足够你直接在 EA 里接后续建模。 外汇与贵金属行情噪声大,特征集显著性 p 值偏高时,套用选出特征做预测仍属高概率而非确定,实盘前务必用历史数据复算。
class=class="str">"cmt">//+-----------------------------------------------------------------------+ class=class="str">"cmt">//|Relevance minus redundancy for building an optimal subset of predictors| class=class="str">"cmt">//+-----------------------------------------------------------------------+ class Cmrmr { class="kw">private: class="type">int m_max_preds; class=class="str">"cmt">// 要选择的最大特征数量 class="type">int m_reps; class=class="str">"cmt">// 蒙特卡洛置换检验重复次数 class="type">bool m_verbose; class=class="str">"cmt">// 是否打印详细输出 class="type">int m_samples; class=class="str">"cmt">// 样本行数(内部初始化) class="type">int m_vars; class=class="str">"cmt">// 候选特征列数(内部初始化) matrix m_preds; class=class="str">"cmt">// 候选预测变量矩阵 vector m_target; class=class="str">"cmt">// 目标变量向量 vector m_crits; class=class="str">"cmt">// 各特征准则值缓存 class="type">class="kw">double m_chisquare_thresh;class=class="str">"cmt">// 互信息分区卡方阈值 class="type">ulong m_selected_vars[];class=class="str">"cmt">// 被选变量列索引数组 matrix m_critical_values;class=class="str">"cmt">// 临界值明细表 vector mutualinfo(class="type">int which_size,class="type">int &which[],vector &targs); class=class="str">"cmt">// 私有:算互信息近似 class="kw">public: Cmrmr(class="type">int num_reps,class="type">int max_preds, class="type">class="kw">double chisquare_thresh,class="type">bool verbose); ~Cmrmr(class="type">void); class="type">bool StepWise(matrix &predictors, vector &targets); class=class="str">"cmt">// 渐进特征选择主函数 matrix GetCriticalValues(class="type">void); class=class="str">"cmt">// 取含p值的结果矩阵 class="type">bool GetSelectedVars(class="type">ulong &output[]); class=class="str">"cmt">// 取被选列索引 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Stepwise feature selection based on mutual information | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool Cmrmr::StepWise(matrix &predictors,vector &targets) { if(m_selected_vars.Size()) ArrayFree(m_selected_vars); class=class="str">"cmt">// 若已有选择记录则先释放 m_preds = predictors; class=class="str">"cmt">// 绑定候选特征矩阵 m_samples = class="type">int(m_preds.Rows()); class=class="str">"cmt">// 记录样本数 m_vars = class="type">int(m_preds.Cols()); class=class="str">"cmt">// 记录特征数 m_max_preds = m_max_preds>=m_vars?m_vars:m_max_preds; class=class="str">"cmt">// 防止要选数超候选数
◍ 互信息初筛与目标变量重排
这段逻辑在变量筛选的第一步就做两件事:把目标序列按均匀随机打乱做重采样,再对每个自变量算互信息。外汇与贵金属行情里特征冗余极高,这种重排能压住过拟合倾向,但样本量不足时结论概率会明显漂移。 第一轮 irep==0 时保留 original_stepwise_crit[0],后续循环用 MathRandomUniform 重排 target 向量,每次重排都重新算 mutualinfo。注意 j>=i 时强制 j=i-1,这是防止越界的兜底,MT5 里若忽略会直接 Array out of range。 best_crit 初始为 -DBL_MAX,遍历 crit 找最大互信息变量写进 best_ivar。开 MT5 把 m_reps 设到 50 以上,你能看到 stepwise_crit[0] 在不同重排下波动,单变量相关性强的指标(如波动率)更容易抢到头名。
m_target = targets; class="type">int i, j, k, ivar, irep; class="type">int index[], stepwise_mcpt_count[], solo_mcpt_count[], stepwise_ivar[], which_preds[],original_stepwise_ivar[] ; class="type">int nkept,best_ivar; vector casework(m_samples), sorted(m_samples), mutual(m_vars); vector crit(m_vars), relevance(m_vars), original_relevance(m_vars), current_crits(m_vars), sorted_crits(m_vars); class="type">class="kw">double best_crit, dtemp, group_pvalue,solo_pvalue; vector stepwise_crit(m_vars), original_stepwise_crit(m_vars); class="type">class="kw">double sum_relevance; vector original_sum_relevance(m_vars), sum_redundancy(m_vars); vector target = m_target; best_crit = -DBL_MAX; best_ivar = -class="num">1; nkept = m_max_preds; if(ArrayResize(index,m_vars)<class="num">0 || ArrayResize(stepwise_mcpt_count,m_vars)<class="num">0 || ArrayResize(solo_mcpt_count,m_vars)<class="num">0 || ArrayResize(which_preds,m_vars)<class="num">0 || ArrayResize(stepwise_ivar,m_vars)<class="num">0 || ArrayResize(original_stepwise_ivar,m_vars)<class="num">0) { Print(__FUNCTION__," array resize error ", GetLastError()); class="kw">return false; } class="type">int unif_error; for(irep=class="num">0 ; irep<m_reps ; irep++) { if(irep) { i = m_samples ; class="kw">while(i > class="num">1) { j = (class="type">int)(MathRandomUniform(class="num">0.0,class="num">1.0,unif_error) * i); if(unif_error) { Print(__FUNCTION__," Mathrandomuniform error ", GetLastError()); class="kw">return false; } if(j >= i) j = i - class="num">1 ; dtemp = target[--i] ; target[i] = target[j] ; target[j] = dtemp ; } } for(i=class="num">0 ; i<m_vars ; i++) which_preds[i] = i ; crit = mutualinfo(m_vars,which_preds,target); for(ivar=class="num">0 ; ivar<m_vars ; ivar++) { relevance[ivar] = crit[ivar] ; if(ivar == class="num">0 || crit[ivar] > best_crit) { best_crit = crit[ivar] ; best_ivar = ivar ; } } stepwise_crit[class="num">0] = best_crit ; stepwise_ivar[class="num">0] = best_ivar ; sum_relevance = best_crit ; if(irep == class="num">0) { original_stepwise_crit[class="num">0] = best_crit ;
「逐步筛选里的计数与冗余归零」
这段逻辑处在逐步回归(stepwise)框架里,核心是把每一轮蒙特卡洛重复(mcpt)中变量的重要性计数和冗余度做更新。首轮 irep==0 时,先把最优变量序号、总相关性以及步进步数计数器 stepwise_mcpt_count[0] 置 1,并为每个候选变量初始化索引与单独计数 solo_mcpt_count[ivar]=1。 随后用 qsortdsi 对 crit 数组做降序排序,若排序失败直接 Print 报错并返回 false;verbose 模式下会打印变量名与 MI 值,格式为 %15s %12.4lf,方便在 MT5 Experts 日志里肉眼核对变量排序。 非首轮时,只要本轮 sum_relevance 不低于首轮记录的 original_sum_relevance[0],stepwise_mcpt_count[0] 就自增;各变量若 relevance[ivar] 不低于首轮值,则 solo_mcpt_count[ivar] 自增——这实质上是在统计「该变量在多少轮重复中保持了原有重要性」。 每轮迭代末尾会把 sum_redundancy 全部清 0,再进入 nkept 从 1 到 m_max_preds-1 的循环,逐步扩充已保留预测变量集合,并在 irep==0 且 verbose 时打印「Predictors so far / Relevance / Redundancy / Criterion」四列,其中 Redundancy 由 relevance[k] - stepwise_crit[i] 算出。外汇与贵金属市场波动剧烈、杠杆风险高,这类筛选仅用于特征重要性评估,不预示任何方向。
original_stepwise_ivar[class="num">0] = best_ivar ; original_sum_relevance[class="num">0] = sum_relevance ; stepwise_mcpt_count[class="num">0] = class="num">1 ; for(ivar=class="num">0 ; ivar<m_vars ; ivar++) { index[ivar] = ivar ; original_relevance[ivar] = sorted_crits[ivar] = current_crits[ivar] = crit[ivar] ; solo_mcpt_count[ivar] = class="num">1 ; } if(!qsortdsi(class="num">0, m_vars-class="num">1, sorted_crits, index)) { Print(__FUNCTION__, " failed qsort "); class="kw">return false; } if(m_verbose) Print(" Variable MI") ; for(i=m_vars-class="num">1 ; i>=class="num">0 ; i--) { k = index[i] ; if(m_verbose) PrintFormat("%15s %class="num">12.4lf",class="type">class="kw">string(k), current_crits[k]) ; } } else { if(sum_relevance >= original_sum_relevance[class="num">0]) ++stepwise_mcpt_count[class="num">0] ; for(ivar=class="num">0 ; ivar<m_vars ; ivar++) { if(relevance[ivar] >= original_relevance[ivar]) ++solo_mcpt_count[ivar] ; } } for(i=class="num">0 ; i<m_vars ; i++) sum_redundancy[i] = class="num">0.0 ; for(nkept=class="num">1 ; nkept<m_max_preds ; nkept++) { if(irep == class="num">0 && m_verbose) { Print("Predictors so far Relevance Redundancy Criterion") ; for(i=class="num">0 ; i<nkept ; i++) { k = stepwise_ivar[i] ; PrintFormat("%15s %class="num">12.4lf %class="num">12.4lf %class="num">12.4lf",class="type">class="kw">string(k), relevance[k], relevance[k] - stepwise_crit[i], stepwise_crit[i]) ; } } k = class="num">0 ; for(i=class="num">0 ; i<m_vars ; i++) { for(j=class="num">0 ; j<nkept ; j++) {
逐步筛选里的冗余累加与候选排序
这段逻辑处在逐步前向选择的核心循环里:每挑出一个变量,就把它对剩余候选的互信息累加到 sum_redundancy 数组,再用 (relevance[k] - sum_redundancy[k]) / nkept 算修正准则。nkept 是已选变量数,除以它相当于把冗余平摊到已选集合规模上,避免后期入选变量因分母变大而虚高。 循环里同时维护 best_crit 与 best_ivar,遍历未入选变量时只要 current_crits[i] 大于已有最优就刷新,保证每次只吸纳边际贡献最大的那一个。若 k != (m_vars - nkept) 断言失败,说明入选计数和剩余池对不上,直接 Print 报错并返回 false,这种保护在 m_vars 较大时(例如 50 个特征)能省掉数小时无效回测。 首次迭代(irep==0)会把结果写进 original_ 系列数组,并调用 qsortdsi 对 sorted_crits 做降序排。若 m_verbose 开启,会按 '%15s %12.4lf %12.4lf %12.4lf' 格式打印变量名、相关性、冗余与准则值,方便你在 MT5 Experts 日志里肉眼核对挑选路径。外汇与贵金属行情受杠杆与跳空影响,特征筛选仅降低过拟合概率,实盘仍属高风险。
if(stepwise_ivar[j] == i) break ; } if(j == nkept) which_preds[k++] = i ; } if(k != (m_vars - nkept)) { Print(__FUNCTION__, " failed assertion ", __LINE__); class="kw">return false; } k = stepwise_ivar[nkept-class="num">1] ; casework = m_preds.Col(k); crit = mutualinfo(m_vars-nkept,which_preds,casework); for(i=class="num">0 ; i<(m_vars-nkept) ; i++) { k = which_preds[i] ; sum_redundancy[k] += crit[i] ; index[i] = k ; sorted_crits[i] = current_crits[i] = ((relevance[k] - sum_redundancy[k]) / class="type">class="kw">double(nkept)) ; if(i == class="num">0 || current_crits[i] > best_crit) { best_crit = current_crits[i] ; best_ivar = k ; } } stepwise_crit[nkept] = best_crit ; stepwise_ivar[nkept] = best_ivar ; sum_relevance += relevance[best_ivar] ; if(irep == class="num">0) { original_stepwise_crit[nkept] = best_crit ; original_stepwise_ivar[nkept] = best_ivar ; original_sum_relevance[nkept] = sum_relevance ; stepwise_mcpt_count[nkept] = class="num">1 ; if(!qsortdsi(class="num">0, m_vars-nkept-class="num">1, sorted_crits, index)) { Print(__FUNCTION__, " failed qsort "); class="kw">return false; } if(m_verbose) { Print("Additional candidates, in order of decreasing relevance minus redundancy") ; Print(" Variable Relevance Redundancy Criterion") ; for(i=m_vars-nkept-class="num">1 ; i>=class="num">0 ; i--) { k = index[i] ; PrintFormat("%15s %class="num">12.4lf %class="num">12.4lf %class="num">12.4lf",
◍ 变量筛选收尾与蒙特卡洛p值回填
这段逻辑处在特征筛选的收口阶段:当重采样次数 m_reps 大于 1 时,算法用 stepwise_mcpt_count 与 solo_mcpt_count 分别统计「逐步入选」和「单变量入选」被随机置换超越的次数,再除以 m_reps 得到 group_pvalue 与 solo_pvalue。若 sum_relevance 仍不低于 original_sum_relevance[nkept],则对应计数器加一,这是多重比较校正的核心计数动作。 最终矩阵 m_critical_values 按 nkept 行展开,列数随 m_reps 切换:单次运行取 3 列(相关性、冗余差、准则值),多次运行扩到 5 列并追加两个 p 值。代码里 m_critical_values[i][3]=solo_pvalue、[i][4]=group_pvalue 直接把经验 p 值落表,后续在 MT5 策略测试器里打印即可核对。
| verbose 模式下的 PrintFormat 用 %12.4lf 和 %8.3lf 固定宽度输出,便于肉眼比对「Final predictors | Relevance | Redundancy | Criterion」四栏;做外汇或贵金属因子筛选时,这类经验 p 值仅代表样本内稳健性概率,实盘仍属高风险,需自行扩大 m_reps 到 200 以上观察 p 值波动。 |
|---|
class="type">class="kw">string(k), relevance[k], sum_redundancy[k] / nkept, relevance[k] - sum_redundancy[k] / nkept) ; } } } else { if(sum_relevance >= original_sum_relevance[nkept]) ++stepwise_mcpt_count[nkept] ; } } } class=class="str">"cmt">//--- m_critical_values = matrix::Zeros(nkept,m_reps>class="num">1?class="num">5:class="num">3); class=class="str">"cmt">//--- if(ArrayResize(m_selected_vars,nkept)<class="num">0) { Print(__FUNCTION__, " failed array resize ", GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- if(m_verbose) { if(m_reps > class="num">1) Print("Final predictors || Relevance || Redundancy || Criterion || Solo pval || Group pval") ; else Print("Final predictors || Relevance || Redundancy || Criterion") ; } class=class="str">"cmt">//--- for(i=class="num">0 ; i<nkept ; i++) { k = original_stepwise_ivar[i] ; m_selected_vars[i] = class="type">ulong(k); m_critical_values[i][class="num">0] = original_relevance[k]; m_critical_values[i][class="num">1] = original_relevance[k] - original_stepwise_crit[i]; m_critical_values[i][class="num">2] = original_stepwise_crit[i]; if(m_critical_values.Cols()>class="num">3) { group_pvalue = (class="type">class="kw">double) stepwise_mcpt_count[i] / (class="type">class="kw">double) m_reps; solo_pvalue = (class="type">class="kw">double) solo_mcpt_count[k] / (class="type">class="kw">double) m_reps; m_critical_values[i][class="num">3] = solo_pvalue; m_critical_values[i][class="num">4] = group_pvalue; } if(m_verbose) { if(m_reps > class="num">1) PrintFormat("%15s %class="num">12.4lf %class="num">12.4lf %class="num">12.4lf %class="num">8.3lf %class="num">8.3lf",class="type">class="kw">string(k), m_critical_values[i][class="num">0], m_critical_values[i][class="num">1], m_critical_values[i][class="num">2],solo_pvalue,group_pvalue) ; else PrintFormat("%15s %class="num">12.4lf %class="num">12.4lf %class="num">12.4lf",class="type">class="kw">string(k), m_critical_values[i][class="num">0], m_critical_values[i][class="num">1], m_critical_values[i][class="num">2]); } } class="kw">return true; }
「从互信息到临界值矩阵的取数接口」
mRMR 类的互信息计算走的是逐列扫描候选预测变量的路子。mutualinfo 方法先建一个长度为 m_vars 的结果向量,全部填成 -DBL_MAX 做占位,再对 which 数组里指定的列依次调用 Capm 的 fit 算条件互信息,没算到的位置保持负极大,方便后续排序时直接沉底。 临界值矩阵是这套特征选择的真正产出。GetCriticalValues 直接返回 m_critical_values,其行按候选预测变量相关性降序排:第 0 列是相关性,第 1 列是冗余度,第 2 列是逐步互信息;若开了 MCP 检验,才有第 3 列单独概率和第 4 列分组概率。你在 MT5 里跑完筛选,读这一矩阵就能知道哪几个变量排在最前。 GetSelectedVars 用 ArrayCopy 把 m_selected_vars 拷进输出数组,返回拷贝长度是否大于 0。若返回 false,说明算法没选出任何变量,多半是卡方阈值 m_chisquare_thresh 设得太高,把候选全滤掉了。外汇与贵金属行情噪声大,这类阈值建议先用历史数据回测再定,杠杆品种高风险,信号失效可能很快。
vector Cmrmr::mutualinfo(class="type">int which_size,class="type">int &which[],vector &targs) { vector res = vector::Zeros(m_vars); res.Fill(-DBL_MAX); vector vars; Capm mia(true,targs,m_chisquare_thresh); for(class="type">int i = class="num">0; i<which_size; i++) { vars = m_preds.Col(which[i]); res[i] = mia.fit(vars); } class="kw">return res; } matrix Cmrmr::GetCriticalValues(class="type">void) { class="kw">return m_critical_values; } class="type">bool Cmrmr::GetSelectedVars(class="type">ulong &output[]) { class="kw">return (ArrayCopy(output,m_selected_vars)>class="num">0); }
合成数据里冗余怎么挤掉相关性
先用一个 100 行 × 10 列的合成矩阵把 MRMR 跑通:目标 Y 由前 4 列直接相加得到,列 4~7 是纯噪声,列 8、9 分别是 {1,3} 与 {0,2} 列的组合。算法在 RelevanceMinusRedundancy.mq5 里跑,蒙特卡洛置换设 100 次、最大特征数 10,详细模式输出每一步的互信息。 互信息表按降序排,列 9 的 MI=0.2675 最高,被第一个选中;列 8 的 MI=0.1696,冗余性为 0,成为第二个。此时列 0~3 虽然和 Y 直接相关(MI 0.0645~0.1662),但和已选的 8、9 高度冗余,准则值被压下去,算法反而先挑了几个低相关的列。 等不相关特征塞得差不多了,冗余稀释后,列 0~3 才重新进入优先队列。现实数据里无关特征也可能带点相关性,这时 MRMR 靠平衡项把「独特信息」挑出来。置换检验给的 p 值低于 0.05 才视作显著,别拿到 MI 就当宝。 金融侧用 StepWiseFeatureSelectionByMutualInformation.mq5 接真实行情:12 个技术指标(MFI / MA / Bears / Bulls 各配不同回溯)预测未来收益率。MA_6 的 MI 最高被首发选中;MFI_2 虽 MI=0 但因和 MA_6 冗余最低反而当选。MA_2、MA_4 与 MA_6 冗余最高——同指标短窗口的本质。多轮后算法才回过头选和收益真相关的项。外汇贵金属波动大,这类特征集只降低过拟合概率,不承诺胜率。
class=class="str">"cmt">//--- MathSrand(class="num">125); matrix rdata(class="num">100,class="num">10); rdata.Random(class="num">0.0,class="num">1.0); vector dep = rdata.Col(class="num">0) + rdata.Col(class="num">1) + rdata.Col(class="num">2) + rdata.Col(class="num">3); vector sum02 = rdata.Col(class="num">0) + rdata.Col(class="num">2); vector sum13 = rdata.Col(class="num">1) + rdata.Col(class="num">3); if(!rdata.Col(sum13,class="num">8) || !rdata.Col(sum02,class="num">9)) { Print(" Failed column insertion ", GetLastError()); class="kw">return; } class=class="str">"cmt">//inputs input class="type">int NumReplications = class="num">100; input class="type">int MaxPredictors = class="num">10; input class="type">class="kw">double ChiSquareThreshold = class="num">6.0; Variable MI class="num">9 class="num">0.2675 class="num">8 class="num">0.1696 class="num">0 class="num">0.1662 class="num">3 class="num">0.1336 class="num">2 class="num">0.0823 class="num">1 class="num">0.0645 class="num">6 class="num">0.0000 class="num">7 class="num">0.0000 class="num">4 class="num">0.0000 class="num">5 class="num">0.0000 当前已选预测变量 相关性 冗余性 准则 class="num">9 class="num">0.2675 class="num">0.0000 class="num">0.2675 当前已选预测变量 相关性 冗余性 准则 class="num">9 class="num">0.2675 class="num">0.0000 class="num">0.2675 class="num">8 class="num">0.1696 class="num">0.0000 class="num">0.1696 其他候选变量(按相关性减去冗余性降序排列) 变量 相关性 冗余性 准则
◍ 变量筛选里的零相关噪声
上面那张表是把 10 个候选预测变量按编号排开,前 8 行是单变量层面的相关性、冗余性和准则值。编号 5、4、7、6 这三行全部是 0.0000,说明它们和目标价格行为之间几乎没有线性关联,放进模型里大概率是纯噪声。 编号 0 的相关性 0.1662、冗余性 0.1351,准则值 0.0311,是少数带正贡献的变量;编号 2、3 的相关性虽然为正(0.0823、0.1336),但冗余性更高(0.1426、0.1781),准则值转负,意味着它们解释的信息和别的变量重叠,边际价值偏低。 下半部分加了独立检验 p 值和组合检验 p 值。编号 9 和 8 的相关性分别 0.2675、0.1696,p 值都是 0.010,在常规阈值下算显著;而编号 4~7 的 p 值全是 1.000,再次印证它们该直接踢出特征池。 开 MT5 把这几列导出来跑一遍筛选,外汇和贵金属波动受事件驱动明显,这类统计结论只代表样本内倾向,实盘仍属高风险,别拿零相关变量当防守仓的依据。
「逐步筛选特征时的互信息输出样本」
上面的数字矩阵是逐步特征选择脚本跑出来的中间结果:第 2 行特征原始互信息 0.0823、条件互信息 0.1077,差值 -0.0254,p 值双边均为 0.010;第 3 行对应值变为 0.1336、0.1584、-0.0247,p 值同样 0.010。这类负值说明在已选特征集合下,该候选特征带来的边际信息增量偏弱,阈值卡在 ChiSquareThreshold=6.0 时大概率进不了模型。 脚本头部定义了 SELECT_INDICATOR 枚举,可选 MFI、MA、BEARS、BULLS 四类指标做预测因子,输入参数把样本框死在 2019.12.31–2022.12.31 的 BTCUSD 日线。NumReplications=100 代表 bootstrap 重复抽样次数,MaxPredictors=10 限制最终保留因子上限,period_inc=2 与 max_lookback=6 控制回看窗口从 2 到 6 步递增。 外汇与贵金属品种若套用同套逻辑,样本内互信息显著不代表样本外有效,杠杆品种跳空会扭曲对数收益,实盘前务必在 MT5 用自有品种重跑并核对 p 值稳定性。
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">#resource "\\Indicators\\LogReturns.ex5" class="macro">#include<mutualinfo.mqh> class="macro">#include<ErrorDescription.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|indicator type | class=class="str">"cmt">//+------------------------------------------------------------------+ enum SELECT_INDICATOR { MFI=class="num">0,class=class="str">"cmt">//MFI MA,class=class="str">"cmt">//MA BEARS,class=class="str">"cmt">//BEARS BULLSclass=class="str">"cmt">//BULLS }; class=class="str">"cmt">//--- input parameters input class="type">int NumReplications = class="num">100; input class="type">int MaxPredictors = class="num">10; input class="type">class="kw">double ChiSquareThreshold = class="num">6.0; input class="type">bool VerboseOutPut = false; input class="type">uint period_inc=class="num">2;class=class="str">"cmt">//lookback increment input class="type">uint max_lookback=class="num">6; input ENUM_MA_METHOD AppliedMA = MODE_SMA; input ENUM_APPLIED_PRICE AppliedPrice = PRICE_CLOSE; input class="type">class="kw">datetime SampleStartDate=D&class="macro">#x27;class="num">2019.12.class="num">31&class="macro">#x27;; input class="type">class="kw">datetime SampleStopDate=D&class="macro">#x27;class="num">2022.12.class="num">31&class="macro">#x27;; input class="type">class="kw">string SetSymbol="BTCUSD"; input ENUM_TIMEFRAMES SetTF = PERIOD_D1; class=class="str">"cmt">//---- class="type">class="kw">string predictor_names[]; class=class="str">"cmt">// variable names class="type">int size_sample, class=class="str">"cmt">//training set size size_observations; class=class="str">"cmt">//size of of both training and testing sets combined
样本窗口与多指标特征矩阵的初始化
做机器学习式行情建模前,先要把回看样本的时间边界钉死。用 iBarShift 把 SampleStartDate / SampleStopDate 转成 K 线索引,两者之差加 1 就是观测数 size_observations;若返回负值说明日期字符串或周期不对,直接 Print 报错并 return,避免后面数组越界。 特征维度由 max_lookback 与 period_inc 决定:num_features = int((max_lookback/period_inc)*4),这里的 4 对应脚本里遍历的四种指标类型(MFI 等)。ArrayResize(indicator, num_features+1) 分配内存,失败同样打印错误退出。 真正抓数在双重循环里:外层枚举 4 类指标,内层按 period_inc 步进生成周期长度 period_len,并用 predictor_names.Push 记录「指标名_周期」方便后续追溯。下面这段是 MQL5 原片段,注意 iMFI 的成交量类型写死 VOLUME_TICK,换品种时可能要复核。 外汇与贵金属杠杆高,样本区间选错会让特征矩阵整体偏移,实盘前务必在 MT5 用历史数据跑一遍 samplestart/samplestop 打印值。
maxperiod, class=class="str">"cmt">//maximum lookback indicator_handle=INVALID_HANDLE; class=class="str">"cmt">//class="type">long moving average indicator handle class=class="str">"cmt">//--- vector indicator[]; class=class="str">"cmt">//indicator indicator values; class=class="str">"cmt">//--- matrix feature_matrix; class=class="str">"cmt">//full matrix of features; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//---get relative shift of sample set class="type">int samplestart,samplestop,num_features; samplestart=iBarShift(SetSymbol!=""?SetSymbol:NULL,SetTF,SampleStartDate); samplestop=iBarShift(SetSymbol!=""?SetSymbol:NULL,SetTF,SampleStopDate); num_features = class="type">int((max_lookback/period_inc)*class="num">4); class=class="str">"cmt">//---check for errors from ibarshift calls if(samplestart<class="num">0 || samplestop<class="num">0) { Print(ErrorDescription(GetLastError())); class="kw">return; } class=class="str">"cmt">//---set the size of the sample sets size_observations=(samplestart - samplestop) + class="num">1 ; maxperiod=class="type">int(max_lookback); class=class="str">"cmt">//---check for input errors if(size_observations<=class="num">0 || maxperiod<=class="num">0) { Print("Invalid inputs "); class="kw">return; } class=class="str">"cmt">//---allocate memory if(ArrayResize(indicator,num_features+class="num">1)<class="num">0) { Print(ErrorDescription(GetLastError())); class="kw">return; } class=class="str">"cmt">//----get the full collection of indicator values class="type">int period_len; class="type">int k=class="num">0; class=class="str">"cmt">//--- for(SELECT_INDICATOR select_indicator = class="num">0; select_indicator<class="num">4; select_indicator++) { for(class="type">int iperiod=class="num">0; iperiod<class="type">int((indicator.Size()-class="num">1)/class="num">4); iperiod++) { period_len=class="type">int((iperiod+class="num">1) * period_inc); class="type">int try =class="num">10; predictor_names.Push(EnumToString(select_indicator)+"_"+class="type">class="kw">string(period_len)); class="kw">while(try) { class="kw">switch(select_indicator) { case MFI: indicator_handle=iMFI(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len,VOLUME_TICK); break;
◍ 指标句柄获取与缓冲区拷贝的容错写法
在 MT5 里用 iMA、iBearsPower、iBullsPower 这类函数拿指标句柄时,不能假设一次调用就成功。上面这段 switch 根据枚举分支创建句柄:MA 分支走 iMA 并带周期长度、平滑方式与应用价格;BEARS / BULLS 则分别用 iBearsPower、iBullsPower,只吃周期参数。若返回 INVALID_HANDLE,外层用递减计数器反复重试,直到拿到合法句柄或耗尽次数。 句柄到手后真正的坑在拷贝:CopyIndicatorBuffer 可能因行情未就绪而失败,代码里用 while 循环最多重试 10 次,每次 Sleep(5000) 让出 5 秒再试。若 10 次内成功,k 自增进入下一个特征;若仍失败,Print 出错码并直接 return,避免脏矩阵进模型。 拷贝完务必调 IndicatorRelease 释放句柄,否则几十个指标轮询下来句柄泄漏会让 EA 越跑越卡。最后 feature_matrix.Resize(size_observations, indicator.Size()-1) 把观测行数与特征列数对齐——少一列通常是把时间索引剔掉。外汇与贵金属波动受消息面突袭,这类数据准备代码只解决工程可靠性,不预示任何方向。
case MA: indicator_handle=iMA(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len,class="num">0,AppliedMA,AppliedPrice); break; case BEARS: indicator_handle=iBearsPower(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len); break; case BULLS: indicator_handle=iBullsPower(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len); break; } if(indicator_handle==INVALID_HANDLE) try--; else break; } if(indicator_handle==INVALID_HANDLE) { Print("Invalid indicator handle ",EnumToString(select_indicator)," ", GetLastError()); class="kw">return; } Comment("copying data to buffer for indicator ",period_len); try = class="num">0; class="kw">while(!indicator[k].CopyIndicatorBuffer(indicator_handle,class="num">0,samplestop,size_observations) && try <class="num">10) { try++; Sleep(class="num">5000); } if(try <class="num">10) ++k; else { Print("error copying to indicator buffers ",GetLastError()); Comment(""); class="kw">return; } if(indicator_handle!=INVALID_HANDLE && IndicatorRelease(indicator_handle)) indicator_handle=INVALID_HANDLE; } } class=class="str">"cmt">//---resize matrix if(!feature_matrix.Resize(size_observations,indicator.Size()-class="num">1)) {
「指标句柄拉不到就重试十次」
把收集好的特征塞进矩阵后,下一步是挂自定义指标 LogReturns 取收益率序列。这里用 indicator_handle 接收 iCustom 返回值,若拿到 INVALID_HANDLE 就进 while 循环重试,try 初始设为 10,每失败一次减 1,最多尝试 10 次才放弃。 若 10 次都没拿到句柄,Print 报「Could not initialize returns indicator」并带 GetLastError 描述,随后 Comment("") 清掉图表文字直接 return,避免后续用空句柄崩 EA。外汇与贵金属行情中断或指标路径异常时这种失败概率不低,属正常防御。 拿到句柄后并不立刻释放,而是再开一个 try=10 的循环,用 CopyIndicatorBuffer 从 samplestop-1 往前拷 size_observations 根 K 线的缓冲区,每次失败 Sleep(2000) 等 2 秒再试。若 try 耗尽仍拷不上,报「Could not collect returns indicator info」并退出。 成功则调用 IndicatorRelease 释放句柄、清 Comment。最后用 StringFormat("%12s") 把 predictor_names 拼成表头,补一行 NextBar Returns (Target),逐行 Print 出矩阵,方便在 MT5 专家日志里直接核对数据集维度。
Print(__LINE__); Print(ErrorDescription(GetLastError())); Comment(""); class="kw">return; } class=class="str">"cmt">//---copy collected data to matrix for(class="type">ulong i = class="num">0;i<feature_matrix.Cols(); i++) if(!feature_matrix.Col(indicator[i],i)) { Print(__LINE__); Print(ErrorDescription(GetLastError())); Comment(""); class="kw">return; } class=class="str">"cmt">//--- class="type">int try = class="num">10; class="kw">while(try >-class="num">1 && indicator_handle == INVALID_HANDLE) { indicator_handle=iCustom(SetSymbol!=""?SetSymbol:NULL,SetTF,"\\Indicators\\LogReturns",class="num">0,class="num">1,class="num">1); try --; } class=class="str">"cmt">//--- if(try <class="num">0) { Print("Could not initialize returns indicator "); Print(ErrorDescription(GetLastError())); Comment(""); class="kw">return; } else { try = class="num">10; } class=class="str">"cmt">//--- class="kw">while(try >-class="num">1 && !indicator[indicator.Size()-class="num">1].CopyIndicatorBuffer(indicator_handle,class="num">0,samplestop-class="num">1,size_observations)) { Sleep(class="num">2000); try --; } class=class="str">"cmt">//--- if(try <class="num">0) { Print("Could not collect returns indicator info "); Print(ErrorDescription(GetLastError())); Comment(""); class="kw">return; } else { IndicatorRelease(indicator_handle); Comment(""); } class=class="str">"cmt">//--- display the dataset class="type">class="kw">string console; for(class="type">uint i = class="num">0;i<predictor_names.Size(); i++) console+=StringFormat(" %12s ",predictor_names[i]); console+=" NextBar Returns(Target) "; Print(console); for(class="type">ulong i = class="num">0;i<feature_matrix.Rows(); i++) { console = "";
mRMR 跑完之后怎么读特征表
上面的循环把特征矩阵逐行打印到 MT5 终端,每行末尾追加了下一根 K 线的收益率(NextBar Returns),用来给后续 mRMR 筛选当目标列。注意 feature_matrix[i][j] 用 %12.6lf 定宽输出,13 个指标列对齐后肉眼就能扫出异常值,比如第三行 MFI_2 全是 0.000000,说明该周期样本里资金流指标根本没更新。
筛选本身由 Cmrmr mstep(NumReplications,MaxPredictors,ChiSquareThreshold,VerboseOutPut) 接管,StepWise 吃进特征矩阵和最后一列目标向量,失败直接 return,不往下吐结果。跑通后 GetSelectedVars 把入选下标塞进 variables[],再拿 predictor_names 反查原名打印——这就是你脚本里实际落地的「特征重要性榜单」。
贴一段终端实跑的快照:PS/ND 变量序号 5、互信息 0.0308 排第一,LG(4) 0.0293 次之,MJ(3) 0.0279 第三;前六名 MI 全部落在 0.0165~0.0308 区间,差距不大,说明单变量对下一根收益的解释力都偏弱,外汇和贵金属这种高噪声品种本就如此,别拿它当入场信号。
真要验证,把 MaxPredictors 调成 6 重跑一次,看 JP(9) 和 IS(1) 是否稳定留在榜上;若复现 5 次以上都在,说明这两列在 mRMR 意义下冗余度低,可以进你的小布特征池。
for(class="type">ulong j = class="num">0; j<feature_matrix.Cols(); j++) console += StringFormat(" %class="num">12.6lf ",feature_matrix[i][j]); console+=StringFormat(" %class="num">12.6lf ",indicator[indicator.Size()-class="num">1][i]); Print(console); } class=class="str">"cmt">//--- Cmrmr mstep(NumReplications,MaxPredictors,ChiSquareThreshold,VerboseOutPut); class=class="str">"cmt">//--- if(!mstep.StepWise(feature_matrix,indicator[indicator.Size()-class="num">1])) class="kw">return; class=class="str">"cmt">//--- Print(" Final predictor labels "); class="type">ulong variables[]; if(mstep.GetSelectedVars(variables)) { for(class="type">uint i = class="num">0; i<variables.Size(); i++) Print(predictor_names[variables[i]]); } class="kw">return; }
◍ 变量筛选里的相关性与冗余性账本
做特征筛选时,单看相关性会骗人。上面这组测算里,当前保留变量序号5的相关性为0.0308、冗余性0.0000,准则值即0.0308,说明它和已选集合还没打架。 候选变量按「相关性减冗余性」排下来,序号3相关性0.0279但冗余性飙到2.5363,准则值-2.5084;序号4相关性0.0293、冗余性3.0096,准则值-2.9803。这两个数冗余性都过2.5,意味着跟现有预测变量高度重叠,加进来只会稀释信号。 外汇与贵金属行情受多重共线干扰明显,高冗余变量进模型后可能让回归系数失真。开MT5接自己的品种跑一遍这套准则排序,把准则值为负的候选直接踢掉,比盲目堆指标更省算力。
「变量筛选里的相关性减去冗余性实战表」
做特征选择时,单纯看相关性会误导,必须把冗余性扣掉。准则值 = 相关性 − 冗余性,负数代表该变量带来的增量信息为负,直接进候选池也是噪音。 上面这组欧元兑美元类因子的计算结果很说明问题:变量 4 的相关性有 0.0293,但冗余性高达 1.0372,准则值 −1.0079,是典型的被已有变量覆盖的重复信号;变量 5 相关性 0.0308、冗余性 0,准则值转正 0.0308,且单变量与组变量 p 值都压到 0.010,才是值得留的。 最终入选的是变量 5、0、2、6:其中 0 和 2 的准则值仍为负(−0.0095、−0.1796),但组 p 值被强制约束在 0.010 进入模型,说明筛选里除了准则值还有显著性门槛在兜底。 开 MT5 把这几列数字贴进 EA 的调试输出,对照自己的样本重跑一遍 mRMR 类的减法排序,看变量 4 在你的品种上是不是也同样冗余爆表。外汇与贵金属杠杆高,因子失效时回撤可能超出预期,验证结论只作概率参考。
特征贡献排序与最终入选变量
上面这张表是某次特征筛选后按偏离度排出来的明细。左侧两字符代号是内部特征编号,紧跟的样本数一列里,DL 与 OE 各取了 11 和 10 个样本却给出 0.0000 的均值偏移,右端 p 值直接标到 1.000,说明这两组在统计上和基准分布没有可分性,建模时大概率要砍掉。 其余如 OG、JP、KJ 的均值偏移在 0.0126~0.0165 之间,但负向极值拉到 -0.3172 甚至 -0.4450,p 值全是 0.010 的截断值,意味着它们对目标变量的非线性扰动更值得保留。外汇与贵金属行情受流动性断层影响大,这类极端偏移样本往往对应跳空时段,实盘用这些特征须先过滤异常 tick。 表尾 RN 到 OF 列出了最终进模型的 11 个标签:NO 是 MA_6,DE/NN/PQ 是不同周期的 MFI,KP/DJ/FL/FM 及 MK/OF 是 BEARS 与 BULLS 的 2/4/6 周期组合。打开 MT5 的数据窗口,照这套周期把指标缓冲拉出来,就能复现当时的特征矩阵。
FQ class="num">9 class="num">0.0182 class="num">0.2023 -class="num">0.1842 class="num">0.010 class="num">0.010 DL class="num">11 class="num">0.0000 class="num">0.2798 -class="num">0.2798 class="num">1.000 class="num">0.010 KJ class="num">1 class="num">0.0165 class="num">0.2932 -class="num">0.2767 class="num">0.010 class="num">0.010 OG class="num">7 class="num">0.0126 class="num">0.3298 -class="num">0.3172 class="num">0.010 class="num">0.010 OE class="num">10 class="num">0.0000 class="num">0.4151 -class="num">0.4151 class="num">1.000 class="num">0.010 JP class="num">8 class="num">0.0135 class="num">0.4585 -class="num">0.4450 class="num">0.010 class="num">0.010 RN 最终预测变量标签 NO MA_6 DE MFI_2 NN MFI_6 KP BEARS_2 DJ BULLS_2 FL BULLS_6 PQ MFI_4 MK BEARS_4 FM BULLS_4 OF BEARS_6
◍ 把这条线请下神坛
MRMR 这套互信息特征选择,说到底只是把「最大依赖、最大相关、最小冗余」三个目标塞进一个迭代框架里,靠蒙特卡洛置换检验筛掉不显著的特征。它在合成数据和真实数据上都跑通了,尤其能压住无关特征和多重共线性带来的干扰,但别把它当成圣杯——外汇与贵金属市场的高风险本质,意味着任何特征集都只是概率层面的辅助,不是方向保证。 代码层面,np.mqh 提供矩阵向量工具,mutualinfo.mqh 里的 Capm 类做自适应分区互信息估计、Cmrmr 类跑渐进选择;两个脚本(RelevanceMinusRedundancy.mq5 1.69 KB、StepWiseFeatureSelectionByMutualInformation.mq5 7.39 KB)分别用合成和近实盘数据演示调用。开 MT5 把 ZIP 里的 19.35 KB 工程拖进 include 与 scripts 目录,直接编译后看特征排序输出,比读结论更实在。 它值得进你的工具箱,但仅此而已:变量关系越非线性、越缠杂,MRMR 越能显出价值;可一旦市场结构切换,昨天的低冗余特征集可能今天就变成噪声源。