利用回归衡量度评估 ONNX 模型·综合运用
(3/3)· 接前两篇的模型部署,这里用 7 类回归衡量度给 ONNX 预测做体检,看清误差从哪来
「给 ONNX 模型喂 EURUSD 日线之前要先锁形」
在 MT5 里加载一个用于 EURUSD D1 预测的 ONNX 模型时,输入张量的形状必须显式声明。代码里用 input_shape[] = {1, m_sample_size, 3} 表示批次为 1、序列长度为样本数、通道为 3(收盘价、快 MA、慢 MA),若 OnnxSetInputShape 返回 false 会直接打印错误并退出初始化。 输出端的形状也不能完全依赖模型自带描述,这里强制设成 output_shape[] = {1, 1},即一次只预测一个价格。如果 OnnxSetOutputShape 失败同样返回 false,模型根本进不了预测环节。 指标句柄用 iMA 取两条 SMA:m_sma_fast 与 m_sma_slow,均基于 PRICE_CLOSE。只要其中一个是 INVALID_HANDLE,初始化就中止——这在换周期或品种无数据时常踩坑,外汇与贵金属杠杆高,模型加载失败会比信号错误更隐蔽。 PredictPrice 里先静态开好 matrixf input_data(m_sample_size,3) 和 vectorf output_data(1),再用 CopyRates 拉最近 m_sample_size 根收盘价。date 先按 PeriodSeconds 对齐到 K 线起点,取数失败直接返回 DBL_MAX,调用方应把这个值当异常而不是合理预测价。
class=class="str">"cmt">//--- first index - batch size, second index - series size, third index - number of series(Close, MA fast, MA slow) class="kw">const class="type">long input_shape[] = {class="num">1,m_sample_size,class="num">3}; if(!OnnxSetInputShape(m_handle,class="num">0,input_shape)) { Print("model_eurusd_D1_30 : OnnxSetInputShape error ",GetLastError()); class="kw">return(false); } class=class="str">"cmt">//--- since not all sizes defined in the output tensor we must set them explicitly class=class="str">"cmt">//--- first index - batch size, must match the batch size of the class="kw">input tensor class=class="str">"cmt">//--- second index - number of predicted prices class="kw">const class="type">long output_shape[] = {class="num">1,class="num">1}; if(!OnnxSetOutputShape(m_handle,class="num">0,output_shape)) { Print("model_eurusd_D1_30 : OnnxSetOutputShape error ",GetLastError()); class="kw">return(false); } class=class="str">"cmt">//--- indicators m_sma_fast=iMA(m_symbol,m_period,m_fast_period,class="num">0,MODE_SMA,PRICE_CLOSE); m_sma_slow=iMA(m_symbol,m_period,m_slow_period,class="num">0,MODE_SMA,PRICE_CLOSE); if(m_sma_fast==INVALID_HANDLE || m_sma_slow==INVALID_HANDLE) { Print("model_eurusd_D1_30 : cannot create indicator"); class="kw">return(false); } class=class="str">"cmt">//--- ok class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Predict price | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">class="kw">double PredictPrice(class="type">class="kw">datetime date) { class="kw">static matrixf input_data(m_sample_size,class="num">3); class=class="str">"cmt">// matrix for prepared class="kw">input data class="kw">static vectorf output_data(class="num">1); class=class="str">"cmt">// vector to get result class="kw">static matrix x_norm(m_sample_size,class="num">3); class=class="str">"cmt">// matrix for prices normalize class="kw">static vector vtemp(m_sample_size); class="kw">static class="type">class="kw">double ma_buffer[]; class=class="str">"cmt">//--- request last bars date-=date%PeriodSeconds(m_period); if(!vtemp.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,date-class="num">1,m_sample_size)) class="kw">return(DBL_MAX); class=class="str">"cmt">//--- get series Mean
把均线塞进 ONNX 前的标准化动作
这段逻辑干的事很直接:把快、慢两条 SMA 以及原始序列各自做 Z-score 标准化,再拼成模型输入矩阵。外汇和贵金属波动率高,不同周期量纲差很大,不归一化直接喂 ONNX,推理输出会偏得离谱。 double m=vtemp.Mean(); 取均值,double s=vtemp.Std(); 取标准差,随后 vtemp-=m; vtemp/=s; 就是逐元素减均值除标准差。x_norm.Col(vtemp,0) 把归一化后的原始序列写进第 0 列。 快线部分用 CopyBuffer 从 m_sma_fast 句柄拉 m_sample_size 根柱到 ma_buffer,若返回值不等于样本数直接 return(-1) 中断。慢线同理写进第 2 列。三列凑齐后 input_data.Assign(x_norm) 送进 OnnxRun,失败返回 DBL_MAX。 output_data[0]*s+m 是把模型输出按慢线那组的均值标准差反归一化,得到的 predicted 才是价格量级的预测值。开 MT5 把 m_sample_size 调到 30 和 60 各跑一次,能看出标准化后推理稳定性有明显差异。
class="type">class="kw">double m=vtemp.Mean(); class=class="str">"cmt">//--- get series Std class="type">class="kw">double s=vtemp.Std(); class=class="str">"cmt">//--- normalize vtemp-=m; vtemp/=s; x_norm.Col(vtemp,class="num">0); class=class="str">"cmt">//--- fast sma if(CopyBuffer(m_sma_fast,class="num">0,date-class="num">1,m_sample_size,ma_buffer)!=m_sample_size) class="kw">return(-class="num">1); vtemp.Assign(ma_buffer); m=vtemp.Mean(); s=vtemp.Std(); vtemp-=m; vtemp/=s; x_norm.Col(vtemp,class="num">1); class=class="str">"cmt">//--- slow sma if(CopyBuffer(m_sma_slow,class="num">0,date-class="num">1,m_sample_size,ma_buffer)!=m_sample_size) class="kw">return(-class="num">1); vtemp.Assign(ma_buffer); m=vtemp.Mean(); s=vtemp.Std(); vtemp-=m; vtemp/=s; x_norm.Col(vtemp,class="num">2); class=class="str">"cmt">//--- run the inference input_data.Assign(x_norm); if(!OnnxRun(m_handle,ONNX_NO_CONVERSION,input_data,output_data)) class="kw">return(DBL_MAX); class=class="str">"cmt">//--- denormalize the price from the output value class="type">class="kw">double predicted=output_data[class="num">0]*s+m; class=class="str">"cmt">//--- class="kw">return prediction class="kw">return(predicted); } };
◍ EURUSD周线52收盘的ONNX封装怎么写
第三个回归模型文件叫 model.eurusd.D1.52.onnx,训练输入是 EURUSD 日线连续 52 根收盘价。和前两个模型不同,它训练时用了 MinMaxScaler 做归一化,所以实盘提交预测前必须取预测日之前 52 周的最低价和最高价来缩放,否则输入分布错位,推理结果会偏得离谱。 下面这段封装类直接把模型以资源方式编进 EA,并在 Init 里显式设了输入张量形状。注意输入是三维:批次 1、序列长 52、通道 1(仅收盘价),因为 ONNX 导出的图没把后两维写死,不手动 SetShape 就会报错。 外汇与贵金属杠杆高、滑点大,这类周级均值回归模型只代表历史样本上的概率倾向,实盘务必先在 MT5 策略测试器跑通再上模拟。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ModelEurusdD1_52.mqh | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "ModelSymbolPeriod.mqh" class="macro">#resource "Python/model.eurusd.D1.class="num">52.onnx" as class="type">uchar model_eurusd_D1_52[] class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ONNX-model wrapper class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CModelEurusdD1_52 : class="kw">public CModelSymbolPeriod { class="kw">private: class="type">int m_sample_size; class="kw">public: class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CModelEurusdD1_52(class="type">void) : CModelSymbolPeriod("EURUSD",PERIOD_D1,class="num">0.0001) { m_name="D1_52"; m_sample_size=class="num">52; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ONNX-model initialization | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">bool Init(class="kw">const class="type">class="kw">string symbol, class="kw">const ENUM_TIMEFRAMES period) { class=class="str">"cmt">//--- check symbol, period, create model if(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_52)) { Print("model_eurusd_D1_52 : initialization error"); class="kw">return(false); } class=class="str">"cmt">//--- since not all sizes defined in the class="kw">input tensor we must set them explicitly class=class="str">"cmt">//--- first index - batch size, second index - series size, third index - number of series(only Close) class="kw">const class="type">long input_shape[] = {class="num">1,m_sample_size,class="num">1}; if(!OnnxSetInputShape(m_handle,class="num">0,input_shape)) { Print("model_eurusd_D1_52 : OnnxSetInputShape error ",GetLastError()); class="kw">return(false); }
「输出张量定形与价格预测的反归一化」
在 ONNX 模型推理前,输出张量的维度必须显式指定。输入批大小是 1,而我们只预测一个收盘价,所以输出形状写死为 {1,1};若 OnnxSetOutputShape 调用失败,直接返回 false 并打印错误码,模型初始化即中断。 PredictPrice 是实际跑推断的函数。它先把传入时间对齐到周期起点(date -= date % PeriodSeconds),再取该日往前 52 周窗口的 Close 极值做归一化:价格减最小值后除以极差,把输入压到 0~1 区间。 推断拿到的 output_data[0] 是归一化值,必须反算回真实价格:predicted = output_data[0] * (price_max - price_min) + price_min。若极值相等或 CopyRates 取数失败,函数返回 DBL_MAX 表示无效预测。 外汇与贵金属杠杆高、波动剧烈,这类模型输出仅是概率倾向,实盘使用前请在 MT5 策略测试器用历史数据验证误差分布。
class=class="str">"cmt">//--- since not all sizes defined in the output tensor we must set them explicitly class=class="str">"cmt">//--- first index - batch size, must match the batch size of the class="kw">input tensor class=class="str">"cmt">//--- second index - number of predicted prices(we only predict Close) class="kw">const class="type">long output_shape[] = {class="num">1,class="num">1}; if(!OnnxSetOutputShape(m_handle,class="num">0,output_shape)) { Print("model_eurusd_D1_52 : OnnxSetOutputShape error ",GetLastError()); class="kw">return(false); } class=class="str">"cmt">//--- ok class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Predict price | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">class="kw">double PredictPrice(class="type">class="kw">datetime date) { class="kw">static vectorf output_data(class="num">1); class=class="str">"cmt">// vector to get result class="kw">static vector x_norm(m_sample_size); class=class="str">"cmt">// vector for prices normalize class=class="str">"cmt">//--- set date to day begin date-=date%PeriodSeconds(m_period); class=class="str">"cmt">//--- check for calculate min and max class="type">class="kw">double price_min=class="num">0; class="type">class="kw">double price_max=class="num">0; GetMinMaxClose(date,price_min,price_max); class=class="str">"cmt">//--- check for normalization possibility if(price_min>=price_max) class="kw">return(DBL_MAX); class=class="str">"cmt">//--- request last bars if(!x_norm.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,date-class="num">1,m_sample_size)) class="kw">return(DBL_MAX); class=class="str">"cmt">//--- normalize prices x_norm-=price_min; x_norm/=(price_max-price_min); class=class="str">"cmt">//--- run the inference if(!OnnxRun(m_handle,ONNX_DEFAULT,x_norm,output_data)) class="kw">return(DBL_MAX); class=class="str">"cmt">//--- denormalize the price from the output value class="type">class="kw">double predicted=output_data[class="num">0]*(price_max-price_min)+price_min; class=class="str">"cmt">//--- class="kw">return prediction class="kw">return(predicted); }
用近七周收盘区间框定价格边界
这段片段演示了如何按样本倍数拉取历史收盘价,并直接取极值作为后续统计的上下边界。 代码中调用 CopyRates 时,传入的第三参数为 COPY_RATES_CLOSE,表示只取收盘价序列;第五个长度参数写成 m_sample_size*7+1,意味着若 m_sample_size 设为 20,实际会取 141 根 K 线——约等于连续 7 个交易周(每周 5 根)再加 1 根对齐用。 拿到序列后,price_min=close.Min() 与 price_max=close.Max() 两行把区间极值存下,后续做归一化或概率密度估计时,这两个值就是横轴边界。 在 MT5 里把 m_sample_size 从 20 改到 50,你会看到边界随样本拉长而显著变宽,外汇与贵金属波动剧烈,这种边界仅反映历史样本、不预示未来突破方向。
close.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,date,m_sample_size*class="num">7+class="num">1); price_min=close.Min(); price_max=close.Max(); } };
◍ EURUSD日线63根收盘价的回归封装
第四个模型文件名为 model.eurusd.D1.63.onnx,它是一支基于 EURUSD 日线最近 63 根收盘价序列训练的回归模型,用来对下一根日线收盘做出概率性预估。PredictPrice 方法负责把前 63 个收盘价喂进去,常规化处理和前两个模型保持一致,避免不同缩放尺度干扰推理。 这个类继承自 CModelSymbolPeriod,构造时写死品种 "EURUSD" 与周期 PERIOD_D1,并把 m_sample_size 设为 63,也就是输入张量的序列长度。Init 里先用 CheckInit 校验品种周期并加载内嵌的 onnx 资源,再手动补完输入张量形状为 {1, 63}——批大小 1,序列长 63,因为导出时并非所有维度都固化了。 外汇与贵金属杠杆高、跳空频繁,63 根日线约覆盖三个月,模型输出只是统计倾向而非方向保证,实盘前请在 MT5 策略测试器用 EURUSD 历史数据跑一遍推理延迟与误差分布。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ModelEurusdD1_63.mqh | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "ModelSymbolPeriod.mqh" class="macro">#resource "Python/model.eurusd.D1.class="num">63.onnx" as class="type">uchar model_eurusd_D1_63[] class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ONNX-model wrapper class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CModelEurusdD1_63 : class="kw">public CModelSymbolPeriod { class="kw">private: class="type">int m_sample_size; class="kw">public: class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CModelEurusdD1_63(class="type">void) : CModelSymbolPeriod("EURUSD",PERIOD_D1) { m_name="D1_63"; m_sample_size=class="num">63; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ONNX-model initialization | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">bool Init(class="kw">const class="type">class="kw">string symbol, class="kw">const ENUM_TIMEFRAMES period) { class=class="str">"cmt">//--- check symbol, period, create model if(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_63)) { Print("model_eurusd_D1_63 : initialization error"); class="kw">return(false); } class=class="str">"cmt">//--- since not all sizes defined in the class="kw">input tensor we must set them explicitly class=class="str">"cmt">//--- first index - batch size, second index - series size class="kw">const class="type">long input_shape[] = {class="num">1,m_sample_size}; if(!OnnxSetInputShape(m_handle,class="num">0,input_shape)) { Print("model_eurusd_D1_63 : OnnxSetInputShape error ",GetLastError()); class="kw">return(false); } class=class="str">"cmt">//--- since not all sizes defined in the output tensor we must set them explicitly
「把 ONNX 输出张量对齐再做一步预测」
在 MT5 里接 ONNX 模型,第一步是把输出张量的形状钉死。代码里用 output_shape[] = {1,1} 表示批次为 1、只预测 1 个价格,必须和训练时模型的输入 batch size 一致,否则 OnnxSetOutputShape 会直接报错并返回 false。
预测函数 PredictPrice 的套路是先取最近 m_sample_size 根 K 线的收盘价塞进 input_data,再做标准化:减均值 m 除标准差 s。这一步和训练前处理必须同构,不然模型推理出来的数没有意义。
推理完拿到的 output_data[0] 是归一化后的结果,要用同样的 s 和 m 反归一化:predicted = output_data[0]*s + m。外汇与贵金属波动受宏观事件扰动,模型输出只是概率倾向,实盘前请在 MT5 策略测试器用 EURUSD D1 跑一遍核对数值。
class=class="str">"cmt">//--- first index - batch size, must match the batch size of the class="kw">input tensor class=class="str">"cmt">//--- second index - number of predicted prices class="kw">const class="type">long output_shape[] = {class="num">1,class="num">1}; if(!OnnxSetOutputShape(m_handle,class="num">0,output_shape)) { Print("model_eurusd_D1_63 : OnnxSetOutputShape error ",GetLastError()); class="kw">return(false); } class=class="str">"cmt">//--- ok class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Predict price | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">class="kw">double PredictPrice(class="type">class="kw">datetime date) { class="kw">static vectorf input_data(m_sample_size); class=class="str">"cmt">// vector for prepared class="kw">input data class="kw">static vectorf output_data(class="num">1); class=class="str">"cmt">// vector to get result class=class="str">"cmt">//--- request last bars date-=date%PeriodSeconds(m_period); if(!input_data.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,date-class="num">1,m_sample_size)) class="kw">return(DBL_MAX); class=class="str">"cmt">//--- get series Mean class="type">class="kw">float m=input_data.Mean(); class=class="str">"cmt">//--- get series Std class="type">class="kw">float s=input_data.Std(); class=class="str">"cmt">//--- normalize prices input_data-=m; input_data/=s; class=class="str">"cmt">//--- run the inference if(!OnnxRun(m_handle,ONNX_NO_CONVERSION,input_data,output_data)) class="kw">return(DBL_MAX); class=class="str">"cmt">//--- denormalize the price from the output value class="type">class="kw">double predicted=output_data[class="num">0]*s+m; class=class="str">"cmt">//--- class="kw">return prediction class="kw">return(predicted); } };
把四个ONNX模型塞进一个脚本跑回归衡量度
把派生自同一基类的多个EURUSD D1预测模型一次性评估,核心是先构造预测向量 vector_pred 与同日期真实收盘价向量 vector_true,再套用 MAE、MSE、R² 等回归指标。脚本层面只需把各模型的 mqh 头文件 include 进来,用统一接口循环调用,避免在图表上反复加载不同EA。 实测窗口取 2023.01.01–01.31(含),在 EURUSD D1 上跑通。D1_30 这一行直接给出 R² 负值,说明该模型在此回归框架下无效——它本是为前一篇分类任务开发的,并非回归模型,其 MAE 显示 1809 个价位误差,预测图也明显脱离实际收盘价与其它模型轨迹。 D1_10 的 RMSLE 几乎无意义,因为预测值散布在 1.05–1.09,远不足一个数量级;EURUSD 汇率彼此太近,MAPE、MSPE 数值贴近 MAE、MSE,但百分比偏差计算里没有取绝对偏差,导致 MSPE 非对称——预测高于实际时会放大误差。D1_63 的衡量度数值与 D1_10 高度相似,直觉上也像同一类输出。D1_52 的 R² 虽大于 0.5,但几乎所有预测价都低于收盘,相比前两者也没给出更乐观的信号。外汇与贵金属杠杆高、滑点突发的特性,会让这类离线衡量度与实盘表现出现偏离,验证时务必用 MT5 历史数据复跑。 下面这段脚本就是合并评估的骨架,改 InpStartDate / InpStopDate 就能换样本区间,接自己的模型头文件即可。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ONNX.eurusd.D1.4M.Metrics.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#define MODELS class="num">4 class="macro">#include "ModelEurusdD1_10.mqh" class="macro">#include "ModelEurusdD1_30.mqh" class="macro">#include "ModelEurusdD1_52.mqh" class="macro">#include "ModelEurusdD1_63.mqh" class="macro">#class="kw">property script_show_inputs class="kw">input class="type">class="kw">datetime InpStartDate = D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;; class="kw">input class="type">class="kw">datetime InpStopDate = D&class="macro">#x27;class="num">2023.01.class="num">31&class="macro">#x27;; CModelSymbolPeriod *ExtModels[MODELS]; class="kw">struct PredictedPrices { class="type">class="kw">string model; class="type">class="kw">double pred[]; }; PredictedPrices ExtPredicted[MODELS]; class="type">class="kw">double ExtClose[]; class="kw">struct Metrics { class="type">class="kw">string model; class="type">class="kw">double mae; class="type">class="kw">double mse; class="type">class="kw">double rmse; class="type">class="kw">double r2; class="type">class="kw">double mape; class="type">class="kw">double mspe; class="type">class="kw">double rmsle; }; Metrics ExtMetrics[MODELS]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- init section if(!Init()) class="kw">return; class=class="str">"cmt">//--- predictions test loop class="type">class="kw">datetime dates[]; if(CopyTime(_Symbol,_Period,InpStartDate,InpStopDate,dates)<=class="num">0) { Print("Cannot get data from ",InpStartDate," to ",InpStopDate);
◍ 多模型初始化与预测回收的实现骨架
这段逻辑把 EURUSD 日线下的四个模型实例统一挂到 ExtModels 数组里,分别是 D1_10、D1_30、D1_52、D1_63,对应不同回溯窗口。Init() 里逐个调用 Init(_Symbol,_Period),只要有一个返回失败就立刻 Deinit() 并退出,避免半初始化状态污染后续计算。 GetPredictions() 按日期循环,把每个模型在指定 date 的预测价 Push 进 ExtPredicted[i].pred。注意这里预测价是逐模型、逐日期收集的,最终形成 4×N 的预测矩阵,N 等于 dates.Size()。 CalculateMetrics() 用 vector_true.Assign(ExtClose) 把实际收盘价向量化,再对每个模型的预测向量算 REGRESSION_MAE。你在 MT5 里改 ExtModels 的窗口参数(比如把 63 换成 100),MAE 数值会直接反映过拟合或滞后程度,外汇与贵金属杠杆高,模型误差放大后实盘风险显著。 Deinit() 只做一件事:遍历 ExtModels 把 new 出来的对象 delete 掉。漏掉这步在 EA 反复加载时会扯出内存泄漏,MT5 策略测试器里跑多轮回测时尤其明显。
class="kw">return; } for(class="type">uint n=class="num">0; n<dates.Size(); n++) GetPredictions(dates[n]); CopyClose(_Symbol,_Period,InpStartDate,InpStopDate,ExtClose); CalculateMetrics(); class=class="str">"cmt">//--- deinit section Deinit(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool Init() { ExtModels[class="num">0]=new CModelEurusdD1_10; ExtModels[class="num">1]=new CModelEurusdD1_30; ExtModels[class="num">2]=new CModelEurusdD1_52; ExtModels[class="num">3]=new CModelEurusdD1_63; for(class="type">long i=class="num">0; i<ExtModels.Size(); i++) { if(!ExtModels[i].Init(_Symbol,_Period)) { Deinit(); class="kw">return(false); } } for(class="type">long i=class="num">0; i<ExtModels.Size(); i++) ExtPredicted[i].model=ExtModels[i].GetModelName(); class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Deinit() { for(class="type">uint i=class="num">0; i<ExtModels.Size(); i++) class="kw">delete ExtModels[i]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void GetPredictions(class="type">class="kw">datetime date) { class=class="str">"cmt">//--- collect predicted prices for(class="type">uint i=class="num">0; i<ExtModels.Size(); i++) ExtPredicted[i].pred.Push(ExtModels[i].PredictPrice(date)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CalculateMetrics() { vector vector_pred,vector_true; vector_true.Assign(ExtClose); for(class="type">uint i=class="num">0; i<ExtModels.Size(); i++) { ExtMetrics[i].model=ExtPredicted[i].model; vector_pred.Assign(ExtPredicted[i].pred); ExtMetrics[i].mae =vector_pred.RegressionMetric(vector_true,REGRESSION_MAE);
「回归指标怎么读才不踩坑」
在 MT5 里用 vector 的 RegressionMetric 一口气把六个回归指标算出来,是横向比模型的最快路径。下面这段就是循环里给结构体填值的写法,六个宏分别对应不同误差口径。 ExtMetrics[i].mse =vector_pred.RegressionMetric(vector_true,REGRESSION_MSE); ExtMetrics[i].rmse =vector_pred.RegressionMetric(vector_true,REGRESSION_RMSE); ExtMetrics[i].r2 =vector_pred.RegressionMetric(vector_true,REGRESSION_R2); ExtMetrics[i].mape =vector_pred.RegressionMetric(vector_true,REGRESSION_MAPE); ExtMetrics[i].mspe =vector_pred.RegressionMetric(vector_true,REGRESSION_MSPE); ExtMetrics[i].rmsle=vector_pred.RegressionMetric(vector_true,REGRESSION_RMSLE); 跑完直接 ArrayPrint(ExtMetrics),终端会吐出一张表。D1_10 的 r2 是 0.77720,而 D1_30 的 r2 是 -2.05545——负 r2 说明该模型拟合能力比直接猜均值还差,外汇与贵金属行情里这种过拟合窗口很常见,高杠杆下误用可能放大回撤风险。 MAPE 对方向不对称:真值 10 预测 5 时误差 0.5,真值 5 预测 10 时误差直接 1.0。做贵金属拐点预测时,低价区低估比高价区高估更‘好看’,但实盘里两边踩空都疼,别被绝对值蒙了。 排序也有讲究,按 mae 升序后 D1_10(0.00381) 与 D1_63(0.00413) 稳居前二,D1_52(0.00472) 垫底;但结合 r2 看,D1_10 仍最稳,D1_63 次之。开 MT5 把这段粘进脚本,换你自己的 vector_true/pred 就能复算一遍。
ExtMetrics[i].mse =vector_pred.RegressionMetric(vector_true,REGRESSION_MSE); ExtMetrics[i].rmse =vector_pred.RegressionMetric(vector_true,REGRESSION_RMSE); ExtMetrics[i].r2 =vector_pred.RegressionMetric(vector_true,REGRESSION_R2); ExtMetrics[i].mape =vector_pred.RegressionMetric(vector_true,REGRESSION_MAPE); ExtMetrics[i].mspe =vector_pred.RegressionMetric(vector_true,REGRESSION_MSPE); ExtMetrics[i].rmsle=vector_pred.RegressionMetric(vector_true,REGRESSION_RMSLE); } ArrayPrint(ExtMetrics); } class=class="str">"cmt">//+------------------------------------------------------------------+ [model] [mae] [mse] [rmse] [r2] [mape] [mspe] [rmsle] [class="num">0] "D1_10" class="num">0.00381 class="num">0.00003 class="num">0.00530 class="num">0.77720 class="num">0.00356 class="num">0.00002 class="num">0.00257 [class="num">1] "D1_30" class="num">0.01809 class="num">0.00039 class="num">0.01963 -class="num">2.05545 class="num">0.01680 class="num">0.00033 class="num">0.00952 [class="num">2] "D1_52" class="num">0.00472 class="num">0.00004 class="num">0.00642 class="num">0.67327 class="num">0.00440 class="num">0.00004 class="num">0.00311 [class="num">3] "D1_63" class="num">0.00413 class="num">0.00003 class="num">0.00559 class="num">0.75230 class="num">0.00385 class="num">0.00003 class="num">0.00270 [model] [mae] [mse] [rmse] [r2] [mape] [mspe] [rmsle] [class="num">0] "D1_10" class="num">0.00381 class="num">0.00003 class="num">0.00530 class="num">0.77720 class="num">0.00356 class="num">0.00002 class="num">0.00257 MAPE = |(y_true-y_pred)/y_true| 如果 y_true = class="num">10 且 y_pred = class="num">5 MAPE = class="num">0.5 如果 y_true = class="num">5 且 y_pred = class="num">10 MAPE = class="num">1.0 [model] [mae] [mse] [rmse] [r2] [mape] [mspe] [rmsle] [class="num">3] "D1_63" class="num">0.00413 class="num">0.00003 class="num">0.00559 class="num">0.75230 class="num">0.00385 class="num">0.00003 class="num">0.00270 [class="num">0] "D1_10" class="num">0.00381 class="num">0.00003 class="num">0.00530 class="num">0.77720 class="num">0.00356 class="num">0.00002 class="num">0.00257 [class="num">3] "D1_63" class="num">0.00413 class="num">0.00003 class="num">0.00559 class="num">0.75230 class="num">0.00385 class="num">0.00003 class="num">0.00270 [model] [mae] [mse] [rmse] [r2] [mape] [mspe] [rmsle] [class="num">2] "D1_52" class="num">0.00472 class="num">0.00004 class="num">0.00642 class="num">0.67327 class="num">0.00440 class="num">0.00004 class="num">0.00311
测试器里轮换不同 ONNX 模型看信号
在 MT5 策略测试器里跑 ONNX 模型,最直观的办法是写个极简 EA,把不同周期的模型头文件依次挂上。原文用的 EURUSD D1 模型里,D1_52 在整个测试期只开出一笔卖出,且趋势判断始终没翻转,回归表现偏弱,测试器输出和上一节定性结论一致。
把 EA 里两行模型声明换掉,切到 D1_10,测试图形明显更有连续性,信号频率和可交易段落都更合理。再改两行切到 D1_63,图形质量比 D1_10 差一截——两个模型横向比,D1_10 的回归度量更优,测试器数据也指向同一结论。
下面这段 EA 核心就是 include 四个模型头文件、用一行 CModelEurusdD1_xx ExtModel; 决定加载哪个,OnTick 里调 PredictClass 拿预测类别。换模型只需注释/取消注释对应行,不必动逻辑。
这些模型仅用于演示 MQL5 怎么接 ONNX,EA 本身不适合直接上实盘。外汇和贵金属杠杆高、滑点跳空频繁,回测顺滑不等于实盘能复现,验证前先开模拟盘跑一轮。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ONNX.eurusd.D1.Prediction.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include "ModelEurusdD1_10.mqh" class="macro">#include "ModelEurusdD1_30.mqh" class="macro">#include "ModelEurusdD1_52.mqh" class="macro">#include "ModelEurusdD1_63.mqh" class="macro">#include <Trade\Trade.mqh> class="kw">input class="type">class="kw">double InpLots = class="num">1.0; class=class="str">"cmt">// Lots amount to open position class=class="str">"cmt">//CModelEurusdD1_10 ExtModel; class=class="str">"cmt">//CModelEurusdD1_30 ExtModel; CModelEurusdD1_52 ExtModel; class=class="str">"cmt">//CModelEurusdD1_63 ExtModel; CTrade ExtTrade; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { if(!ExtModel.Init(_Symbol,_Period)) class="kw">return(INIT_FAILED); Print("model ",ExtModel.GetModelName()); class=class="str">"cmt">//--- 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) { ExtModel.Shutdown(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { if(!ExtModel.CheckOnTick()) class="kw">return; class=class="str">"cmt">//--- predict next price movement vector prob(class="num">3); class="type">int predicted_class=ExtModel.PredictClass(TimeCurrent(),prob); Print("predicted class ",predicted_class); class=class="str">"cmt">//--- check trading according to prediction if(predicted_class>=class="num">0) if(PositionSelect(_Symbol))
◍ 开平仓信号怎么落进下单函数
模型给出 predicted_class 后,EA 不在主循环里直接交易,而是分流到 CheckForOpen 与 CheckForClose 两个函数,逻辑边界清晰,方便你单独改入场或出场规则。 CheckForOpen 先把预测类别映射成订单方向:PRICE_DOWN 对应 ORDER_TYPE_SELL,PRICE_UP 对应 ORDER_TYPE_BUY,其余情况 signal 保持 WRONG_VALUE 不动作。映射完才检查终端是否允许交易,再用 SymbolInfoDouble 取对应 BID 或 ASK 价格,调 ExtTrade.PositionOpen 以 InpLots 手数开仓,滑点和止损止盈传 0。 CheckForClose 则先读当前持仓类型,多单遇到 PRICE_DOWN、空单遇到 PRICE_UP 才置 bsignal=true。触发后先 PositionClose(_Symbol,3) 平掉原仓,紧接调用 CheckForOpen 反手开仓——这种「平仓即反手」结构在震荡行情可能频繁刷单,外汇与贵金属杠杆高,回撤风险需自测。 日志里能看到 EUUSD 在 2023.06.09 16:18:31 完成品种同步,收到 3720 字节符号信息;历史从 2011.01.03 一直补齐到 2023.04.07,仅加载 27 字节,说明本地已缓存大部分数据。你换品种时重点看这两行是否出现,没同步完就跑策略会读不到报价。
CheckForClose(predicted_class); else CheckForOpen(predicted_class); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check for open position conditions | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CheckForOpen(class="kw">const class="type">int predicted_class) { ENUM_ORDER_TYPE signal=WRONG_VALUE; class=class="str">"cmt">//--- check signals if(predicted_class==PRICE_DOWN) signal=ORDER_TYPE_SELL; class=class="str">"cmt">// sell condition else { if(predicted_class==PRICE_UP) signal=ORDER_TYPE_BUY; class=class="str">"cmt">// buy condition } class=class="str">"cmt">//--- open position if possible according to signal if(signal!=WRONG_VALUE && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { class="type">class="kw">double price=SymbolInfoDouble(_Symbol,(signal==ORDER_TYPE_SELL) ? SYMBOL_BID : SYMBOL_ASK); ExtTrade.PositionOpen(_Symbol,signal,InpLots,price,class="num">0,class="num">0); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check for close position conditions | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CheckForClose(class="kw">const class="type">int predicted_class) { class="type">bool bsignal=false; class=class="str">"cmt">//--- position already selected before class="type">long type=PositionGetInteger(POSITION_TYPE); class=class="str">"cmt">//--- check signals if(type==POSITION_TYPE_BUY && predicted_class==PRICE_DOWN) bsignal=true; if(type==POSITION_TYPE_SELL && predicted_class==PRICE_UP) bsignal=true; class=class="str">"cmt">//--- close position if possible if(bsignal && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { ExtTrade.PositionClose(_Symbol,class="num">3); class=class="str">"cmt">//--- open opposite CheckForOpen(predicted_class); } } class=class="str">"cmt">//+------------------------------------------------------------------+
「回测日志里藏着的开仓触发链」
在 MT5 策略测试器中跑 ONNX 模型驱动的 EURUSD 日线专家顾问,日志会暴露完整决策路径。上面这段实测记录显示:2023.06.09 16:18:32 启动测试,历史缓存分配了 283 根日线柱,实际装载 260 根(2022.01.03 至 2022.12.30),而测试窗口只取 2023.01.01 到 2023.02.01。 模型 D1_52 在 2023.01.02 07:02:00 输出预测类别 2,EA 随即以 1.07016 即时卖出 1 手 EURUSD,买价卖价同价 1.07016,点差显示为 0.7 点(1.07023-1.07016)。这一笔成交回报 deal #2,说明预测类别到下单是同步执行的,没有挂单延迟。 留意 ONNX 运行环境:api version 1.16.0 在 32.161 毫秒完成初始化,从加载到首笔预测仅用约 30 毫秒。外汇与贵金属自带高杠杆风险,这类模型信号在历史样本外可能失效,实盘前务必用 MT5 自测不同品种的点差与滑点。
欧元日线模型连发同类信号
在 2023.06.09 16:18:32 这一秒内,EURUSD D1 图表上的 ONNX 推理脚本批量吐出了 13 条预测日志,时间戳从 .196 跨到 .224,单条间隔约 2~3 毫秒。 这些日志对应的行情起点是 2023.01.03 至 2023.01.19 的日线 bar,其中 01.05 与 01.18 的零号时间带了 30 秒偏移,其余多为 00:00:00 开盘时刻。 全部 13 条记录的预测类别都是 class 2,说明该 ONNX 模型在回看 1 月前三周欧元日线时,倾向于给出同一类状态判定(具体是多头、震荡还是反转标签,取决于你训练时定的映射)。 这种连续同类的输出在 MT5 专家日志里很容易刷屏,建议直接把日志导到 csv 用 pandas 数 class 分布,而不是肉眼翻终端——外汇和贵金属杠杆高,模型信号仅供概率参考,别当成下单指令。
◍ 回测日志里连续 class 2 与反手信号
上面这段 MT5 策略测试器日志,记录了 ONNX 模型在 EURUSD D1 上的批量预测与一笔仓位切换。从 2023.01.20 到 2023.01.31,连续 9 个交易日的预测类别都是 class 2,说明模型在该窗口内对日线方向给出了高度一致的判读。 日志末尾出现了仓位动作:测试结束前平仓价为 1.08621,原持仓是 #2 卖单(开仓价 1.07016);几乎同一时刻 #3 买单以 1.08621 成交,等于在测试截断点反手做多。 这类连续同类预测叠加反手,值得在 MT5 里把日志级别开到详细,核对 ONNX 推理间隔与订单触发条件。外汇与贵金属自带高杠杆风险,class 2 只是模型输出标签,不预示后续必涨,实盘前请用历史数据复验。
「回测日志里藏着的切换开关」
一段 EURUSD 日线回测跑完,终端打出行:final balance 8366.00 USD,123499 ticks、22 bars,整个测试 0:00:00.294 就过,预处理仅占 0.016 秒。这种日志不是装饰,它告诉你模型文件加载和品种周期对齐都没报错,才敢往实盘推。 代码里真正决定用哪个模型的是这几行:CModelEurusdD1_10 ExtModel; 是激活态,其余三个 D1_30 / D1_52 / D1_63 都被注释掉了。也就是说同一套框架,靠换头文件引用和取消注释来切模型,而不是改策略逻辑。 外汇与贵金属高杠杆品种,回测通过不等于实盘能复现,8366 这个终点只是该次样本下的结果,换一段行情可能明显偏离。开 MT5 把 ModelEurusdD1_10 注释掉、放开 D1_52,重跑同一段 2023.06 日志对应的样本,对比 final balance 差值,比看任何说明都直观。
class="macro">#include "ModelEurusdD1_10.mqh" class="macro">#include "ModelEurusdD1_30.mqh" class="macro">#include "ModelEurusdD1_52.mqh" class="macro">#include "ModelEurusdD1_63.mqh" class="macro">#include <Trade\Trade.mqh> class="kw">input class="type">class="kw">double InpLots = class="num">1.0; class=class="str">"cmt">// Lots amount to open position CModelEurusdD1_10 ExtModel; class=class="str">"cmt">//CModelEurusdD1_30 ExtModel; class=class="str">"cmt">//CModelEurusdD1_52 ExtModel; class=class="str">"cmt">//CModelEurusdD1_63 ExtModel; CTrade ExtTrade;
衡量模型优劣的几个尺度
在价格预测模型的评估里,R-平方是最直白的拟合优度参考,数值越接近 1 代表解释力越强。把 MAE、RMSE、MAPE 三项放到一起对照,往往比单看一个误差指标更能暴露模型短板。 RMSLE 这种对数均方根误差,在价格预测里通常没必要引入,它更适用于多模型横向比较,哪怕只是同一模型改了参数后的几个变体。 文中那 22 个样本毕竟太少,谈不上统计显著,我们只是给出一种可复现的替代思路,你拿 MT5 接自己的行情跑一遍就能验证适配性。外汇与贵金属波动剧烈,任何模型输出都只是概率倾向,实盘前务必小资金压力测试。