在任何市场中获得优势(第四部分):CBOE欧元和黄金波动率指数·综合运用
📘

在任何市场中获得优势(第四部分):CBOE欧元和黄金波动率指数·综合运用

第 3/3 篇

「导出ONNX前先把数据标准化」

要把训练好的模型丢进 MetaTrader 5 终端跑推理,第一步不是导模型,而是让终端能复现你训练时的数据尺度。如果训练用标准化数据、终端用原始报价,模型输出会直接失真。 做法很直接:对每一个预测列,先减掉该列均值,再除以该列标准差,得到均值为0、标准差为1的序列。这些均值和标准差就是缩放因子,必须单独存出来给终端用。 原文把因子写进 scale_factors.csv,包含两行——mean 和 standard deviation,列数与 predictors 一致。MT5 侧加载模型前,需先读这个 CSV,对实时特征做同样减法除法,否则输入分布对不上。外汇与贵金属杠杆高、滑点跳空频繁,特征缩放错一位都可能让信号反转,务必在策略测试器里核对数值。

MQL5 / C++
class="macro">#Let us now prepare to class="kw">export our model to onnx format
scale_factors = pd.DataFrame(columns=predictors,index=["mean","standard deviation"])
for i in np.arange(class="num">0,len(predictors)):
  scale_factors.iloc[class="num">0,i] = merged_data.loc[:,predictors[i]].mean()
  scale_factors.iloc[class="num">1,i] =  merged_data.loc[:,predictors[i]].std()
  merged_data.loc[:,predictors[i]] = (merged_data.loc[:,predictors[i]] - merged_data.loc[:,predictors[i]].mean())/merged_data.loc[:,predictors[i]].std()
scale_factors
class="macro">#Save the scale factors to CSV
scale_factors.to_csv("scale_factors.csv")

把训练好的模型落盘成ONNX

ONNX是一套跨语言交换机器学习模型的协议,借助MQL5的ONNX API,可以把训练好的深度网络直接嵌进EA里跑推理,不必在MT5里重写模型逻辑。 导出时最容易踩的坑是输入形状丢失——很多转换工具不会自动保留张量维度,所以要在代码里显式声明。上面这段Python用FloatTensorType([1,6])把输入钉死成单行6维特征,对应6个预测变量。 转换时target_opset=12指定了算子集版本,太低某些算子不被MT5支持,太高又可能和老环境不兼容;12在多数MT5 build里是稳妥选择。最后存成的文件叫“XAUEUR FRED D1.onnx”,命名里带品种周期方便后续在MQL5侧按名加载。 外汇与贵金属市场波动剧烈、杠杆风险高,任何模型导出后都先在历史数据上验证推理一致性,再考虑接实盘。

MQL5 / C++
class="macro">#Exporting 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">#Fit the model on all the data we have
customized_model.fit(merged_data.loc[:,predictors],merged_data.loc[:,target])
# Define the input type
initial_types = [("float_input",FloatTensorType([class="num">1,class="num">6]))]
# Create the ONNX representation
onnx_model = convert_sklearn(customized_model,initial_types=initial_types,target_opset=class="num">12)
# Save the ONNX model
onnx_name = "XAUEUR FRED D1.onnx"
onnx.save_model(onnx_model,onnx_name)

◍ 用 Python 把 FRED 恐慌指数落地成 MT5 可读 CSV

想在 MT5 里用宏观隐含波动率做择时,第一步不是写 EA,而是先让外部数据每天自动流进终端的 Files 目录。FRED 的 EVZCLS(欧元兑美元隐含波动率)和 GVZCLS(黄金隐含波动率)是两个现成观察变量,用 Python 拉取后落盘,EA 才能离线读取,不依赖实时联网。 下面这段脚本核心是一个无限循环:每次调用写盘函数,然后把进程休眠 24×60×60 秒,也就是整整一天。这样每天只更新一次,既避开 FRED 调用频率限制,也够日内交易参考。

MQL5 / C++
class="macro">#A function to write out our alternative data to CSV
def write_out_alternative_data():
    euro = fred.get_series("EVZCLS")
    euro = euro.iloc[-class="num">1]
    gold = fred.get_series("GVZCLS")
    gold = gold.iloc[-class="num">1]
    data = pd.DataFrame(np.array([euro,gold]),columns=["Data"],index=["Fred Euro","Fred Gold"])
    data.to_csv("C:\\ENTER\\YOUR\\PATH\\HERE\\MetaQuotes\\Terminal\\D0E8209F77C8CF37AD8BF550E51FF075\\MQL5\\Files\\fred_xau_eur.csv")
