使用MQL5和Python构建自优化的EA(第四部分):模型堆叠(基础篇)
◍ 为什么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再放大样本。
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,就能直接拉出小时级裸数据。
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 那种正向数据框架。
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