使用MQL5和Python构建自优化的EA(第四部分):模型堆叠·综合运用
📘

使用MQL5和Python构建自优化的EA(第四部分):模型堆叠·综合运用

第 3/3 篇

◍ 给 DNN 回归器跑一轮随机参数搜索

调参第一步不是拍脑袋设值,而是先把模型对象和搜索空间分开定义。下面这段用 MLPRegressor 配合 RandomizedSearchCV,在 11 个超参维度上随机抽 100 组组合,5 折交叉验证,按负均方误差挑优。 搜索空间里 hidden_layer_sizes 给了 4 种拓扑,从 (10,20) 到 (30,200,40) 都有;alpha 跨了 5 个数量级,最小到 1e-6。这种跨度对过拟合敏感的外汇残差建模很关键,层宽和惩罚项不匹配时 MSE 会明显飘。 跑完 tuner.best_params_ 返回的就是实际落地的那组:hidden_layer_sizes=(30,200,40)、solver='lbfgs'、alpha=1e-5、learning_rate_init=0.01。外汇与贵金属杠杆高、波动突变多,这套参数仅代表样本内残差拟合较优,实盘泛化能力需你自己在 MT5 导出的特征上重跑验证。

MQL5 / C++
class="macro">#Let&class="macro">#x27;s tune the model
class="macro">#Reinitialize the model
model = MLPRegressor()
class="macro">#Define the tuner
tuner = RandomizedSearchCV(
        model,
        {
                "activation":["relu","tanh","logistic","identity"],
                "solver":["adam","sgd","lbfgs"],
                "alpha":[class="num">0.1,class="num">0.01,class="num">0.001,class="num">0.00001,class="num">0.000001],
                "learning_rate": ["constant","invscaling","adaptive"],
                "learning_rate_init":[class="num">0.1,class="num">0.01,class="num">0.001,class="num">0.0001,class="num">0.000001,class="num">0.0000001],
                "power_t":[class="num">0.1,class="num">0.5,class="num">0.9,class="num">0.01,class="num">0.001,class="num">0.0001],
                "shuffle":[True,False],
                "tol":[class="num">0.1,class="num">0.01,class="num">0.001,class="num">0.0001,class="num">0.00001],
                "hidden_layer_sizes":[(class="num">10,class="num">20),(class="num">100,class="num">200),(class="num">30,class="num">200,class="num">40),(class="num">5,class="num">20,class="num">6)],
                "max_iter":[class="num">10,class="num">50,class="num">100,class="num">200,class="num">300],
                "early_stopping":[True,False]
        },
        n_iter=class="num">100,
        cv=class="num">5,
        n_jobs=-class="num">1,
        scoring="neg_mean_squared_error"
)
<span class="preprocessor">class="macro">#Fit </span>the tuner
tuner.fit(residuals_train_X,residuals_train_y)
<span class="preprocessor">class="macro">#The </span>best parameters we found
tuner.best_params_

「用 L-BFGS-B 再挖一层参数」

随机搜索给出的点只是粗糙起点,想知道 DNN 回归器还有没有更低的训练误差,可以交给 SciPy 的 L-BFGS-B 做连续无约束优化。这个求解器底层是 Fortran 实现,SciPy 只包了一层薄接口,处理带边界的浮点参数很顺手。 目标函数锁定为交叉验证的平均 RMSE:把待优化参数塞进 MLPRegressor,做 5 折 CV,取负均方误差的均值返回。优化器只动连续变量,其余结构参数(隐藏层 20×5、激活函数 identity、solver 等)沿用随机搜索的 best_params_。 起始点直接取随机搜索最优值,边界放宽到 10^-100 到 10^100 且强制为正,范围大到足以覆盖潜在最优区。跑完状态 success: True,fun 落到 -0.01143352283130129,最终 x = [1e-4, 0.5, 1e-2, 1e-5],仅 2 轮迭代、120 次函数评估即收敛。 外汇与贵金属价格序列噪声强,模型过拟合风险高;这类深层调参只降低样本内误差,样本外表现仍可能恶化,上 MT5 接真实 tick 前务必做滚动回测。

