基于MQL5和Python的自优化EA(第六部分):利用深度双重下降算法·进阶篇
📘

基于MQL5和Python的自优化EA(第六部分):利用深度双重下降算法·进阶篇

第 2/3 篇

迭代次数翻倍后误差反而先升后崩

把前面搜到的最优参数套回去:用 x_axis 里 res 按列取最小的位置当预测步长 look_ahead,y_axis 里 res 按行取最小的位置当差分周期 difference_period。接着对 Close/Open/High/Low 做差分,再丢进时间序列交叉验证。 我们让神经网络的最大训练迭代次数取 2 的连续幂:1、2、4…直到 2^max_epoch。前 6 次(1 到 32 次迭代)里,测试误差先降后升,又出现更高的低点;训练与验证误差则先升、再落到一个略高的低点、随后再抬头。32 次迭代只是冰山一角。 继续跑到 64~256 次区间,误差在发散后开始向某个最小值收敛。但 scikit-learn 的 MLPRegressor 默认只跑 200 次(略小于 2^8),若开了提前停止,模型极可能困在左侧那个骗人的局部最优里。 真正的最低测试误差出现在迭代次数超过 10 亿次时——精确说是 2^30 次,图中红垂直线标的位置。我们平时因怕过拟合而停手,其实大都卡在红线左侧的次优区。外汇与贵金属市场高风险,这种过拟合边界需自己在 MT5 导出的数据上重跑确认。 下面这段是可复现的实验代码,注意 max_epoch 设成 50 只是为画图省时间,真要触到双重下降拐点得把上限推到 30 以上:

