基于预测的统计套利·综合运用
(3/3)· 当协整与皮尔逊系数遇上 MT5 实盘,前两步铺垫后这一步决定策略能否活过震荡
「用MT5测试器跑ONNX套利EA的反向验证」
拿到训练好的ONNX模型后,EA即可在MT5里运行。我选了NZDUSD与AUDUSD这对组合做演示:实操时它们原本协整且相关,但当下协整度已掉到0.05阈值以下,未过筛选;为省去重训模型的成本,教学里仍沿用这两类品种。 测试设定为:品种在1分钟周期上成交,ONNX模型基于1天周期预测,止损与止盈都设1500点,预测样本区间2023年1月1日至2024年1月初。外汇与贵金属杠杆高、点差跳变频繁,这类历史回测只反映过去概率,实盘可能明显偏离。 想换交易对,直接改代码里的 symbol2 输入参数,并把两个 #resource 指向对应品种的.onnx文件即可,不必动主体逻辑。下面这段声明了回测所需的模型路径与基础风控参数。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Hybrid Arbitrage_Statistic ONNX.mq5| class=class="str">"cmt">//| Copyright class="num">2024, Javier S. Gastón de Iriarte Cabrera. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, Javier S. Gastón de Iriarte Cabrera." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property strict class="macro">#include <Trade\Trade.mqh> input class="type">class="kw">double lotSize = class="num">0.1; class=class="str">"cmt">//input class="type">class="kw">double slippage = class="num">3; input class="type">class="kw">double stopLoss = class="num">1500; input class="type">class="kw">double takeProfit = class="num">1500; class=class="str">"cmt">//input class="type">class="kw">double maxSpreadPoints = class="num">10.0; class="macro">#resource "/Files/art/hybrid/NZDUSD_D1.onnx" as class="type">uchar ExtModel[] class="macro">#resource "/Files/art/hybrid/AUDUSD_D1.onnx" as class="type">uchar ExtModel2[] class="macro">#define SAMPLE_SIZE class="num">120 class="type">class="kw">string symbol1 = _Symbol; input class="type">class="kw">string symbol2 = "AUDUSD"; class="type">class="kw">ulong ticket1 = class="num">0; class="type">class="kw">ulong ticket2 = class="num">0; input class="type">bool isArbitrageActive = true; CTrade ExtTrade; class="type">class="kw">double spreads[class="num">1000]; class=class="str">"cmt">// Array para almacenar hasta class="num">1000 spreads class="type">int spreadIndex = class="num">0; class=class="str">"cmt">// Índice para el próximo spread a almacenar class="type">long ExtHandle=INVALID_HANDLE; class=class="str">"cmt">//class="type">int ExtPredictedClass=-class="num">1; class="type">class="kw">datetime ExtNextBar=class="num">0; class="type">class="kw">datetime ExtNextDay=class="num">0; class="type">class="kw">float ExtMin=class="num">0.0; class="type">class="kw">float ExtMax=class="num">0.0; class="type">long ExtHandle2=INVALID_HANDLE; class=class="str">"cmt">//class="type">int ExtPredictedClass=-class="num">1; class="type">class="kw">datetime ExtNextBar2=class="num">0; class="type">class="kw">datetime ExtNextDay2=class="num">0; class="type">class="kw">float ExtMin2=class="num">0.0; class="type">class="kw">float ExtMax2=class="num">0.0; class="type">class="kw">float predicted=class="num">0.0; class="type">class="kw">float predicted2=class="num">0.0; class="type">class="kw">float lastPredicted1=class="num">0.0; class="type">class="kw">float lastPredicted2=class="num">0.0; class="type">int Order=class="num">0; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { Print("EA de arbitraje ONNX iniciado"); class=class="str">"cmt">//--- create a model from class="kw">static buffer ExtHandle=OnnxCreateFromBuffer(ExtModel,ONNX_DEFAULT); if(ExtHandle==INVALID_HANDLE) {
给 ONNX 张量显式锁死维度
从静态缓冲区建好 ONNX 模型后,不能直接丢数据进去跑。很多导出的模型在输入、输出张量里没写死全部维度,MT5 端必须手动用 OnnxSetInputShape / OnnxSetOutputShape 把形状钉住,否则初始化会直接失败返回 INIT_FAILED。 输入形状按 {1, SAMPLE_SIZE, 1} 设:第一维是 batch size(一次只推 1 条),第二维是序列长度由 SAMPLE_SIZE 控制,第三维是通道数——这里只喂 Close 所以为 1。输出形状设 {1, 1},批次数必须和输入对齐,第二维是预测价格个数(同样只预测 Close)。 代码里建了两个模型句柄 ExtHandle 和 ExtHandle2,各自独立走一遍创建加设维度的流程,任一步 GetLastError 非 0 就打印并回 INIT_FAILED。实盘加载 EA 前,建议先在策略测试器里看日志有没有 OnnxSetInputShape error,外汇和贵金属波动大,模型维度错配会比手工信号更慢暴露问题。 退出时 OnDeinit 里对两个句柄判不等于 INVALID_HANDLE 才调 OnnxRelease 并置回 INVALID_HANDLE,避免重复释放句柄泄漏。开 MT5 把这段抄进你的 EA 初始化函数,改 SAMPLE_SIZE 就能验证不同回看窗口是否报维度错。
Print("OnnxCreateFromBuffer error ",GetLastError()); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- since not all sizes defined in the 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,SAMPLE_SIZE,class="num">1}; if(!OnnxSetInputShape(ExtHandle,ONNX_DEFAULT,input_shape)) { Print("OnnxSetInputShape error ",GetLastError()); class="kw">return(INIT_FAILED); } 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 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(ExtHandle,class="num">0,output_shape)) { Print("OnnxSetOutputShape error ",GetLastError()); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- create a model from class="kw">static buffer ExtHandle2=OnnxCreateFromBuffer(ExtModel2,ONNX_DEFAULT); if(ExtHandle2==INVALID_HANDLE) { Print("OnnxCreateFromBuffer error ",GetLastError()); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- since not all sizes defined in the 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_shape2[] = {class="num">1,SAMPLE_SIZE,class="num">1}; if(!OnnxSetInputShape(ExtHandle2,ONNX_DEFAULT,input_shape2)) { Print("OnnxSetInputShape error ",GetLastError()); class="kw">return(INIT_FAILED); } 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 input tensor class=class="str">"cmt">//--- second index - number of predicted prices(we only predict Close) class="kw">const class="type">long output_shape2[] = {class="num">1,class="num">1}; if(!OnnxSetOutputShape(ExtHandle2,class="num">0,output_shape2)) { Print("OnnxSetOutputShape error ",GetLastError()); class="kw">return(INIT_FAILED); } 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) { if(ExtHandle!=INVALID_HANDLE) { OnnxRelease(ExtHandle); ExtHandle=INVALID_HANDLE; } if(ExtHandle2!=INVALID_HANDLE) { OnnxRelease(ExtHandle2);
◍ 跨品种价差与日内极值跟踪的 tick 逻辑
这段 OnTick 是跨品种套利 EA 的心脏:每天零点重置一次极值捕获,每根 bar 刷新当前价与预测价,再把实时价差塞进环形缓冲。外汇与贵金属跨品种组合的高波动特性意味着,价差跳变可能在数秒内吞掉当日盈利,必须靠 tick 级监控而非日线收盘才动作。 日界判断用 TimeCurrent()>=ExtNextDay 触发 GetMinMax / GetMinMax2,随后把 ExtNextDay 对齐到 PERIOD_D1 的整点边界。注意源码里那段 PositionClose 的注释块——作者刻意留空,说明日切时不强制平仓,实盘若套用需自担隔夜跳空风险。 新 bar 未到就直接 return,避免重复计算;到了则重算 ExtNextBar 并即时更新 ExtMin/ExtMax 与 ExtMin2/ExtMax2。这里用 iClose(symbol, PERIOD_D1, 0) 取日线收盘价做极值,若你跑 XAUUSD 对比 XAGUSD,日线收盘价刷新慢,tick 内极值可能滞后于真实高低点。 价差采集部分把 currentSpread 写进 spreads[spreadIndex % 1000],索引自增,最多保留 1000 个样本。count = MathMin(spreadIndex, 1000) 决定标准差计算的可用长度——这意味着冷启动前几百 tick 的统计置信度偏低,信号可能偏噪。开 MT5 把这段贴进 EA,打印 Predicted spread 与 Last Predicted spread,你能直接看到预测价差收敛或发散的概率倾向。
ExtHandle2=INVALID_HANDLE; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- check new day if(TimeCurrent()>=ExtNextDay) { GetMinMax(); GetMinMax2(); class=class="str">"cmt">//--- set next day time ExtNextDay=TimeCurrent(); ExtNextDay-=ExtNextDay%PeriodSeconds(PERIOD_D1); ExtNextDay+=PeriodSeconds(PERIOD_D1); class=class="str">"cmt">/*ExtTrade.PositionClose(symbol1); ExtTrade.PositionClose(symbol2); ticket1 = class="num">0; ticket2 = class="num">0;*/ } class=class="str">"cmt">//--- check new bar if(TimeCurrent()<ExtNextBar) { class="kw">return; } class=class="str">"cmt">//--- set next bar time ExtNextBar=TimeCurrent(); ExtNextBar-=ExtNextBar%PeriodSeconds(); ExtNextBar+=PeriodSeconds(); class=class="str">"cmt">//--- check min and max class="type">class="kw">float close=(class="type">class="kw">float)iClose(symbol1,PERIOD_D1,class="num">0); if(ExtMin>close) ExtMin=close; if(ExtMax<close) ExtMax=close; class="type">class="kw">float close2=(class="type">class="kw">float)iClose(symbol2,PERIOD_D1,class="num">0); if(ExtMin2>close2) ExtMin2=close2; if(ExtMax2<close2) ExtMax2=close2; lastPredicted1=predicted; lastPredicted2=predicted2; class=class="str">"cmt">//--- predict next price PredictPrice(); PredictPrice2(); if(!isArbitrageActive || ArePositionsOpen()) { Print("Arbitraje inactivo o ya hay posiciones abiertas."); class="kw">return; } class="type">class="kw">double price1 = SymbolInfoDouble(symbol1, SYMBOL_BID); class="type">class="kw">double price2 = SymbolInfoDouble(symbol2, SYMBOL_ASK); class="type">class="kw">double currentSpread = MathAbs(price1 - price2); Print("current spread ", currentSpread); Print("Price1 ",price1); Print("Price2 ",price2); Print("PricePredicted1 ",predicted); Print("PricePredicted2 ",predicted2); Print("Last PricePredicted1 ",lastPredicted1); Print("Last PricePredicted2 ",lastPredicted2); class="type">class="kw">double predictedSpread = MathAbs(predicted - predicted2); Print("Predicted spread ", predictedSpread); class="type">class="kw">double LastpredictedSpread = MathAbs(lastPredicted1 - lastPredicted2); Print("Last Predicted spread ", LastpredictedSpread); class=class="str">"cmt">// Almacenar el spread actual en el array y actualizar el índice spreads[spreadIndex % class="num">1000] = currentSpread; spreadIndex++; class=class="str">"cmt">// Verifica si hay suficientes datos para calcular la desviación estándar class="type">int count = MathMin(spreadIndex, class="num">1000); class=class="str">"cmt">// Utiliza todos los datos disponibles hasta class="num">1000
「用标准差框住价差套利触发线」
当预测价差低于当前价差、且套利状态已激活时,代码会先算均值价差与整体样本标准差。均值取两个预测值的平均:meanSpread = (lastPredicted1 + lastPredicted2) / 2.0,标准差则基于全部 spread 数组重算,而非前面只取前 count 个样本的局部值。 触发线按均值 ±2 倍标准差铺设:shortEntry = meanSpread - 2*stdDevSpread 作为空单入场,longEntry = meanSpread + 2*stdDevSpread 作为多单入场,两者都以回归均值(meanSpread)为退出参考。这种 2σ 边界在外汇与贵金属价差序列中,约对应 95% 置信带,极端价差穿透后倾向均值回归,但属概率事件而非必然。 若 price1 跌破 shortEntry 且两持仓票为空,就下 Buy symbol1 / Sell symbol2 的对冲仓;若 price2 跌破 shortEntry 则反向开仓。开仓后立刻把 ticket 置 0,相当于强制等待下一轮条件重判。外汇与贵金属价差套利受滑点、点差跳变影响,实盘前务必在 MT5 策略测试器用真实 tick 回测该 2σ 逻辑。
class="type">class="kw">double stdDevSpread = CalculateStdDev(spreads, class="num">0, count); class=class="str">"cmt">//Print("StdDevSpread ", stdDevSpread); class=class="str">"cmt">// Verifica si el spread es lo suficientemente bajo para el arbitraje if(LastpredictedSpread< currentSpread) { class=class="str">"cmt">// Inicia el arbitraje si aún no está activo if(isArbitrageActive) { class=class="str">"cmt">//Print("max spread : ",maxSpreadPoints * _Point); class="type">class="kw">double meanSpread = (lastPredicted1 + lastPredicted2) / class="num">2.0; Print("mean spread: ",meanSpread); class="type">class="kw">double stdDevSpread = CalculateStdDev(spreads, class="num">0, ArraySize(spreads)); Print("StdDevSpread ", stdDevSpread); class="type">class="kw">double shortEntry = meanSpread - class="num">2 * stdDevSpread ; class="type">class="kw">double shortExit = meanSpread; class="type">class="kw">double longEntry = meanSpread + class="num">2 * stdDevSpread ; class="type">class="kw">double longExit = meanSpread; Print("Long Entry: ", longEntry, " Short Entry: ", shortEntry); class=class="str">"cmt">// Comprueba si la condición de entrada corta se cumple para el arbitraje if(price1 < shortEntry && (ticket1 == class="num">0 || ticket2 == class="num">0)) { Print("Preparando para abrir órdenes"); Order = class="num">1; Print("Error al abrir posiciones de arbitraje: ", GetLastError()); ticket1 = ExtTrade.PositionOpen(symbol1, ORDER_TYPE_BUY, lotSize, price1, price1 - stopLoss * _Point, price1 + takeProfit * _Point, "Arbitraje"); ticket2 = ExtTrade.PositionOpen(symbol2, ORDER_TYPE_SELL, lotSize, price2, price2 + stopLoss * _Point, price2 - takeProfit * _Point, "Arbitraje"); ticket1=class="num">0; ticket2=class="num">0; } else if(price2 < shortEntry && (ticket1 == class="num">0 || ticket2 == class="num">0)) { Print("Preparando para abrir órdenes"); Order = class="num">2; Print("Error al abrir posiciones de arbitraje: ", GetLastError()); ticket1 = ExtTrade.PositionOpen(symbol1, ORDER_TYPE_SELL, lotSize, price1, price1 + stopLoss * _Point, price1 - takeProfit * _Point, "Arbitraje"); ticket2 = ExtTrade.PositionOpen(symbol2, ORDER_TYPE_BUY, lotSize, price2, price2 - stopLoss * _Point, price2 + takeProfit * _Point, "Arbitraje"); ticket1=class="num">0; ticket2=class="num">0; }
价差突破后的双向开仓分支
这段逻辑处理的是统计套利里「价差偏离均值超过阈值」之后的下单动作。当 price1 大于 longEntry 且两个订单号至少有一个为 0 时,程序先把 Order 标记为 3,随后以 symbol1 卖、symbol2 买的方式建立对冲仓,止损止盈都按 stopLoss、takeProfit 乘 _Point 换算到点值。 另一侧若 price2 大于 longEntry 且ticket未占满,则走 Order=4 的镜像分支:symbol1 买、symbol2 卖,方向完全反过来。两个分支在开仓后都把 ticket1、ticket2 重置为 0,意味着下一根 K 线若条件再触发会重新尝试建仓,而不是补单。 真正决定「偏哪边开」的是后面这三行:meanSpread 取两个预测值均值,stdDevSpread2 用全样本标准差,shortEntry 直接钉在 meanSpread 减 2 倍标准差。也就是说,只有价差掉到均值下方两个标准差才考虑空价差出场,实战里这属于典型的 95% 置信带外触发,外汇与贵金属价差受隔夜利息干扰大,此类阈值在高波动时段可能频繁假突破,属高风险操作。
}
else
if(price1 > longEntry && (ticket1 == class="num">0 || ticket2 == class="num">0))
{
Print("Preparando para abrir órdenes");
Order = class="num">3;
Print("Error al abrir posiciones de arbitraje: ", GetLastError());
ticket1 = ExtTrade.PositionOpen(symbol1, ORDER_TYPE_SELL, lotSize, price1, price1 + stopLoss * _Point, price1 - takeProfit * _Point, "Arbitraje");
ticket2 = ExtTrade.PositionOpen(symbol2, ORDER_TYPE_BUY, lotSize, price2, price2 - stopLoss * _Point, price2 + takeProfit * _Point, "Arbitraje");
ticket1=class="num">0;
ticket2=class="num">0;
}
else
if(price2 > longEntry && (ticket1 == class="num">0 || ticket2 == class="num">0))
{
Print("Preparando para abrir órdenes");
Order = class="num">4;
Print("Error al abrir posiciones de arbitraje: ", GetLastError());
ticket1 = ExtTrade.PositionOpen(symbol1, ORDER_TYPE_BUY, lotSize, price1, price1 - stopLoss * _Point, price1 + takeProfit * _Point, "Arbitraje");
ticket2 = ExtTrade.PositionOpen(symbol2, ORDER_TYPE_SELL, lotSize, price2, price2 + stopLoss * _Point, price2 - takeProfit * _Point, "Arbitraje");
ticket1=class="num">0;
ticket2=class="num">0;
}
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double meanSpread = (lastPredicted1 + lastPredicted2) / class="num">2.0;
class=class="str">"cmt">//Print("mean spread: ",meanSpread);
class="type">class="kw">double stdDevSpread2 = CalculateStdDev(spreads, class="num">0, ArraySize(spreads));
class=class="str">"cmt">//Print("StdDevSpread ", stdDevSpread);
class="type">class="kw">double shortEntry = meanSpread - class="num">2 * stdDevSpread2 ;
class="type">class="kw">double shortExit = meanSpread;◍ 价差回归与标准差计算的底层的平仓逻辑
配对套利里,价差偏离均值 2 个标准差常被当作触发线。下面这行把多头入场阈值钉在 meanSpread + 2 * stdDevSpread2,回中到 meanSpread 就视为离场信号: double longEntry = meanSpread + 2 * stdDevSpread2 ; double longExit = meanSpread; 平仓判断把四种持仓组合都收口到同一次清仓:symbol2 价格跌破 longExit 且持 ticket2(Order==4)、symbol1 升破 shortExit 且持 ticket1(Order==1/3)、symbol2 升破 shortExit 且持 ticket1(Order==2)。任一满足就双向 PositionClose 并把 ticket 归零,日志打“Arbitraje detenido”。 标准差用样本方差开方,CalculateStdDev 对 data[start..start+count] 累加与平方和,variance = sumSq/count - mean^2,返回 MathSqrt(variance)。实盘里若 count 偏小(如 < 30),stdDev 估计会抖,触发线可能虚高。 ArePositionsOpen 只认 symbol1/symbol2 上 volume>0 的持仓,用来挡掉重复开仓;PredictPrice 则先做归一化保护(ExtMin>=ExtMax 直接 return),再用 CopyRates 取日线收盘价填 x_norm。外汇与贵金属价差策略滑点大,回测达标不等于实盘能成交,属高风险。
class="type">class="kw">double longEntry = meanSpread + class="num">2 * stdDevSpread2 ; class="type">class="kw">double longExit = meanSpread; if((price2 < longExit && ticket2 != class="num">0 && Order==class="num">4) || (price1 > shortExit && ticket1 != class="num">0 && Order==class="num">1) || (price2 > shortExit && ticket1 != class="num">0 && Order==class="num">2) || (price1 < longExit && ticket2 != class="num">0 && Order==class="num">3)) { ExtTrade.PositionClose(ticket1); ExtTrade.PositionClose(ticket2); ticket1 = class="num">0; ticket2 = class="num">0; Print("Arbitraje detenido - Cerrando órdenes"); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CalculateStdDev(class="type">class="kw">double &data[], class="type">int start, class="type">int count) { class="type">class="kw">double sum = class="num">0; class="type">class="kw">double sumSq = class="num">0; for(class="type">int i = start; i < start + count; i++) { sum += data[i]; sumSq += data[i] * data[i]; } class="type">class="kw">double mean = sum / count; class="type">class="kw">double variance = (sumSq / count) - (mean * mean); class="kw">return MathSqrt(variance); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool ArePositionsOpen() { class=class="str">"cmt">// Check for positions on symbol1 if(PositionSelect(symbol1) && PositionGetDouble(POSITION_VOLUME) > class="num">0) class="kw">return true; class=class="str">"cmt">// Check for positions on symbol2 if(PositionSelect(symbol2) && PositionGetDouble(POSITION_VOLUME) > class="num">0) class="kw">return true; class="kw">return class="kw">false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void PredictPrice(class="type">void) { class="kw">static vectorf output_data(class="num">1); class=class="str">"cmt">// vector to get result class="kw">static vectorf x_norm(SAMPLE_SIZE); class=class="str">"cmt">// vector for prices normalize class=class="str">"cmt">//--- check for normalization possibility if(ExtMin>=ExtMax) { Print("ExtMin>=ExtMax"); class=class="str">"cmt">//ExtPredictedClass=-class="num">1; class="kw">return; } class=class="str">"cmt">//--- request last bars if(!x_norm.CopyRates(_Symbol,PERIOD_D1,COPY_RATES_CLOSE,class="num">1,SAMPLE_SIZE)) { Print("CopyRates ",x_norm.Size());
「双模型并行推理的价格归一化实现」
这段逻辑里出现了两个对称的预测函数 PredictPrice 与 PredictPrice2,分别针对主品种和第二个品种(symbol2)做 ONNX 推理。两者都依赖 SAMPLE_SIZE 个日线收盘价,代码里 GetMinMax 与 GetMinMax2 用 CopyRates 拉取 PERIOD_D1 的最近 120 根 K 线(SAMPLE_SIZE=120),取收盘价的极小值和极大值做归一化边界。 归一化步骤很直接:x_norm 减去 ExtMin 再除以 (ExtMax-ExtMin),把价格压到 0~1 区间喂给 OnnxRun。推理失败后 Print 报错并直接 return,不会向下写预测值;成功则从 output_data[0] 反归一化:predicted = output_data[0]*(ExtMax-ExtMin)+ExtMin,还原成真实价格量纲。 第二个品种完全复刻该流程,但独立维护 ExtMin2/ExtMax2 与 ExtHandle2。若 ExtMin2>=ExtMax2 会先拦截并打印,避免除零。外汇与贵金属日线波动大,这种 120 日极值归一化在趋势突变周可能压缩近期信号,调参时建议先打印 last_close 与边界值核对。 开 MT5 把这两段挂到 EA 里,分别指定 symbol2 为 XAUUSD 与 EURUSD,跑一周观察 predicted 与盘面收盘的偏离,能快速验证模型输出是否脱钩。
class=class="str">"cmt">//ExtPredictedClass=-class="num">1; class="kw">return; } class="type">class="kw">float last_close=x_norm[SAMPLE_SIZE-class="num">1]; class=class="str">"cmt">//--- normalize prices x_norm-=ExtMin; x_norm/=(ExtMax-ExtMin); class=class="str">"cmt">//--- run the inference if(!OnnxRun(ExtHandle,ONNX_NO_CONVERSION,x_norm,output_data)) { Print("OnnxRun"); class=class="str">"cmt">//ExtPredictedClass=-class="num">1; class="kw">return; } class=class="str">"cmt">//--- denormalize the price from the output value predicted=output_data[class="num">0]*(ExtMax-ExtMin)+ExtMin; class=class="str">"cmt">//class="kw">return predicted; } class="type">void PredictPrice2(class="type">void) { class="kw">static vectorf output_data2(class="num">1); class=class="str">"cmt">// vector to get result class="kw">static vectorf x_norm2(SAMPLE_SIZE); class=class="str">"cmt">// vector for prices normalize class=class="str">"cmt">//--- check for normalization possibility if(ExtMin2>=ExtMax2) { Print("ExtMin2>=ExtMax2"); class=class="str">"cmt">//ExtPredictedClass=-class="num">1; class="kw">return; } class=class="str">"cmt">//--- request last bars if(!x_norm2.CopyRates(symbol2,PERIOD_D1,COPY_RATES_CLOSE,class="num">1,SAMPLE_SIZE)) { Print("CopyRates ",x_norm2.Size()); class=class="str">"cmt">//ExtPredictedClass=-class="num">1; class="kw">return; } class="type">class="kw">float last_close2=x_norm2[SAMPLE_SIZE-class="num">1]; class=class="str">"cmt">//--- normalize prices x_norm2-=ExtMin2; x_norm2/=(ExtMax2-ExtMin2); class=class="str">"cmt">//--- run the inference if(!OnnxRun(ExtHandle2,ONNX_NO_CONVERSION,x_norm2,output_data2)) { Print("OnnxRun"); class=class="str">"cmt">//ExtPredictedClass=-class="num">1; class="kw">return; } class=class="str">"cmt">//--- denormalize the price from the output value predicted2=output_data2[class="num">0]*(ExtMax2-ExtMin2)+ExtMin2; class=class="str">"cmt">//--- classify predicted price movement class=class="str">"cmt">//class="kw">return predicted2; } class="type">void GetMinMax(class="type">void) { vectorf close; close.CopyRates(_Symbol,PERIOD_D1,COPY_RATES_CLOSE,class="num">0,SAMPLE_SIZE); ExtMin=close.Min(); ExtMax=close.Max(); } class="type">void GetMinMax2(class="type">void) { vectorf close2;
用 D1 收盘价给双品种定极值
在跨品种相关性扫描里,第二只标的的日线收盘序列要先抓满 SAMPLE_SIZE 根,再用 CopyRates 拉 PERIOD_D1 的 COPY_RATES_CLOSE 字段,避免用盘中价污染极值。 ExtMin2 与 ExtMax2 直接取该序列的 Min() 和 Max(),这两个值后续用来做归一化或阈值判定。若 SAMPLE_SIZE 设得太小,极值会被近期噪声主导,跨品种比对会失真。 下面这段 Python 侧先把符号池收窄到 EUR/USD 相关组:凡是以 EUR 开头、USD 开头,或以 USD 结尾的品种才保留,降低无关交叉盘的干扰。外汇与贵金属杠杆高,这类筛选只解决样本相关性,不预示任何方向。
close2.CopyRates(symbol2,PERIOD_D1,COPY_RATES_CLOSE,class="num">0,SAMPLE_SIZE); ExtMin2=close2.Min(); ExtMax2=close2.Max(); } symbols = [s.name for s in symbols if s.name.startswith(&class="macro">#x27;EUR&class="macro">#x27;) or s.name.startswith(&class="macro">#x27;USD&class="macro">#x27;) or s.name.endswith(&class="macro">#x27;USD&class="macro">#x27;)]
◍ 拿纳斯达克配对跑一遍协整筛选
股票领域的统计套利思路搬到外汇/贵金属之外也成立,我用纳斯达克对冲组合做了第二个例子。原脚本里只圈定 EUR/USD 系品种,我改成了从 MT5 全部品种里捞出路径含 NASDAQ 且带 # 号、不含小数点的 CFD 符号,再把协整 p 值小于 0.05 的配对写进 CSV。 脚本跑完打印出『强相关且协整的配对数』,实际筛完我选了 Amazon(#AMZN) 与 Netflix(#NFLX) 这一对,二者相关系数 0.9666、协整 p 值 0.0217,样本内显著。 预测区间取 2023-01-01 到 2024-01-01,为提升拟合我把样本量扩到 120*25*3 = 9000 根 bar,覆盖一年以上 D1 数据。初版止损 400、止盈 800,后经快速优化微调参数,回测指标里出现 6.86、47.01、0.9395、25.98、674.74、0.9736 等数值,可作为你重跑时的对照基准。 外汇/贵金属及股指 CFD 均属高风险品种,这套流程只是研究示例而非即插即用 EA。你要复现就得自己改 .py 里的日期重新训 ONNX,并在策略测试器同步换日期,我建议每周或每月重训一次模型而非长期不换。
symbols = [s.name for s in symbols if s.name.startswith(&class="macro">#x27;EUR&class="macro">#x27;) or s.name.startswith(&class="macro">#x27;USD&class="macro">#x27;) or s.name.endswith(&class="macro">#x27;USD&class="macro">#x27;)] # Crea un DataFrame con la información completa de los símbolos symbols_df = pd.DataFrame([{&class="macro">#x27;Symbol&class="macro">#x27;: symbol.name, &class="macro">#x27;Path&class="macro">#x27;: symbol.path} for symbol in all_symbols]) # Filtra adicionalmente para obtener solo los CFDs de NASDAQ # Asumiendo que los CFDs tienen un identificador único en el &class="macro">#x27;Path&class="macro">#x27; nasdaq_group4_df = symbols_df[symbols_df[&class="macro">#x27;Path&class="macro">#x27;].str.contains(&class="macro">#x27;NASDAQ&class="macro">#x27;)] # Filtra aún más para obtener solo los símbolos que NO contienen &class="macro">#x27;.&class="macro">#x27; nasdaq_group4_df3 = nasdaq_group4_df[nasdaq_group4_df[&class="macro">#x27;Symbol&class="macro">#x27;].str.contains(&class="macro">#x27;#&class="macro">#x27;)] nasdaq_group4_df2 = nasdaq_group4_df3[~nasdaq_group4_df3[&class="macro">#x27;Symbol&class="macro">#x27;].str.contains(&class="macro">#x27;\.&class="macro">#x27;)] # Ahora, obtenemos la lista de símbolos filtrados filtered_symbols = nasdaq_group4_df2[&class="macro">#x27;Symbol&class="macro">#x27;].tolist() # Descargar datos históricos y almacenar en un diccionario symbols = filtered_symbols # Filtrar y guardar solo los pares cointegrados con p-valor menor de class="num">0.05 en un archivo CSV result_df = pd.DataFrame(cointegrated_pairs, columns=[&class="macro">#x27;Symbol1&class="macro">#x27;, &class="macro">#x27;Symbol2&class="macro">#x27;, &class="macro">#x27;Correlation&class="macro">#x27;, &class="macro">#x27;Cointegration P-value&class="macro">#x27;]) result_df.to_csv(&class="macro">#x27;cointegrated_pairs.csv&class="macro">#x27;, index=False) # Imprimir el total de pares cointegrados print(f&class="macro">#x27;Total de pares con fuerte correlación y cointegrados: {len(cointegrated_pairs)}&class="macro">#x27;) class="macro">#AMZN class="macro">#NFLX class="num">0.966605859 class="num">0.021683012 sample_size = class="num">120*class="num">25*class="num">3 class="num">6.856399020501732 class="num">47.010207528337105 class="num">0.9395402850007741 class="num">25.975755379462548 class="num">674.7398675336775 class="num">0.9735838717570285
「收束」
把皮尔逊系数指标和 ONNX 预测型套利 EA 跑通后,真正拉开差距的往往不是模型本身,而是配对筛选。用文末 .py 筛选器吐出的 cointegrated_pairs.csv(约 220 KB,含多组协整对)去喂 EA,比盲目上全部品种更稳。 实盘前建议在 MT5 里手动拧一遍 SL/TP:原文提到微调止损止盈能改善结果,但外汇与贵金属杠杆高、滑点跳空频繁,任何回测优值都只是概率倾向,不等于实盘必然。 文件落位别搞错——ONNX 模型进 MQL5/Files,mq5 指标进 Indicators,EA 进 Experts,否则 EA 加载会直接报空引用。写完这套流程,剩下就是你自己开 MT5 验证哪组配对在你经纪商点差下还能活。