使用MQL5和Python构建自优化的EA(第四部分):模型堆叠·综合运用
◍ 给 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 导出的特征上重跑验证。
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 前务必做滚动回测。
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 循环,先验证你自己的品种是不是也这么极端。
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,说明拿开高低补残差这条路值得继续调;外汇与贵金属波动跳空多,这类交叉验证结果仅代表历史样本概率,实盘仍属高风险。
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。
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 文件位置即可。
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说明推理链没通。
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,可用来验证你的模型输入输出是否对齐。
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 匹配与推理延迟,再决定是否接信号。
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 形状变量,意味着它们的输出维必须一致;若你改了其中一侧网络结构,记得同步核对另一侧,别等实盘才发现残差分支吐不出既定长度的向量。
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 才真正值得托付仓位。