在任何市场中获得优势(第四部分):CBOE欧元和黄金波动率指数·综合运用
「导出ONNX前先把数据标准化」
要把训练好的模型丢进 MetaTrader 5 终端跑推理,第一步不是导模型,而是让终端能复现你训练时的数据尺度。如果训练用标准化数据、终端用原始报价,模型输出会直接失真。 做法很直接:对每一个预测列,先减掉该列均值,再除以该列标准差,得到均值为0、标准差为1的序列。这些均值和标准差就是缩放因子,必须单独存出来给终端用。 原文把因子写进 scale_factors.csv,包含两行——mean 和 standard deviation,列数与 predictors 一致。MT5 侧加载模型前,需先读这个 CSV,对实时特征做同样减法除法,否则输入分布对不上。外汇与贵金属杠杆高、滑点跳空频繁,特征缩放错一位都可能让信号反转,务必在策略测试器里核对数值。
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侧按名加载。 外汇与贵金属市场波动剧烈、杠杆风险高,任何模型导出后都先在历史数据上验证推理一致性,再考虑接实盘。
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 调用频率限制,也够日内交易参考。
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)
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路径换成你自己的模型名,就能直接编译验证。
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 推理仅作概率参考,实盘前务必在策略测试器跑通这批常数。
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_READ | FILE_CSV | FILE_ANSI 打开,按逗号分隔。 |
|---|
读文件时有个硬细节:counter > 10 就 break,也就是最多读 10 行就停。如果你真拿这个跑黄金(XAU 相关),外汇和贵金属杠杆高、滑点跳空频繁,这种截断逻辑可能让你喂给模型的宏观特征只有部分生效,预测倾向失真。开 MT5 把 fred_xau_eur.csv 放进 MQL5/Files 目录,改 counter 阈值验证不同读取长度对输入向量的影响。
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 是否按预期归一。
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 周线能复现信号频率。
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 年以后的日线验证,别一上来就挂实盘。 这类另类数据策略不是印钞机,它只是把信息边界往外推了一点;推不推得动你的净值,取决于特征工程和过拟合控制。