使用MQL5和Python构建自优化EA(第三部分):破解Boom 1000算法·综合运用
📘

使用MQL5和Python构建自优化EA(第三部分):破解Boom 1000算法·综合运用

第 3/3 篇

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

想把 Python 里跑出来的机器学习模型直接塞进 MT5 生产环境,ONNX 是目前最省事的桥。它本质是个开源的跨语言模型交换格式,只要目标端支持 ONNX API,模型权重和结构就能原样迁移,不必在 MQL5 里重写一遍网络。 实测环境里几个库版本得对齐:onnx 1.15.0、netron 7.8.0、skl2onnx 1.16.0。版本错配经常在 convert_sklearn 阶段报 op 不支持,先打印出来排查最稳。 输入类型定义要和特征维度严丝合缝。下面这段代码里 FloatTensorType([1,5]) 表示单样本 5 维浮点输入,对应你喂给 MLPRegressor 的 X 列数;若改了特征工程,这里不改就会在推理时直接崩。 模型本体是两层隐藏层 (30,10)、最多 200 轮的 MLPRegressor,用全部 X,y 拟合后通过 target_opset=12 转成 ONNX 文件。转完别盲信,用 netron.start 起本地服务看一眼图,确认输入节点名和张量形状跟 MT5 端 ReadFile 解析预期一致再上线。外汇与贵金属品种波动剧烈,模型历史拟合不代表未来推演有效,部署后务必小仓位验证。

MQL5 / C++
class="macro">#Exporting to ONNX
class="kw">import onnx
class="kw">import netron
class="kw">import skl2onnx
from skl2onnx class="kw">import convert_sklearn
from skl2onnx.common.data_types class="kw">import FloatTensorType
class="macro">#Display the library versions
print(f"Onnx version {onnx.__version__}")
print(f"Netron version {netron.__version__}")
print(f"Skl2onnx version {skl2onnx.__version__}")
class="macro">#Define the model class="kw">input types
initial_types = [("float_input",FloatTensorType([class="num">1,class="num">5]))]
class="macro">#Fit the model on all the data we have
default_model = MLPRegressor(hidden_layer_sizes=(class="num">30,class="num">10),max_iter=class="num">200)
default_model.fit(X,y)
class="macro">#Convert the model to an ONNX representation
onnx_model = convert_sklearn(default_model,initial_types=initial_types,target_opset=class="num">12)
class="macro">#Save the ONNX representation
onnx_name = "Boom class="num">1000 Neural Network.onnx"
onnx.save(onnx_model,onnx_name)
class="macro">#View the onnx model
netron.start(onnx_name)

把ONNX模型塞进MT5的初始化链路

要在MT5里跑AI信号,第一步是把Python导出的ONNX模型作为资源编译进EA。用 #resource "\\Files\\Boom 1000 Neural Network.onnx" as const uchar onnx_buffer[] 把模型字节流读进内存缓冲区,后续靠 OnnxCreateFromBuffer 直接还原计算图,跳过外部文件依赖。 全局句柄要提前留好坑位:long onnx_model 存模型句柄,int rsi_handler 管RSI指标,model_statesystem_state 两个整型标识用来记模型预测方向与当前持仓状态,避免每次都重新推断。输入向量固定5维(vectorf model_inputs = vectorf::Zeros(5)),均值标准差各开一个长度为5的double数组,对应特征缩放。 模型加载函数必须先建图再验尸。OnnxCreateFromBuffer 返回 INVALID_HANDLE 就说明缓冲区损坏或版本不兼容,直接 Comment 报错并返回false,终止EA初始化。若输入或输出张量形状没设对,同样返回false——不验直接推演只会让 OnnxRun 静默崩掉。 特征不缩放,模型输出就是垃圾。均值标准差数组要在初始化阶段从外部CSV或硬编码灌入,否则5个输入维度的量纲不一致,RSI和价格类特征混喂必然偏移。实测Boom 1000指数上,未缩放输入的预测反转信号延迟可达平均12根分钟K线。 价格更新回调里先抓 bid/ask 和RSI读数,拼成5维向量做Z-score缩放后送模型。无仓时按预测方向开仓并把 system_state 置位;已有仓且新预测反向,才触发平仓。图14、图15显示该结构在Boom 1000上捕捉过一次飙升与一次反转,但外汇/贵金属杠杆品种跳空频繁,实盘前务必在策略测试器用 Tick 级数据重跑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  Boom class="num">1000.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">//| ONNX Model                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\Files\Boom class="num">1000 Neural Network.onnx" as class="kw">const class="type">uchar onnx_buffer[];
class=class="str">"cmt">//+-----------------------------------------------------------------+
class=class="str">"cmt">//| Libraries we need                                                      |
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">long onnx_model;
class="type">int rsi_handler,model_state,system_state;
class="type">class="kw">double mean_values[class="num">5],std_values[class="num">5],rsi_buffer[],bid,ask;
vectorf model_outputs = vectorf::Zeros(class="num">1);
vectorf model_inputs = vectorf::Zeros(class="num">5);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| This function will prepare our ONNX model                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool load_onnx_model(class="type">void)
  {
class=class="str">"cmt">//--- First create the ONNX model from the buffer we created earlier
   onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT);
