名义变量的序数编码·综合运用
(3/3)·从Python到MQL5双实现,拆解13种编码路径如何避免算法误读金融形态
给针形K线编个2、纺锤线编个1,很多交易者以为这样就能直接丢进神经网络。模型悄悄学会了"2比1好",可市场里哪有这种天生等级。本篇用比特币日线把这套坑一层层拆开。
◍ 独热编码后的 BTCUSD 日线特征快照
在 MT5 策略测试器里跑一段特征工程脚本,BTCUSD 日线样本会在日志里直接吐出原始与变换后的预测因子。上面这段输出截取自 16:40:41.760 的同一毫秒批次,说明编码函数对 12 根 K 线一次性完成了映射。 原始预测因子是一个 7 维向量,前 4 维为 OHLC 类数值(如 13743、13855、12362.69、13347),后 3 维为类别型字段(如 0、2、0)。独热展开后,日志把整批样本包在双层方括号里返回,方便后续矩阵训练直接吃进去。 从数据看,类别位第 5 维在 12 行里出现 0 和 1 两种取值,第 6 维取值分布在 1~3,第 7 维多为 0 偶尔 2。这种稀疏分布如果直接喂给回归模型,可能偏向多数类,做特征选择时建议先统计各维度的频数再决定降维。 外汇与贵金属品种走这种编码流程时波动更剧烈,高风险特征本就明显,上实盘前务必用历史样本回测确认维度膨胀没有拖慢推理速度。
Print(" transformed predictors \n", transformed);
}独热编码把 BTCUSD 日线特征拍平了
在 BTCUSD 的 D1 周期上跑独热编码示例,日志在同一时间戳 16:40:41.762 一次性吐出 9 个样本的转换结果,前缀 PH 标记的是 transformed predictors 总览,后面 KP、NF、JI 等每行是一个具体样本。 每个样本数组前 4 个值是原始价格字段:以 KP 为例为 [13348, 15381, 12535.67, 14689],对应开、高、低、收;后面 9 位才是独热后的类别向量。 看类别段能直接反推样本属性:KP 的 [0,1,1,0,0,0,1,0,0] 表示第 2、3、7 个哑变量置 1,而 PH 首行 [1,0,1,0,0,0,1,0,0] 则是第 1、3、7 位激活,差异在第 1 与第 2 位互换,说明这两条样本落在不同分箱或状态组。 开 MT5 把这段日志贴进专家日志对照,数一下每行第 5~13 位的 1 的分布,就能确认编码器是不是按你预设的 9 类在切分。加密货币与外汇贵金属一样属高风险品种,独热后的向量仅用于特征表达,不预示任何方向。
「独热编码在 BTCUSD 日线上的实跑样本」
下面这段日志是 OneHotEncoding_demo 在 BTCUSD 的 D1 周期上跑出的三条记录,时间戳均为 16:40:41.762。每条末尾的 13 维向量里,前 4 位是 OHLC 与某派生值(如 QK 行:14405、14876、12969.58、14876),后 9 位为类别哑变量,可见第 6、9、11 位在三条样本上稳定为 1,其余倾向 0,说明这几列映射的是同一类状态。 这种输出直接暴露了名义变量被展开后的稀疏结构:同一根 D1 K 线,QK 的高点 14876 与收盘 14876 重合,而 PL 行高点 14927、收盘仅 13245,哑变量第 5 位变成 1、第 6 位变 0,状态切换被编码捕获。外汇与贵金属虽不像 BTC 这样跳空剧烈,但用同样手法处理 session 或央行日标签,也可能析出可交易的稀疏模式,注意杠杆品种高风险。 代码层给出的是 CNomOrd 类的骨架,构造函数与析构函数为空,fit 方法开头先清零降维标记与映射标记,再判断传入列索引:若 cols 为空且预测矩阵有列,就用 arange 自动生成 0~列数-1 的索引序列,失败则打印错误并返回未映射状态。
class="kw">public: class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CNomOrd(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ ~CNomOrd(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| fit mapping to training data | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool fit(matrix &preds_in, class="type">ulong &cols[], vector &target) { m_dim_reduce = class="num">0; mapped = false; class=class="str">"cmt">//--- if(cols.Size()==class="num">0 && preds_in.Cols()) { m_pred = class="type">int(preds_in.Cols()); if(!np::arange(m_colindices,m_pred,class="type">ulong(class="num">0),class="type">ulong(class="num">1))) { Print(__FUNCTION__, " arange error "); class="kw">return mapped; } } else {
◍ 类别特征的有序化映射落地
这段逻辑干的事,是把名义变量(nominal)按训练阶段学到的映射,转成带序信息的数值矩阵,方便后续模型直接吃。核心在内存分配与列遍历两段,任何一步 ArrayResize 或 Resize 失败都会直接 Print 错误并 return false,MT5 终端日志里能看到具体的 GetLastError 码。
| 内存申请那串 | 判断覆盖了 m_pred、m_rows 维度的多个数组:m_mean_rankings 长度取 cols.Size(),m_rankings / m_indices 取 preds_in.Rows(),m_mapping 取 preds_in.Cols()。若 cols 非空还要把列索引拷进 m_colindices 并排序,任何一项返回负或 false 即中断。 |
|---|
列循环里对每列取向量 var = preds_in.Col(m_colindices[col]),用 np::unique 拿到去重映射 m_mapping[col],再建 m_class_counts 零向量。内层双循环用 MathAbs(var[i]-m_mapping[col][j])<=1.e-15 做浮点等值判定,命中就把类别 id 写进 m_class_ids 并给对应计数 +1——这个 1e-15 容差在外汇特征工程里容易因点位缩放踩坑,建议开 MT5 用样例矩阵跑一遍看归类是否符合预期。 最后把 m_target 存下,逐列调 train(cid,ccounts,m_target,m_median[i]) 算出 m_mean_rankings,mapped 置 true 返回。整个流程跑通后,名义特征就完成了向序数空间的转换,贵金属与外汇数据的高维类别字段可据此降噪,但汇率波动高风险仍在,映射不代表方向确定性。
m_pred = class="type">int(cols.Size()); } class=class="str">"cmt">//--- m_rows = class="type">int(preds_in.Rows()) ; m_cols = class="type">int(preds_in.Cols()); class=class="str">"cmt">//--- if(ArrayResize(m_mean_rankings,m_pred)<class="num">0 || ArrayResize(m_rankings,m_rows)<class="num">0 || ArrayResize(m_indices,m_rows)<class="num">0 || ArrayResize(m_mapping,m_pred)<class="num">0 || ArrayResize(m_class_counts,m_pred)<class="num">0 || !m_median.Resize(m_pred) || !m_class_ids.Resize(m_rows,m_pred) || !shuffle_target.Resize(m_rows,class="num">2) || (cols.Size()>class="num">0 && ArrayCopy(m_colindices,cols)<class="num">0) || !ArraySort(m_colindices)) { Print(__FUNCTION__, " Memory allocation failure ", GetLastError()); class="kw">return mapped; } class=class="str">"cmt">//--- for(class="type">uint col = class="num">0; col<m_colindices.Size(); col++) { vector var = preds_in.Col(m_colindices[col]); m_mapping[col] = np::unique(var); m_class_counts[col] = vector::Zeros(m_mapping[col].Size()); for(class="type">ulong i = class="num">0; i<var.Size(); i++) { for(class="type">ulong j = class="num">0; j<m_mapping[col].Size(); j++) { if(MathAbs(var[i]-m_mapping[col][j])<=class="num">1.e-15) { m_class_ids[i][col]=class="type">class="kw">double(j); ++m_class_counts[col][j]; class="kw">break; } } } } m_target = target; for(class="type">uint i = class="num">0; i<m_colindices.Size(); i++) { vector cid = m_class_ids.Col(i); vector ccounts = m_class_counts[i]; m_mean_rankings[i] = train(cid,ccounts,m_target,m_median[i]); } mapped = true; class="kw">return mapped; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| transform nominal to ordinal based on learned mapping | class=class="str">"cmt">//+------------------------------------------------------------------+ matrix transform(matrix &data_in) {
映射转换与关系检验的防御式写法
做特征离散化之后,真正落地的两步是 transform 映射和 score 关系检验,这两段代码都先卡死前置状态再算,避免拿未训练数据硬跑。 先看 transform 段:若开了降维却直接调映射、或 fit 没跑成功、或输入列数不等于训练时的 m_cols,统统打印原因并返回 1×1 零矩阵。这种返回形态在 MT5 里能让你立刻从矩阵尺寸发现调用链断了,而不是拿到一堆错值还以为模型正常。 核心映射循环用 m_colindices 定位要处理的列,对每列取值在 m_mapping[col] 里找绝对差 ≤1e-15 的匹配项,命中就把 out 对应位置写成 m_mean_rankings[col][j] 后 break。1e-15 这个容差基本就是双精度浮点的相等判定边界,复制去跑时别手滑改成 1e-6,否则相近但不等的原始值会被误映射。 score 函数同样先查 mapped 标志,未训练直接返 -1.0;若没降维且 selectedVar 超出 m_colindices 大小也返 -1.0。它要求 test_target.Size() 必须等于训练行数 m_rows,这等于强制你检验样本和训练样本同维度,外汇与贵金属行情里样本错位会直接污染显著性结论,属高风险操作。
if(m_dim_reduce) { Print(__FUNCTION__, " Invalid method call, Use fitTransform() or call fit() "); class="kw">return matrix::Zeros(class="num">1,class="num">1); } if(!mapped) { Print(__FUNCTION__, " Invalid method call, training either failed or was not done. Call fit() first. "); class="kw">return matrix::Zeros(class="num">1,class="num">1); } if(data_in.Cols()!=class="type">ulong(m_cols)) { Print(__FUNCTION__, " Unexpected input data shape, doesnot match training data "); class="kw">return matrix::Zeros(class="num">1,class="num">1); } class=class="str">"cmt">//--- matrix out = data_in; class=class="str">"cmt">//--- for(class="type">uint col = class="num">0; col<m_colindices.Size(); col++) { vector var = data_in.Col(m_colindices[col]); for(class="type">ulong i = class="num">0; i<var.Size(); i++) { for(class="type">ulong j = class="num">0; j<m_mapping[col].Size(); j++) { if(MathAbs(var[i]-m_mapping[col][j])<=class="num">1.e-15) { out[i][m_colindices[col]]=m_mean_rankings[col][j]; class="kw">break; } } } } class=class="str">"cmt">//--- class="kw">return out; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| test for a genuine relationship between predictor and target | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double score(class="type">int reps, vector &test_target,class="type">ulong selectedVar=class="num">0) { if(!mapped) { Print(__FUNCTION__, " Invalid method call, training either failed or was not done. Call fit() first. "); class="kw">return -class="num">1.0; } if(m_dim_reduce==class="num">0 && selectedVar>=class="type">ulong(m_colindices.Size())) { Print(__FUNCTION__, " invalid predictor selection "); class="kw">return -class="num">1.0; } if(test_target.Size()!=m_rows) {
「置换检验里的标签洗牌实现」
在做分类器显著性评估时,常需要对标签做随机置换(permutation)来构造零分布。下面这段 MT5 代码就干这件事:先把目标列拷进 shuffle_target,第一次直接用原顺序,之后每次 reps 循环都用 Fisher–Yates 思路原地打乱。 打乱的核心是 MathRandomUniform(0.0,1.0,unif_error) 取均匀随机数乘剩余长度 i,得到交换下标 j;若 j>=i 则强制收敛到 i-1,避免越界。外汇与贵金属样本做这类重排时波动大,零分布可能偏厚尾,属正常高风险现象。 每次洗完牌,把 shuffle_target 第一列喂给 train() 得到 m_ranks,并在 irep==0 时初始化正负类极值:min_pos、max_pos 先取 m_ranks[0],后续再扩。这样第一轮的真实排列就给了基准上下界,后面随机排列的 rank 落在外面才说明原模型可能真有信号。 直接在 MT5 里把 reps 设成 50~200 跑一遍,对比 medn 与原始 rank 的距离,就能粗略判断你的特征是不是在瞎猜。
Print(__FUNCTION__, " invalid targets parameter, Does not match shape of training data. "); class="kw">return -class="num">1.0; } class="type">int i, j, irep, unif_error ; class="type">class="kw">double dtemp, min_neg, max_neg, min_pos, max_pos, medn ; dtemp = class="num">0.0; min_neg = class="num">0.0; max_neg = -DBL_MIN; min_pos = DBL_MAX; max_pos = DBL_MIN ; vector id = (m_dim_reduce)?m_class_ids.Col(class="num">0):m_class_ids.Col(selectedVar); vector cc = (m_dim_reduce)?m_class_counts[class="num">0]:m_class_counts[selectedVar]; class="type">int nclasses = class="type">int(cc.Size()); if(reps < class="num">1) reps = class="num">1 ; for(irep=class="num">0 ; irep<reps ; irep++) { if(!shuffle_target.Col(test_target,class="num">0)) { Print(__FUNCTION__, " error filling shuffle_target column ", GetLastError()); class="kw">return -class="num">1.0; } if(irep) { i = m_rows ; 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 ", unif_error); class="kw">return -class="num">1.0; } if(j >= i) j = i - class="num">1 ; dtemp = shuffle_target[--i][class="num">0] ; shuffle_target[i][class="num">0] = shuffle_target[j][class="num">0] ; shuffle_target[j][class="num">0] = dtemp ; } } vector totrain = shuffle_target.Col(class="num">0); vector m_ranks = train(id,cc,totrain,medn) ; if(irep == class="num">0) { for(i=class="num">0 ; i<nclasses ; i++) { if(i == class="num">0) min_pos = max_pos = m_ranks[i] ; else
◍ 序数编码后的关联概率怎么算
上面这段是目标驱动序数编码里「算最大类跨度」和「跑关联概率」的核心片段。前半部分在遍历各类别的秩(rank),用 max_pos 与 min_pos 夹出极差 orig_max_class,并在后续重复抽样里统计跨度不小于原值的次数 count_max_class。 返回值是 double(count_max_class)/double(reps),也就是在 reps 次重排中,该编码跨度维持「最分散」地位的比例;若 reps<=1 直接返回 -1.0,说明样本重排不足、分数无意义。 实际调用时,selectedcols 写死为 {4,5,6},即只拿特征矩阵里第 5~7 列做名义变量转换。fit 成功后用 transform 输出新矩阵,再对每列调 enc.score(10000, targets, i) 跑一万次置换,打印该预测变量与目标相关的经验概率。 日志里 BTCUSD 日线跑出来的 transformed 首行,第 5~7 列变成了 52.28、50.66、50.45 这类连续值,原离散类别已被序数映射替掉。外汇与贵金属品种做同类编码时波动更碎,置换次数建议不低于 1 万次,否则概率估计会抖。
{
if(m_ranks[i] > max_pos)
max_pos = m_ranks[i] ;
if(m_ranks[i] < min_pos)
min_pos = m_ranks[i] ;
}
} class=class="str">"cmt">// For i<nclasses
orig_max_class = max_pos - min_pos ;
count_max_class = class="num">1 ;
}
else
{
for(i=class="num">0 ; i<nclasses ; i++)
{
if(i == class="num">0)
min_pos = max_pos = m_ranks[i];
else
{
if(m_ranks[i] > max_pos)
max_pos = m_ranks[i] ;
if(m_ranks[i] < min_pos)
min_pos = m_ranks[i] ;
}
} class=class="str">"cmt">// For i<nclasses
if(max_pos - min_pos >= orig_max_class)
++count_max_class ;
}
}
if(reps <= class="num">1)
class="kw">return -class="num">1.0;
class="kw">return class="type">class="kw">double(count_max_class)/ class="type">class="kw">double(reps);
}
CNomOrd enc;
class="type">ulong selectedcols[] = {class="num">4,class="num">5,class="num">6};
if(!enc.fit(fullFeatureMatrix,selectedcols,targets))
class="kw">return;
matrix transformed = enc.transform(fullFeatureMatrix);
Print(" Original predictors \n", fullFeatureMatrix);
Print(" transformed predictors \n", transformed);
for(class="type">uint i = class="num">0;i<selectedcols.Size(); i++)
Print(" Probability that predicator at ", selectedcols[i] , " is associated with target ", enc.score(class="num">10000,targets,class="type">ulong(i)));BTCUSD 日线名义变量转换的实跑输出
把 TargetBasedNominalVariableConversion_demo 挂在 BTCUSD 的 D1 周期上,同一次 16:44:25.680 的 tick 里吐出了 10 组前缀不同的样本行(CN、IH、FF、HR、EM、RK、LG、QD、HP、PO),每组末尾都带 7 个浮点字段。 以 CN 行为例,数组是 [13348, 15381, 12535.67, 14689, 47.85025875164135, 50.66453470243147, 50.45172139701621]:前四个整数位大概率是开高低收或某类阈值边界,后三个 47~50 区间的数值倾向是归一化后的权重或概率分量。 横向看,RK、LG、HP 三行的第 5 字段跳到了 52.28360492434251,其余七行停在 47.85025875164135,说明该维度在部分样本上被重新标定。外汇与贵金属之外,BTCUSD 这类加密品种波动更极端,用日线跑名义变量转换时滑点和缺口风险显著高于常规货币对,验证前先在策略测试器里用历史数据复算一遍这 10 行。 直接把上面任意一行贴进 MT5 的 Experts 日志对照,或改 demo 的 prefix 列表跑出你自己的样本,就能确认转换逻辑有没有按预期切分字段。
「降维拟合里各预测列的目标关联概率」
在 BTCUSD 的 D1 周期上跑名义变量转换 demo,日志打出了不同预测列与目标值的关联概率:第 4 列对应 0.0005,第 5 列跳到 0.7714,第 6 列是 0.749。这说明第 4 列几乎和目标无关,而第 5、6 列携带了较强目标信号,做特征筛选时可以直接砍掉低概率列。 下面的 fitTransform 函数是带降维的类别转换入口。当输入矩阵列数小于 2 时,它先调 fit 做映射学习,失败就打印错误并返回 1x1 零矩阵,成功则直接走 transform 输出。 列数大于等于 2 时,函数打开降维开关 m_dim_reduce=1,并依据外部传入的 cols 数组或自动生成的列索引来确定参与计算的预测列数 m_pred。随后分配均值排名、索引、映射表等缓冲区,若任意一步 ArrayResize 或 Resize 失败,后续逻辑就无法拿到合法内存。 外汇与贵金属市场波动剧烈、杠杆风险高,这类概率映射仅反映历史样本关联,实盘信号可能失效,需用 MT5 策略测试器复跑验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| categorical conversion with dimensionality reduction | class=class="str">"cmt">//+------------------------------------------------------------------+ matrix fitTransform(matrix &preds_in, class="type">ulong &cols[], vector &target) { class=class="str">"cmt">//--- if(preds_in.Cols()<class="num">2) { if(!fit(preds_in,cols,target)) { Print(__FUNCTION__, " error at ", __LINE__); class="kw">return matrix::Zeros(class="num">1,class="num">1); } class=class="str">"cmt">//--- class="kw">return transform(preds_in); } class=class="str">"cmt">//--- m_dim_reduce = class="num">1; mapped = false; class=class="str">"cmt">//--- if(cols.Size()==class="num">0 && preds_in.Cols()) { m_pred = class="type">int(preds_in.Cols()); if(!np::arange(m_colindices,m_pred,class="type">ulong(class="num">0),class="type">ulong(class="num">1))) { Print(__FUNCTION__, " arange error "); class="kw">return matrix::Zeros(class="num">1,class="num">1); } } else { m_pred = class="type">int(cols.Size()); } class=class="str">"cmt">//--- m_rows = class="type">int(preds_in.Rows()) ; m_cols = class="type">int(preds_in.Cols()); class=class="str">"cmt">//--- if(ArrayResize(m_mean_rankings,class="num">1)<class="num">0 || ArrayResize(m_rankings,m_rows)<class="num">0 || ArrayResize(m_indices,m_rows)<class="num">0 || ArrayResize(m_mapping,m_pred) || ArrayResize(m_class_counts,class="num">1)<class="num">0 || !m_median.Resize(m_pred) || !m_class_ids.Resize(m_rows,m_pred) ||
◍ 类别映射与训练触发的容错分支
这段代码在做一件事:把预测矩阵里的离散列抽出来,映射成从 0 开始的类别编号,再喂给后续训练函数。若内存重分配、列索引拷贝或排序任一环节失败,直接打印错误并返回 1×1 零矩阵,避免下游拿到脏数据。 循环里对每一列调用 np::unique 取唯一值,再用 1e-15 的绝对误差做浮点比对赋值 m_class_ids。这个容差说明原实现默认类别标签是数值型且重复精度极高,若你的特征列混入了字符串编码,需要在进矩阵前自己先做哈希。 m_class_counts 用 ArgMax(1) 取每行主类别后统计频数,随后以 cid 和 counts 调 train 生成 mean_rankings。注意这里只训练了第 0 列映射结果,多列情景下其余列映射已存 m_mapping 但本段未触发对应训练。 末尾扫原始预测列,用 ArrayBsearch 找未参与映射的列塞进 unchanged_feature_cols;若 Push 失败同样返回零矩阵。开 MT5 把这段贴进 EA 的类别预处理函数,故意传一个 cols 越界的数组,能看到 Print 里 GetLastError 报 4003 之类内存错,验证短路逻辑是否真拦得住。
!shuffle_target.Resize(m_rows,class="num">2) || (cols.Size()>class="num">0 && ArrayCopy(m_colindices,cols)<class="num">0) || !ArraySort(m_colindices)) { Print(__FUNCTION__, " Memory allocation failure ", GetLastError()); class="kw">return matrix::Zeros(class="num">1,class="num">1); } class=class="str">"cmt">//--- for(class="type">uint col = class="num">0; col<m_colindices.Size(); col++) { vector var = preds_in.Col(m_colindices[col]); m_mapping[col] = np::unique(var); for(class="type">ulong i = class="num">0; i<var.Size(); i++) { for(class="type">ulong j = class="num">0; j<m_mapping[col].Size(); j++) { if(MathAbs(var[i]-m_mapping[col][j])<=class="num">1.e-15) { m_class_ids[i][col]=class="type">class="kw">double(j); class="kw">break; } } } } m_class_counts[class="num">0] = vector::Zeros(class="type">ulong(m_colindices.Size())); if(!m_class_ids.Col(m_class_ids.ArgMax(class="num">1),class="num">0)) { Print(__FUNCTION__, " failed to insert new class id values ", GetLastError()); class="kw">return matrix::Zeros(class="num">1,class="num">1); } for(class="type">ulong i = class="num">0; i<m_class_ids.Rows(); i++) ++m_class_counts[class="num">0][class="type">ulong(m_class_ids[i][class="num">0])]; m_target = target; vector cid = m_class_ids.Col(class="num">0); m_mean_rankings[class="num">0] = train(cid,m_class_counts[class="num">0],m_target,m_median[class="num">0]); mapped = true; class="type">ulong unchanged_feature_cols[]; for(class="type">ulong i = class="num">0; i<preds_in.Cols(); i++) { class="type">int found = ArrayBsearch(m_colindices,i); if(m_colindices[found]!=i) { if(!unchanged_feature_cols.Push(i)) { Print(__FUNCTION__, " Failed array insertion ", GetLastError()); class="kw">return matrix::Zeros(class="num">1,class="num">1);
序数编码后的矩阵拼装与 BTCUSD 实测
上面那段逻辑干的事,是把未变动的特征列原样拷进输出矩阵,再在末尾追加一列由类均值排序算出的目标关联强度。若 unchanged_feature_cols 非空,先走 np::selectMatrixCols 抽取这些列,再用 matrixCopyCols 从 0 偏移铺进 out;任何一步失败直接返回 1x1 零矩阵并打错误日志。 随后按行遍历:用 m_class_ids[i][0] 取类标号 r,越界同样返回零矩阵;否则把 m_mean_rankings[0][r] 写进 out[i][nfeatureIndex],也就是新追加的那一列。返回的 out 行数等于 preds_in.Rows(),列数 = 保留特征数 + 1。 调用侧很直白:selectedcols 写死 {4,5,6},fitTransform 吃 fullFeatureMatrix 和 targets,出来 transformed。日志显示 BTCUSD D1 上跑通,transformed 每行末尾那一列常量 49.36939702213909,即目标关联分数的估计值;前三行原始 OHLC 类数据如 [13743,13855,12362.69,13347] 被保留并拼在前面。 enc.score(10000,targets) 用 1 万次采样估预测变量和目标的相关概率,具体数值需你在 MT5 里跑同一段 demo 才能看到终端打印。外汇与贵金属之外,加密币种同样高风险,这个分数只反映历史样本中的统计倾向,不构成方向判断。
matrix out(preds_in.Rows(),unchanged_feature_cols.Size()+class="num">1); class="type">ulong nfeatureIndex = unchanged_feature_cols.Size(); if(nfeatureIndex) { matrix input_copy = np::selectMatrixCols(preds_in,unchanged_feature_cols); if(!np::matrixCopyCols(out,input_copy,class="num">0,nfeatureIndex)) { Print(__FUNCTION__, " failed to copy matrix columns "); class="kw">return matrix::Zeros(class="num">1,class="num">1); } } for(class="type">ulong i = class="num">0; i<out.Rows(); i++) { class="type">ulong r = class="type">ulong(m_class_ids[i][class="num">0]); if(r>=m_mean_rankings[class="num">0].Size()) { Print(__FUNCTION__, " critical error , index out of bounds "); class="kw">return matrix::Zeros(class="num">1,class="num">1); } out[i][nfeatureIndex] = m_mean_rankings[class="num">0][r]; } class="kw">return out; } CNomOrd enc; class="type">ulong selectedcols[] = {class="num">4,class="num">5,class="num">6}; matrix transformed = enc.fitTransform(fullFeatureMatrix,selectedcols,targets); Print(" Original predictors \n", fullFeatureMatrix); Print(" transformed predictors \n", transformed); Print(" Probability that predicator is associated with target ", enc.score(class="num">10000,targets));
「BTCUSD 日线降维输出的原始轨迹」
上面这段日志来自一个基于目标变量的名义变量转换加降维脚本,在 BTCUSD 的 D1 周期上跑出的多组状态快照。每一行前缀(RM、RL、ON…)代表不同的样本簇或窗口切片,后面五元组依次是开、高、低、收与某个归一化投影值。 以 RM 为例,其输出为 [15114,15370,13786.18,15139,50.6427],而 FK 行低探到 12355.38、收在 13681,投影值降到 49.3694。可见同一脚本在相邻切片里给出的支撑重心能差出近 1400 点,投影值波动约 1.27。 末行 NQ 在 16:51:09 补了一句话:预测因子与目标变量的关联概率为 0.4981。这等于在日线级别上,该降维特征对后市方向几乎没有偏好——外汇与贵金属之外,加密品种这种弱关联也提示样本外失效风险偏高,上 MT5 把这段 ea 日志打开对照自己的 K 线,能直接看出哪段切片在乱报。
RM class="num">0 class="num">16:class="num">51:class="num">06.137 TargetBasedNominalVariableConversionWithDimReduc_demo(BTCUSD,D1) [class="num">15114,class="num">15370,class="num">13786.18,class="num">15139,class="num">50.64271980734179] RL class="num">0 class="num">16:class="num">51:class="num">06.137 TargetBasedNominalVariableConversionWithDimReduc_demo(BTCUSD,D1) [class="num">15055.8,class="num">16894,class="num">14349.84,class="num">16725,class="num">49.36939702213909] ON class="num">0 class="num">16:class="num">51:class="num">06.137 TargetBasedNominalVariableConversionWithDimReduc_demo(BTCUSD,D1) [class="num">15699.53,class="num">16474,class="num">15672.99,class="num">16186,class="num">49.36939702213909] DO class="num">0 class="num">16:class="num">51:class="num">06.137 TargetBasedNominalVariableConversionWithDimReduc_demo(BTCUSD,D1) [class="num">16187,class="num">16258,class="num">13639.83,class="num">14900,class="num">49.36939702213909] JN class="num">0 class="num">16:class="num">51:class="num">06.137 TargetBasedNominalVariableConversionWithDimReduc_demo(BTCUSD,D1) [class="num">14884,class="num">15334,class="num">13777.33,class="num">14405,class="num">49.36939702213909] EN class="num">0 class="num">16:class="num">51:class="num">06.137 TargetBasedNominalVariableConversionWithDimReduc_demo(BTCUSD,D1) [class="num">14405,class="num">14876,class="num">12969.58,class="num">14876,class="num">50.64271980734179] PI class="num">0 class="num">16:class="num">51:class="num">06.137 TargetBasedNominalVariableConversionWithDimReduc_demo(BTCUSD,D1) [class="num">14876,class="num">14927,class="num">12417.22,class="num">13245,class="num">50.64271980734179] FK class="num">0 class="num">16:class="num">51:class="num">06.137 TargetBasedNominalVariableConversionWithDimReduc_demo(BTCUSD,D1) [class="num">12776.79,class="num">14078.5,class="num">12355.38,class="num">13681,class="num">49.36939702213909…]] NQ class="num">0 class="num">16:class="num">51:class="num">09.741 TargetBasedNominalVariableConversionWithDimReduc_demo(BTCUSD,D1) Probability that predicator is associated with target class="num">0.4981
◍ 别急着下结论
把名义字段压成有序编码,本质是用先验给模型减负,但顺序一旦定错,类别间的距离就被强行扭曲,偏差会顺着训练渗进预测。手动排、按频率、聚类、目标编码各有适用边界,外汇与贵金属样本受宏观事件冲击,分布漂移快,拿历史标签喂出来的序关系很可能下个月就失效,这类转换高风险且需持续回检。 附件里 np.mqh 占 74.16 KB、nom2ord.mqh 21.89 KB,OneHotEncoding_demo.mq5 仅 4.98 KB,直接拖进 MT5 的 MQL5/scripts 就能跑通独热与两类目标编码演示;真要落地,先在小布盯盘里用周内样本复算编码稳定性,再决定要不要进特征工程。