名义变量的序数编码:给机器学习喂料前先搞懂类别怎么转数字(基础篇)
🔢

名义变量的序数编码:给机器学习喂料前先搞懂类别怎么转数字(基础篇)

(1/3)·很多交易者把K线类型随手标成1、2、3,却不知道算法正偷偷脑补出并不存在的等级

偏理论 第 1/3 篇
给多头K线标1、空头标0看起来无伤大雅,但线性模型会顺着数字暗自假设「1比0更优」。名义变量本无顺序,硬编成序数可能让模型学歪。先弄清哪类数据该转、哪类转了反而添乱,比急着写代码更重要。

给名义变量做序数编码的坑

在 MT5 的策略回测与特征工程里,名义变量(如品种名、周期标签、信号来源)不能直接喂给多数统计与机器学习模块,需要先转成整数索引。很多新手用 0、1、2 顺手编号,却忽略了这种序数编码隐含了“大小关系”——模型可能误以为 EURUSD 比 XAUUSD“大”。 一个可验证的现象:在 2025-07-22 发布的示例工程中,作者 Francis Dube 对 454 个样本做名义变量映射,若用连续整数编码且未做独热展开,部分回归类指标在跨品种推断时会出现伪排序偏差。打开 MT5 把同一套特征分别用整数序数和独热向量跑一遍,能直接看到拟合残差分布不同。 实操上,若你的变量本质无序,优先用独热或哈希编码;只有当顺序本身有交易含义(比如趋势强度分级)才保留序数。外汇与贵金属波动剧烈、滑点随机,这类编码错误不会让你爆仓,但会让 AIGC 辅助出的信号可信度悄悄打折。

「名义变量为何要先转序数」

做分类建模时,名义变量(如品种名、时段标签)本身带信息,但 MT5 里大多数数值型算法不吃字符串。直接喂进去会报错或静默丢弃特征,所以得先转成序数编码。 这一步的核心不是随便编个号,而是看后续算法是否对编码大小敏感。树模型通常不敏感,线性类或距离类模型会被错误序关系带偏。 本文主线用 Python 演示多种赋值技术,但落地到 EA 侧,我们会用纯 MQL5 实现两种通用转换,方便你在策略里直接调。外汇与贵金属市场波动剧烈、杠杆风险高,任何特征工程都只是提高概率,不保证方向。

◍ 名义与序数分类在K线特征里的坑

名义变量只是贴标签,类别之间本无高下。把K线分成针形、纺锤、锤子,或按星期几归类,都属于这一类——多头K线不比空头K线更「高级」,周一也不天然优于周二。若直接给这些类别塞任意整数喂给机器学习,算法可能误读成「数值大=等级高」,把定性信息扭曲成虚假顺序。 序数变量则自带层级,例如趋势强度分强、中、弱,波动率分高、低,这种顺序本身有交易含义。做特征工程时,混用两类编码会直接污染模型输入。 以比特币日线为例,可拉 2017-2023 年 BTCUSD 的 D1 数据,生成三个名义变量预测次日收益:bar_type 分 bullish/bearish;body_type 按实体占全影线比切四档(>=0.75、>=0.5<0.75、>=0.25<0.5、<0.25);bar_pattern 看相邻两根是否共牛且高低点上移,标 higherHigh,反向标 lowerLow,其余归第三类。下面这段 Python 先抓 MT5 历史,再打标签,注意类别先用文字、后续 MQL5 才转整数。 外汇与贵金属品种同样存在此类编码误用风险,高杠杆下特征失真可能放大回撤,实盘前务必在 MT5 用历史数据复算一遍标签逻辑。

MQL5 / C++
# Copyright class="num">2024, MetaQuotes Ltd.
# [MQL5官方文档]
# imports
from class="type">class="kw">datetime class="kw">import class="type">class="kw">datetime
class="kw">import MetaTrader5 as mt5
class="kw">import pandas as pd
class="kw">import numpy  as np
class="kw">import pytz
class="kw">import os
from category_encoders class="kw">import OrdinalEncoder, OneHotEncoder, BinaryEncoder,TargetEncoder, CountEncoder, HashingEncoder, LeaveOneOutEncoder,JamesSteinEncoder
if not mt5.initialize():
    print("initialize() failed ")
    mt5.shutdown()
    exit()
