名义变量的序数编码:给机器学习喂料前先搞懂类别怎么转数字(基础篇)
(1/3)·很多交易者把K线类型随手标成1、2、3,却不知道算法正偷偷脑补出并不存在的等级
给名义变量做序数编码的坑
在 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 用历史数据复算一遍标签逻辑。
# 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,其余留待下一段。外汇与贵金属波动大,样本外表现可能明显逊于样本内,实盘前务必自测。 |
|---|
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 看样本分布变化,外汇与贵金属波动大,这类标签仅作概率参考,实盘高风险。
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 就能跑。 当某个名义字段和目标变量共享信息量大时,提测量级别通常划算。即便原值没数值含义,也能按它跟目标的相关性人工编个序数值。实际没必要硬拉到和目标同级别,序数化已够用,能在保信息与抑噪声间取平衡。 下篇会拆具体转换手法与防数据失真要点,这一节先记住:线性模型前先问一句,这列标签序数化没有。