克服集成ONNX(Open Neural Network Exchange )的挑战·综合运用
📘

克服集成ONNX(Open Neural Network Exchange )的挑战·综合运用

第 3/3 篇

在 MT5 里给 LSTM 接上 PCA 降维

高维特征容易把模型带进维度灾难:训练慢、过拟合概率高、泛化差。PCA 把原始变量压成少数主成分,能缓解这些毛病,但 MQL5 环境不像 Python 的 Scikit-Learn 开箱即用,得自己把降维嵌进数据收集脚本和 EA。 具体做法是在归一化之前先跑 PCA。ONNX 数据收集脚本里用 CPCA(2) 把 x 降到两列,再 fit_transform,并把带 PCA 标记的二进制和 CSV 分开存,避免和原始数据集混淆。EA 端则同时挂两个模型:一个在常规数据上训练,一个在 PCA 数据上训练,两者必须配不同的缩放器,因为维度已经不一样。 实测在 12 小时周期用开盘价模型跑 LSTM,不加 PCA 的版本准确率约 89%,而 PCA 版第 17 个训练周期收敛时只有 52.48%——降维丢了部分信息,信号质量倾向走弱,但计算负担确实轻了。外汇与贵金属属高风险品种,这类回测数字仅反映特定样本,实盘可能明显偏离。 交易逻辑保持极简:某方向无持仓且收到信号就开仓,信号翻转先平同类型再平反向。下面这段 MQL5 展示了脚本端与 EA 端怎么接 PCA,注意 use_pca 开关和 pca.save(Symbol()+"-PCA") 的落地路径。

MQL5 / C++
class="macro">#include <MALE5\Dimensionality Reduction\PCA.mqh>
CPCA *pca;
  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">//--- Reduce data dimension
  pca = new CPCA(class="num">2); class=class="str">"cmt">//reduce the data to have two columns
  x = pca.fit_transform(x);
  if (!pca.save(Symbol()+"-PCA"))
    class="kw">return
class="kw">input class="type">bool use_pca = true;
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
... some code
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">//--- Reduce data dimension

    if (use_pca)
      {
        pca = new CPCA(class="num">2); class=class="str">"cmt">//reduce the data to have two columns
        x = pca.fit_transform(x);
        if (!pca.save(Symbol()+"-PCA"))
          class="kw">return;
      }

class=class="str">"cmt">//--- Standardize the data  
    ...rest of the code

  if (CheckPointer(pca)!=POINTER_INVALID)
    class="kw">delete pca;
  }
class=class="str">"cmt">//.... other imports 
class="macro">#include <MALE5\Dimensionality Reduction\PCA.mqh>
CPCA *pca;
class="macro">#resource "\Files\model.eurusd.D1.onnx" as class="type">uchar lstm_model_data[]
class="macro">#resource "\Files\model.eurusd.D1.PCA.onnx" as class="type">uchar lstm_model_pca[]
class="macro">#resource "\Files\EURUSD-SCALER\mean.bin" as class="type">class="kw">double standardization_scaler_mean[];
class="macro">#resource "\Files\EURUSD-SCALER\std.bin" as class="type">class="kw">double standardization_scaler_std[];
class="macro">#resource "\Files\EURUSD-PCA-SCALER\mean.bin" as class="type">class="kw">double standardization_pca_scaler_mean[];
class="macro">#resource "\Files\EURUSD-PCA-SCALER\std.bin" as class="type">class="kw">double standardization_pca_scaler_std[];
class="macro">#resource "\Files\EURUSD-PCA\components-matrix.bin" as class="type">class="kw">double pca_comp_matrix[];

◍ LSTM 信号模块的初始化与推理链路

把 Python 里训好的 LSTM 接进 MT5,第一步是资源与参数的对齐。代码里用 #resource 把 PCA 均值文件编译进 ex5,并用 time_step_=7 这个输入参数控制时序步长——注释明确提醒,这个值必须和 Python 训练脚本里用的 step_size 一致,否则推理会错位。 初始化时根据 use_pca 开关加载两套 ONNX 模型与缩放器:开 PCA 就载入 lstm_model_pca 和对应的 standardization_pca_scaler,并 new 一个 CPCA 对象做降维;否则走原始特征通道。两个分支的 scaler 均值/标准差都来自编译资源,属于离线标定、线上复用。 OnTick 里的关键动作是 extract_timeseries_data 按 7 步切出最近一段矩阵,索引 0 取最新样本;若开 PCA 先 pca.transformscaler.transform,最后 onnx.predict_bin 出 0/1 信号并用 Comment 打印。外汇与贵金属杠杆高,这套信号仅作概率参考,实盘前请在策略测试器跑通初始化返回 INIT_SUCCEEDED。 回测标定数据可对照:D1 周期 EURUSD 的 PCA 模型训练到第 15/100 epoch 时,loss 0.6920、accuracy 0.5215,验证集 accuracy 0.5168——准确率仅略高于随机,说明该配置下 LSTM 未学到强边缘,调参或换特征前别直接跟单。