class="macro">#set up timezone infomation  
tz=pytz.timezone("Etc/UTC")
class="macro">#use time zone to set correct date for history data extraction
startdate = class="type">class="kw">datetime(class="num">2023,class="num">12,class="num">31,hour=class="num">23,minute=class="num">59,second=class="num">59,tzinfo=tz)
stopdate = class="type">class="kw">datetime(class="num">2017,class="num">12,class="num">31,hour=class="num">23,minute=class="num">59,second=class="num">59,tzinfo=tz)
class="macro">#list the symbol
symbol = "BTCUSD"
class="macro">#get price history
prices = pd.DataFrame(mt5.copy_rates_range(symbol,mt5.TIMEFRAME_D1,stopdate,startdate))
if len(prices) < class="num">1:
    print(" Error downloading rates history ")
    mt5.shutdown()
    exit()
class="macro">#shutdown mt5 tether
mt5.shutdown()
class="macro">#drop unnecessary columns
prices.drop(labels=["time","tick_volume","spread","real_volume"],axis=class="num">1,inplace=True)
class="macro">#initialize categorical features
prices["bar_type"] = np.where(prices["close"]>=prices["open"],"bullish","bearish")
prices["body_type"] = np.empty((len(prices),),dtype=&class="macro">#x27;str&class="macro">#x27;)
prices["bar_pattern"] = np.empty((len(prices),),dtype=&class="macro">#x27;str&class="macro">#x27;)
class="macro">#set feature values
for i in np.arange(len(prices)):
    bodyratio = np.abs(prices.iloc[i,class="num">3]-prices.iloc[i,class="num">0])/np.abs(prices.iloc[i,class="num">1]-prices.iloc[i,class="num">2])
    if bodyratio >= class="num">0.75:
        prices.iloc[i,class="num">5] = ">=class="num">0.75"
    elif bodyratio < class="num">0.75 and bodyratio >= class="num">0.5:
        prices.iloc[i,class="num">5]=" >=class="num">0.5<class="num">0.75"
    elif bodyratio < class="num">0.5 and bodyratio >= class="num">0.25:
        prices.iloc[i,class="num">5]=" >=class="num">0.25<class="num">0.5"
    else:
        prices.iloc[i,class="num">5]=" <class="num">0.25"
    if i < class="num">1:
        prices.iloc[i,class="num">6] = None
        class="kw">continue
    if(prices.iloc[i,class="num">4]=="bullish" and prices.iloc[i-class="num">1,class="num">4]=="bullish") and(prices.iloc[i,class="num">1]>prices.iloc[i-class="num">1,class="num">1]) and(prices.iloc[i,class="num">2]>prices.iloc[i-class="num">1,class="num">2]):
        prices.iloc[i,class="num">6] = "higherHigh"

样本切割与阴阳线特征矩阵怎么搭

先把趋势状态打标:连续两根 bearish 且当前 K 线的收盘价、最低价都低于前一根,就记为 lowerLow;其余情况归为 flat。这一步是在 pandas 里用 iloc 按列索引取值,列 4 是方向字段,列 2、1 分别是 low 与 close。 target 用对数收益率做前向差分:look_ahead=1 时,target = log(close).diff(1).shift(-1),也就是拿下一根 K 线的对数收益当标签。dropna 之后样本量会随起止日期变动,建议 print(prices.head()) 核对特征矩阵前几行,避免索引错位。 MT5 侧用 iBarShift 把训练起止日期转成 bar 下标,trainstart 和 trainstop 任一为负就直接 Print 错误并返回。size_insample = (trainstop-trainstart)+1,若 ≤0 说明日期参数填反了。predictors.Resize(size_insample,3) 建三维特征容器,CopyRates 拉 OHLC 垂直数据,失败同样打印错误退出。

实体占比 bodyratio =close-open/high-low,按 0.75 与 0.5 两道阈值离散化:≥0.75 记 0.0,0.5~0.75 记 1.0,其余留待下一段。外汇与贵金属波动大,样本外表现可能明显逊于样本内,实盘前务必自测。
MQL5 / C++
elif(prices.iloc[i,class="num">4]=="bearish" and prices.iloc[i-class="num">1,class="num">4]=="bearish") and(prices.iloc[i,class="num">2]<prices.iloc[i-class="num">1,class="num">2]) and(prices.iloc[i,class="num">1]<prices.iloc[i-class="num">1,class="num">1]):
      prices.iloc[i,class="num">6] = "lowerLow"
   else :
      prices.iloc[i,class="num">6] = "flat"
class="macro">#calculate target
look_ahead = class="num">1
prices["target"] = np.log(prices["close"])
prices["target"] = prices["target"].diff(look_ahead)
prices["target"] = prices["target"].shift(-look_ahead)
class="macro">#drop rows with NA values
prices.dropna(axis=class="num">0,inplace=True,ignore_index=True)
print("Full feature matrix \n",prices.head())
class=class="str">"cmt">//get relative shift of is and oos sets
   class="type">int trainstart,trainstop;
   trainstart=iBarShift(SetSymbol!=""?SetSymbol:NULL,tf,TrainingSampleStartDate);
   trainstop=iBarShift(SetSymbol!=""?SetSymbol:NULL,tf,TrainingSampleStopDate);
class=class="str">"cmt">//check for errors from ibarshift calls
   if(trainstart<class="num">0 || trainstop<class="num">0)
     {
      Print(ErrorDescription(GetLastError()));
      class="kw">return;
     }
class=class="str">"cmt">//---set the size of the sample sets
   size_insample=(trainstop - trainstart) + class="num">1;
class=class="str">"cmt">//---check for input errors
   if(size_insample<=class="num">0)
     {
      Print("Invalid inputs ");
      class="kw">return;
     }
class=class="str">"cmt">//---
   if(!predictors.Resize(size_insample,class="num">3))
     {
      Print("ArrayResize error ",ErrorDescription(GetLastError()));
      class="kw">return;
     }
class=class="str">"cmt">//---
   if(!prices.CopyRates(SetSymbol,tf,COPY_RATES_VERTICAL|COPY_RATES_OHLC,TrainingSampleStartDate,TrainingSampleStopDate))
     {
      Print("Copyrates error ",ErrorDescription(GetLastError()));
      class="kw">return;
     }
class=class="str">"cmt">//---
   targets = log(prices.Col(class="num">3));
   targets = np::diff(targets);
class=class="str">"cmt">//---
   class="type">class="kw">double bodyratio = class="num">0.0;
   for(class="type">ulong i = class="num">0; i<prices.Rows(); i++)
     {
      if(prices[i][class="num">3]<prices[i][class="num">0])
         predictors[i][class="num">0] = class="num">0.0;
      else
         predictors[i][class="num">0] = class="num">1.0;
      bodyratio = MathAbs(prices[i][class="num">3]-prices[i][class="num">0])/MathAbs(prices[i][class="num">1]-prices[i][class="num">2]);
      if(bodyratio >=class="num">0.75)
         predictors[i][class="num">1] = class="num">0.0;
      else
       if(bodyratio<class="num">0.75 && bodyratio>=class="num">0.5)
         predictors[i][class="num">1] = class="num">1.0;
      else

「实体占比与连续K线的特征编码」

这段逻辑在做两件事:先用实体占比 bodyratio 给单根 K 线打类别标签,再结合前一根 K 线状态生成序列特征。当 bodyratio 落在 0.25 到 0.5 之间时,predictors[i][1] 赋 2.0,否则赋 3.0,相当于把「中等实体」和「其他实体」分开。 对序列首根(i<1)直接令 predictors[i][2]=0.0 并跳过,避免越界访问前一根。之后若当前与前一根同为 predictors[0]==1.0(例如都是某类阳线),且高价、低价均抬高,则 predictors[i][2]=2.0,标记连续走强;若同为 0.0 且低价下移、高价上移,则赋 1.0;其余情况归 0.0。 末尾把 targets、prices、predictors 同步截掉首行,再拼成 fullFeatureMatrix:先拷 prices 列、再接 predictors 列,任一步失败就 Print 并 return。开 MT5 把这段塞进你的特征工程函数,改 bodyratio 阈值 0.25/0.5 看样本分布变化,外汇与贵金属波动大,这类标签仅作概率参考,实盘高风险。

MQL5 / C++
if(bodyratio<class="num">0.5 && bodyratio>=class="num">0.25)
      predictors[i][class="num">1] = class="num">2.0;
   else
      predictors[i][class="num">1] = class="num">3.0;
   if(i<class="num">1)
     {
      predictors[i][class="num">2] = class="num">0.0;
      class="kw">continue;
     }
   if(predictors[i][class="num">0]==class="num">1.0 && predictors[i-class="num">1][class="num">0]==class="num">1.0 && prices[i][class="num">1]>prices[i-class="num">1][class="num">1] && prices[i][class="num">2]>prices[i-class="num">1][class="num">2])
      predictors[i][class="num">2] = class="num">2.0;
   else
     if(predictors[i][class="num">0]==class="num">0.0 && predictors[i-class="num">1][class="num">0]==class="num">0.0 && prices[i][class="num">2]<prices[i-class="num">1][class="num">2] && prices[i][class="num">1]>prices[i-class="num">1][class="num">1])
        predictors[i][class="num">2] = class="num">1.0;
     else
        predictors[i][class="num">2] = class="num">0.0;
   }
 targets = np::sliceVector(targets,class="num">1);
 prices = np::sliceMatrixRows(prices,class="num">1,predictors.Rows()-class="num">1);
 predictors = np::sliceMatrixRows(predictors,class="num">1,predictors.Rows()-class="num">1);
 matrix fullFeatureMatrix(predictors.Rows(),predictors.Cols()+prices.Cols());
 if(!np::matrixCopyCols(fullFeatureMatrix,prices,class="num">0,prices.Cols()) ||
    !np::matrixCopyCols(fullFeatureMatrix,predictors,prices.Cols()))
   {
    Print("Failed to merge matrices");
    class="kw">return;
   }

◍ 名义转序数能给线性模型喂数值

决策树这类模型能直接啃名义变量,但逻辑回归、神经网络等线性结构只认数值输入。把名义变量抬到序数尺度,类别间就有了先后顺序,模型能多抓一层关系,学得更有效率。 外汇与贵金属行情里这类标签很多,比如把「美联储表态」分成温和、中性、鹰派三档,若直接当字符串喂给线性模型会报错;转成 0/1/2 之后 MT5 的 Python 环境或外部 NN 就能跑。 当某个名义字段和目标变量共享信息量大时,提测量级别通常划算。即便原值没数值含义,也能按它跟目标的相关性人工编个序数值。实际没必要硬拉到和目标同级别,序数化已够用,能在保信息与抑噪声间取平衡。 下篇会拆具体转换手法与防数据失真要点,这一节先记住:线性模型前先问一句,这列标签序数化没有。

让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 K 线形态被归类的分布,你只需判断要不要拿去喂模型。

常见问题

名义变量如K线类型(锤子、纺锤)无内在顺序;序数变量如趋势强度(强、中、弱)有等级。错把名义当序数编码会引入虚假层级。
逻辑回归等线性模型只接受数值输入,类别需先转成数字。若用任意整数代名义类别,模型可能误读大小关系。
当两根皆多头且当前最低最高均高于前根对应值时标为更高高点;完全相反为更低的低点;其余归第三类。
小布盯盘内置了K线形态归类与分布视图,可辅助你确认类别边界,但具体序数映射仍建议在策略里显式定义。
MQL5通常用整数区分类别而非文字名,需保证训练与推理时编码一致,避免线上线下标签错位。