基于Python和MQL5的特征工程(第二部分):价格角度·进阶篇
📐

基于Python和MQL5的特征工程(第二部分):价格角度·进阶篇

(2/3)· 从价格差除以时间差到ONNX导出,拆解斜率策略在MT5里的真实误差陷阱

新手友好 第 2/3 篇
把周末跳空直接塞进价格差除以时间差,算出来的斜率会系统性偏高。很多人拿着这条「趋势线」当确认信号,却从没想过终端根本没记录休市流逝的时间。在真金白银前,先搞清楚你的角度到底偏了多少。

「建模前的数据切分与缩放陷阱」

做价格行为建模,第一步是把样本拆成训练与测试两块。这里用 train_test_splitshuffle=False,对 MT5 导出的时间序列而言很关键——乱序会把未来价格搅进过去,回测会虚高。 缩放器只能在训练集上 fit。若对整个数据集拟合,缩放参数会悄悄把测试集的分布泄露给模型,相当于用未来信息训过去,实盘外汇/贵金属这种高波动品种上容易直接崩。 候选模型池放了 12 个:从随机森林到 MLP。输入分三套测——纯 OHLC、仅斜率角度、全量特征。交叉验证用 5 折,评分取负均方误差均值。 结果里有个硬数据点:KNeighbors 在新特征下准确率提升 20%,是当时池子里最靓的;线性回归整体仍最优,但说明特征变换空间还没挖完,可能有别的方式反而拉低误差。 别把正态当圣经:StandardScaler 假设特征近似正态,OHLC 差值分布常有厚尾,实盘前最好对缩放后的训练集跑一遍分布检查。

MQL5 / C++
from sklearn.model_selection class="kw">import train_test_split,cross_val_score
from sklearn.preprocessing class="kw">import StandardScaler
from sklearn.ensemble class="kw">import RandomForestRegressor, BaggingRegressor, GradientBoostingRegressor,AdaBoostRegressor
from sklearn.svm class="kw">import LinearSVR
from sklearn.linear_model class="kw">import LinearRegression, Ridge, Lasso, ElasticNet
from sklearn.neighbors class="kw">import KNeighborsRegressor
from sklearn.tree class="kw">import DecisionTreeRegressor
from sklearn.neural_network class="kw">import MLPRegressor
from sklearn.metrics class="kw">import mean_squared_error
class="macro">#Let&class="macro">#x27;s split our data into train test splits
train_data, test_data = train_test_split(data,test_size=class="num">0.5,shuffle=False)
class="macro">#Scale the data
scaler = StandardScaler()
scaler.fit(train_data[all_inputs])
train_scaled= pd.DataFrame(scaler.transform(train_data[all_inputs]),columns=all_inputs)
test_scaled = pd.DataFrame(scaler.transform(test_data[all_inputs]),columns=all_inputs)
class="macro">#Create a dataframe to store our accuracy in training and testing
columns = [
    "Random Forest",
    "Bagging",
    "Gradient Boosting",
    "AdaBoost",
    "Linear SVR",
    "Linear Regression",
    "Ridge",
    "Lasso",
    "Elastic Net",
    "K Neighbors",
    "Decision Tree",
    "Neural Network"
]
index = ["OHLC","Angle","All"]
accuracy = pd.DataFrame(columns=columns,index=index)
class="macro">#Store the models
models = [
  RandomForestRegressor(),
  BaggingRegressor(),
  GradientBoostingRegressor(),
  AdaBoostRegressor(),
  LinearSVR(),
  LinearRegression(),
  Ridge(),
  Lasso(),
  ElasticNet(),
  KNeighborsRegressor(),
  DecisionTreeRegressor(),
  MLPRegressor(hidden_layer_sizes=(class="num">4,class="num">6))
]
class="macro">#Cross validate the models
class="macro">#First we have to iterate over the inputs
for k in np.arange(class="num">0,len(cv_inputs)):
  current_inputs = cv_inputs[k]
  class="macro">#Then fit each model on that set of inputs
  for i in np.arange(class="num">0,len(models)):
    score = cross_val_score(models[i],train_scaled[current_inputs],train_data[target],cv=class="num">5,scoring="neg_mean_squared_error",n_jobs=-class="num">1)
    accuracy.iloc[k,i] = -score.mean()

◍ 用随机搜索给回归器挑参数