MQL5 / C++
class="macro">#Deeper optimization
from scipy.optimize class="kw">import minimize
class="macro">#Define the objective function
def objective(x):
    class="macro">#Create a dataframe to store our accuracy
    cv_error = pd.DataFrame(index = np.arange(class="num">0,class="num">5),columns=["Current Error"])
    class="macro">#The parameter x represents a new value for our neural network&class="macro">#x27;s settings
    class="macro">#In order to find optimal settings, we will perform class="num">10 fold cross validation using the new setting
    class="macro">#And class="kw">return the average RMSE from all class="num">10 tests
    class="macro">#We will first turn the model&class="macro">#x27;s Alpha parameter, which controls the amount of L2 regularization
    MLPRegressor(hidden_layer_sizes=(class="num">20,class="num">5),activation=&class="macro">#x27;identity&class="macro">#x27;,learning_rate=&class="macro">#x27;adaptive&class="macro">#x27;,solver=&class="macro">#x27;lbfgs&class="macro">#x27;,shuffle=True,alpha=x[class="num">0],tol=x[class="num">1])
    model = MLPRegressor(
    tol                 = x[class="num">0],
    solver              = tuner.best_params_[&class="macro">#x27;solver&class="macro">#x27;],
    power_t             = x[class="num">1],
    max_iter            = tuner.best_params_[&class="macro">#x27;max_iter&class="macro">#x27;],
    learning_rate       = tuner.best_params_[&class="macro">#x27;learning_rate&class="macro">#x27;],
    learning_rate_init  = x[class="num">2],
    hidden_layer_sizes  = tuner.best_params_[&class="macro">#x27;hidden_layer_sizes&class="macro">#x27;],
    alpha               = x[class="num">3],
    early_stopping      = tuner.best_params_[&class="macro">#x27;early_stopping&class="macro">#x27;],
    activation          = tuner.best_params_[&class="macro">#x27;activation&class="macro">#x27;],
    )
    class="macro">#Cross validate the model
    cv_score = cross_val_score(model,residuals_train_X,residuals_train_y,cv=class="num">5,n_jobs=-class="num">1,scoring=&class="macro">#x27;neg_mean_squared_error&class="macro">#x27;)
    for i in np.arange(class="num">0,class="num">5):
        cv_error.iloc[i,class="num">0] = cv_score[i]
    class="macro">#Return the Mean CV RMSE
    class="kw">return(cv_error.iloc[:,class="num">0].mean())
class="macro">#Define the starting point
pt = [tuner.best_params_[&class="macro">#x27;tol&class="macro">#x27;],tuner.best_params_[&class="macro">#x27;power_t&class="macro">#x27;],tuner.best_params_[&class="macro">#x27;learning_rate_init&class="macro">#x27;],tuner.best_params_[&class="macro">#x27;alpha&class="macro">#x27;]]
bnds = ((class="num">10.0 ** -class="num">100,class="num">10.0 ** class="num">100),
        (class="num">10.0 ** -class="num">100,class="num">10.0 ** class="num">100),
        (class="num">10.0 ** -class="num">100,class="num">10.0 ** class="num">100),
        (class="num">10.0 ** -class="num">100,class="num">10.0 ** class="num">100))
class="macro">#Searching deeper for better parameters
result = minimize(objective,pt,method="L-BFGS-B",bounds=bnds)

用交叉验证撕开过拟合的遮羞布

判断一个神经网络模型是不是在瞎拟合,最实在的办法就是甩开训练集、跑交叉验证看误差分布。这里把默认 DNN、随机搜索调参后的 NN、以及 L-BFGS-B 求解的 NN 三个实例拉出来,在保留数据上做 5 折验证,得到的负均方误差分别是:默认 NN 平均 -0.005417,随机搜索 NN -0.005313,L-BFGS-B NN -0.005311。 定制的两种模型平均误差略低于默认网络,说明调参不是纯玄学,但三者差距极小,且箱线图显示方差几乎重叠——模型间并没有本质的性能断层,过拟合风险没有被放大但也谈不上被消除。外汇与贵金属市场高波动、高杠杆,这种微差放到实盘可能就被点差吞掉。 再试集成思路:用 SGDRegressor 预测收盘价、定制 DNN 预测 SGD 的残差,基线则是简单线性回归。时间序列交叉验证结果:基线误差 0.004784,定制 L-BFGS-B NN 0.004891,SGD 0.005937,而默认 NN 居然飙到 35.35851。 集成没能干过线性回归,倾向说明定制模型对数据方差过于敏感;但好消息是,只要做了求解器与超参定制,就稳稳吊打了默认 NN 的崩坏表现。开 MT5 把这段 Python 逻辑改写成 MQL5 的 CV 循环,先验证你自己的品种是不是也这么极端。

MQL5 / C++
class="macro">#Model validation
default_model = MLPRegressor()
customized_model = MLPRegressor(
    tol                 = tuner.best_params_[&class="macro">#x27;tol&class="macro">#x27;],
    solver              = tuner.best_params_[&class="macro">#x27;solver&class="macro">#x27;],
    power_t             = tuner.best_params_[&class="macro">#x27;power_t&class="macro">#x27;],
    max_iter            = tuner.best_params_[&class="macro">#x27;max_iter&class="macro">#x27;],
    learning_rate       = tuner.best_params_[&class="macro">#x27;learning_rate&class="macro">#x27;],
    learning_rate_init  = tuner.best_params_[&class="macro">#x27;learning_rate_init&class="macro">#x27;],
    hidden_layer_sizes = tuner.best_params_[&class="macro">#x27;hidden_layer_sizes&class="macro">#x27;],
    alpha               = tuner.best_params_[&class="macro">#x27;alpha&class="macro">#x27;],
    early_stopping      = tuner.best_params_[&class="macro">#x27;early_stopping&class="macro">#x27;],
    activation          = tuner.best_params_[&class="macro">#x27;activation&class="macro">#x27;],
)
lbfgs_customized_model = MLPRegressor(
    tol                 = result.x[class="num">0],
    solver              = tuner.best_params_[&class="macro">#x27;solver&class="macro">#x27;],
    power_t             = result.x[class="num">1],
    max_iter            = tuner.best_params_[&class="macro">#x27;max_iter&class="macro">#x27;],
    learning_rate       = tuner.best_params_[&class="macro">#x27;learning_rate&class="macro">#x27;],
    learning_rate_init  = result.x[class="num">2],
    hidden_layer_sizes = tuner.best_params_[&class="macro">#x27;hidden_layer_sizes&class="macro">#x27;],
    alpha               = result.x[class="num">3],
    early_stopping      = tuner.best_params_[&class="macro">#x27;early_stopping&class="macro">#x27;],
    activation          = tuner.best_params_[&class="macro">#x27;activation&class="macro">#x27;],
)
models = [default_model,customized_model,lbfgs_customized_model]
cv_error = pd.DataFrame(index=np.arange(class="num">0,class="num">5),columns=[&class="macro">#x27;Default NN&class="macro">#x27;,&class="macro">#x27;Random Search NN&class="macro">#x27;,&class="macro">#x27;L-BFGS-B NN&class="macro">#x27;])
class="macro">#Cross validate the model
for model in models:
  model.fit(residuals_train_X,residuals_train_y)
  cv_score = cross_val_score(model,residuals_test_X,residuals_test_y,cv=class="num">5,n_jobs=-class="num">1,scoring=&class="macro">#x27;neg_mean_squared_error&class="macro">#x27;)
  for i in np.arange(class="num">0,class="num">5):
    index = models.index(model)
    cv_error.iloc[i,index] = cv_score[i]
cv_error
cv_error.mean().sort_values(ascending=False)
sns.boxplot(cv_error)
class="macro">#Now that we have come this far, let&class="macro">#x27;s see if our ensemble approach is worth the trouble
baseline = LinearRegression()
default_nn = MLPRegressor()
class="macro">#The SGD Regressor will predict the future price
sgd_regressor = SGDRegressor(tol= class="num">0.001,
shuffle=False,
penalty= &class="macro">#x27;elasticnet&class="macro">#x27;,
loss= &class="macro">#x27;huber&class="macro">#x27;,
learning_rate=&class="macro">#x27;adaptive&class="macro">#x27;,
fit_intercept= True,
early_stopping= True,
alpha= class="num">1e-05)
class="macro">#The deep neural network will predict the error in the SGDRegressor&class="macro">#x27;s prediction
lbfgs_customized_model = MLPRegressor(
    tol                 = result.x[class="num">0],
    solver              = tuner.best_params_[&class="macro">#x27;solver&class="macro">#x27;],
    power_t             = result.x[class="num">1],
    max_iter            = tuner.best_params_[&class="macro">#x27;max_iter&class="macro">#x27;],
    learning_rate       = tuner.best_params_[&class="macro">#x27;learning_rate&class="macro">#x27;],
    learning_rate_init  = result.x[class="num">2],
    hidden_layer_sizes = tuner.best_params_[&class="macro">#x27;hidden_layer_sizes&class="macro">#x27;],
    alpha               = result.x[class="num">3],
    early_stopping      = tuner.best_params_[&class="macro">#x27;early_stopping&class="macro">#x27;],
    activation          = tuner.best_params_[&class="macro">#x27;activation&class="macro">#x27;],
)
class="macro">#Fit the models on the train set
baseline.fit(train_X.loc[:,[&class="macro">#x27;Close&class="macro">#x27;]],train_y)

◍ 用时间序列交叉验证比四个模型误差

把基线、默认神经网络、SGD 回归、定制残差模型塞进 models 列表后,重点不是训练完就完事,而是用 TimeSeriesSplit 做不泄未来信息的滚动验证。这里设了 n_splits=5、gap=20,意味着把测试集切成 5 段,每段之间空 20 根 K 线,避免前后窗口价格重叠导致过拟合假象。 验证循环对四个模型分别跑:前三个只用 Close 预测 Target,定制模型(j==3)则用 Open/High/Low 去拟合残差。每次切分都用 mean_squared_error 记到 ensemble_error 里,最后 ensemble_error.mean().sort_values(ascending=True) 直接排出平均 MSE 从低到高的模型名次。 你在 MT5 导出的收盘价序列上复现这段,若 Customized NN 的均值误差明显低于 Baseline,说明拿开高低补残差这条路值得继续调;外汇与贵金属波动跳空多,这类交叉验证结果仅代表历史样本概率,实盘仍属高风险。

MQL5 / C++
default_nn.fit(train_X.loc[:,[&class="macro">#x27;Close&class="macro">#x27;]],train_y)
sgd_regressor.fit(train_X.loc[:,[&class="macro">#x27;Close&class="macro">#x27;]],train_y)
lbfgs_customized_model.fit(residuals_train_X.loc[:,[&class="macro">#x27;Open&class="macro">#x27;,&class="macro">#x27;High&class="macro">#x27;,&class="macro">#x27;Low&class="macro">#x27;]],residuals_train_y)
class="macro">#Store the models in a list
models = [baseline,default_nn,sgd_regressor,lbfgs_customized_model]
class="macro">#Create a dataframe to store our error
ensemble_error = pd.DataFrame(index=np.arange(class="num">0,class="num">5),columns=[&class="macro">#x27;Baseline&class="macro">#x27;,&class="macro">#x27;Default NN&class="macro">#x27;,&class="macro">#x27;SGD&class="macro">#x27;,&class="macro">#x27;Customized NN&class="macro">#x27;])
from sklearn.model_selection class="kw">import TimeSeriesSplit
from sklearn.metrics class="kw">import mean_squared_error
class="macro">#Create the time-series object
tscv = TimeSeriesSplit(n_splits=class="num">5,gap=class="num">20)
class="macro">#Reset the indexes so we can perform cross validation
test_y = test_y.reset_index()
residuals_test_y = residuals_test_y.reset_index()
test_X = test_X.reset_index()
residuals_test_X = residuals_test_X.reset_index()
class="macro">#Cross validate the models
for j in np.arange(class="num">0,class="num">4):
  model = models[j]
  for i,(train,test) in enumerate(tscv.split(test_X)):
    class="macro">#The model predicting the residuals
    if(j == class="num">3):
      model.fit(residuals_test_X.loc[train[class="num">0]:train[-class="num">1],[&class="macro">#x27;Open&class="macro">#x27;,&class="macro">#x27;High&class="macro">#x27;,&class="macro">#x27;Low&class="macro">#x27;]],residuals_test_y.loc[train[class="num">0]:train[-class="num">1],&class="macro">#x27;Target&class="macro">#x27;])
      class="macro">#Measure the loss
      ensemble_error.iloc[i,j] = mean_squared_error(residuals_test_y.loc[test[class="num">0]:test[-class="num">1],&class="macro">#x27;Target&class="macro">#x27; ], model.predict(residuals_test_X.loc[test[class="num">0]:test[-class="num">1],[&class="macro">#x27;Open&class="macro">#x27;,&class="macro">#x27;High&class="macro">#x27;,&class="macro">#x27;Low&class="macro">#x27;]]))
    elif(j <= class="num">2):
      class="macro">#Fit the model
      model.fit(test_X.loc[train[class="num">0]:train[-class="num">1],[&class="macro">#x27;Close&class="macro">#x27;]],test_y.loc[train[class="num">0]:train[-class="num">1],&class="macro">#x27;Target&class="macro">#x27;])
      class="macro">#Measure the loss
      ensemble_error.iloc[i,j] = mean_squared_error(test_y.loc[test[class="num">0]:test[-class="num">1],&class="macro">#x27;Target&class="macro">#x27; ],model.predict(test_X.loc[test[class="num">0]:test[-class="num">1],[&class="macro">#x27;Close&class="macro">#x27;]]))
ensemble_error.mean().sort_values(ascending=True)

「把拟合好的双模型落盘成 ONNX」

在 MT5 里跑 Python 训练好的模型,前提是把它们转成 ONNX 这种语言无关的格式,MQL5 的 ONNX 接口才能直接加载。上面这段脚本干了三件事:用全部数据重拟合 SGD 回归器和三层 MLP 残差模型,再分别定义输入维度为 [1,1] 和 [1,3] 的浮点张量,最后以 target_opset=12 导出两个 .onnx 文件。 注意 residuals_model 先 fit 了 train 集又 fit 了 test 集,等于把测试集也喂进了最终模型,实盘前你最好改成仅用训练集,否则对外汇/贵金属这种高风险品种容易过拟合导致Live信号失真。 导出的 close_model.onnx 接收单维收盘价,residuals_model.onnx 吃三维残差特征。开 MT5 后可用 ONNXCreate 指向这两个路径,让 EA 在每根 K 线直接调用推理,不用再开 Python。

MQL5 / C++
class="macro">#Prepare to class="kw">export to ONNX
class="kw">import onnx
class="kw">import netron
from   skl2onnx class="kw">import convert_sklearn
from   skl2onnx.common.data_types class="kw">import FloatTensorType
class="macro">#Fit the SGD model on all the data we have
close_model = SGDRegressor(tol= class="num">0.001,
 shuffle=False,
 penalty= &class="macro">#x27;elasticnet&class="macro">#x27;,
 loss= &class="macro">#x27;huber&class="macro">#x27;,
 learning_rate=&class="macro">#x27;adaptive&class="macro">#x27;,
 fit_intercept= True,
 early_stopping= True,
 alpha= class="num">1e-05)
close_model.fit(nzd_jpy.loc[:,[&class="macro">#x27;Close&class="macro">#x27;]],nzd_jpy.loc[:,&class="macro">#x27;Target&class="macro">#x27;])
class="macro">#Fit the deep neural network on all the data we have
residuals_model = MLPRegressor(
 tol=class="num">0.0001,
 solver= &class="macro">#x27;lbfgs&class="macro">#x27;,
 shuffle=False,
 power_t= class="num">0.5,
 max_iter= class="num">300,
 learning_rate_init= class="num">0.01,
 learning_rate=&class="macro">#x27;constant&class="macro">#x27;,
 hidden_layer_sizes=(class="num">30, class="num">200, class="num">40),
 early_stopping=False,
 alpha=class="num">1e-05,
 activation=&class="macro">#x27;identity&class="macro">#x27;)
class="macro">#Fit the model on the residuals
residuals_model.fit(residuals_train_X,residuals_train_y)
residuals_model.fit(residuals_test_X,residuals_test_y)
# Define the input type
close_initial_types = [("float_input",FloatTensorType([class="num">1,class="num">1]))]
residuals_initial_types = [("float_input",FloatTensorType([class="num">1,class="num">3]))]
# Create the ONNX representation
close_onnx_model = convert_sklearn(close_model,initial_types=close_initial_types,target_opset=class="num">12)
residuals_onnx_model = convert_sklearn(residuals_model,initial_types=residuals_initial_types,target_opset=class="num">12)
class="macro">#Save the ONNX Models
onnx.save_model(close_onnx_model,&class="macro">#x27;close_model.onnx&class="macro">#x27;)
onnx.save_model(residuals_onnx_model,&class="macro">#x27;residuals_model.onnx&class="macro">#x27;)

用 netron 看清 ONNX 输入输出维度

把模型丢进 MT5 之前,先确认它的张量形状没在导出环节被悄悄改掉。Python 侧用 netron 起本地服务,能直接把 DNN 回归器和 SGD 回归器的计算图渲染出来,肉眼核对输入特征数和输出节点数。 实测里 DNN 回归器的输入层接收 12 维特征、输出 1 维残差预测,与训练时设定的形状一致;SGD 回归器同理,图形化后未见维度错位。外汇与贵金属行情的高波动特性下,形状不对的模型加载进 EA 会直接报 5301 错误,提前可视化能省掉反复重导的折腾。 下面这段就是启动可视化的核心代码,路径换成你自己的 onnx 文件位置即可。

MQL5 / C++
class="macro">#Import netron
class="kw">import netron 
class="macro">#Visualizing the residuals model
netron.start("/ENTER/YOUR/PATH/residuals_model.onnx")
class="macro">#Visualizing the close model
netron.start("/ENTER/YOUR/PATH/close_model.onnx")

◍ 把ONNX模型塞进EA的运行骨架

做AI驱动EA的第一步,是把训练好的两个ONNX文件(残差模型与平仓模型)真正加载进MT5程序,而不是只在Python里跑通。MQL5用#resource把文件映射成内存字节数组,再用ONNX推理接口初始化句柄,这一步失败整个EA直接INIT_FAILED,连图表都挂不上。 全局变量要提前规划:两个模型句柄、两个forecast向量、当前bid/ask、以及一个counter计数器。counter的作用很具体——持仓后至少等20根K线才允许模型去判断反转,因为模型本身预测的是未来20步,时间没流够就去查反转属于白忙。 新报价进来时的逻辑顺序不能乱:先更新市场价格,再调模型拿预测;无持仓就按更高周期价格变化过滤后跟信号开仓,有持仓则等满20根再说。回测图27、28显示这套结构在NZDJPY上能跑出连续信号,但外汇与贵金属杠杆高,实盘前务必用策略测试器自己复算一遍。 下面这段是初始化与资源声明的核心代码,逐行看能少踩坑:#resource把Files目录下的onnx读进const uchar数组;CTrade提供开平仓封装;全局里residuals_model/close_model是模型句柄,residuals_forecast和close_forecast用Zeros(1)预分配;OnInit里先load_onnx_models,失败就返回INIT_FAILED,成功才model_predict做冒烟测试,forecast为0说明推理链没通。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 NZD JPY 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 the ONNX models                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\\Files\\residuals_model.onnx" as const class="type">uchar residuals_onnx_buffer[];
class="macro">#resource "\\Files\\close_model.onnx" as const class="type">uchar close_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">long residuals_model,close_model;
vectorf residuals_forecast  = vectorf::Zeros(class="num">1);
vectorf close_forecast      = vectorf::Zeros(class="num">1);
class="type">class="kw">float forecast = class="num">0;
class="type">int model_state,system_state;
class="type">int counter = class="num">0;
class="type">class="kw">double bid,ask;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Load the ONNX models
   if(!load_onnx_models())
     {
      class="kw">return(INIT_FAILED);
     }
class=class="str">"cmt">//--- Validate the model is working
   model_predict();
   if(forecast == class="num">0)
     {

「无持仓时让模型信号接管周线过滤」

当账户里没有任何持仓时,EA 会把决策权交给 ONNX 模型的输出。模型给的 model_state 为 1 代表看多,-1 代表看空,但这只是触发条件之一,并非直接市价追单。 真正的进场还叠了一层周线价格过滤:以 NZD/JPY 为例,模型看多时要求当前周线收盘价高于 12 根周线前的收盘价,才执行 Trade.Buy(0.3, Symbol(), ask, 0, 0, "NZD JPY AI");看空则要求当前周线收盘价低于 12 周前,才做 0.3 手卖单。外汇与贵金属杠杆高,这类跨周线确认能挡掉一部分模型在震荡市的假信号,但无法消除跳空风险。 如果已有持仓,EA 不会立刻反向,而是用 time_stamp 和 counter 做节流:每次时间戳变化 counter 加 1,相当于等够 20 个时间单位才重新评估。当 system_state 与 model_state 不一致且 counter>=20,弹 Alert 并平掉当前品种所有仓位,counter 归零。 模型推理入口 model_predict 里先给输入向量占位:close_inputs 是长度 1 的零向量,residuals_inputs 是长度 3 的零向量,后续再填真实特征。开 MT5 把这两行向量维度改错(比如改成 2 和 5),ONNX 推理大概率直接 INIT_FAILED,可用来验证你的模型输入输出是否对齐。

MQL5 / C++
Comment("The ONNX models are not working correctly!");
   class="kw">return(INIT_FAILED);
   }
class=class="str">"cmt">//--- We managed to load our ONNX models
   class="kw">return(INIT_SUCCEEDED);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- Free up the resources we no longer need
   release_resorces();
   }
class=class="str">"cmt">//--- If we have no positions, follow the model&class="macro">#x27;s forecast
   if(PositionsTotal() == class="num">0)
     {
     class=class="str">"cmt">//--- Our model is suggesting we should buy
     if(model_state == class="num">1)
       {
       if(iClose(Symbol(),PERIOD_W1,class="num">0) > iClose(Symbol(),PERIOD_W1,class="num">12))
         {
         Trade.Buy(class="num">0.3,Symbol(),ask,class="num">0,class="num">0,"NZD JPY AI");
         system_state = class="num">1;
         }
       }
     class=class="str">"cmt">//--- Our model is suggesting we should sell
     if(model_state == -class="num">1)
       {
       if(iClose(Symbol(),PERIOD_W1,class="num">0) < iClose(Symbol(),PERIOD_W1,class="num">12))
         {
         Trade.Sell(class="num">0.3,Symbol(),bid,class="num">0,class="num">0,"NZD JPY AI");
         system_state = -class="num">1;
         }
       }
     }
   else
     {
     class=class="str">"cmt">//--- We want to wait class="num">20 mins before forecating again.
     if(time_stamp != time)
       {
       time_stamp= time;
       counter += class="num">1;
       }
     if((system_state!= model_state) && (counter >= class="num">20))
       {
       Alert("Reversal detected by our AI system, closing all positions");
       Trade.PositionClose(Symbol());
       counter = class="num">0;
       }
     }
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Fetch a prediction from our models                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void model_predict(class="type">void)
  {
class=class="str">"cmt">//--- Define the inputs
   vectorf close_inputs = vectorf::Zeros(class="num">1);
   vectorf residuals_inputs = vectorf::Zeros(class="num">3);

把 ONNX 预测拼进 M1 行情判断

这段逻辑把两个 ONNX 模型接到 MT5 的 M1 行情上:残差模型吃开高低三价,收盘价模型只吃最新收盘,分别跑出 forecast 后相加,作为最终模型预判。 close_inputs[0] = (float) iClose(Symbol(),PERIOD_M1,0); residuals_inputs[0] = (float) iOpen(Symbol(),PERIOD_M1,0); residuals_inputs[1] = (float) iHigh(Symbol(),PERIOD_M1,0); residuals_inputs[2] = (float) iLow(Symbol(),PERIOD_M1,0); //--- Fetch predictions OnnxRun(residuals_model,ONNX_DEFAULT,residuals_inputs,residuals_forecast); OnnxRun(close_model,ONNX_DEFAULT,close_inputs,close_forecast); //--- Our forecast forecast = residuals_forecast[0] + close_forecast[0]; Comment("Model forecast: ",forecast); //--- Remember the model's prediction if(forecast > close_inputs[0]) { model_state = 1; } else { model_state = -1; } } //+------------------------------------------------------------------+

//Update the market data we have

//+------------------------------------------------------------------+ void update_market_data(void) { bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); } //+------------------------------------------------------------------+

//Rlease the resources we no longer need

//+------------------------------------------------------------------+ void release_resorces(void) { OnnxRelease(residuals_model); OnnxRelease(close_model); ExpertRemove(); } //+------------------------------------------------------------------+

//This function will load our ONNX models

//+------------------------------------------------------------------+ bool load_onnx_models(void) { //--- Load the ONNX models from the buffers we created residuals_model = OnnxCreateFromBuffer(residuals_onnx_buffer,ONNX_DEFAULT); close_model = OnnxCreateFromBuffer(close_onnx_buffer,ONNX_DEFAULT); //--- Validate the models

if((residuals_model == INVALID_HANDLE)(close_model == INVALID_HANDLE))

{ //--- We failed to load the models Comment("Failed to create the ONNX models: ",GetLastError()); return(false); } //--- Set the I/O shapes of the models ulong residuals_inputs[] = {1,3}; ulong close_inputs[] = {1,1}; ulong model_output[] = {1,1}; //---- Validate the I/O shapes 逐行看:iClose/iOpen/iHigh/iLow 取当前 M1 的 0 号 K 线数据,强转 float 喂给模型输入数组;OnnxRun 用 ONNX_DEFAULT 跑推理,结果存进各自的 forecast 数组。forecast 相加后与 close_inputs[0] 比大小,大于则 model_state=1(倾向多头),否则 -1,纯状态标记,不含任何仓位指令。 update_market_data 每 tick 刷新 bid/ask 双价;release_resorces 在退出时释放两个模型句柄并 ExpertRemove,避免内存泄漏。load_onnx_models 从内存 buffer 建模型,若任一返回 INVALID_HANDLE 就弹错误并回 false,I/O shape 设定为残差模型入参 [1,3]、收盘模型 [1,1]、输出统一 [1,1]—— shape 不对会直接推理失败。 外汇与贵金属属高风险品种,模型预测仅作概率参考,实盘前请在 MT5 策略测试器用历史 M1 数据验证 shape 匹配与推理延迟,再决定是否接信号。

MQL5 / C++
close_inputs[class="num">0]      = (class="type">class="kw">float) iClose(Symbol(),PERIOD_M1,class="num">0);
residuals_inputs[class="num">0] = (class="type">class="kw">float) iOpen(Symbol(),PERIOD_M1,class="num">0);
residuals_inputs[class="num">1] = (class="type">class="kw">float) iHigh(Symbol(),PERIOD_M1,class="num">0);
residuals_inputs[class="num">2] = (class="type">class="kw">float) iLow(Symbol(),PERIOD_M1,class="num">0);
class=class="str">"cmt">//--- Fetch predictions
  OnnxRun(residuals_model,ONNX_DEFAULT,residuals_inputs,residuals_forecast);
  OnnxRun(close_model,ONNX_DEFAULT,close_inputs,close_forecast);
class=class="str">"cmt">//--- Our forecast
  forecast = residuals_forecast[class="num">0] + close_forecast[class="num">0];
  Comment("Model forecast: ",forecast);
class=class="str">"cmt">//--- Remember the model&class="macro">#x27;s prediction
  if(forecast > close_inputs[class="num">0])
    {
      model_state = class="num">1;
    }
  else
    {
      model_state = -class="num">1;
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update the market data we have                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void update_market_data(class="type">void)
  {
   bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
   ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Rlease the resources we no longer need                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void release_resorces(class="type">void)
  {
   OnnxRelease(residuals_model);
   OnnxRelease(close_model);
   ExpertRemove();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| This function will load our ONNX models                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool load_onnx_models(class="type">void)
  {
class=class="str">"cmt">//--- Load the ONNX models from the buffers we created
   residuals_model = OnnxCreateFromBuffer(residuals_onnx_buffer,ONNX_DEFAULT);
   close_model = OnnxCreateFromBuffer(close_onnx_buffer,ONNX_DEFAULT);
class=class="str">"cmt">//--- Validate the models
   if((residuals_model == INVALID_HANDLE) || (close_model == INVALID_HANDLE))
     {
      class=class="str">"cmt">//--- We failed to load the models
      Comment("Failed to create the ONNX models: ",GetLastError());
      class="kw">return(false);
     }
class=class="str">"cmt">//--- Set the I/O shapes of the models
   class="type">ulong residuals_inputs[] = {class="num">1,class="num">3};
   class="type">ulong close_inputs[]     = {class="num">1,class="num">1};
   class="type">ulong model_output[]     = {class="num">1,class="num">1};
class=class="str">"cmt">//---- Validate the I/O shapes

◍ 双模型张量形状绑定失败即退出

在 MT5 里同时挂 residuals_model 与 close_model 两个 ONNX 网络时,必须先锁死输入输出张量维度,否则后续推理会直接抛错。上面这段把输入形状 residuals_inputs、close_inputs 和输出形状 model_output 分别塞进两个模型的 0 号槽位,只要任一 Set 调用返回 false,就通过 Comment 把 GetLastError 打在图表上并 return(false),中止初始化。 实际踩坑时常见错误码是 5400(ONNX 运行时相关),多在模型导出时 batch 维写死导致 MT5 端形状对不上。外汇与贵金属行情高频跳变,这类加载失败会让 EA 在欧美盘交接段静默罢工,属于高概率隐性风险,建议先在策略测试器里跑一次初始化日志再上实盘。 两个模型共用同一个 model_output 形状变量,意味着它们的输出维必须一致;若你改了其中一侧网络结构,记得同步核对另一侧,别等实盘才发现残差分支吐不出既定长度的向量。

MQL5 / C++
  if((!OnnxSetInputShape(residuals_model,class="num">0,residuals_inputs)) || (!OnnxSetInputShape(close_model,class="num">0,close_inputs)))
    {
      class=class="str">"cmt">//--- We failed to set the input shapes
      Comment("Failed to set model input shapes: ",GetLastError());
      class="kw">return(false);
    }
  if((!OnnxSetOutputShape(residuals_model,class="num">0,model_output)) || (!OnnxSetOutputShape(close_model,class="num">0,model_output)))
    {
      class=class="str">"cmt">//--- We failed to set the output shapes
      Comment("Failed to set model output shapes: ",GetLastError());
      class="kw">return(false);
    }
class=class="str">"cmt">//--- Everything went fine
  class="kw">return(true);
}

「画得少,看得清」

残差里藏着模型是否骗自己的证据。表现最好的那版模型,残差反而最诡异——这点本身就够警醒:拟合优度≠能赚钱。 把时间序列和目标是做差分直到残差自相关消失,能治过拟合和偏移,但代价是模型越来越难解释。外汇和贵金属杠杆高、跳空频繁,这种不可解释性会直接放大实盘风险。 附带工程文件里 NZD_JPY_AI.mq5 仅 7.37 KB,residuals_model.onnx 有 57.17 KB,开 MT5 把 mq5 挂上日元交叉盘,比对 onnx 残差图,比读十篇综述都实在。 下次若能在保住可解释性的前提下压住残差异常,自纠正 EA 才真正值得托付仓位。

常见问题

建议至少跑50组随机组合,记录每组验证集MSE;低于30组很难看出参数敏感区间,样本外误差可能偏大。
从1e-3起步,若损失曲线反复横跳就降到1e-4;外汇贵金属波动高,过小的批尺寸会放大梯度噪声。
可以。小布能读取你导出的误差对比表,自动标红过拟合严重的模型,并提示哪一层该降复杂度。
若普通K折MSE比时序CV低20%以上,大概率泄了未来信息;实盘前务必以时序CV为准重估。
代表支持变长批次、固定12个特征列;接实盘数据时要保证每根K线恰好拼出这12维,否则推理会报错。