class="kw">while True:
    class="macro">#Update the fred data for our MT5 EA
    write_out_alternative_data()
    class="macro">#If we have finished all checks then we can wait for one day before checking for new data
    time.sleep(class="num">24 * class="num">60 * class="num">60)
逐行拆一下:第 2 行从 FRED 取 EVZCLS 序列;第 3 行用 iloc[-1] 只留最新一个值;第 4–5 行对 GVZCLS 做同样处理;第 6 行把两个数值拼成 DataFrame,索引标为 Fred Euro / Fred Gold;第 7 行写进终端 Files 下的 fred_xau_eur.csv,路径里的 D0E8209F77C8CF37AD8BF550E51FF075 是你本机终端实例 ID,必须换成自己的。 while True 之后先写盘再 sleep 一天,意味着你每天开盘前 CSV 里已经是最新波动率。外汇与贵金属波动受宏观情绪驱动明显,这类数据仅作概率参考,实盘仍属高风险,参数路径不对 EA 会读空文件。

MQL5 / C++
class="macro">#A function to write out our alternative data to CSV
def write_out_alternative_data():
    euro = fred.get_series("EVZCLS")
    euro = euro.iloc[-class="num">1]
    gold = fred.get_series("GVZCLS")
    gold = gold.iloc[-class="num">1]
    data = pd.DataFrame(np.array([euro,gold]),columns=["Data"],index=["Fred Euro","Fred Gold"])
    data.to_csv("C:\ENTER\YOUR\PATH\HERE\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Files\fred_xau_eur.csv")
class="kw">while True:
    class="macro">#Update the fred data for our MT5 EA
    write_out_alternative_data()
    class="macro">#If we have finished all checks then we can wait for one day before checking for new data
    time.sleep(class="num">24 * class="num">60 * class="num">60)

「把ONNX模型塞进EA的骨架」

EA启动的第一件事不是下单,而是把之前导出的ONNX文件从资源缓冲区里读出来并验证。MQL5用#resource把二进制模型编进ex5,运行时靠OnnxCreateFromBuffer还原句柄;任何一步拿不到有效句柄就直接返回false,初始化中止,避免带着残模型跑实盘。 全局层先铺好共享状态:onnx_model存模型句柄,mean_values/std_values/model_inputs都是长度6的向量,对应6个输入特征(XAUEUR的OHLC加上FRED宏观字段),model_output长度1存预测整数。CTrade对象接管下单平仓,bid/ask实时报价由Tick回调刷写。 归一化缩放因子在init阶段从外部载入,推理前必须把CopyRates()抓到的XAUEUR日线OHLC和CSV里的最新FRED值做标准化,再塞进model_inputs。模型吐出的整数预测用来快速识别反转倾向——没有持仓时,仅当更高周期价格行为也佐证模型方向才跟;已有持仓时,模型预期反转就平仓,倾向落在市场正确一侧的概率更高,但外汇/贵金属杠杆高,信号错判会放大亏损。 资源释放写在Deinit里,图25跑起来的EA就是这套逻辑:先建模型、载缩放,失败全 abort;Tick进来先存价再决策。开MT5把代码里\Files\\XAUEUR FRED D1.onnx路径换成你自己的模型名,就能直接编译验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                EURXAU Fred AI.mq5 |
class=class="str">"cmt">//|                                              Gamuchirai Zororo Ndawana |
class=class="str">"cmt">//|                                     [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Volatility Doctor"
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\\XAUEUR FRED D1.onnx" as class="kw">const class="type">uchar onnx_buffer[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Libraries                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include  <Trade/Trade.mqh>
CTrade Trade;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global variables                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">long    onnx_model;
vector   mean_values  = vector::Zeros(class="num">6);
vector   std_values   = vector::Zeros(class="num">6);
vectorf model_inputs = vectorf::Zeros(class="num">6);
vectorf model_output = vectorf::Zeros(class="num">1);
class="type">class="kw">double   bid,ask;
class="type">int      system_state,model_sate;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load the ONNX file                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool load_onnx_file(class="type">void)
  {
class=class="str">"cmt">//--- Create the ONNX model from the buffer we loaded earlier
   onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT);
