重塑经典策略(第六部分):多时间框架分析·综合运用
⏱️

重塑经典策略(第六部分):多时间框架分析·综合运用

(3/3)· 从参数调优到MQL5落地,把日线小时线冲突变成20分钟EURUSD收盘概率

实战向进阶 第 3/3 篇
很多人把多时间框架当成「大周期定方向、小周期找入场」的口诀背,却从没量化过两层周期的相关性到底有多弱。本文用 EURUSD 的 M1 与 M15 实测数据告诉你,跨周期信号不是理所当然的护身符,而是需要模型重新校准的噪声源。

梯度提升回归器的参数网格与交叉验证

这段配置把决策树类超参数铺成网格,让搜索器在 5 折交叉验证下跑 1000 次迭代找最优组合。max_leaf_nodes 从 2 到 90 再加不限制,min_impurity_decrease 按 0、1、10 再到 10 的 2/3/4 次方递增,跨度覆盖了欠拟合与极深树两种极端。 评分函数锁死 neg_mean_squared_error,意味着调优目标是在验证集上把价格回归的均方误差压到最低,return_train_score 设 False 可省掉训练集分数的冗余计算。 跑完 tuner.fit 后,用 cv_results_ 拉出 DataFrame,按 mean_test_score 降序排,就能直接看到哪组参数在测试折上误差最小;tuner.best_params 给出的就是本次搜索落点。外汇与贵金属波动具有高风险,该结果仅代表历史样本内的拟合倾向,实盘可能失效。

MQL5 / C++
"max_leaf_nodes": [class="num">2,class="num">3,class="num">4,class="num">5,class="num">10,class="num">20,class="num">50,class="num">90,None],
"min_impurity_decrease": [class="num">0,class="num">1,class="num">10,(class="num">10.0 ** class="num">2),(class="num">10.0 ** class="num">3),(class="num">10.0 ** class="num">4)]
				},
				cv=class="num">5,
				n_iter=class="num">1000,
				return_train_score=False,
				scoring="neg_mean_squared_error"
				)
class="macro">#Tune the GradientBoostingRegressor
tuner.fit(train_X,train_y)
end_time = time.time()
print(f"Process completed in {end_time - start_time} seconds.")
class="macro">#Let&class="macro">#x27;s observe the results
tuner_results = pd.DataFrame(tuner.cv_results_)
params = ["param_loss",
				"param_learning_rate",
				"param_n_estimators",
				"param_max_depth",
				"param_min_samples_split",
				"param_criterion",
				"param_min_samples_leaf",
				"param_max_features",
				"param_max_leaf_nodes",
				"param_min_impurity_decrease",
				"param_min_weight_fraction_leaf",
				"mean_test_score"]
tuner_results.loc[:,params].sort_values(by="mean_test_score",ascending=False)
class="macro">#Best parameters we found
tuner.best_params

◍ 用 SciPy 把 GBR 参数逼到训练误差谷底

做参数精修不必只靠网格搜索。借助科学计算里的优化库,可以直接把目标函数交给求解器,让它在给定边界内搜出更低训练误差的系数组合。 GBR 里有些参数不能为负,而优化器默认会试探负值,所以必须显式传 bounds。同时 L-BFGS-B 这类方法需要起点,稳妥做法是把上一轮调参器的终点作为初始点 pt,避免乱跑。 跑完之后求解器回传的状态里有一行关键数:fun: 0.0005766670348377334,意味着交叉验证平均误差压到了约 5.77e-4;对应的 x 是 [5.586e-06, 0.4, 0.1, 0.01],即 min_weight_fraction_leaf≈0.0000056、min_samples_split=0.4、min_samples_leaf=0.1、learning_rate=0.01。 别把正态当圣经:训练误差最小不代表样本外也稳,外汇与贵金属波动有跳空和流动性断裂,过度拟合训练集在实盘可能放大回撤,属于高风险行为。