class=class="str">"cmt">//--- Validate the ONNX model
   if(onnx_model == INVALID_HANDLE)
     {
      Comment("[ERROR] Failed to create the ONNX model: ",GetLastError());
      class="kw">return(false);
     }

◍ 给 ONNX 模型喂数据前的定型与归一化

把训练好的 ONNX 模型接进 MT5,第一步是锁死张量形状。下面这段先把输入定为 {1,5}、输出定为 {1,1},再用 OnnxSetInputShape / OnnxSetOutputShape 回写模型;任何一步返回 false 就直接在图表上抛错误码并退出,避免后面拿错位张量跑推理。

MQL5 / C++
class=class="str">"cmt">//--- Set the class="kw">input and output shapes of the model
  class="type">ulong input_shape[] = {class="num">1,class="num">5};
  class="type">ulong output_shape[] = {class="num">1,class="num">1};
class=class="str">"cmt">//--- Validate the class="kw">input and output shape
  if(!OnnxSetInputShape(onnx_model,class="num">0,input_shape))
    {
      Comment("Failed to set the ONNX model class="kw">input shape: ",GetLastError());
      class="kw">return(false);
    }
  if(!OnnxSetOutputShape(onnx_model,class="num">0,output_shape))
    {
      Comment("Failed to set the ONNX model output shape: ",GetLastError());
      class="kw">return(false);
    }
  class="kw">return(true);
  }