默认参数往往只是通用起点,放到具体品种节奏里未必够用。这里拿 K 近邻回归器做例子,用随机搜索在训练集上扫一遍超参空间,看能不能捞出更贴合样本的配置。 搜索跑完给出一组最优参数:weights 取 uniform、p 为 1、n_neighbors 是 10、leaf_size 设 100、algorithm 用 ball_tree。训练集上的最优得分约 0.7107(71% 上下),但这个数只说明拟合程度,不代表拿去实盘能复现。 真正要紧的是模型在遇到没见过的数据时表现如何,训练误差低反而可能藏着过拟合。外汇与贵金属波动受消息面扰动大,这类回归信号仅作概率参考,杠杆品种高风险,别直接当方向依据。 下面这段是搜索过程的代码,逐行拆一下方便你直接抄去改范围: from sklearn.model_selection import RandomizedSearchCV # 引入随机搜索交叉验证工具 model = KNeighborsRegressor(n_jobs=-1) # 建 KNN 回归器,n_jobs=-1 吃满 CPU tuner = RandomizedSearchCV(model, # 把模型丢进搜索器 { "n_neighbors": [2,3,4,5,6,7,8,9,10], # 邻居数候选 "weights": ["uniform","distance"], # 均匀或距离加权 "algorithm": ["auto","ball_tree","kd_tree","brute"], # 近邻查找算法 "leaf_size": [1,2,3,4,5,10,20,30,40,50,60,100,200,300,400,500,1000], # 树叶子尺寸 "p": [1,2] # 曼哈顿或欧氏距离 }, n_iter = 100, # 随机抽 100 组组合 n_jobs=-1, # 并行跑 cv=5 # 5 折交叉验证 ) tuner.fit(train_scaled.loc[:,all_inputs],train_data[target]) # 用缩放后的特征和目标列拟合 best_params_ # 查看最优参数组合 tuner.best_score_ # 查看交叉验证最优得分

MQL5 / C++
from sklearn.model_selection class="kw">import RandomizedSearchCV
model = KNeighborsRegressor(n_jobs=-class="num">1)
tuner = RandomizedSearchCV(model,
  {
    "n_neighbors": [class="num">2,class="num">3,class="num">4,class="num">5,class="num">6,class="num">7,class="num">8,class="num">9,class="num">10],
    "weights": ["uniform","distance"],
    "algorithm": ["auto","ball_tree","kd_tree","brute"],
    "leaf_size": [class="num">1,class="num">2,class="num">3,class="num">4,class="num">5,class="num">10,class="num">20,class="num">30,class="num">40,class="num">50,class="num">60,class="num">100,class="num">200,class="num">300,class="num">400,class="num">500,class="num">1000],
    "p": [class="num">1,class="num">2]
  },
    n_iter = class="num">100,
    n_jobs=-class="num">1,
    cv=class="num">5
)
tuner.fit(train_scaled.loc[:,all_inputs],train_data[target])
tuner.best_params_
tuner.best_score_

用默认模型当照妖镜

过拟合的本质是模型在训练集里啃下了噪声而非规律。一种低成本验证法:拿一个没做任何参数定制的默认模型,和你的自定义模型同台比武。 若自定义版连默认实例都赢不了,那基本可判定训练阶段已吞下无意义信息,泛化能力存疑。外汇与贵金属市场噪声极高,这类过拟合在实盘里会迅速失效,属典型高风险陷阱。 本例实测数值:默认模型得分 0.0009797322460441842,自定义模型 0.0009697248896608824。自定义略优,暂未落入过拟合区间,但优势极薄,仅约 1% 量级,需持续用样本外数据盯防。