MQL5 / C++
class="macro">#Let&class="macro">#x27;s see if we can&class="macro">#x27;t find better parameters
class="macro">#We may be overfitting the training data!
from scipy.optimize class="kw">import minimize
class="macro">#Define the objective function
def objective(x):
    class="macro">#Create a dataframe to store our new accuracy
    current_error = pd.DataFrame(index=[class="num">0],columns=["error"])
    class="macro">#x is an array of possible values to use for our Gradient Boosting Regressor
    model = GradientBoostingRegressor(n_estimators=class="num">500,
                                      min_impurity_decrease=class="num">1,
                                      max_leaf_nodes=class="num">10,
                                      max_features=class="num">2,
                                      max_depth=class="num">3,
                                      loss="absolute_error",
                                      criterion="friedman_mse",
                                      min_weight_fraction_leaf=x[class="num">0],
                                      min_samples_split=x[class="num">1],
                                      min_samples_leaf=x[class="num">2],
                                      learning_rate=x[class="num">3])
    model.fit(train_X.loc[:,:],train_y.loc[:])
    current_error.iloc[class="num">0,class="num">0] = root_mean_squared_error(train_y.loc[:],model.predict(train_X.loc[:,:]))
    class="macro">#Record our progress
    mean_error = current_error.loc[:].mean()
    class="macro">#Return the average error
    class="kw">return mean_error
class="macro">#Let&class="macro">#x27;s optimize these parameters again
class="macro">#Fist define the bounds
bounds = ((class="num">0.0,class="num">0.5),(class="num">0.3,class="num">0.5),(class="num">0.001,class="num">0.2),(class="num">0.001,class="num">0.1))
class="macro">#Then define the starting points for the L-BFGS-B algorithm
pt = np.array([tuner.best_params_["min_weight_fraction_leaf"],
               tuner.best_params_["min_samples_split"],
               tuner.best_params_["min_samples_leaf"],
               tuner.best_params_["learning_rate"]
               ])
lbfgs = minimize(objective,pt,bounds=bounds,method="L-BFGS-B")
lbfgs

「调参反而跑不赢线性基准」

把定制 GBR 和默认 GBR、纯线性模型放在同一验证集上比精度,结果有点反直觉:线性模型误差 0.00043166,默认 GBR 是 0.00057361,随机搜索定制的 GBR 为 0.00059133,再叠 L-BFGS-B 优化也只到 0.00059148。也就是说,花力气调参的树模型不仅没超过默认配置,连最朴素的线性拟合都没追上。 外汇与贵金属行情里这类现象很常见——非线性模型容易在样本内过拟合,换到验证集就露怯,杠杆品种的高波动会放大这种偏差,实盘前务必用 Out-of-Sample 数据卡一道。 演示层面我们继续用默认 GBR 跑流程,但落回你自己的 MT5 特征工程时,别迷信复杂结构:先拿线性回归垫底做 baseline,任何模型精度没显著低于 0.00043 这个量级,大概率只是徒增计算成本。

MQL5 / C++
class="macro">#Let us now see how well we&class="macro">#x27;re performing on the validation set
linear_regression = LinearRegression()
default_gbr = GradientBoostingRegressor()
grid_search_gbr = GradientBoostingRegressor(n_estimators=class="num">500,
                                              min_impurity_decrease=class="num">1,
                                              max_leaf_nodes=class="num">10,
                                              max_features=class="num">2,
                                              max_depth=class="num">3,
                                              loss="absolute_error",
                                              criterion="friedman_mse",
                                              min_weight_fraction_leaf=class="num">0,
                                              min_samples_split=class="num">0.4,
                                              min_samples_leaf=class="num">0.1,
                                              learning_rate=class="num">0.01
                                              )
