重构经典策略(第五部分):基于USDZAR的多品种分析·综合运用
📘

重构经典策略(第五部分):基于USDZAR的多品种分析·综合运用

第 3/3 篇

把模型转成ONNX并在MT5里复现标准化

ONNX 是一套与编程语言无关的模型交换框架,模型一旦导出成这个格式,就能在支持 ONNX API 的环境里直接调用,不必绑死在 Python 训练端。对做贵金属和外汇策略的人来说,这意味着训练好的信号模型可以无缝交给 MetaTrader 5 终端执行,而不用每次都开 Python 重算。 关键前提是:训练时做的缩放和标准化,必须能在 MT5 里用相同参数重放。否则终端拿到的实时特征和训练分布错位,模型输出会漂移。做法是对每个特征列先减均值让数据居中,再除以该列标准差,把不同量纲的变量拉到可比较的尺度。 下面这段 Python 先把每列均值和标准差存进 scale_factors 表,这两行数字就是后面 MQL5 里要写死的常量。外汇与贵金属杠杆高、滑点突变频繁,标准化参数若用样本内数据外推,实盘仍可能失效,需留样本外验证。 代码逐行看:导入 onnx 与 skl2onnx 相关库,准备把 sklearn 模型转格式;建一个只有 mean 和 standard deviation 两行的 DataFrame;循环遍历所有特征列,分别填均值和标准差;最后把表打印出来备导出。

MQL5 / C++
Import the libraries we need
class="macro">#Let&class="macro">#x27;s prepare to class="kw">export the customized model to ONNX format
class="kw">import onnx
from skl2onnx class="kw">import convert_sklearn
from skl2onnx.common.data_types class="kw">import FloatTensorType
class="macro">#Train the model on all the data we have
class="macro">#But before doing that we need to first scale the data in a way we can repeat in MQL5
scale_factors = pd.DataFrame(columns=all_predictors,index=["mean","standard deviation"])
for i in np.arange(class="num">0,len(all_predictors)):
    scale_factors.iloc[class="num">0,i] = merged_df.loc[:,all_predictors[i]].mean()
    scale_factors.iloc[class="num">1,i] = merged_df.loc[:,all_predictors[i]].std()
scale_factors

「把特征压进标准尺度再导出ONNX」

做模型前先把每个预测列做Z-score处理,公式是(值-均值)/标准差,这样不同量纲的特征不会让KNN被数值大的列带偏。图11里能看到缩放后数据集中在零附近,但原文没贴全列,验证时自己print头部即可。 代码里用循环对all_predictors逐列标准化,再拿网格搜索rs.best_params_里的p、weights、n_neighbors、leaf_size、algorithm去实例化KNeighborsRegressor并fit目标列Target。外汇与贵金属数据的高波动特性意味着这套流程在USDZAR这类成对货币上可能过拟合,实盘前需在MT5导出的其他周期重跑。 模型训完用FloatTensorType([1,train_X.shape[1]])定义输入形态,convert_sklearn转成ONNX,存为USDZAR_FLOAT_M1.onnx。你可以直接把这段搬去自己的notebook,把all_predictors换成MT5用零代码导出的指标列名就能生成可部署模型文件。

MQL5 / C++
for i in all_predictors:
    merged_df.loc[:,i] = (merged_df.loc[:,i] - merged_df.loc[:,i].mean()) / merged_df.loc[:,i].std()
merged_df
customized_model = KNeighborsRegressor(p=rs.best_params_["p"],weights=rs.best_params_["weights"],n_neighbors=rs.best_params_["n_neighbors"],leaf_size=rs.best_params_["leaf_size"],algorithm=rs.best_params_["algorithm"])
customized_model.fit(merged_df.loc[:,all_predictors],merged_df.loc[:,"Target"])
class="macro">#Define the class="kw">input shape and type
initial_type = [("float_tensor_type",FloatTensorType([class="num">1,train_X.shape[class="num">1]]))]
class="macro">#Create an ONNX representation
onnx_model = convert_sklearn(customized_model,initial_types=initial_type)
class="macro">#Store the ONNX model
onnx_model_name = "USDZAR_FLOAT_M1.onnx"
onnx.save_model(onnx_model,onnx_model_name)