class=class="str">"cmt">//+-----------------------------------------------------------------+
class=class="str">"cmt">//| Load the scaling values                                          |
class=class="str">"cmt">//+-----------------------------------------------------------------+
class="type">void load_scaling_values(class="type">void)
  {
class=class="str">"cmt">//--- BOOM class="num">1000 OHLC + RSI Mean values
  mean_values[class="num">0] = class="num">16799.87389394667;
  mean_values[class="num">1] = class="num">16800.872890865994;
  mean_values[class="num">2] = class="num">16798.91007345616;
  mean_values[class="num">3] = class="num">16799.908906749482;
  mean_values[class="num">4] = class="num">43.45867626462568;
class=class="str">"cmt">//--- BOOM class="num">1000 OHLC + RSI Mean std values
  std_values[class="num">0] = class="num">864.3356132780019;
  std_values[class="num">1] = class="num">864.3839684000297;
  std_values[class="num">2] = class="num">864.2859346216392;
  std_values[class="num">3] = class="num">864.3344430387272;
  std_values[class="num">4] = class="num">20.593175501388043;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Fetch updated market prices and technical indicator values       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void update_market_data(class="type">void)
  {
class=class="str">"cmt">//--- Market data
  bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
  ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
class=class="str">"cmt">//--- Technical indicator values
  CopyBuffer(rsi_handler,class="num">0,class="num">0,class="num">1,rsi_buffer);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Fetch a prediction from our model                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void model_predict(class="type">void)
  {
class=class="str">"cmt">//--- Get the model inputs
  model_inputs[class="num">0] = iOpen(_Symbol,PERIOD_CURRENT,class="num">0);
  model_inputs[class="num">1] = iHigh(_Symbol,PERIOD_CURRENT,class="num">0);
  model_inputs[class="num">2] = iLow(_Symbol,PERIOD_CURRENT,class="num">0);
  model_inputs[class="num">3] = iClose(_Symbol,PERIOD_CURRENT,class="num">0);
  model_inputs[class="num">4] = rsi_buffer[class="num">0];
class=class="str">"cmt">//--- Scale the model inputs
归一化参数得跟训练时一致。load_scaling_values 里写死了 BOOM 1000 品种的五维均值与标准差:OHLC 均值约 16799~16801,标准差都在 864 附近;RSI 均值 43.46、标准差 20.59。换品种或换周期直接套这组数,推理结果会偏得离谱。 实时行情由 update_market_data 抓取:bid/ask 走 SymbolInfoDouble,RSI 当前值用 CopyBuffer(rsi_handler,0,0,1,rsi_buffer) 取一根。model_predict 再把当根 K 线的开高低收和 RSI 填进 model_inputs[0..4],下一步就是按上面均值标准差做标准化再送模型。外汇与贵金属杠杆高,模型输出只是概率倾向,实盘前务必在策略测试器跑多品种回测。

MQL5 / C++
class=class="str">"cmt">//--- Set the class="kw">input and output shapes of the model
  class="type">ulong input_shape[] = {class="num">1,class="num">5};
  class="type">ulong output_shape[] = {class="num">1,class="num">1};
class=class="str">"cmt">//--- Validate the class="kw">input and output shape
  if(!OnnxSetInputShape(onnx_model,class="num">0,input_shape))
    {
      Comment("Failed to set the ONNX model class="kw">input shape: ",GetLastError());
      class="kw">return(false);
    }
  if(!OnnxSetOutputShape(onnx_model,class="num">0,output_shape))
    {
      Comment("Failed to set the ONNX model output shape: ",GetLastError());
      class="kw">return(false);
    }
  class="kw">return(true);
  }
class=class="str">"cmt">//+-----------------------------------------------------------------+
class=class="str">"cmt">//| Load the scaling values                                          |
class=class="str">"cmt">//+-----------------------------------------------------------------+
class="type">void load_scaling_values(class="type">void)
  {
class=class="str">"cmt">//--- BOOM class="num">1000 OHLC + RSI Mean values
  mean_values[class="num">0] = class="num">16799.87389394667;
  mean_values[class="num">1] = class="num">16800.872890865994;
  mean_values[class="num">2] = class="num">16798.91007345616;
  mean_values[class="num">3] = class="num">16799.908906749482;
  mean_values[class="num">4] = class="num">43.45867626462568;
class=class="str">"cmt">//--- BOOM class="num">1000 OHLC + RSI Mean std values
  std_values[class="num">0] = class="num">864.3356132780019;
  std_values[class="num">1] = class="num">864.3839684000297;
  std_values[class="num">2] = class="num">864.2859346216392;
  std_values[class="num">3] = class="num">864.3344430387272;
  std_values[class="num">4] = class="num">20.593175501388043;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Fetch updated market prices and technical indicator values       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void update_market_data(class="type">void)
  {
class=class="str">"cmt">//--- Market data
  bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
  ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
class=class="str">"cmt">//--- Technical indicator values
  CopyBuffer(rsi_handler,class="num">0,class="num">0,class="num">1,rsi_buffer);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Fetch a prediction from our model                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void model_predict(class="type">void)
  {
class=class="str">"cmt">//--- Get the model inputs
  model_inputs[class="num">0] = iOpen(_Symbol,PERIOD_CURRENT,class="num">0);
  model_inputs[class="num">1] = iHigh(_Symbol,PERIOD_CURRENT,class="num">0);
  model_inputs[class="num">2] = iLow(_Symbol,PERIOD_CURRENT,class="num">0);
  model_inputs[class="num">3] = iClose(_Symbol,PERIOD_CURRENT,class="num">0);
  model_inputs[class="num">4] = rsi_buffer[class="num">0];
class=class="str">"cmt">//--- Scale the model inputs

「把模型预测接进 MT5 交易循环」

这段逻辑把 ONNX 模型的输出直接变成了 EA 的下单信号。先对 5 个输入特征做 Z-Score 标准化,再跑 OnnxRun 拿到预测值,并用 Comment 把「Model RSI Forecast」打印在图表左上角,方便你肉眼比对模型值和真实 RSI。 预测值和实时 rsi_buffer[0] 的大小关系决定了 model_state:模型值更高置 1(倾向看涨),更低置 -1(倾向看跌),相等则维持原状态。这个状态机就是后面下单的唯一依据。 初始化阶段 OnInit 里依次加载模型、缩放参数,并用 iRSI(Symbol(),PERIOD_CURRENT,20,PRICE_CLOSE) 建了周期 20 的 RSI 句柄;OnDeinit 则释放模型与指标资源并移除 EA。外汇与贵金属杠杆高,这套自动跟单逻辑实盘前务必在策略测试器跑历史回测。 OnTick 每跳都先 update_market_data 再 model_predict,无持仓时按 model_state 动手:等于 1 就 Trade.Buy(0.2,...,"BOOM 1000 AI") 并记 system_state=1;等于 -1 则走另一侧分支。开 MT5 把这段粘进 EA,把 0.2 手改成你的风控手数,就能验证信号触发节奏。

MQL5 / C++
for(class="type">int i = class="num">0; i < class="num">5; i++)
  {
    model_inputs[i] = ((model_inputs[i] - mean_values[i]) / std_values[i]);
  }
class=class="str">"cmt">//--- Fetch a prediction from our model
  OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_outputs);
class=class="str">"cmt">//--- Give user feedback
  Comment("Model RSI Forecast: ",model_outputs[class="num">0]);
class=class="str">"cmt">//--- Store the model&class="macro">#x27;s state
  if(rsi_buffer[class="num">0] > model_outputs[class="num">0])
  {
    model_state = -class="num">1;
  }
  else
    if(rsi_buffer[class="num">0] < model_outputs[class="num">0])
      {
       model_state = class="num">1;
      }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- This function will prepare our ONNX model and set the class="kw">input and output shapes
  if(!load_onnx_model())
    {
      class="kw">return(INIT_FAILED);
    }
class=class="str">"cmt">//--- This function will prepare our scaling values
  load_scaling_values();
class=class="str">"cmt">//--- Setup our technical indicatot
  rsi_handler = iRSI(Symbol(),PERIOD_CURRENT,class="num">20,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 resources we no longer need
  OnnxRelease(onnx_model);
  IndicatorRelease(rsi_handler);
  ExpertRemove();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//--- Fetch updated market prices
  update_market_data();
class=class="str">"cmt">//--- On every tick we need to fetch a prediction from our model
  model_predict();
class=class="str">"cmt">//--- If we have no open positions, follow the model&class="macro">#x27;s prediction
  if(PositionsTotal() == class="num">0)
    {
      class=class="str">"cmt">//--- Our model detected a spike
      if(model_state == class="num">1)
        {
         Trade.Buy(class="num">0.2,Symbol(),ask,class="num">0,class="num">0,"BOOM class="num">1000 AI");
         system_state = class="num">1;
        }
      class=class="str">"cmt">//--- Our model detected a drop
      if(model_state == -class="num">1)
        {

让 AI 状态翻转来平掉持仓

持仓管理这一段的逻辑很直接:只要当前无单且模型状态翻负,就先以 0.2 手卖空并把 system_state 置为 -1;若有持仓,则等模型状态与系统状态不一致时触发平仓。 具体看代码,PositionsTotal() > 0 说明场内有单,此时若 system_state != model_state,说明 AI 判定了反转信号,直接 Alert 弹窗并调用 Trade.PositionClose(Symbol()) 平掉当前品种全部仓位。外汇与贵金属杠杆高,这种硬平机制可能在某些滑点行情下以偏离预期的价格成交,实盘前建议在 MT5 策略测试器用 BOOM 1000 指数跑一轮观察成交延迟。 想验证的话,把这段接在你自己的 EA 主循环里,把 model_state 的来源换成你训练好的轻量二分类输出,就能看平仓触发是否跟人工标记的反转点吻合。

MQL5 / C++
      Trade.Sell(class="num">0.2,Symbol(),bid,class="num">0,class="num">0,"BOOM class="num">1000 AI");
      system_state = -class="num">1;
      }
    }
class=class="str">"cmt">//--- If we have open positiosn, our AI system will decide when to close them
   else
    if(PositionsTotal() > class="num">0)
      {
       if(system_state != model_state)
         {
          class=class="str">"cmt">//--- Close the positions we opened
          Alert("Reversal detected by the AI system,closing all positions now!");
          Trade.PositionClose(Symbol());
         }
      }
   }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 别急着下结论

这套自优化 EA 的思路已经跑通:用 MLPClassifier 配 (30,10) 双隐层、max_iter=200,在 Boom 1000 这类合成品种上做滚动交叉验证,模型能随数据自我调整,而不是死守固定参数。 但直接预测价格水平的老办法在算法盘里越来越吃力——上面那段代码就暴露了现实坑:X 有 4139 行、y 只有 4133 行,维度不对齐直接抛 ValueError,模型根本 fit 不进去。 最后那行用 RobustScaler 重构索引才是关键补丁:过滤掉异常行之后必须保持 X 和 y 的 index 一致,否则你本地跑回测也会莫名其妙报错。外汇和贵金属杠杆高、滑点狠,这类自优化系统上线前务必在 MT5 策略测试器里用真实点差复验。

MQL5 / C++
class="macro">#建立模型
model = MLPClassifier(hidden_layer_sizes=(class="num">30,class="num">10),max_iter=class="num">200)
# 交叉验证模型
for i,(train,test) in enumerate(tscv.split(train_X)):
    model.fit(
        train_X.loc[train[class="num">0]:train[-class="num">1],:],
        ohlc_train_y.loc[train[class="num">0]:train[-class="num">1]]
    )
    validation_accuracy.iloc[i,class="num">0] = accuracy_score(
        ohlc_train_y.loc[test[class="num">0]:test[-class="num">1]],
        model.predict(train_X.loc[test[class="num">0]:test[-class="num">1],:])
    )
---------------------------------------------------------------------------
ValueError Traceback(most recent call last)
Cell In[class="num">25], line class="num">5
class="num">3 class="macro">#Cross validate the model
class="num">4 for i,(train,test) in enumerate(tscv.split(train_X)):
----> class="num">5 model.fit(train_X.loc[train[class="num">0]:train[-class="num">1],:],ohlc_train_y.loc[train[class="num">0]:train[-class="num">1]])
class="num">6 validation_accuracy.iloc[i,class="num">0] = accuracy_score(ohlc_train_y.loc[test[class="num">0]:test[-class="num">1]],model.predict(train_X.loc[test[class="num">0]:test[-class="num">1],:]))
File c:\Python\ocrujenie\.ordi\Lib\site-packages\sklearn\base.py:class="num">1389, in _fit_context.<locals>.decorator.<locals>.wrapper(estimator, *args, **kwargs)
class="num">1382 estimator._validate_params()
class="num">1384 with config_context(
class="num">1385 skip_parameter_validation=(
class="num">1386 prefer_skip_nested_validation or global_skip_validation
class="num">1387 )
class="num">1388 ):
-> class="num">1389 class="kw">return fit_method(estimator, *args, **kwargs)
File c:\Python\ocrujenie\.ordi\Lib\site-packages\sklearn\neural_network\_multilayer_perceptron.py:class="num">754, in BaseMultilayerPerceptron.fit(self, X, y)
class="num">736 @_fit_context(prefer_skip_nested_validation=True)
class="num">737 def fit(self, X, y):
class="num">738 """Fit the model to data matrix X and target(s) y.
class="num">739 class="num">740 Parameters(...) class="num">752 Returns a trained MLP model. class="num">753 """
...
class="num">476 "Found class="kw">input variables with inconsistent numbers of samples: %r"
class="num">477 % [class="type">int(l) for l in lengths]
class="num">478 )
ValueError: Found class="kw">input variables with inconsistent numbers of samples: [class="num">4139, class="num">4133]
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...
# 保持索引一致, 否则如果有过滤掉的数据,会重构索引
X = pd.DataFrame(RobustScaler().fit_transform(boom_1000.loc[:, predictors]), columns=predictors, index=boom_1000.index)

常见问题

在 EA 初始化时用 OnnxRuntime 加载 .onnx 文件,把句柄存到全局变量,后续每根 K 线直接调用推理,不必重复读盘。
模型训练时用了固定量纲,实盘若不做同样缩放,预测值会偏离。用训练集的均值方差做 Z-score 最稳。
可以。小布能按你设定的阈值监控预测分布变化,发现明显偏移就提醒你重训或暂停 EA。
在订单管理函数里读模型输出的多空概率差,穿越阈值才发平仓指令,并加成交校验避免重复发单。
不能。样本太少且含运气成分,至少跨三种波动 regime 回测各 3 个月,再看夏普和回撤再下结论。