克服集成ONNX(Open Neural Network Exchange )的挑战(基础篇)
「在MT5里跑ONNX模型先过的坎」
MT5 从 build 2875 起原生支持 ONNX 推理,但把训练好的模型直接丢进 EA 并不像文档写的那么顺。最常见的是输入输出张量形状不匹配:很多 PyTorch 导出的 ONNX 带动态 batch 维,而 MT5 的 OnnxRun 只接受定长一维数组,需在导出时把 batch 写死为 1。 另一个隐性坑是算子支持度。截至 2024 年 11 月,MT5 的 ONNX 运行时未实现 Resize、GridSample 等视觉类算子,纯时间序列用的 LSTM、MatMul、Relu 基本能跑通。若模型含不支持节点,终端会在专家日志里报 'unsupported operator' 且不加载模型。 实操上建议先用 Python 的 onnxruntime 做一遍数值对齐:把同一根已收盘 K 线的特征喂给原模型和 MT5 导出的权重,对比输出误差应小于 1e-5。外汇与贵金属杠杆高、滑点大,模型推理只是信号环节,实盘前务必在策略测试器用真实点差回测。
◍ MT5 里跑 ONNX 的真实门槛
ONNX 让 MT5 接住外部训练的神经网络模型,但本身只是个静态容器,只负责存模型权重与结构,不附带任何推理运行环境。想在 MQL5 里真正调用,运行端怎么搭、数据怎么喂,全得自己写。 简单的前馈网络可能让你误以为部署很轻,实际项目往往要拉时间序列、做降维、还可能同时挂多个 ONNX 模型,复杂度会陡然上升。本文聚焦三个具体坑:缩放与归一化、降维接入、时间序列预测下的部署。 外汇与贵金属市场高波动、高杠杆,用 ONNX 做信号辅助时任何模型输出都只是概率倾向,不能直接当入场指令。本系列默认你已经用 MQL5 跑通过一两次 ONNX 推理,且懂基础机器学习概念。
把缩放器搬进 MT5 前先想清楚
做特征缩放不是 Python 圈的专利,但在把训练好的模型塞进 MQL5 之前,预处理这一步最容易翻车。KNN、SVM 这类基于距离的算法,若特征尺度差几个数量级(比如一个在 2000、另一个在 4),距离计算会被大尺度特征主导,模型性能大概率劣化;梯度下降也会因梯度幅度参差而收敛更慢。 归一化(Min-Max)把值压到 0~1 或 -1~1,标准化(Z-score)用均值和标准差居中。关键陷阱是:训练时拟合出的 scaler 参数必须原样套到测试集和实盘数据上。换把“尺子”量身高,训练和推理的参照框架就断了,模型看到的是扭曲分布。 真正麻烦出在部署。Python 里 sklearn 的 scaler 存成对象很方便,但 MetaEditor 读不了那种私有序列化格式。务实路线是在 MQL5 侧自己做预处理、把 scaler 参数存下来,缩放后的数据写进 CSV,再让 Python 读去训练。下面这段 Python 演示了最小最大缩放的拟合与变换,MQL5 里你需用数组手动复刻同一公式。 别把正态当圣经:缩放只是消除量纲干扰,外汇与贵金属价格序列本身高波动、高杠杆风险,缩放不当或穿越未来数据都会让回测虚高,实盘可能直接腰斩。
from sklearn.preprocessing class="kw">import MinMaxScaler, StandardScaler class="kw">import numpy as np # Example data data = np.array([[class="num">1000, class="num">2], [class="num">500, class="num">1], [class="num">2000, class="num">4], [class="num">800, class="num">3]]) # Create a MinMaxScaler object scaler_minmax = MinMaxScaler() # Fit the scaler on the training data(learn min/max values) scaler_minmax.fit(data) # Transform the data using the fitted scaler data_scaled_minmax = scaler_minmax.transform(data) print("Original data:\n", data) print("\nMin Max Scaled data:\n", data_scaled_minmax)
「日线千根K线怎么喂给LSTM」
做模式识别的第一步,是在 MT5 日线图拉出足够多的历史 K 线。原文脚本默认收集 data_size = 10000 根,但思路上用 1000 根日线(约 4 年交易日的量)也能跑通:把每根的开高低和「收是否高于开」打包成样本,收高于开标 1(偏多信号),否则标 0(偏空信号),这就成了一个二分类问题,后面丢给 LSTM 学的是「这类形态长什么样」。 外汇和贵金属杠杆高、跳空频繁,日线信号只是概率倾向,不是方向保证。脚本里先按时间倒序取 CopyRates,再逐根填进 4 列矩阵,最后一列 y 是标签,前 3 列(或含开价的 4 列拆法视库而定)进 x 当自变量。 缩放只作用于自变量 x,不碰 y。用 StandardizationScaler 做 z-score 标准化,能让不同量纲的价格列落到相近分布,模型收敛更稳。下面这段是收集与拆分的骨架,直接挂 MT5 脚本跑就能看到 x、y 分出来。
class="macro">#include <MALE5\preprocessing.mqh> class=class="str">"cmt">//This library contains the normalization techniques for machine learning class="macro">#include <MALE5\MatrixExtend.mqh> StandardizationScaler scaler; class=class="str">"cmt">//We want to use z-normalization/standardization technique for this project class="kw">input class="type">int data_size = class="num">10000; class=class="str">"cmt">//number of bars to collect for our dataset class="type">MqlRates rates[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- vector.CopyRates is lacking we are going to copy rates in normal way ArraySetAsSeries(rates, true); if (CopyRates(Symbol(), PERIOD_D1, class="num">1, data_size, rates)<-class="num">1) { printf("Failed to collect data Err=%d",GetLastError()); class="kw">return; } matrix OHLC(data_size, class="num">4); for (class="type">int i=class="num">0;i<data_size;i++) class=class="str">"cmt">//Get OHLC values and save them to a matrix { OHLC[i][class="num">0] = rates[i].open; OHLC[i][class="num">1] = rates[i].high; OHLC[i][class="num">2] = rates[i].low; if (rates[i].close>rates[i].open) OHLC[i][class="num">3] = class="num">1; class=class="str">"cmt">//Buy signal else if (rates[i].close<rates[i].open) OHLC[i][class="num">3] = class="num">0; class=class="str">"cmt">//sell signal } class=class="str">"cmt">//--- } matrix x; vector y; MatrixExtend::XandYSplitMatrices(OHLC, x, y); class=class="str">"cmt">//WE split the data into x and y | The last column in the matrix will be assigned to the y vector class=class="str">"cmt">//--- Standardize the data x = scaler.fit_transform(x);
◍ 把训练好的缩放器落盘
做归一化之前先得把缩放器存下来,否则下次加载模型时输入分布对不上,推理就会偏。上面那段逻辑就是在训练完之后把缩放器按品种名加“-SCALER”的标识写进二进制文件,运行后会在终端目录里生成一对参数文件。 这两个文件记录了该缩放器的最小值、最大值或均值方差类参数,后面加载已训练模型时直接读回即可,不必重新拟合。 外汇与贵金属行情非平稳,存盘参数仅代表训练窗口特征,换时段使用前建议在 MT5 里核对文件时间戳与品种是否一致,高风险品种尤需手动确认。
if (!scaler.save(Symbol()+"-SCALER")) class="kw">return;
把缩放结果落盘成CSV供Python接力
矩阵拼接完还不是终点,得把缩放后的特征和目标列一起写进CSV,否则Python侧拿不到干净的训练样本。上面这段代码先调用concatenate把y信号列追加到已缩放的x矩阵后面,等价于XandYsplitMatrices的反操作。 WriteCsv以「品种名-OHLSignal.csv」命名文件,表头写死为open,high,low,signal,小数精度锁在8位,不追加写(覆盖模式)。若返回失败直接DebugBreak并退出,避免后续用空文件跑模型。 在MT5里跑这段前,确认MatrixExtend库已挂好;实盘或回测环境均适用,但外汇与贵金属波动剧烈、杠杆高风险,CSV只是中间产物,别把它当信号源直接下单。
OHLC = MatrixExtend::concatenate(x, y); class=class="str">"cmt">//We apped the y column to the scaled x matrix, this is the opposite of XandYsplitMatrices function if (!MatrixExtend::WriteCsv(Symbol()+"-OHLSignal.csv",OHLC,"open,high,low,signal",false,class="num">8)) { DebugBreak(); class="kw">return; }