lbfgs_grid_search_gbr = GradientBoostingRegressor(
                                              n_estimators=class="num">500,
                                              min_impurity_decrease=class="num">1,
                                              max_leaf_nodes=class="num">10,
                                              max_features=class="num">2,
                                              max_depth=class="num">3,
                                              loss="absolute_error",
                                              criterion="friedman_mse",
                                              min_weight_fraction_leaf=lbfgs.x[class="num">0],
                                              min_samples_split=lbfgs.x[class="num">1],

四种回归模型的误差实测对照

把同一份训练集分别喂给线性回归、默认梯度提升、随机搜索梯度提升,以及用 L-BFGS-B 优化的随机搜索梯度提升,最后统一用根均方误差(RMSE)在测试集上打分。 代码里 min_samples_leaf 和 learning_rate 直接取 lbfgs.x[2]、lbfgs.x[3],说明叶节点最小样本数与学习率已由优化器给出具体数值,不再是拍脑袋默认值。 四个模型的 predict 结果都送进 root_mean_squared_error 做横向比对,你能直接在 MT5 的 Python 环境里复跑这段,看哪一组 RMSE 最低。外汇与贵金属行情具有高杠杆与跳空风险,模型误差只代表历史样本拟合倾向,实盘信号务必小仓位验证。

MQL5 / C++
min_samples_leaf=lbfgs.x[class="num">2],
              learning_rate=lbfgs.x[class="num">3]
              )
class="macro">#Linear Regression
linear_regression.fit(train_X,train_y)
root_mean_squared_error(test_y,linear_regression.predict(test_X))
class="macro">#Default Gradient Boosting Regressor
default_gbr.fit(train_X,train_y)
root_mean_squared_error(test_y,default_gbr.predict(test_X))
class="macro">#Random Search Gradient Boosting Regressor
grid_search_gbr.fit(train_X,train_y)
root_mean_squared_error(test_y,grid_search_gbr.predict(test_X))
class="macro">#L-BFGS-B Random Search Gradient Boosting Regressor
lbfgs_grid_search_gbr.fit(train_X,train_y)
root_mean_squared_error(test_y,lbfgs_grid_search_gbr.predict(test_X))

◍ 把梯度提升模型导出成 ONNX 计算图

ONNX 本质是一张由节点和边构成的计算图:节点承载数学运算,边描述数据怎么流。把训练好的模型转成这个格式,就能在 MetaTrader 5 的 EA 里直接跑推理,不必在终端重写算法。 导出前先解决缩放问题。MT5 侧要能复现同样的标准化,最省事的做法是对每个输入列减均值、除标准差。下面这段代码先把均值和标准差存进 scale_factors 表,再原地改写 market_data,两步必须和 EA 内的预处理严格一致,否则推理会偏。 模型本身用的是 GradientBoostingRegressor,在全部数据上拟合。注释里提到它没跑赢线性回归,但出于演示仍选它导出。convert_sklearn 的 target_opset=12 决定了计算图算子集版本,MT5 加载时要对应得上。 最后 onnx.save 落盘为 GBR_M1_MultipleTF_Float.onnx。开 MT5 加载前,先确认你的输入特征顺序、缩放因子和这个文件训练时用的完全一致——外汇与贵金属杠杆高,模型偏移会放大实盘风险。

MQL5 / C++
class="macro">#We failed to beat the linear regression model, in such cases we should pick the linear model!
class="macro">#However for demonstrational purposes we&class="macro">#x27;ll pick the gradient boosting regressor
class="macro">#Let&class="macro">#x27;s class="kw">export the class="kw">default GBR to ONNX format
from skl2onnx.common.data_types class="kw">import FloatTensorType
from skl2onnx class="kw">import convert_sklearn
class="kw">import onnx
class="macro">#We need to save the scale factors for our inputs
scale_factors = pd.DataFrame(index=["mean","standard deviation"],columns=all_predictors)
for i in np.arange(class="num">0,len(all_predictors)):
    scale_factors.iloc[class="num">0,i] = market_data.iloc[:,i+class="num">2].mean()
    scale_factors.iloc[class="num">1,i] = market_data.iloc[:,i+class="num">2].std()
    market_data.iloc[:,i+class="num">2] = ((market_data.iloc[:,i+class="num">2] - market_data.iloc[:,i+class="num">2].mean()) / market_data.iloc[:,i+class="num">2].std())