MQL5 / C++
class="macro">#The best settings we have found so far
look_ahead = x_axis[res.min(axis=class="num">0).argmin()]
difference_period = y_axis[res.min(axis=class="num">1).argmin()]
data["Target"] = data["Close"].shift(-look_ahead)
class="macro">#Apply the differencing
data["Close"] = data["Close"].diff(difference_period)
data["Open"] = data["Open"].diff(difference_period)
data["High"] = data["High"].diff(difference_period)
data["Low"] = data["Low"].diff(difference_period)
data.dropna(inplace=True)
data.reset_index(drop=True,inplace=True)
data
from sklearn.model_selection class="kw">import train_test_split,TimeSeriesSplit
from sklearn.metrics        class="kw">import mean_squared_error
max_epoch = class="num">50
err_rates = pd.DataFrame(columns = np.arange(class="num">0,max_epoch),index=["Train","Validation","Test"])
tscv = TimeSeriesSplit(n_splits=class="num">5,gap=look_ahead)
train , test = train_test_split(data,shuffle=False,test_size=class="num">0.5)
for j in np.arange(class="num">0,max_epoch):
  class="macro">#Define our model and measure its error
  current_train_err = []
  current_val_err = []
  model = MLPRegressor(hidden_layer_sizes=(class="num">6,class="num">5),max_iter=(class="num">2 ** j))
  for i,(train_index,test_index) in enumerate(tscv.split(train)):
    class="macro">#Assess the model
    model.fit(train.loc[train_index,["Open","High","Low","Close"]],train.loc[train_index,&class="macro">#x27;Target&class="macro">#x27;])
    current_train_err.append(mean_squared_error(train.loc[train_index,&class="macro">#x27;Target&class="macro">#x27;],model.predict(train.loc[train_index,["Open","High","Low","Close"]])))
    current_val_err.append(mean_squared_error(train.loc[test_index,&class="macro">#x27;Target&class="macro">#x27;],model.predict(train.loc[test_index,["Open","High","Low","Close"]])))
  class="macro">#Record our observations
  err_rates.loc["Train",j]      = np.mean(current_train_err)
  err_rates.loc["Validation",j] = np.mean(current_val_err)
  err_rates.loc["Test",j]       = mean_squared_error(test[&class="macro">#x27;Target&class="macro">#x27;],model.predict(test.loc[:,["Open","High","Low","Close"]]))
plt.plot(err_rates.iloc[class="num">0,class="num">0:class="num">5])
plt.plot(err_rates.iloc[class="num">1,class="num">0:class="num">5])
plt.plot(err_rates.iloc[class="num">2,class="num">0:class="num">5])
plt.legend(["Train Error","Validation Error","Test Error"])
plt.ylabel("RMSE")
plt.xlabel("Epochs: Our Epochs Are Indices of class="num">2")
plt.title("Neural Network Accuracy Forecasting GBPUSD Daily Close")
plt.plot(err_rates.iloc[class="num">0,class="num">0:class="num">9])
plt.plot(err_rates.iloc[class="num">1,class="num">0:class="num">9])
plt.plot(err_rates.iloc[class="num">2,class="num">0:class="num">9])
plt.legend(["Train Error","Validation Error","Test Error"])
plt.ylabel("RMSE")
plt.xlabel("Epochs: Our Epochs Are Indices of class="num">2")
plt.title("Neural Network Accuracy Forecasting GBPUSD Daily Close")
plt.plot(err_rates.iloc[class="num">0,:])
plt.plot(err_rates.iloc[class="num">1,:])
plt.plot(err_rates.iloc[class="num">2,:])
plt.axvline(err_rates.loc["Test",:].argmin(),class="type">color=&class="macro">#x27;red&class="macro">#x27;)
plt.legend(["Train Error","Validation Error","Test Error","Double Descent Error"])
plt.ylabel("RMSE")

「把训练轮次映射到 2 的幂次」

在 GBPUSD 日线收盘价的神经网络预测脚本里,横轴标签直接写明了“Epochs: Our Epochs Are Indices of 2”,意思是训练轮次不是连续整数,而是以 2 为底的指数序列:第 0 轮对应 2^0=1,第 1 轮对应 2^1=2,第 n 轮对应 2^n。 这种 indexing 方式会让前几轮迭代极快、后期跨度猛增,适合验证学习率在不同数量级轮次下的收敛表现。外汇与贵金属市场波动剧烈、杠杆风险高,任何预测模型都只是概率参考,开 MT5 接 Python 环境跑一遍这段标签设定,能直观看到横轴非线性分布对损失曲线的视觉压缩效果。

MQL5 / C++
plt.xlabel("Epochs: Our Epochs Are Indices of class="num">2")
plt.title("Neural Network Accuracy Forecasting GBPUSD Daily Close")

◍ 放心跑参数搜索的调优实操

早先做神经网络回归时,大家习惯把迭代次数压得很低,核心顾虑就是训练集过拟合。引入交叉验证式的随机搜索后,这个顾虑可以放下了——调优器在 cv=5 折里挑参数,过拟合训练数据的概率明显下降。 具体落地时,先按测试集误差最小位置重置模型的最大迭代:model = MLPRegressor(max_iter=...)。随后用 RandomizedSearchCV 把激活函数、求解器、隐藏层结构等 11 类参数丢进搜索空间,n_iter 设成 2**9(即 512 次抽样),n_jobs=-1 表示吃满本机所有核心。 搜索空间里 hidden_layer_sizes 给了 7 种组合,最优解落到了 (5, 8, 10) 三层结构;learning_rate_init 低至 1e-06 配合 adaptive 策略,alpha regularization 取到 1e-05。这些数值直接决定了模型在外汇或贵金属日线预测上的拟合倾向,但都属于样本内结果,实盘仍受点差与滑点扰动,属高风险验证。 把下面这段代码贴进你的 Python 环境(需 sklearn 与 MT5 导出的 OHLC 数据),tuner.best_params_ 会回吐上面那组最优字典,可立即拿去重训模型做对比。

MQL5 / C++
from sklearn.model_selection class="kw">import RandomizedSearchCV
class="macro">#Reinitialize the model
model = MLPRegressor(max_iter=(err_rates.loc["Test",:].argmin()))
class="macro">#Define the tuner
tuner = RandomizedSearchCV(
            model,
            {
            "activation" : ["relu","logistic","tanh","identity"],
            "solver":["adam","sgd","lbfgs"],
            "alpha":[class="num">0.1,class="num">0.01,class="num">0.001,class="num">0.0001,class="num">0.00001,class="num">0.00001,class="num">0.0000001],
            "tol":[class="num">0.1,class="num">0.01,class="num">0.001,class="num">0.0001,class="num">0.00001,class="num">0.000001,class="num">0.0000001],
            "learning_rate":[&class="macro">#x27;constant&class="macro">#x27;,&class="macro">#x27;adaptive&class="macro">#x27;,&class="macro">#x27;invscaling&class="macro">#x27;],
            "learning_rate_init":[class="num">0.1,class="num">0.01,class="num">0.001,class="num">0.0001,class="num">0.00001,class="num">0.000001,class="num">0.0000001],
            "hidden_layer_sizes":[(class="num">1,class="num">4),(class="num">5,class="num">8,class="num">10),(class="num">5,class="num">10,class="num">20),(class="num">10,class="num">50,class="num">10),(class="num">20,class="num">5),(class="num">1,class="num">5),(class="num">20,class="num">10)],
            "early_stopping":[True,False],
            "warm_start":[True,False],
            "shuffle": [True,False]
            },
            n_iter=class="num">2**class="num">9,
            cv=class="num">5,
            n_jobs=-class="num">1,
            scoring="neg_mean_squared_error"
)
tuner.fit(train.loc[:,["Open","High","Low","Close"]],train.loc[:,"Target"])
tuner.best_params_

把调好的模型压成 ONNX 给 MT5 吃

训练完的 scikit-learn 模型若想直接在 MT5 终端里跑推理,中间必须过一道 ONNX 转换。ONNX 是开放神经网络交换协议,脱离 Python 运行时也能被支持该规范的平台加载,MQL5 原生支持导入这类模型文件。 代码里先拿调优器选出的最佳估计器,用训练集的 Open/High/Low/Close 四列去 fit,目标列是 Target。输入张量形状锁死为 [1,4],对应单根 K 线的四个报价字段,这一步若写错维度,后面 MT5 加载会直接报错。 convert_sklearn 把模型序列化成 ONNX 计算图,onnx.save 落地成 "GBPUSD DAILY.onnx"。实盘前你应在 Python 侧用 onnxruntime 跑一次相同输入,比对输出与 sklearn 原生 predict 是否一致,避免转换精度漂移。外汇与贵金属杠杆高,模型信号仅作概率参考,不能直接当入场指令。

MQL5 / C++
class="kw">import onnx
from skl2onnx class="kw">import convert_sklearn
from skl2onnx.common.data_types class="kw">import FloatTensorType
model = tuner.best_estimator_.fit(train.loc[:,[“Open”,“High”,“Low”,“Close”]],train.loc[:,“Target”])
class="macro">#Define the class="kw">input shape of class="num">1,class="num">4
initial_type = [(&class="macro">#x27;float_input&class="macro">#x27;, FloatTensorType([class="num">1, class="num">4]))]
class="macro">#Specify the class="kw">input shape
onnx_model = convert_sklearn(model, initial_types=initial_type)
class="macro">#Save the onnx model
onnx.save(onnx_model,“GBPUSD DAILY.onnx”)

「把AI信号接进MT5实盘框架」

策略落地在日线周期,布林带中线被价格穿越视作趋势触发点,但裸信号噪声太大,所以用最高价与最低价各自的移动平均线拼出一条通道做过滤。只有当两条均线同时穿过布林中带,且本地ONNX模型也给出同方向预测,才认定为有效入场。 平仓逻辑以移动平均通道是否退回布林带内为准,或者通道突破后重新回到带内即离场,取先发生者。代码中用全局布尔量 patience 控制:建仓时若通道尚未突破中带,patience 置 true,直到通道真正突破才翻 false,回落后才允许平仓,避免被毛刺洗出去。 回测用策略测试器跑了约3年 GBPUSD 日线数据。模型训练集覆盖2016–2024年,因此这次回测本质是在模型见过的样本上验证,余额曲线仍出现大幅起落。说明即便训练充分,AI也不会像人一样‘记住’行情,它只学到一种泛化公式,在已知数据上也可能出错。外汇与贵金属杠杆高,这类AI策略实盘前务必自行在MT5重跑验证。 下面这段是EA头部的资源与全局定义,可直接拷进 MetaEditor 改路径用。注意 onnx_buffer 指向的是 \Files\ 下的模型文件,换品种要同步换模型。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 GBPUSD AI.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">//| Load our ONNX file                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource  "\\Files\\GBPUSD DAILY.onnx" as class="kw">const class="type">uchar onnx_buffer[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Libraries                                                           |
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">bool   patience = true;
class="type">long   onnx_model;
class="type">int    bb_handler,ma_h_handler,ma_l_handler;
class="type">class="kw">double ma_h_buffer[],ma_l_buffer[];
class="type">class="kw">double bb_h_buffer[],bb_m_buffer[],bb_l_buffer[];
class="type">int    state;
class="type">class="kw">double bid,ask;
vectorf model_forecast = vectorf::Zeros(class="num">1);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| User Inputs                                                        |

常见问题

这是深度双重下降的典型现象,误差会在临界区先升后崩;建议把轮次按 2 的幂次(128/256/512)离散搜索,避开临界带再定型。
用 2 的幂次映射训练轮次做粗筛,再在表现区做窄区间细搜;每次保留验证集独立切分,不拿测试集调参。
可以,小布能按 2 的幂次帮你排程训练轮次搜索,并标出误差先升后崩的临界区,你只需确认最终模型。
把调好的模型导出为 ONNX,再用对应框架做轻量推理封装,就能在交易终端里喂实时行情出信号。
把模型输出接进既有信号框架的入口,用固定阈值转多空状态,再走你原用的下单与风控模块即可。