使用MQL5和Python构建自优化的EA(第四部分):模型堆叠(基础篇)
📘

使用MQL5和Python构建自优化的EA(第四部分):模型堆叠(基础篇)

第 1/3 篇

◍ 为什么EA要能自己换打法

这套系列文章聚焦一个现实问题:市场状态一直在漂移,写死的交易逻辑迟早失效。作者 Gamuchirai Zororo Ndawana 在 2025 年 5 月 16 日发布的示例里提到,用 MQL5 配合 Python 搭建可动态适应行情的 EA,是其中一种实证路线。 解决「自适应」的思路理论上有无限多种,但原文明确点出:并非所有方案都有效。本系列的目标不是堆砌花活,而是把不同可能的解决方案拿来回测和实证,掰开看优缺点,帮你少踩坑。 外汇与贵金属杠杆高、跳空频繁,任何「自适应」机制都只是提高存活概率,不保证盈利。开 MT5 跑通一套能切换参数的 EA,比读十篇理论更有用。

「用堆叠模型绕开人为偏见」

以 NZDJPY 为样本,从 MT5 终端抽取历史 tick 与分钟数据,让算法自己学出一套交易逻辑,而不是由人拍脑袋定规则。人做决策时往往会不自觉地偏向已有仓位和主观判断,机器学习同样有偏差——这里的偏差指模型内置假设被市场真实分布打破的程度。 我们采用两个模型集成:模型 A 专门预测 NZDJPY 未来 20 分钟收盘价;模型 B 不直接预测价格,而是预测模型 A 的误差大小。这种结构在技术上叫 stacking(堆叠)。 把预测值和误差预测叠在一起,理论上能部分抵消单模型偏差,也可能比人主观进出更稳。但外汇/贵金属属高杠杆品种,模型在历史表现好不等于实盘能复制,上线前务必用 MT5 策略测试器跑一遍。

NZDJPY 的 M1 建模踩坑实录

我们从 MT5 终端拉取了约 9000 行 NZDJPY 的 M1 数据,先画了二维和三维散点图,没看出明显关系;做时间序列分解后,才确认存在明显下降趋势和强季节性效应。 把数据拆成训练集和测试集后,用 15 种模型拟合,随机梯度下降(SGD)回归器跑分最好。特征重要性上,最高价的互信息(MI)分数最高,是最有信息量的收盘价预测因子;scikit-learn 的 RFE 算法虽把全部因子都判为重要,但关系存在不等于能建出来。 用残差分析法测过拟合时,发现 SGD 残差和自身滞后项高度相关——充分学习的模型残差本应无相关性,说明模型可能没学透,或漏了关键数据。 我们在 SGD 训练残差上交叉验证 15 种模型,Lasso 最优,但选了排第三的深度神经网络(DNN):DNN 可调参数多,比 Lasso 更有微调余地。 DNN 调参用两步法预测 SGD 残差:先随机搜索 100 次得第一版,再用其连续参数作 L-BFGS-B 全局优化起点得第二版。两版在验证集都胜过默认 DNN,末版最优。外汇/贵金属高波动,这类残差堆叠方案仅降低误差、不保证方向判断。 最终两模型导出 ONNX,接进能自我纠错的 AI EA 里跑。

◍ 用脚本把历史报价拽进CSV

做价格行为统计前,先得把MT5里的裸数据弄出来。下面这段脚本不依赖任何第三方接口,直接在终端里抓当前品种、当前周期的历史K线,默认拉10万根(size=100000),写进Files文件夹的CSV,方便后续用Python或Excel跑分布。 脚本先用iRSI挂一个20周期收盘价RSI句柄,再用CopyBuffer把指标缓冲读进数组,并ArraySetAsSeries置为时间序列。真正落盘时,FileOpen以CSV+ANSI打开,表头写Time/Open/High/Low/Close,随后倒序循环把每根K线的iTime、iOpen等逐行写文件。 外汇与贵金属杠杆高、点差跳空频繁,导出的10万根样本若含重大数据行情,分布尾部会失真,后续建模需先剔除异常波动段。开MT5把size改小到5000先跑一遍,确认Files目录生成了带品种名的CSV再放大样本。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 ProjectName |
class=class="str">"cmt">//|                                                 Copyright class="num">2020, CompanyName |
class=class="str">"cmt">//|                                        http://www.companyname.net |
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="macro">#class="kw">property script_show_inputs
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script Inputs                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
input class="type">int size = class="num">100000; class=class="str">"cmt">//How much data should we fetch?
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global variables                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int rsi_handler;
class="type">class="kw">double rsi_buffer[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| On start function                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- Load indicator
   rsi_handler = iRSI(_Symbol,PERIOD_CURRENT,class="num">20,PRICE_CLOSE);
   CopyBuffer(rsi_handler,class="num">0,class="num">0,size,rsi_buffer);
   ArraySetAsSeries(rsi_buffer,true);
class=class="str">"cmt">//--- File name
   class="type">class="kw">string file_name = "Market Data " + Symbol() + ".csv";
class=class="str">"cmt">//--- Write to file
   class="type">int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");
   for(class="type">int i= size;i>=class="num">0;i--)
     {
      if(i == size)
        {
       FileWrite(file_handle,"Time","Open","High","Low","Close");
        }
      else
        {
       FileWrite(file_handle,iTime(Symbol(),PERIOD_CURRENT,i),
                 iOpen(Symbol(),PERIOD_CURRENT,i),

「把K线高低收写进文件的那几行」

这段片段是 MT5 里把当前图表每根 K 线的高、低、收三个价格批量落盘的核心调用,配合前面的文件打开与循环,才能生成可供回测或外部分析用的 CSV 或 TXT。 iHigh(Symbol(),PERIOD_CURRENT,i) 取当前品种、当前周期、第 i 根 K 线的最高价;iLow 取最低价;iClose 取收盘价。三个函数共用同一根索引 i,顺序写进同一行,就构成一条完整的裸 K 记录。 循环跑完所有 i 后,FileClose(file_handle) 必须执行,否则句柄泄漏,MT5 终端可能在下次打开同文件时报「文件被占用」。开 MT5 按 F4 把这段塞进你自己的导出脚本,改 PERIOD_CURRENT 为 PERIOD_H1,就能直接拉出小时级裸数据。

MQL5 / C++
iHigh(Symbol(),PERIOD_CURRENT,i),
 iLow(Symbol(),PERIOD_CURRENT,i),
 iClose(Symbol(),PERIOD_CURRENT,i)
 );
 }
 }
class=class="str">"cmt">//--- Close the file
 FileClose(file_handle);
}
class=class="str">"cmt">//+------------------------------------------------------------------+