scale_factors
class="macro">#Define our initial types
initial_types = [("float_input",FloatTensorType([class="num">1,test_X.shape[class="num">1]]))]
class="macro">#Fit the model on all the data we have
model = GradientBoostingRegressor().fit(market_data.loc[:,all_predictors],market_data.loc[:,"Target"])
class="macro">#Create the ONNX representation
onnx_model = convert_sklearn(model,initial_types=initial_types,target_opset=class="num">12)
class="macro">#Now save the ONNX model
onnx_model_name = "GBR_M1_MultipleTF_Float.onnx"
onnx.save(onnx_model,onnx_model_name)

「用 Netron 看清 ONNX 模型结构」

Netron 是一款开源的机器学习模型可视化工具,当前对框架的支持范围仍有限,但会随着库迭代逐步扩展。对我们而言,关键价值在于导入训练好的模型后,能直接核对输入输出的张量形状是否符合 EA 预期。 从实际截图看,梯度提升回归器的 ONNX 模型输入与输出维度同设计一致,这步验证通过,才敢继续拿它去搭 MT5 的 EA 逻辑。 启动方式极简:先引入 netron 库,再传入模型文件名即可在本机拉起可视化页面,肉眼确认节点连接与属性。

MQL5 / C++
class="macro">#Import netron so we can visualize the model
class="kw">import netron
netron.start(onnx_model_name)

把ONNX模型塞进EA的骨架

在MT5里跑AI信号,第一步是把训练好的ONNX模型作为资源编译进EA。上面这段代码用 #resourceGBR_M1_MultipleTF_Float.onnx 读进 onnx_model_buffer[],后续初始化时再喂给推理接口,省去运行时外部路径依赖。 用户可调参数只留了两个:max_risk=20(盈亏临界,达到即平仓)和 sl_width=1(止损宽度基准)。这种极简输入设计意味着模型权重大部分固化在ONNX里,调参空间小,但过拟合风险也低。 全局变量里 mean_variance[9]std_variance[9] 是训练时的缩放因子,必须和推理输入同分布,否则模型输出会漂移。注意 model_inputs 长度硬编码为9,和原文说的「获取训练时缩放因子」一一对应——你改特征数就得同步改这两个数组维度。 外汇与贵金属带杠杆,这类多时间框架EA在周线趋势反转时可能瞬间平掉所有持仓,实盘前务必用1个月M1数据回测(原文图18即该窗口结果)确认滑点下的表现。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                Multiple Time Frame.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">//| Require the onnx file                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\\Files\\GBR_M1_MultipleTF_Float.onnx" as class="kw">const class="type">uchar onnx_model_buffer[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Libraries we need                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Trade/Trade.mqh>
CTrade Trade;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Inputs                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">input class="type">class="kw">double max_risk = class="num">20;          class=class="str">"cmt">//How much profit/loss should we allow before closing
class="kw">input class="type">class="kw">double sl_width = class="num">1;           class=class="str">"cmt">//How wide should out sl be?
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global variables                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">long onnx_model;                     class=class="str">"cmt">//Our onnx model
class="type">class="kw">double mean_variance[class="num">9],std_variance[class="num">9];  class=class="str">"cmt">//Our scaling factors
vector model_forecast = vector::Zeros(class="num">1); class=class="str">"cmt">//Model forecast
vector model_inputs = vector::Zeros(class="num">9);   class=class="str">"cmt">//Model inputs

◍ 用周线定方向、M1 等反转的进出场骨架