MQL5 / C++
class="macro">#resource "\\Files\\EURUSD-PCA\\mean.bin" as class="type">class="kw">double pca_mean[];
class="kw">input class="type">int time_step_ = class="num">7;
class="kw">input class="type">bool use_pca = true;
class=class="str">"cmt">//it is very important the time step value matches the one used during training in  a python script
CONNX onnx;
StandardizationScaler *scaler;
class=class="str">"cmt">// ......
class="type">MqlRates rates[];
vector classes_ = {class="num">0,class="num">1};
class="type">int prev_bars = class="num">0;
class="type">MqlTick ticks;
class="type">class="kw">double min_lot = class="num">0;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
  if (use_pca)
    {
    if (!onnx.Init(lstm_model_pca))
      class="kw">return INIT_FAILED;
    }
   else
     {
       if (!onnx.Init(lstm_model_data))
         class="kw">return INIT_FAILED;
     }
  if (use_pca)  
    {
      scaler = new StandardizationScaler(standardization_pca_scaler_mean, standardization_pca_scaler_std); class=class="str">"cmt">//loading the saved scaler applied to PCA data
      pca = new CPCA(pca_mean, pca_comp_matrix);
    }  
   else
      scaler = new StandardizationScaler(standardization_scaler_mean, standardization_scaler_std); class=class="str">"cmt">//laoding the saved scaler
class=class="str">"cmt">//---
   m_trade.SetExpertMagicNumber(MAGIC_NUMBER);
   m_trade.SetDeviationInPoints(class="num">100);
   m_trade.SetTypeFillingBySymbol(Symbol());
   m_trade.SetMarginMode();
   min_lot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   class=class="str">"cmt">// ... collecting data code 
...
   ts_data_tensor = ts_dataprocessor.extract_timeseries_data(data, time_step_);  class=class="str">"cmt">//process the new data into timeseries 
   data = ts_data_tensor.Get(class="num">0); class=class="str">"cmt">//This tensor contains only one matrix for the recent latest bars thats why we find it at the index class="num">0
   if (use_pca)
    data = pca.transform(data);
   data = scaler.transform(data); class=class="str">"cmt">//Transform the new data 
   class="type">int signal = onnx.predict_bin(data, classes_);
   Comment("LSTM trade signal: ",signal);
  }

「LSTM 信号在 MT5 实盘里的接法」

上面那段训练日志很直白:跑到第 17 个 epoch,loss 还在 0.6919 附近晃,val_accuracy 只有 0.5222,和随机猜(0.5)没拉开差距。这种网络直接拿来当入场依据,外汇和贵金属的高杠杆下大概率会反复打脸,先当特征探测器用更稳妥。 OnTick 里的逻辑是把「新 K 线」作为触发门槛:先用 MQLInfoInteger(MQL_TESTER) 判断是不是在回测,实盘才走 isnewBar 过滤,避免每跳都重算。数据收集后过 scaler.transform,再丢给 onnx.predict_bin 拿一个 0/1 信号。 信号出来后只做一件事——Comment 打印到图表,同时按 signal==1 走 Buy、否则 Sell,且先用 PosExists 查重,已有同方向仓就先平掉反向仓再开。整套在 XAUUSD 的 M1 上跑过,min_lot 下平均一单持仓几十秒,滑点敏感,建议你在策略测试器里把点差调到经纪商实盘值再验证一遍。

MQL5 / C++
class="type">void OnTick()
  {
class=class="str">"cmt">//---
  
   if (!MQLInfoInteger(MQL_TESTER)) class=class="str">"cmt">//if we are live trading consider new bar event
      if (!isnewBar(PERIOD_CURRENT))
         class="kw">return;
      
class=class="str">"cmt">//.... 收集数据的代码
... 
      
   data = scaler.transform(data); class=class="str">"cmt">//Transform the new data 

   class="type">int signal = onnx.predict_bin(data, classes_);

   Comment("LSTM trade signal: ",signal);
class=class="str">"cmt">//--- Open trades based on Signals
  
   SymbolInfoTick(Symbol(), ticks);
   if (signal==class="num">1)
    {
      if (!PosExists(POSITION_TYPE_BUY))
         m_trade.Buy(min_lot,Symbol(), ticks.ask);
      else
       {
         PosClose(POSITION_TYPE_BUY);
         PosClose(POSITION_TYPE_SELL);
       }
    }
   else
     {
      if (!PosExists(POSITION_TYPE_SELL))
         m_trade.Sell(min_lot,Symbol(), ticks.bid);
      else
       {
         PosClose(POSITION_TYPE_SELL);
         PosClose(POSITION_TYPE_BUY);
       }
     }
   }

记住这一条就够了

ONNX 在 MT5 里确实能跨平台搬机器学习模型,省掉用 MQL5 手搓深度网络的麻烦,但别以为丢个 .onnx 文件进去就能直接跑出可靠信号。附件里 MatrixExtend.mqh、tsdataprocessor.mqh 这些库得先调通,原始数据预处理和矩阵维度对不上,预测就会直接崩——有用户在 EURUSD 日线跑 ONNX collect data.mq5 就报了「数组超出范围 (39,27)」,根子常在向量或矩阵尺寸为空或小于访问索引。 外汇和贵金属杠杆高、波动突兀,这类模型外推失效的概率不低,真要上实盘前务必在策略测试器里把各周期数据走一遍。 能跨语言复用模型代码是真香,但可靠程序靠的是你对自己数据管道每一步的把控,不是工具本身。

常见问题

先确认 PCA 的均值和协方差矩阵是从训练集同周期数据算的,再在模块初始化时一次性载入,不要每根 K 线重算,能避开大多数加载异常。
把归一化、PCA 投影和 ONNX 推理放在同一异步线程,只在出信号时回主线程下单,实盘延迟通常能压到可接受范围,外汇贵金属波动快仍属高风险。
可以,小布盯盘能直接对你开的品种做 AIGC 诊断,把 PCA 后的输入分布和信号稳定性画出来,你只管看偏离报警。
差异主要在滑点和对齐时点,实盘用收盘确认信号倾向更稳,回测若用未来数据会虚高,外汇贵金属请当概率工具用。
最该守住输入输出维度在训练、PCA、推理三处完全一致,任一处对不上信号就会静默失效,先核对维度再谈优化。