基于MQL5和Python的自优化EA(第六部分):利用深度双重下降算法·进阶篇
迭代次数翻倍后误差反而先升后崩
把前面搜到的最优参数套回去:用 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 以上:
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 环境跑一遍这段标签设定,能直观看到横轴非线性分布对损失曲线的视觉压缩效果。
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_ 会回吐上面那组最优字典,可立即拿去重训模型做对比。
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 是否一致,避免转换精度漂移。外汇与贵金属杠杆高,模型信号仅作概率参考,不能直接当入场指令。
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\ 下的模型文件,换品种要同步换模型。
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 |