把 NZDJPY 数据翻正并打标签

做价格行为建模前,先得让数据按时间正向排列。原文里 NZDJPY 的 CSV 是倒序存的,最早一根 K 线在末尾,不翻过来后面算 shift 目标会全错。 用 pandas 读进来的第一步就是 nzd_jpy[::-1] 把行序倒转,再 reset_index 丢掉旧索引,让从 0 开始的新索引对应真实时间流。这一步不做,任何基于行位移的特征都会偏移 20 根以上。 目标列直接用 Close 向前移 20 根(shift(-20))生成,意思是『20 根后的收盘价』,随后 dropna 剔掉尾部空值。再加一列 Binary Target 方便画图:当前收高于 20 根后收的标 1,否则标 0,肉眼看分布倾向更直观。 外汇与贵金属属高风险品种,20 根后的收盘价关系只反映历史样本中的统计倾向,不构成方向承诺。下面这段代码跑完,你的 dataframe 就处于可建模状态,开 Jupyter 贴进去就能看到图 1 那种正向数据框架。

MQL5 / C++
class="macro">#Import the libraries we need
class="kw">import pandas as pd
class="kw">import numpy as np
class="macro">#Read in the market data
nzd_jpy = pd.read_csv(&class="macro">#x27;Market Data NZDJPY.csv&class="macro">#x27;)
class="macro">#Format the data
nzd_jpy = nzd_jpy[::-class="num">1]
nzd_jpy.reset_index(inplace=True, drop=True)
class="macro">#Labelling the data
nzd_jpy[&class="macro">#x27;Target&class="macro">#x27;] = nzd_jpy[&class="macro">#x27;Close&class="macro">#x27;].shift(-class="num">20)
nzd_jpy.dropna(inplace=True)
class="macro">#Add binary target for plotting
nzd_jpy[&class="macro">#x27;Binary Target&class="macro">#x27;] = np.nan
nzd_jpy.loc[nzd_jpy[&class="macro">#x27;Close&class="macro">#x27;] > nzd_jpy[&class="macro">#x27;Target&class="macro">#x27;],&class="macro">#x27;Binary Target&class="macro">#x27;] = class="num">1
nzd_jpy.loc[nzd_jpy[&class="macro">#x27;Close&class="macro">#x27;] <= nzd_jpy[&class="macro">#x27;Target&class="macro">#x27;],&class="macro">#x27;Binary Target&class="macro">#x27;] = class="num">0
class="macro">#Current state of our dataframe
nzd_jpy

常见问题

可以。让EA内置多套模型并按近期行情表现自动切换或加权,比死守单一策略更抗变。先跑历史分段回测确认切换逻辑有效再上实盘。
把多个基模型输出作为新特征交给二层模型自动学权重,人只定框架不拍脑袋选指标。注意外汇高风险,堆叠只是降偏见不降亏损概率。
小布盯盘的AIGC会读品种页的实时结构,提示当前行情倾向哪种模型更稳。你把切换阈值设好,小布替你盯信号别漏判。
部分数据源该货币对报价顺序需翻转校正,否则标签全错。导入后按公式取倒数或重排买卖列,再打涨跌标签才靠谱。
至少写时间、开、高、低、收四列,缺高低的区间信息模型没法学波动。用换行分隔每根K线,后续Python读CSV直接喂训练。