class=class="str">"cmt">//--- Validate the model we just created
   if(onnx_model == INVALID_HANDLE)
     {

定形张量并灌入归一化常数

把 ONNX 模型接进 MT5 后,第一道关是锁死输入输出维度。代码里输入形状设为 {1,6},对应 6 个特征的一维批;输出形状 {1,1},即模型只回一个标量预测。若 OnnxSetInputShape 或 OnnxSetOutputShape 返回失败,立刻用 Comment 把 GetLastError 抛到图表上并 return(false),初始化直接中断,避免拿错张量跑推理。 归一化常数必须和训练时一一对齐,否则推理结果会整体偏移。load_scaling_factors 里写死了 6 组 mean 与 std:均值从 1331.49(XAUUSD 类价格中枢)到 16.35(某种区间宽度),标准差最低 2.19、最高 332.11。这些数字直接来自离线统计,复制时别手改,改错一位模型输出就可能失真。 校验逻辑很朴素——只要 mean_values 与 std_values 各数组求和都大于 0 就视为加载成功。这拦不住单值填反,但能挡住空数组。 退出时 release_resources 负责收尾:OnnxRelease 清模型句柄,ExpertRemove 卸 EA,Print 留一句 Volatility Doctor 的告别日志。外汇与贵金属波动剧烈,ONNX 推理仅作概率参考,实盘前务必在策略测试器跑通这批常数。

MQL5 / C++
class=class="str">"cmt">//--- Define the I/O shape
 class="type">ulong input_shape [] = {class="num">1,class="num">6};
class=class="str">"cmt">//--- Validate the input shape
 if(!OnnxSetInputShape(onnx_model,class="num">0,input_shape))
  {
   class=class="str">"cmt">//--- Give the user feedback
   Comment("Failed to define the ONNX input shape: ",GetLastError());
   class=class="str">"cmt">//--- Break initialization
   class="kw">return(false);
  }
 class="type">ulong output_shape [] = {class="num">1,class="num">1};
class=class="str">"cmt">//--- Validate the output shape
 if(!OnnxSetOutputShape(onnx_model,class="num">0,output_shape))
  {
   class=class="str">"cmt">//--- Give the user feedback
   Comment("Failed to define the ONNX output shape: ",GetLastError());
   class=class="str">"cmt">//--- Break initialization
   class="kw">return(false);
  }
class=class="str">"cmt">//--- We&class="macro">#x27;ve finished
 class="kw">return(true);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load our scaling factors                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool load_scaling_factors(class="type">void)
  {
class=class="str">"cmt">//--- Load the scaling values
  mean_values[class="num">0] = class="num">1331.4964525595044;
  mean_values[class="num">1] = class="num">1340.2280958591457;
  mean_values[class="num">2] = class="num">1323.3776328659928;
  mean_values[class="num">3] = class="num">1331.706768829475;
  mean_values[class="num">4] = class="num">8.258127607767035;
  mean_values[class="num">5] = class="num">16.35582438284101;
  std_values[class="num">0] = class="num">329.7222075527991;
  std_values[class="num">1] = class="num">332.11495530642173;
  std_values[class="num">2] = class="num">327.732778866831;
  std_values[class="num">3] = class="num">330.1146052811378;
  std_values[class="num">4] = class="num">2.199782202942867;
  std_values[class="num">5] = class="num">4.241112965400358;
class=class="str">"cmt">//--- Validate the values loaded correctly
  if((mean_values.Sum() > class="num">0) && (std_values.Sum() > class="num">0))
   {
    class="kw">return(true);
   }
class=class="str">"cmt">//--- We failed to load the scaling values
  class="kw">return(false);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Free up the resources we no longer need                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void release_resources(class="type">void)
  {
class=class="str">"cmt">//--- Free up all the resources we have used so far
  OnnxRelease(onnx_model);
  ExpertRemove();
  Print("Thank you for choosing Volatility Doctor");
  }

◍ 把金价与宏观数据喂给预测模型

EA 里抓行情和读外部数据是两个独立动作。fetch_market_data 只干一件事:用 SymbolInfoDouble 把当前品种的 bid、ask 刷进全局变量,供后续逻辑调用。 model_predict 负责拼模型输入。它开一个 1×4 的矩阵 xau_eur_ohlc,用 CopyRates 拉 XAU/EUR 的日线 OHLC(PERIOD_D1,取最新 1 根)。前 4 个输入做标准化:(原始值 - mean_values[i]) / std_values[i],转成 float 塞进 model_inputs。

第 5、6 个输入不走行情,而是调 read_fred_data 读外部 CSV。文件写死为 fred_xau_eur.csv,用 FileOpen 以 FILE_READFILE_CSVFILE_ANSI 打开,按逗号分隔。

读文件时有个硬细节:counter > 10 就 break,也就是最多读 10 行就停。如果你真拿这个跑黄金(XAU 相关),外汇和贵金属杠杆高、滑点跳空频繁,这种截断逻辑可能让你喂给模型的宏观特征只有部分生效,预测倾向失真。开 MT5 把 fred_xau_eur.csv 放进 MQL5/Files 目录,改 counter 阈值验证不同读取长度对输入向量的影响。

MQL5 / C++
class="type">void fetch_market_data(class="type">void)
  {
class=class="str">"cmt">//--- Update the market data
   bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
   ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
  }
class="type">void model_predict(class="type">void)
  {
class=class="str">"cmt">//--- Get the input data ready
   for(class="type">int i =class="num">0; i < class="num">6; i++)
     {
      class=class="str">"cmt">//--- The first class="num">4 inputs will be fetched from the market
      matrix xau_eur_ohlc = matrix::Zeros(class="num">1,class="num">4);
      xau_eur_ohlc.CopyRates(Symbol(),PERIOD_D1,COPY_RATES_OHLC,class="num">0,class="num">1);
      class=class="str">"cmt">//--- Fill in the data
      if(i<class="num">4)
        {
         model_inputs[i] = (class="type">float)((xau_eur_ohlc[i,class="num">0] - mean_values[i])/ std_values[i]);
        }
      class=class="str">"cmt">//--- We have to read in the fred alternative data
      else
        {
         read_fred_data();
        }
     }
  }
class="type">void read_fred_data(class="type">void)
  {
class=class="str">"cmt">//--- Read in the file
   class="type">class="kw">string file_name = "fred_xau_eur.csv";
class=class="str">"cmt">//--- Try open the file
   class="type">int result = FileOpen(file_name,FILE_READ|FILE_CSV|FILE_ANSI,",");
class=class="str">"cmt">//--- Check the result
   if(result != INVALID_HANDLE)
     {
      Print("Opened the file");
      class=class="str">"cmt">//--- Store the values of the file
      class="type">int counter = class="num">0;
      class="type">class="kw">string value = "";
      class="kw">while(!FileIsEnding(result) && !IsStopped())
        {
         if(counter > class="num">10)
           break;
         value = FileReadString(result);
         Print("Counter: ");
         Print(counter);

「把 FRED 数据喂进 ONNX 并标记多空倾向」

这段逻辑发生在文件读取循环里:当计数器走到 3,把读到的字符串当作欧元相关数据,按均值 std_values[4]、标准差 mean_values[4] 做 z-score 标准化后写进模型输入数组第 4 位;计数器到 5 时同理处理黄金数据进第 5 位。标准化公式 ((float)value - mean)/std 直接决定模型输入尺度,外汇与贵金属行情波动大,均值和标准差若来自不同时间窗会明显偏移预测。 文件读完先 Print(model_inputs) 把整组输入打到日志,FileClose 收尾,随后 OnnxRun 跑推理,Comment 在图表左上角显示 model_output[0]。若输出高于日线收盘价 iClose(Symbol(),PERIOD_D1,0),model_sate 置 1(偏多),否则置 -1(偏空);这只是概率倾向,实盘仍受滑点与点差侵蚀。 OnInit 里先后调 load_onnx_file 与 load_scaling_factors,任一返回 false 就 INIT_FAILED 终止 EA,两个都过才 INIT_SUCCEEDED。想验证就开 MT5 接一份真实 FRED csv,把 counter==3/5 的索引和你的 mean/std 文件对齐,看日志里 Euro/Gold data 是否按预期归一。

MQL5 / C++
Print("Trying to read class="type">class="kw">string: ",value);
if(counter == class="num">3)
  {
  Print("Fred Euro data: ",value);
  model_inputs[class="num">4] = (class="type">float)((((class="type">float) value) - mean_values[class="num">4])/std_values[class="num">4]);
  }
if(counter == class="num">5)
  {
  Print("Fred Gold data: ",value);
  model_inputs[class="num">5] = (class="type">float)((((class="type">float) value) - mean_values[class="num">5])/std_values[class="num">5]);
  }
if(FileIsLineEnding(result))
  {
  Print("row++");
  }
counter++;
class=class="str">"cmt">//--- Show the input and Fred data
Print("Input Data: ");
Print(model_inputs);
class=class="str">"cmt">//---Close the file
FileClose(result);
class=class="str">"cmt">//--- Store the model prediction
OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_output);
Comment("Model Forecast",model_output[class="num">0]);
if(model_output[class="num">0] > iClose(Symbol(),PERIOD_D1,class="num">0))
  {
  model_sate = class="num">1;
  }
else
  {
  model_sate = -class="num">1;
  }

EA 生命周期与 XAUEUR 周线过滤下单

MT5 的 EA 在退出时会触发 OnDeinit,这里只做一件事:调用 release_resources() 释放指标句柄、数组缓存等不再需要的资源,避免残留占用终端内存。 真正的交易逻辑全压在 OnTick 里。每次报价先 fetch_market_data() 拉最新行情,再 model_predict() 让模型给方向;若当前无持仓(PositionsTotal()==0),才允许开仓。 开仓不是无脑跟模型:模型看多(model_sate==1)时,还要求周线收盘 iClose(Symbol(),PERIOD_W1,0) 大于 12 周前收盘 iClose(Symbol(),PERIOD_W1,12),才以 0.3 手买入 XAUEUR,备注名 "XAUEUR Fred AI"。看空逻辑对称,需周线收盘低于 12 周前才卖 0.3 手。 若已有持仓但模型方向与 system_state 不一致,直接 PositionClose 平掉。外汇与贵金属杠杆高、滑点跳空频繁,这套周线过滤只是降低噪音,不消除 XAUEUR 单向波动的爆仓可能,上 MT5 用策略测试器跑 2020–2024 周线能复现信号频率。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//--- Free up the resources we no longer need
   release_resources();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//--- Update market data
   fetch_market_data();
class=class="str">"cmt">//--- Fetch a prediction from our model
   model_predict();
class=class="str">"cmt">//--- If we have no positions follow the model&class="macro">#x27;s lead
   if(PositionsTotal() == class="num">0)
     {
      class=class="str">"cmt">//--- Buy position
      if(model_sate == 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,"XAUEUR Fred AI");
            system_state = class="num">1;
           }
        };
      class=class="str">"cmt">//--- Sell position
      if(model_sate == -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,"XAUEUR Fred AI");
            system_state = -class="num">1;
           }
        };
     }
