名义变量的序数编码·综合运用
🔢

名义变量的序数编码·综合运用

(3/3)·从Python到MQL5双实现,拆解13种编码路径如何避免算法误读金融形态

案例拆解 第 3/3 篇

给针形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。这种稀疏分布如果直接喂给回归模型,可能偏向多数类,做特征选择时建议先统计各维度的频数再决定降维。 外汇与贵金属品种走这种编码流程时波动更剧烈,高风险特征本就明显,上实盘前务必用历史样本回测确认维度膨胀没有拖慢推理速度。

MQL5 / C++
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 的索引序列,失败则打印错误并返回未映射状态。

MQL5 / C++
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 返回。整个流程跑通后,名义特征就完成了向序数空间的转换,贵金属与外汇数据的高维类别字段可据此降噪,但汇率波动高风险仍在,映射不代表方向确定性。

MQL5 / C++
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,这等于强制你检验样本和训练样本同维度,外汇与贵金属行情里样本错位会直接污染显著性结论,属高风险操作。

MQL5 / C++
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 的距离,就能粗略判断你的特征是不是在瞎猜。

MQL5 / C++
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 万次,否则概率估计会抖。

MQL5 / C++
{
         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 策略测试器复跑验证。

MQL5 / C++
  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 之类内存错,验证短路逻辑是否真拦得住。

MQL5 / C++
  !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 才能看到终端打印。外汇与贵金属之外,加密币种同样高风险,这个分数只反映历史样本中的统计倾向,不构成方向判断。

MQL5 / C++
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 线,能直接看出哪段切片在乱报。

MQL5 / C++
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 就能跑通独热与两类目标编码演示;真要落地,先在小布盯盘里用周内样本复算编码稳定性,再决定要不要进特征工程。

让小布替你跑这套
把K线形态识别和序数映射的重复劳动交给小布盯盘,你专注决策;多品种编码一致性校验这类脏活,AIGC面板里点一下就能批量出。

常见问题

星期本身无内在顺序,任意整数会让线性模型误判周五相对周一有数值上的"更高层级",从而学到虚假周期,预测倾向失真。
MQL5通常用整数区分类别且无原生分类类型,需自行保证映射表与训练端一致,否则实盘推断会和回测分布偏移。
可以,小布内置了常见价格行为形态的标注与编码模板,切换品种时自动复用同一套映射,减少手工出错。
用分位数切分比固定阈值更抗波动率变化,样本外鲁棒性可能更好,但需防范极端行情下分位坍缩。
若后续接线性模型或神经网络,建议对序数特征做缩放,不然不同编码区间会主导梯度更新,外汇贵金属属高风险,结论仅作概率参考。