◍ 用 Netron 核对 ONNX 输入输出规格

Netron 是开源的机器学习模型可视化工具,不只认 ONNX,也支持 Keras 等框架。在把模型塞进 EA 之前,先拿它确认输入输出张量的形状与排布是否和设想一致,能省掉后面大半调试时间。 代码里先 import netron,再调用 netron.start(onnx_model_name) 把模型文件丢进本地可视化服务。浏览器打开后能看到两张关键图:一张是模型规格(输入输出的名字、维度、数据类型),一张是节点连接结构。 只要图里显示输入和输出格式准确落在预期位置,就说明这个 ONNX 模型可以直接拿去写 EA 了。外汇与贵金属行情高波动、模型误判会带来实盘亏损,验证环节不能跳。 别把可视化当走流程 很多交易者跳过 Netron 直接加载,结果 EA 跑起来报 shape mismatch。花两分钟看一眼节点图,比后面查三小时日志划算。

MQL5 / C++
class="macro">#Let&class="macro">#x27;s visualize the model in netron
class="kw">import netron
<span class="preprocessor">class="macro">#Run </span>netron
netron.start(onnx_model_name)

把ONNX模型塞进EA的骨架

在MT5里跑AI策略,第一步是把训练好的ONNX文件编译进EA。用 #resource.onnx 以字节数组形式打进 .ex5,模型随程序走,不依赖外部路径。 用户可调参数先定三个:止损宽度 sl_width=0.4、手数倍数 lot_multiple=10(以最小手数倍乘)、总盈亏平仓线 max_risk=10。这组默认值针对USDZAR这类宽幅货币对,换品种建议先回测再改。 全局变量里留了 onnx_model 句柄,以及两个长度12的数组 mean_valuesstd_values 存缩放因子——模型输入必须按训练时的均值标准差做标准化,否则预测会偏。外汇与贵金属杠杆高,模型误判可能快速放大亏损,实盘前务必在策略测试器验证。 辅助函数分四块:风控平仓(浮盈浮亏超 max_risk 自动清仓)、AI反转检测(预测不利则平仓并弹终端提醒)、入场过滤(模型信号需与更高周期价位变化吻合才开仓)、预测取数(取现价减均值除标准差后送模型)。多品种要把标的加进市场观察窗口,另写函数加载缩放因子与ONNX。 EA主逻辑不复杂:OnInit 加载模型与缩放值、OnDeinit 释放资源、OnTick 重新预测并管理持仓。下面这段是声明与全局定义,逐行拆完你就能照抄框架。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 USDZAR.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\\USDZAR_FLOAT_M1.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="type">class="kw">double class="kw">input sl_width = class="num">0.4;           class=class="str">"cmt">//How tight should our stop loss be?
class="type">int class="kw">input lot_multiple = class="num">10;           class=class="str">"cmt">//How many times bigger than minimum lot should we enter?
class="type">class="kw">double class="kw">input max_risk = class="num">10;            class=class="str">"cmt">//After how much profit/loss should we close?
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_values[class="num">12],std_values[class="num">12]; class=class="str">"cmt">//The scaling factors we used for our data

「把风控与反转判断接进 EA 主循环」

这段代码片段展示了一个 USDZAR 策略 EA 的核心状态容器与三个决策函数,直接决定了仓位生死。模型输入固定为 12 维向量,输出仅有 1 维预测值,说明特征工程被压得很窄,实战中容易对突发新闻钝化。 风险阻断函数 check_risk_level 只用 MathAbs(PositionGetDouble(POSITION_PROFIT)) 与 max_risk 比较,一旦浮亏或浮盈绝对值越过阈值就市价平仓。外汇与贵金属属高杠杆品种,这种硬砍方式可能截断趋势利润,也可能在跳空时滑点超限,建议把 max_risk 在策略测试器里按点值反推而非拍脑袋。 反转函数 check_reversal 的逻辑是:持多时若模型预测价低于 M1 收盘价,或持空时预测价高于 M1 收盘价,就平掉 USDZAR 并弹窗报警。它依赖模型前瞻能力,若预测滞后,平仓信号往往晚于肉眼可见的拐点 1~3 根 K 线。 找入口的 find_entry 用 D1 收盘对比 21 根前收盘定方向,只在日线走高时才允许模型预测价大于 M1 收盘价去 Buy,并写入 state=1。注意 sl_width 同时用作止盈与止损距离,属于对称包围单,震荡市容易被双向扫损。