class=class="str">"cmt">//--- If we allready have positions open, let&class="macro">#x27;s manage them
   if(model_sate != system_state)
     {
      Trade.PositionClose(Symbol());
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 另类数据进模型的边界

把 FRED 的 CBOE 波动率指数塞进机器学习模型,回测里对 XAU/EUR 方向判别的提升是肉眼可见的,但样本外表现会随宏观 regime 切换衰减,这点做外汇和贵金属的人得有心理准备——杠杆品种里波动率突变向来是高回撤源头。 前文给的 EURXAU_Fred_AI.mq5 和 XAUEUR_FRED_D1.onnx 可以直接在 MT5 里跑,先拿 2020 年以后的日线验证,别一上来就挂实盘。 这类另类数据策略不是印钞机,它只是把信息边界往外推了一点;推不推得动你的净值,取决于特征工程和过拟合控制。

常见问题

不标准化会导致不同量纲特征权重失衡,模型推理偏差大。先对输入做Z-score或Min-Max归一化,再把同样的均值方差常数写进EA里复用。
用pandas拉FRED接口存成两列(时间,数值)的CSV,表头别带单位,逗号分隔。直接丢进数据目录,EA按行读取即可。
可以。小布内置AIGC分析,能定时拉取宏观波动率数据并套用你导出的模型给出概率倾向,打开品种页就能看结果。
先打印输入张量的shape和类型,确认和导出时完全一致(如1x5的float32)。归一化常数也要和训练时一一对应。
会。特征列顺序必须和训练时严格一致,建议在EA里用结构体固定字段顺序,避免CSV列漂移导致信号反向。