MQL5 / C++
class="macro">#Testing for over fitting
model = KNeighborsRegressor(n_jobs=-class="num">1)
custom_model = KNeighborsRegressor(n_jobs=-class="num">1,weights= &class="macro">#x27;uniform&class="macro">#x27;,p=class="num">1,n_neighbors= class="num">10,leaf_size= class="num">100,algorithm=&class="macro">#x27;ball_tree&class="macro">#x27;)
model.fit(train_scaled.loc[:,all_inputs],train_data[target])
custom_model.fit(train_scaled.loc[:,all_inputs],train_data[target])
代码逐行拆解:第1行建默认K近邻回归器,n_jobs=-1表示调用全部CPU核心;第2行建自定义版,weights='uniform'等权、p=1曼哈顿距离、n_neighbors=10取十邻、leaf_size=100、algorithm='ball_tree';第3、4行分别对默认与自定义模型在缩放训练集上拟合目标列。开MT5接Python桥可复跑这段,把得分差盯成日常巡检项。

MQL5 / C++
class="macro">#Testing for over fitting
model = KNeighborsRegressor(n_jobs=-class="num">1)
custom_model = KNeighborsRegressor(n_jobs=-class="num">1,weights= &class="macro">#x27;uniform&class="macro">#x27;,p=class="num">1,n_neighbors= class="num">10,leaf_size= class="num">100,algorithm=&class="macro">#x27;ball_tree&class="macro">#x27;)
model.fit(train_scaled.loc[:,all_inputs],train_data[target])
custom_model.fit(train_scaled.loc[:,all_inputs],train_data[target])

「把训练好的模型塞进ONNX给MT5用」

想让Python里跑出来的AI模型在MQL5里复现,第一步不是直接导模型,而是先把价格特征的预处理参数固化下来。这里以USDZAR的M1数据为例,我们把所有输入列的均值和标准差分别写进两个CSV,后续MT5端做标准化时必须用同一组数值,否则推理结果会和训练时偏掉。 标准化本身很简单,就是减均值除标准差,让每列特征接近零均值单位方差。这一步在Python侧做完,模型才用skl2onnx转成ONNX——注意target_opset=12,低版本MT5的ONNX推理引擎可能认不了。 下面这段是实际导出流程的核心代码,包含均值/标准差落盘、标准化、以及模型转换保存。外汇与贵金属杠杆高、点差跳变频繁,USDZAR这种 exotic 对流动性风险更突出,模型在历史数据上表现好不代表实盘概率占优,导入后务必用MT5策略测试器先跑一遍样本外。

MQL5 / C++
data.loc[:,all_inputs].mean().to_csv("USDZAR M1 MEAN.csv")
data.loc[:,all_inputs].std().to_csv("USDZAR M1 STD.csv")
data.loc[:,all_inputs] = ((data.loc[:,all_inputs] - data.loc[:,all_inputs].mean())/ data.loc[:,all_inputs].std())
class="kw">import onnx
from skl2onnx class="kw">import convert_sklearn
from skl2onnx.common.data_types class="kw">import FloatTensorType
class="macro">#Define the input shape
initial_type = [(&class="macro">#x27;float_input&class="macro">#x27;, FloatTensorType([class="num">1, len(all_inputs)]))]
class="macro">#Fit the model on all the data we have
custom_model.fit(data.loc[:,all_inputs],data.loc[:,"Target"])
class="macro">#Convert the model to ONNX format
onnx_model = convert_sklearn(model, initial_types=initial_type,target_opset=class="num">12)
class="macro">#Save the ONNX model
onnx.save(onnx_model,"USDZAR M1 OHLC Angle.onnx")

◍ 把ONNX模型塞进EA的初始化骨架

做AI辅助交易,第一步是把训练好的ONNX模型加载进MT5的EA里,而不是在图表上手动看信号。上面这段源码用 #resourceUSDZAR M1 OHLC Angle.onnx 以字节数组 onnx_buffer[] 的形式编进程序,运行时再喂给推理接口,省掉了外部文件路径依赖。 全局变量里有两个数组直接决定了模型输入的标准化方式:mean_values[5]std_values[5],数值分别是 {18.143698, 18.145870, 18.141644, 18.143724, 0.608216}{0.112957, 0.113113, 0.112835, 0.112970, 0.580481}。这5个维度对应M1的OHLC角度特征,推理前必须用它们做减均值除标准差,否则模型输出会偏移。 仓位与风控参数写死在全局段:vol = 0.3 手、profit_target = 10(应该是报价点或小额度盈利目标,具体看品种小数点)、system_state = 0 表示初始空闲。外汇与贵金属杠杆高,0.3手在USDZAR这种宽幅品种上也可能在单根日线回调中吃掉远超10点浮盈的回撤,实盘前务必在策略测试器里改小 vol 验证。 指标句柄预留了 macd_handle 和两组快慢均线(美元指数与USDZAR各一对),但初始化函数里只加载ONNX和申请句柄,真正 CopyBuffer 抓数据是在每次新报价事件里做。你可以直接复制这段全局声明到自己的EA头部,把 onnx 文件名换成你导出的模型,就能在MT5里跑通加载环节。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Slope AI.mq5 |
class=class="str">"cmt">//|                                         Gamuchirai Zororo Ndawana |
class=class="str">"cmt">//|                                         [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Gamuchirai Zororo Ndawana"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load the ONNX files                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\Files\USDZAR M1 OHLC Angle.onnx" as  class="kw">const class="type">uchar onnx_buffer[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Libraries                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Trade\Trade.mqh>
CTrade Trade;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global variables                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
 class="type">class="kw">double mean_values[class="num">5] = {class="num">18.143698,class="num">18.145870,class="num">18.141644,class="num">18.143724,class="num">0.608216};
 class="type">class="kw">double std_values[class="num">5] = {class="num">0.112957,class="num">0.113113,class="num">0.112835,class="num">0.112970,class="num">0.580481};
 class="type">long   onnx_model;
 class="type">int    macd_handle;
 class="type">int    usd_ma_slow,usd_ma_fast;
 class="type">int    usd_zar_slow,usd_zar_fast;
 class="type">class="kw">double macd_s[],macd_m[],usd_zar_s[],usd_zar_f[],usd_s[],usd_f[];
 class="type">class="kw">double bid,ask;
 class="type">class="kw">double vol = class="num">0.3;
 class="type">class="kw">double profit_target = class="num">10;
 class="type">int    system_state = class="num">0;
 vectorf model_forecast = vectorf::Zeros(class="num">1);
class=class="str">"cmt">//+------------------------------------------------------------------+

EA 初始化与逐笔更新的骨架

把 ONNX 模型接进 MT5 专家顾问,第一步是在 OnInit 里把模型和跨品种句柄一次性加载完。代码先调 onnx_load(),失败直接返回 INIT_FAILED,避免后续空跑;随后用 iMACD 抓 EURUSD 当前周期的 12/26/9 参数柱,再分别向 USDZAR 日线和自定义 DXY_Z4 日线要 20 与 60 周期 EMA——这几条线是后面模型特征的基础。 OnDeinit 里挨个 Release 模型和指标句柄,不然切换周期或重编译会漏内存。OnTick 的逻辑很薄:先 update() 刷最新 bid/ask 与 MACD 缓冲,再 model_predict() 取模型输出;无持仓就 find_entry(),有持仓就 manage_positions(),把进出场决策完全拆到别的函数里。 update() 内部用 SymbolInfoDouble 取当前品种买卖价,CopyBuffer(macd_handle,0,0,1,macd_m) 只拉最新一根 MACD 主线值。实盘里若 USDZAR 或 DXY_Z4 报价中断,iMA 返回的句柄会是 INVALID_HANDLE,EA 不会崩但特征会 stale,建议在 update 里加一句句柄有效性检查再喂模型。外汇与贵金属杠杆高,这类跨品种 ONNX 策略回测顺滑不代表 live 能复现,先开 MT5 用策略测试器跑一周 tick 级验证再上资金。

MQL5 / C++
class=class="str">"cmt">//| Expert initialization function                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Load the ONNX file
   if(!onnx_load())
     {
class=class="str">"cmt">//--- We failed to load the ONNX file
       class="kw">return(INIT_FAILED);
     }
class=class="str">"cmt">//--- Load the MACD Indicator
   macd_handle  = iMACD("EURUSD",PERIOD_CURRENT,class="num">12,class="num">26,class="num">9,PRICE_CLOSE);
   usd_zar_fast = iMA("USDZAR",PERIOD_D1,class="num">20,class="num">0,MODE_EMA,PRICE_CLOSE);
   usd_zar_slow = iMA("USDZAR",PERIOD_D1,class="num">60,class="num">0,MODE_EMA,PRICE_CLOSE);
   usd_ma_fast  = iMA("DXY_Z4",PERIOD_D1,class="num">20,class="num">0,MODE_EMA,PRICE_CLOSE);
   usd_ma_slow  = iMA("DXY_Z4",PERIOD_D1,class="num">60,class="num">0,MODE_EMA,PRICE_CLOSE);
class=class="str">"cmt">//--- Everything went fine
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//--- Release the handles don&class="macro">#x27;t need
   OnnxRelease(onnx_model);
   IndicatorRelease(macd_handle);
   IndicatorRelease(usd_zar_fast);
   IndicatorRelease(usd_zar_slow);
   IndicatorRelease(usd_ma_fast);
   IndicatorRelease(usd_ma_slow);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//--- Update our market data
   update();
class=class="str">"cmt">//--- Get a prediction from our model
   model_predict();
   if(PositionsTotal() == class="num">0)
     {
       find_entry();
     }
   if(PositionsTotal() > class="num">0)
     {
       manage_positions();
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update our market data                                                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void update(class="type">void)
  {
   bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
   ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
   CopyBuffer(macd_handle,class="num">0,class="num">0,class="num">1,macd_m);
让小布替你跑这套斜率诊断
这些关于斜率偏差与过拟合的检测,小布盯盘的 AIGC 已内置,打开对应品种页即可看到不同模型架构下的角度特征表现,你只需专注策略取舍。

常见问题

因为终端不记录周末等休市时间,若价格跳空而时间差被压缩,分母变小导致系数估计过高,需手动推断缺失时间。
趋势线本身主观,简单斜率模型对时间表示极敏感,同一数据换种时间处理就得出不同方向,确认信号并不可靠。
通常先检测过拟合,再导出到ONNX格式,最后在MQL5中构建EA调用该模型做实时推理。
可以,小布内置了多模型架构下的角度特征诊断视图,省去你本地跑脚本比对误差的重复劳动。
模型对输入与目标关系的表达敏感度不同,有的线性架构吃斜率尺度,树模型更吃分桶,需按架构选转换。