MQL5 / C++
vector model_inputs = vector::Zeros(class="num">12);   class=class="str">"cmt">//Our model&class="macro">#x27;s inputs
vector model_forecast = vector::Zeros(class="num">1); class=class="str">"cmt">//Our model&class="macro">#x27;s output
class="type">class="kw">double bid,ask;                            class=class="str">"cmt">//Market prices
class="type">class="kw">double minimum_volume;                     class=class="str">"cmt">//Smallest lot size
class="type">class="kw">double state = class="num">0;                          class=class="str">"cmt">//class="num">0 means we have no positions, class="num">1 means we have buy position, class="num">2 means we have sell position.
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if we have reached our risk level                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_risk_level(class="type">void)
  {
class=class="str">"cmt">//--- Check if we have surpassed our maximum risk level
   if(MathAbs(PositionGetDouble(POSITION_PROFIT)) > max_risk)
     {
class=class="str">"cmt">//--- We should close our positions
      Trade.PositionClose("USDZAR");
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if there is a reversal may be coming                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_reversal(class="type">void)
  {
   if(((state == class="num">1) && (model_forecast[class="num">0] < iClose("USDZAR",PERIOD_M1,class="num">0))) ||((state == class="num">2) && (model_forecast[class="num">0] > iClose("USDZAR",PERIOD_M1,class="num">0))))
     {
class=class="str">"cmt">//--- There may be a reversal coming
      Trade.PositionClose("USDZAR");
class=class="str">"cmt">//--- Give the user feedback
      Alert("Potential reversal detected");
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Find an entry opportunity                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void find_entry(class="type">void)
  {
class=class="str">"cmt">//---Check for the change in price on higher timeframes
   if(iClose("USDZAR",PERIOD_D1,class="num">0) > iClose("USDZAR",PERIOD_D1,class="num">21))
     {
class=class="str">"cmt">//--- We&class="macro">#x27;re looking for buy oppurtunities
      if(model_forecast[class="num">0] > iClose("USDZAR",PERIOD_M1,class="num">0))
        {
class=class="str">"cmt">//--- Open the position
         Trade.Buy(minimum_volume,"USDZAR",ask,(ask - sl_width),(ask + sl_width),"USDZAR AI");
class=class="str">"cmt">//--- Update the system state
         state = class="num">1;
        }
     }
class=class="str">"cmt">//---Check for the change in price on higher timeframes
   else

◍ 把模型预测喂给 USDZAR 的卖单触发

这段逻辑只干一件事:当 USDZAR 日线收在 21 根前收盘价下方,系统判定处于偏空背景,只在模型分钟级预测低于当前 M1 收盘价时才丢出卖单。外汇与贵金属杠杆高,USDZAR 这种新兴市场货币对单日波动可能超 1.5%,实盘前务必在 MT5 策略测试器跑通。 下面这段是原文里的条件分支,逐行拆开看: if(iClose("USDZAR",PERIOD_D1,0) < iClose("USDZAR",PERIOD_D1,21)) // 日线最新收价低于 21 根前日收价,偏空滤网 if(model_forecast[0] < iClose("USDZAR",PERIOD_M1,0)) // 模型预测值低于当前 1 分钟收价 Trade.Sell(minimum_volume,"USDZAR",bid,(bid + sl_width),(bid - sl_width),"USDZAR AI"); // 以买价开最小手卖单,止损止盈各宽 sl_width state = 2; // 标记系统状态为 2,避免重复触发 模型输入侧用了 12 个标准化特征:USDZAR、WTI 原油(XTIUSD) 与黄金(XAUUSD) 各自的 M1 开高低收,全部减均值除标准差。标准化后数值通常在 -3 到 3 之间,喂给模型前建议打印 model_inputs 数组确认无 NaN。 别把正态当圣经 均值方差若用固定历史窗口算,遇央行干预或油价跳空时 std_values 会失真,模型输入可能瞬间拉到 ±5,这时候预测不可直接信。

MQL5 / C++
if(iClose("USDZAR",PERIOD_D1,class="num">0) < iClose("USDZAR",PERIOD_D1,class="num">21))
  {
   class=class="str">"cmt">//--- We&class="macro">#x27;re looking for sell oppurtunities
   if(model_forecast[class="num">0] < iClose("USDZAR",PERIOD_M1,class="num">0))
     {
      class=class="str">"cmt">//--- Open sell position
      Trade.Sell(minimum_volume,"USDZAR",bid,(bid + sl_width),(bid - sl_width),"USDZAR AI");
      class=class="str">"cmt">//--- Update the system state
      state = class="num">2;
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Obtain a forecast from our model                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void model_predict(class="type">void)
  {
class=class="str">"cmt">//Let&class="macro">#x27;s fetch our model&class="macro">#x27;s inputs
class=class="str">"cmt">//--- USDZAR
  model_inputs[class="num">0] = ((iOpen("USDZAR",PERIOD_M1,class="num">0) - mean_values[class="num">0]) / std_values[class="num">0]);
  model_inputs[class="num">1] = ((iHigh("USDZAR",PERIOD_M1,class="num">0) - mean_values[class="num">1]) / std_values[class="num">1]);
  model_inputs[class="num">2] = ((iLow("USDZAR",PERIOD_M1,class="num">0) - mean_values[class="num">2]) / std_values[class="num">2]);
  model_inputs[class="num">3] = ((iClose("USDZAR",PERIOD_M1,class="num">0) - mean_values[class="num">3]) / std_values[class="num">3]);
class=class="str">"cmt">//--- XTI OIL US
  model_inputs[class="num">4] = ((iOpen("XTIUSD",PERIOD_M1,class="num">0) - mean_values[class="num">4]) / std_values[class="num">4]);
  model_inputs[class="num">5] = ((iHigh("XTIUSD",PERIOD_M1,class="num">0) - mean_values[class="num">5]) / std_values[class="num">5]);
  model_inputs[class="num">6] = ((iLow("XTIUSD",PERIOD_M1,class="num">0) - mean_values[class="num">6]) / std_values[class="num">6]);
  model_inputs[class="num">7] = ((iClose("XTIUSD",PERIOD_M1,class="num">0) - mean_values[class="num">7]) / std_values[class="num">7]);
class=class="str">"cmt">//--- GOLD SA
  model_inputs[class="num">8] = ((iOpen("XAUUSD",PERIOD_M1,class="num">0) - mean_values[class="num">8]) / std_values[class="num">8]);
  model_inputs[class="num">9] = ((iHigh("XAUUSD",PERIOD_M1,class="num">0) - mean_values[class="num">9]) / std_values[class="num">9]);
  model_inputs[class="num">10] = ((iLow("XAUUSD",PERIOD_M1,class="num">0) - mean_values[class="num">10]) / std_values[class="num">10]);
  model_inputs[class="num">11] = ((iClose("XAUUSD",PERIOD_M1,class="num">0) - mean_values[class="num">11]) / std_values[class="num">11]);
class=class="str">"cmt">//--- Get a prediction

把三个品种塞进模型前的标准化准备

做跨品种推理前,得先把 XAUUSD、XTIUSD、USDZAR 三个符号加进市场报价窗,否则后面取不到实时数据。MT5 里用 SymbolSelect 带 true 参数就能静默添加,不需要手动在终端里点。 模型吃的是标准化后的 12 维序列(每个品种 4 步),所以加载阶段要把均值和标准差写死进数组。从实测数值看,黄金(XAUUSD)均值约 2430.5、标准差约 26.4,原油(XTIUSD)均值约 80.77、标准差约 0.98,美兰特(USDZAR)均值约 18.14、标准差约 0.113——这些量级差异很大,不归一化直接喂 ONNX 会偏置。 ONNX 模型从内存 buffer 创建,输入形状锁死为 {1,12}。若 OnnxSetInputShape 返回 false,说明模型实际输入维度和代码假设不符,直接 Comment 报错并退出,避免后面跑出静默 NaN。外汇与贵金属杠杆高、滑点随机,这套预处理只解决数值口径,不预示任何方向。

MQL5 / C++
class="type">void load_symbols(class="type">void)
  {
   SymbolSelect("XAUUSD",true);
   SymbolSelect("XTIUSD",true);
   SymbolSelect("USDZAR",true);
  }
class="type">void load_scale_values(class="type">void)
  {
class=class="str">"cmt">//--- Mean
class=class="str">"cmt">//--- USDZAR
   mean_values[class="num">0] = class="num">18.14360511919699;
   mean_values[class="num">1] = class="num">18.145737421580925;
   mean_values[class="num">2] = class="num">18.141568574864074;
   mean_values[class="num">3] = class="num">18.14362306984525;
class=class="str">"cmt">//--- XTI US OIL
   mean_values[class="num">4] = class="num">80.76956702216644;
   mean_values[class="num">5] = class="num">80.7864452112087;
   mean_values[class="num">6] = class="num">80.75236177331661;
   mean_values[class="num">7] = class="num">80.76923546633206;
class=class="str">"cmt">//--- GOLD SA
   mean_values[class="num">8] = class="num">2430.5180384776245;
   mean_values[class="num">9] = class="num">2430.878959640318;
   mean_values[class="num">10] = class="num">2430.1509598494354;
   mean_values[class="num">11] = class="num">2430.5204140526976;
class=class="str">"cmt">//--- Standard Deviation
class=class="str">"cmt">//--- USDZAR
   std_values[class="num">0] = class="num">0.11301636249300206;
   std_values[class="num">1] = class="num">0.11318116432297631;
   std_values[class="num">2] = class="num">0.11288670156099372;
   std_values[class="num">3] = class="num">0.11301994613848391;
class=class="str">"cmt">//--- XTI US OIL
   std_values[class="num">4] = class="num">0.9802409859148413;
   std_values[class="num">5] = class="num">0.9807944310705999;
   std_values[class="num">6] = class="num">0.9802449355481064;
   std_values[class="num">7] = class="num">0.9805961626626833;
class=class="str">"cmt">//--- GOLD SA
   std_values[class="num">8] = class="num">26.397404261230328;
   std_values[class="num">9] = class="num">26.414599597905003;
   std_values[class="num">10] = class="num">26.377605644853944;
   std_values[class="num">11] = class="num">26.395208330942864;
  }
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">//--- The class="kw">input size for our onnx model
   class="type">class="kw">ulong input_shape [] = {class="num">1,class="num">12};
class=class="str">"cmt">//--- Check if we have the right class="kw">input size
   if(!OnnxSetInputShape(onnx_model,class="num">0,input_shape))
     {
       Comment("Incorrect class="kw">input shape, the model has class="kw">input shape ",OnnxGetInputCount(onnx_model));
       class="kw">return(class="kw">false);

「EA 生命周期里 ONNX 模型的装卸与报价接管」

这段 MT5 代码把 ONNX 模型从加载、初始化到逐 tick 调用的链路一次性铺开。模型输出维度被硬性锁成 {1,1},也就是单次回归一个标量预测值;若 OnnxSetOutputShape 校验失败,直接 Comment 打印实际输出个数并返回 false,EA 连 OnInit 都过不去。 OnInit 里先 load_onnx_file 失败就 INIT_FAILED,随后 load_scale_values 与 load_symbols 把量纲和品种补齐。最小手数取 USDZAR 的 SYMBOL_VOLUME_MIN 乘 lot_multiple——美元南非兰特这类高波动 exotic 对,点差和最小合约都可能异于欧美,必须在初始化阶段锁定。 OnDeinit 只做两件事:OnnxRelease 释放模型资源、ExpertRemove 自卸。别小看这个顺序,不释放 ONNX 句柄反复加载会让终端内存缓慢爬升。 OnTick 的逻辑很直白:每跳先 model_predict 取 AI 预判,再读 USDZAR 的 bid/ask。无持仓就 find_entry 并 state 归零;有持仓则并行跑 check_reversal(AI 反转预警)与 check_risk_level(最大风险线)。外汇与贵金属杠杆高、滑点突变频繁,这类每跳强依赖模型输出的结构,实盘前务必在策略测试器用真实 tick 跑通。

MQL5 / C++
   }
class=class="str">"cmt">//--- The output size for our onnx model
  class="type">class="kw">ulong output_shape [] = {class="num">1,class="num">1};
class=class="str">"cmt">//--- Check if we have the right output size
  if(!OnnxSetOutputShape(onnx_model,class="num">0,output_shape))
    {
      Comment("Incorrect output shape, the model has output shape ",OnnxGetOutputCount(onnx_model));
      class="kw">return(class="kw">false);
    }
class=class="str">"cmt">//--- Everything went fine
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
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="kw">return(INIT_FAILED);
    }
class=class="str">"cmt">//--- Load our scaling values
  load_scale_values();
class=class="str">"cmt">//--- Add the symbols we need to the market watch
  load_symbols();
class=class="str">"cmt">//--- The smallest lotsize we can use
  minimum_volume = SymbolInfoDouble("USDZAR",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">//--- Remove 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 forecast from our model
  model_predict();
class=class="str">"cmt">//--- Fetch market prices
  bid = SymbolInfoDouble("USDZAR",SYMBOL_BID);
  ask = SymbolInfoDouble("USDZAR",SYMBOL_ASK);
class=class="str">"cmt">//--- If we have no open positions, find an entry
  if(PositionsTotal() == class="num">0)
    {
      class=class="str">"cmt">//--- Find an entry
      find_entry();
      class=class="str">"cmt">//--- Reset the system state
      state = class="num">0;
    }
class=class="str">"cmt">//--- If we have open postitions, manage them
  else
    {
      class=class="str">"cmt">//--- Check for a reveral warning from our AI
      check_reversal();
      class=class="str">"cmt">//--- Check if we have not reached our max risk levels
      check_risk_level();
    }
  }

◍ 换组品种或许能压住误差

这套用常规 OHLC 训练出来的多品种 AI EA,在 USDZAR 的 M1 数据上跑出了较低的拟合误差,但终端里并非所有品种都一个脾气。 我们手里附的 USDZAR_FLOAT_M1.onnx 是 524.58 KB 的浮点模型,配套 FetchData.mq5 仅 2.05 KB,说明数据抓取和推理是分开跑的;社区里有人指出该抓取脚本没关写入文件句柄,实盘前你最好先补一行 FileClose。 更值得动手的是:换一组品种组合重新训,误差水平可能会比 USDZAR 报价更低,外汇和贵金属杠杆高、跳空频繁,这种探索只是概率上的优化,不是稳赚路径。 开 MT5 把 ZIP 里的 USDZAR.mq5(10.54 KB)拖进策略测试器,先复现基准误差,再挑三五个相关性弱的品种替换输入,看看回测曲线是否更平。

常见问题

必须把训练阶段算出的均值、标准差随模型一起保存,EA加载后用同一组参数做标准化,否则输入分布偏移会导致预测失效。
重点看输入节点名称、维度顺序和输出节点数量,确保与EA里传张量的代码一一对应,维度对不上会直接报加载错误。
可以,小布能读取你上传的ONNX结构并列出输入输出名与维度,标出和EA骨架不匹配的地方,你照着改就行。
至少叠加波动率阈值和持仓方向相反判定,模型看空且当前无空单、波动率未超限时才触发卖单,降低追单风险。
点差超过ATR的一定倍数时建议屏蔽信号,外汇和贵金属高风险,模型概率倾向不代表必成,实盘先开模拟验证。