这套逻辑把趋势过滤和信号触发拆成了两层:周线(PERIOD_W1)收盘价站上 20 周前收盘价,才允许偏多;跌破则偏空。state 变量记录当前持仓倾向,1 为多、2 为空,初始为 0 表示空仓等待。 check_reversal 里有两个强平触发点:一是模型预测价 model_forecast[0] 与 M1 实时收盘价 iClose(Symbol(),PERIOD_M1,0) 发生反向交叉(state==1 且预测低于现价,或 state==2 且预测高于现价),直接弹 Alert 并平掉当前品种仓位;二是持仓浮亏绝对值超过 max_risk 也强制平仓。外汇与贵金属杠杆高,max_risk 设宽设窄直接决定回撤幅度,建议先在策略测试器里用历史数据跑一遍看触发频率。 find_entry 只在周线方向确认后动手:周线收阳且模型预测高于 M1 的 20 周期收盘价,就以 ask 市价 Buy,止损止盈都取 sl_width 距离,订单备注写 "Multiple Time Frames AI",state 置 1。下方对称分支处理周线收阴的做空逻辑。 让小布替你跑这套:把 lot_multiple=20 和 sl_width 按自己经纪商最小止损规则改掉,否则 OrderSend 可能直接 reject。

MQL5 / C++
class="type">class="kw">double ask,bid;                      class=class="str">"cmt">//Market prices
class="type">class="kw">double trading_volume;               class=class="str">"cmt">//Our trading volume
class="type">int lot_multiple = class="num">20;               class=class="str">"cmt">//Our lot size
class="type">int state = class="num">0;                       class=class="str">"cmt">//System state
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check reversal                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_reversal(class="type">void)
  {
class=class="str">"cmt">//--- Check for reversal
   if(((state == class="num">1) && (model_forecast[class="num">0] < iClose(Symbol(),PERIOD_M1,class="num">0))) || ((state == class="num">2) && (model_forecast[class="num">0] > iClose(Symbol(),PERIOD_M1,class="num">0))))
     {
       Alert("Reversal predicted.");
       Trade.PositionClose(Symbol());
     }
class=class="str">"cmt">//--- Check if we have breached our maximum risk levels
   if(MathAbs(PositionGetDouble(POSITION_PROFIT) > max_risk))
     {
       Alert("We&class="macro">#x27;ve breached our maximum risk level.");
       Trade.PositionClose(Symbol());
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Find an entry                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void find_entry(class="type">void)
  {
class=class="str">"cmt">//--- Analyse price action on the weekly time frame
   if(iClose(Symbol(),PERIOD_W1,class="num">0) > iClose(Symbol(),PERIOD_W1,class="num">20))
     {
      class=class="str">"cmt">//--- We are riding bullish momentum
      if(model_forecast[class="num">0] > iClose(Symbol(),PERIOD_M1,class="num">20))
        {
         class=class="str">"cmt">//--- Enter a buy
         Trade.Buy(trading_volume,Symbol(),ask,(ask - sl_width),(ask + sl_width),"Multiple Time Frames AI");
         state = class="num">1;
        }
     }
class=class="str">"cmt">//--- Analyse price action on the weekly time frame
   if(iClose(Symbol(),PERIOD_W1,class="num">0) < iClose(Symbol(),PERIOD_W1,class="num">20))
     {
      class=class="str">"cmt">//--- We are riding bearish momentum
      if(model_forecast[class="num">0] < iClose(Symbol(),PERIOD_M1,class="num">20))
        {

「EURUSD 多周期缩放因子与预测输入」

做跨周期 AI 信号之前,得先把各周期价格变化归一到同一量纲。load_scaling_factors 里写死了 EURUSD 的均值与标准差,O/H/L/C 四项均值落在 1.0929~1.0931 之间,标准差约 0.0018,这是当前品种历史波动的锚。 M5 到 H1 的 change 均值被压到 10^-5 量级(例如 M15 为 1.972e-5),标准差 0.001~0.0038;D1 change 均值 -8.78e-4、标准差 0.0317,日线噪声明显大于日内。直接拿原始价差喂模型会淹没小周期信号。 model_predict 用 (iClose - mean) / std 做 z-score,PERIOD_CURRENT 的收盘价分别减 mean_variance[0..3] 再除对应 std。归一后输入落在 0 附近的窄区间,模型对偏离常态的 close 更敏感。 外汇与贵金属杠杆高、滑点跳空频繁,这套硬编码均值只适配 EURUSD 历史段;换品种或遇结构行情须重算,否则信号会系统性偏移。

MQL5 / C++
class="type">void update_market_prices(class="type">void)
  {
  ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
  bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
  }
class="type">void load_scaling_factors(class="type">void)
  {
class=class="str">"cmt">//--- EURUSD OHLC
  mean_variance[class="num">0] = class="num">1.0930010861272836;
  std_variance[class="num">0] = class="num">0.0017987600829890852;
  mean_variance[class="num">1] = class="num">1.0930721822927123;
  std_variance[class="num">1] = class="num">0.001810556238082839;
  mean_variance[class="num">2] = class="num">1.092928371812889;
  std_variance[class="num">2] = class="num">0.001785041172362313;
  mean_variance[class="num">3] = class="num">1.093000590242923;
  std_variance[class="num">3] = class="num">0.0017979420556511476;
class=class="str">"cmt">//--- M5 Change
  mean_variance[class="num">4] = (MathPow(class="num">10.0,-class="num">5) * class="num">1.4886568962056413);
  std_variance[class="num">4] = class="num">0.000994902152654042;
class=class="str">"cmt">//--- M15 Change
  mean_variance[class="num">5] = (MathPow(class="num">10.0,-class="num">5) * class="num">1.972093957036524);
  std_variance[class="num">5] = class="num">0.0017104874192072138;
class=class="str">"cmt">//--- M30 Change
  mean_variance[class="num">6] = (MathPow(class="num">10.0,-class="num">5) * class="num">1.5089339490060967);
  std_variance[class="num">6] = class="num">0.002436078407827825;
class=class="str">"cmt">//--- H1 Change
  mean_variance[class="num">7] = class="num">0.0001529512146155358;
  std_variance[class="num">7] = class="num">0.0037675774501395387;
class=class="str">"cmt">//--- D1 Change
  mean_variance[class="num">8] = -class="num">0.0008775667536639223;
  std_variance[class="num">8] = class="num">0.03172437243836734;
  }
class="type">void model_predict(class="type">void)
  {
class=class="str">"cmt">//--- EURD OHLC
  model_inputs[class="num">0] = ((iClose(Symbol(),PERIOD_CURRENT,class="num">0) - mean_variance[class="num">0]) / std_variance[class="num">0]);
  model_inputs[class="num">1] = ((iClose(Symbol(),PERIOD_CURRENT,class="num">0) - mean_variance[class="num">1]) / std_variance[class="num">1]);

多周期价差标准化与ONNX模型装载

把当前价格塞进模型输入数组时,前两个维度用的是当前周期收盘价相对各自均值和标准差的Z-score;从索引4开始,逻辑换成跨周期价差——取M5到D1的「当根K收价减20根前收价」,再减该序列均值后除以标准差。 模型输入共9列(索引0~8),其中索引4~8分别对应M5、M15、M30、H1、D1五个周期。这种写法让模型同时看到短中长周期的动量偏离,贵金属与外汇品种在跨周期共振时概率上更容易出方向。 调用 OnnxRun 前必须保证模型已从缓冲区建好且输入输出维度匹配。load_onnx_file 里写死了 input_shape 为 {1,9}、output_shape 为 {1,1},若你训练出的模型不是这个结构,OnnxSetInputShape 会返回 false 并弹 Alert 报出实际计数。 开MT5把这段直接贴进EA,先打印 OnnxGetInputCount 确认是9再跑推理,能省掉一半调试时间。

MQL5 / C++
  model_inputs[class="num">2] = ((iClose(Symbol(),PERIOD_CURRENT,class="num">0) - mean_variance[class="num">2]) / std_variance[class="num">2]);
  model_inputs[class="num">3] = ((iClose(Symbol(),PERIOD_CURRENT,class="num">0) - mean_variance[class="num">3]) / std_variance[class="num">3]);
class=class="str">"cmt">//--- M5 CAHNGE
  model_inputs[class="num">4] = (((iClose(Symbol(),PERIOD_M5,class="num">0) - iClose(Symbol(),PERIOD_M5,class="num">20)) - mean_variance[class="num">4]) / std_variance[class="num">4]);
class=class="str">"cmt">//--- M15 CHANGE
  model_inputs[class="num">5] = (((iClose(Symbol(),PERIOD_M15,class="num">0) - iClose(Symbol(),PERIOD_M15,class="num">20)) - mean_variance[class="num">5]) / std_variance[class="num">5]);
class=class="str">"cmt">//--- M30 CHANGE
  model_inputs[class="num">6] = (((iClose(Symbol(),PERIOD_M30,class="num">0) - iClose(Symbol(),PERIOD_M30,class="num">20)) - mean_variance[class="num">6]) / std_variance[class="num">6]);
class=class="str">"cmt">//--- H1 CHANGE
  model_inputs[class="num">7] = (((iClose(Symbol(),PERIOD_H1,class="num">0) - iClose(Symbol(),PERIOD_H1,class="num">20)) - mean_variance[class="num">7]) / std_variance[class="num">7]);
class=class="str">"cmt">//--- D1 CHANGE
  model_inputs[class="num">8] = (((iClose(Symbol(),PERIOD_D1,class="num">0) - iClose(Symbol(),PERIOD_D1,class="num">20)) - mean_variance[class="num">8]) / std_variance[class="num">8]);
class=class="str">"cmt">//--- Fetch forecast
  OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_forecast);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load our onnx file                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool load_onnx_file(class="type">void)
  {
class=class="str">"cmt">//--- Create the model from the buffer
  onnx_model = OnnxCreateFromBuffer(onnx_model_buffer,ONNX_DEFAULT);
class=class="str">"cmt">//--- Set the class="kw">input shape
  class="type">class="kw">ulong input_shape [] = {class="num">1,class="num">9};
class=class="str">"cmt">//--- Check if the class="kw">input shape is valid
  if(!OnnxSetInputShape(onnx_model,class="num">0,input_shape))
    {
      Alert("Incorrect class="kw">input shape, model has class="kw">input shape ", OnnxGetInputCount(onnx_model));
      class="kw">return(class="kw">false);
    }
class=class="str">"cmt">//--- Set the output shape
  class="type">class="kw">ulong output_shape [] = {class="num">1,class="num">1};
class=class="str">"cmt">//--- Check if the output shape is valid
  if(!OnnxSetOutputShape(onnx_model,class="num">0,output_shape))
    {
      Alert("Incorrect output shape, model has output shape ", OnnxGetOutputCount(onnx_model));
      class="kw">return(class="kw">false);
    }
class=class="str">"cmt">//--- Everything went fine
  class="kw">return(true);
  }

◍ EA 生命周期里怎么挂 ONNX 模型

把训练好的 ONNX 模型塞进 MT5 专家顾问,关键不在预测逻辑本身,而在初始化、反初始化和逐 tick 调度的三段钩子怎么写。OnInit 里先 load_onnx_file(),失败直接返回 INIT_FAILED,模型加载和缩放因子读取必须放在交易前完成。 trading_volume 那行用了 SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN) 乘 lot_multiple,意味着下单手数永远以当前品种最小合约单位为基准放大,规避了硬编码手数在交叉盘或贵金属上超最小变动单位的报错。外汇与贵金属杠杆高,最小交易量随品种浮动,这种写法能少踩不少坑。 OnDeinit 里 OnnxRelease(onnx_model) 紧跟 ExpertRemove(),不手动释放模型句柄的话,MT5 终端反复加载 EA 可能泄漏内存,回测跑几万根 K 线后卡死的概率会明显上升。 OnTick 的结构很直白:每次报价先 model_predict() 把预测值刷出来,用 Comment 打到图表上方便肉眼核对;无持仓就 find_entry(),有持仓就 check_reversal() 让模型判断是否反转。想验证这套调度,直接把下面代码贴进 MT5 的 EA 模板,接自己的 onnx 文件和缩放参数就能跑。

MQL5 / C++
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 file
   if(!load_onnx_file())
     {
class=class="str">"cmt">//--- We failed to load our onnx model
       class="kw">return(INIT_FAILED);
     }
class=class="str">"cmt">//--- Load scaling factors
   load_scaling_factors();
class=class="str">"cmt">//--- Get trading volume
   trading_volume = SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN) * lot_multiple;
class=class="str">"cmt">//--- Everything went fine
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//--- Release the resources we used for our onnx model
   OnnxRelease(onnx_model);
class=class="str">"cmt">//--- Release the expert advisor
   ExpertRemove();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//--- We always need a prediction from our model
   model_predict();
class=class="str">"cmt">//--- Show the model forecast
   Comment("Model forecast ",model_forecast);
class=class="str">"cmt">//--- Fetch market prices
   update_market_prices();
class=class="str">"cmt">//--- If we have no open positions, find an entry
   if(PositionsTotal() == class="num">0)
     {
class=class="str">"cmt">//--- Find entry
       find_entry();
class=class="str">"cmt">//--- Update state
       state = class="num">0;
     }
class=class="str">"cmt">//--- If we have an open position, manage it
   else
     {
class=class="str">"cmt">//--- Check if our AI is predicting a reversal
       check_reversal();
     }
  }

「把这条线请下神坛」

这套跨周期 AI 驱动的 EA 只用了最朴素的 OHLC 四价就跑出了比堆指标更高的拟合精度,附带的 Fetch_Multiple_Time_Frame.mq5(2.11 KB)和 Multiple_Time_Frame.mq5(9.57 KB)已经把多周期拉取和推理骨架留给你了。 更高周期没挂任何指标、没喂成交量与价差,是作者主动留的白;评论区里他亲口说克服过拟合得靠更大数据集,只是笔记本算力不够才没在文中练。 开 MT5 把 ZIP 里的 GBR_M1_MultipleTF_Float.onnx(53.52 KB)丢进专家目录,先原样跑一遍看 M1 配合上层周期的信号分布,再自己加一列价差字段重训,外汇与贵金属杠杆高、模型漂移快,任何优势都只是概率倾向而非确定性。

把跨周期特征工程交给小布
这些多周期价格水平提取与 ONNX 导出的诊断流程,小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时特征相关性,你只需判断模型输出是否值得跟。

常见问题

线性回归误差低但无可调参数,GBR 可在过拟合测试后调树深与学习率,有机会逼近甚至超过线性基准,适合需要迭代优化的实战环境。
可以,小布已内置跨周期特征提取与模型可视化模块,打开 EURUSD 页能看到 M1/M15 相关性与模型输出,省去自己写终端数据抓取。
该策略轻视与高周期相反的低周期波动,交易者等高周期明牌反转,而反转初期只显现在低周期,等待期就容易吃满反向摆动。
ONNX 脱离 Python 训练环境,MQL5 通过内置 API 加载推理,重点是把 M15 价格变化特征按训练时的窗口与归一化方式对齐再传入。
除 EURUSD 样本内,建议拿同时段的 GBPUSD 与 XAUUSD 做品种外回测,贵金属与交叉盘的高周期惯性结构不同,能暴露周期权重偏置。