Cómo integrar correctamente un modelo ONNX en un EA de MetaTrader 5

27 agosto 2026, 12:00
German Emilio Camon
0
4

Diagnóstico del error 5805, dimensiones de tensores y reproducción exacta de variables

Resumen

Integrar un modelo ONNX en MetaTrader 5 no consiste solamente en cargar un archivo y llamar a OnnxRun. El EA debe respetar un contrato preciso: cantidad y orden de entradas y salidas, dimensiones de cada tensor, tipos de datos y el mismo procedimiento de preparación de variables utilizado durante el entrenamiento.

Esta publicación técnica presenta un caso real en el que el Strategy Tester detenía el EA durante OnInit con el mensaje:

Error configurando forma de salida ONNX. Código: 5805
tester stopped because OnInit returns non-zero code 1

El origen no era el Strategy Tester ni la ubicación del archivo. El modelo tenía una entrada de 60 variables y dos salidas diferentes: una etiqueta y un vector de dos probabilidades. Configurar una única salida con forma [1, 1] no describía ese modelo.

Además de corregir las dimensiones, veremos cómo impedir un fallo más peligroso: obtener inferencias aparentemente válidas con variables construidas de forma distinta a las usadas en Python.

IMG_01

Flujo reproducible de integración ONNX entre Python y MetaTrader 5

1. El contrato entre el modelo y el EA

Un archivo ONNX define una interfaz. Antes de escribir la lógica de trading hay que conocer:

  • Cuántas entradas tiene el modelo.
  • El nombre, tipo y dimensiones de cada entrada.
  • Cuántas salidas tiene.
  • El nombre, tipo y dimensiones de cada salida.
  • El orden exacto de las variables.
  • La normalización aplicada durante el entrenamiento.
  • El significado de cada posición de la salida.

En el caso analizado, la inspección del modelo produjo esta interfaz:

Índice Nombre Tipo ONNX Dimensión exportada Dimensión usada en MT5
Entrada 0 input float32 [batch, 60] [1, 60]
Salida 0 label int64 [batch] [1]
Salida 1 probabilities float32 [batch, 2] [1, 2]

La primera dimensión era dinámica. Para inferir una observación por vez, el EA debía resolver batch como 1.

Esta tabla es el contrato de inferencia. Si cambia el modelo, el contrato debe revisarse de nuevo.

IMG_02

Contrato de entradas y salidas del modelo ONNX


2. ¿Qué significa realmente el error 5805?

En MQL5, el código 5805 corresponde a ERR_ONNX_INVALID_PARAMETER: valor de parámetro ONNX incorrecto.

Puede aparecer, entre otros casos, cuando:

  • Se intenta configurar un índice de entrada o salida inexistente.
  • La forma declarada no es compatible con el tensor del modelo.
  • Se supone que existe una sola salida cuando el modelo exportó dos.
  • Se confunde una etiqueta [1] con una matriz de probabilidades [1, 2].
  • Se copia la forma usada por otro modelo sin inspeccionar el archivo actual.

La dimensión [1, 1] no es incorrecta por naturaleza. Es apropiada para determinados modelos de regresión o modelos con una única salida escalar. El error consiste en asumir que sirve para cualquier modelo.

IMG_04

Comparación entre la forma de salida incorrecta y la interfaz real

En este caso concreto:

const long output_shape[] = {1, 1};
OnnxSetOutputShape(handle, 0, output_shape);

No representaba la interfaz real. El modelo exportado por el clasificador tenía dos salidas:

  1. Label: clase prevista, tipo int64, forma [batch].
  2. Probabilities: probabilidades de las dos clases, tipo float32, forma [batch, 2].

La configuración correcta fue:

const long label_shape[]       = {1};
const long probability_shape[] = {1, 2};

if(!OnnxSetOutputShape(handle, 0, label_shape))
  {
   Print("No se pudo configurar label. Error: ", GetLastError());
   return(INIT_FAILED);
  }

if(!OnnxSetOutputShape(handle, 1, probability_shape))
  {
   Print("No se pudieron configurar probabilities. Error: ", GetLastError());
   return(INIT_FAILED);
  }

Observe que no solo cambia la forma: también se configura explícitamente la salida de índice 1.


3. Inspeccionar el modelo antes de programar

3.1. Inspección rápida con Python

La forma más sencilla de comprobar la interfaz es usar ONNX Runtime:

import onnxruntime as ort

model_path = "ai_signal_h1_target100.onnx"

session = ort.InferenceSession(
    model_path,
    providers=["CPUExecutionProvider"]
)

print("ENTRADAS")
for i, item in enumerate(session.get_inputs()):
    print(i, item.name, item.type, item.shape)

print("SALIDAS")
for i, item in enumerate(session.get_outputs()):
    print(i, item.name, item.type, item.shape)

Para el modelo de este caso, la salida equivalente es:

ENTRADAS
0 input tensor(float) [None, 60]

SALIDAS
0 label tensor(int64) [None]
1 probabilities tensor(float) [None, 2]

None indica una dimensión dinámica. No debe copiarse como cero. Si el EA procesa una sola observación, se resuelve como 1.

3.2. Inspección desde MQL5

MQL5 también permite consultar el modelo mediante:

  • OnnxGetInputCount y OnnxGetOutputCount.
  • OnnxGetInputName y OnnxGetOutputName.
  • OnnxGetInputTypeInfo y OnnxGetOutputTypeInfo.

Como comprobación mínima:

long inputs  = OnnxGetInputCount(handle);
long outputs = OnnxGetOutputCount(handle);

Print("Entradas ONNX: ", inputs);
Print("Salidas ONNX: ", outputs);

for(long i = 0; i < inputs; i++)
   Print("Input ", i, ": ", OnnxGetInputName(handle, i));

for(long i = 0; i < outputs; i++)
   Print("Output ", i, ": ", OnnxGetOutputName(handle, i));

Este control evita continuar si el archivo cargado no es el modelo para el cual fue escrito el EA.


4. Cargar el modelo como recurso

Para un EA autónomo resulta conveniente compilar el modelo como recurso:

#resource "ai_signal_h1_target100.onnx" as const uchar ModelData[];

long g_onnx_handle = INVALID_HANDLE;

Luego, durante OnInit:

g_onnx_handle = OnnxCreateFromBuffer(ModelData, ONNX_DEFAULT);

if(g_onnx_handle == INVALID_HANDLE)
  {
   Print("OnnxCreateFromBuffer falló. Error: ", GetLastError());
   return(INIT_FAILED);
  }

Ventajas de este enfoque:

  • No depende de copiar manualmente el.onnx a MQL5\Files.
  • Funciona mejor en el entorno aislado del Strategy Tester.
  • El modelo viaja dentro del ejecutable compilado.
  • Reduce errores de rutas y nombres de archivo.

Hay una consecuencia importante: si se reemplaza el modelo, hay que recompilar el EA. El ejecutable anterior sigue conteniendo el recurso anterior.


5. Configurar todas las dimensiones

El modelo recibe una ventana de diez velas y seis variables por vela:

10 velas × 6 variables = 60 entradas

La inicialización completa queda así:

int ConfigureOnnxModel(const long handle)
  {
   if(OnnxGetInputCount(handle) != 1)
     {
      Print("Interfaz inesperada: se esperaba 1 entrada.");
      return(INIT_FAILED);
     }

   if(OnnxGetOutputCount(handle) != 2)
     {
      Print("Interfaz inesperada: se esperaban 2 salidas.");
      return(INIT_FAILED);
     }

   const long input_shape[]       = {1, 60};
   const long label_shape[]       = {1};
   const long probability_shape[] = {1, 2};

   ResetLastError();
   if(!OnnxSetInputShape(handle, 0, input_shape))
     {
      Print("OnnxSetInputShape falló. Error: ", GetLastError());
      return(INIT_FAILED);
     }

   ResetLastError();
   if(!OnnxSetOutputShape(handle, 0, label_shape))
     {
      Print("Salida label inválida. Error: ", GetLastError());
      return(INIT_FAILED);
     }

   ResetLastError();
   if(!OnnxSetOutputShape(handle, 1, probability_shape))
     {
      Print("Salida probabilities inválida. Error: ", GetLastError());
      return(INIT_FAILED);
     }

   return(INIT_SUCCEEDED);
  }

Es preferible validar los conteos antes de definir las formas. Así, un modelo sustituido accidentalmente genera un mensaje comprensible en lugar de un fallo ambiguo.


6. El problema silencioso: reproducir exactamente las variables

Corregir el error 5805 permite inicializar el modelo, pero no garantiza predicciones correctas.

Un modelo puede aceptar 60 números y devolver probabilidades sin detectar que el EA cambió:

  • El orden de las variables.
  • La dirección temporal de la ventana.
  • La fórmula de una mecha.
  • La escala de normalización.
  • La fuente de precios.
  • El tratamiento de divisiones por cero.
  • El uso de la vela actual en lugar de velas cerradas.

Ese tipo de error no produce un código 5805. Produce inferencias técnicamente válidas, pero semánticamente incorrectas.

6.1. Contrato de variables del ejemplo

Para cada una de las diez velas cerradas se construyeron seis variables, siempre en este orden:

  1. Proporción del cuerpo.
  2. Proporción de la mecha superior.
  3. Proporción de la mecha inferior.
  4. Dirección de la vela.
  5. Retorno logarítmico.
  6. Rango relativo.

Las fórmulas fueron:

rango      = máximo - mínimo
cuerpo     = |cierre - apertura|
mecha_sup  = máximo - max(apertura, cierre)
mecha_inf  = min(apertura, cierre) - mínimo

body_pct   = cuerpo / rango
upper_pct  = mecha_sup / rango
lower_pct  = mecha_inf / rango
direction  = +1 si cierre >= apertura; -1 en caso contrario
log_return = ln(cierre / cierre_anterior)
range_pct  = rango / cierre

La implementación MQL5 equivalente es:

double range = high_price - low_price;
if(range <= 0.0)
   range = 0.00001;

double body       = MathAbs(close_price - open_price);
double upper_wick = high_price - MathMax(open_price, close_price);
double lower_wick = MathMin(open_price, close_price) - low_price;

input_data[index++] = (float)(body / range);
input_data[index++] = (float)(upper_wick / range);
input_data[index++] = (float)(lower_wick / range);
input_data[index++] = (close_price >= open_price) ? 1.0f: -1.0f;
input_data[index++] = (float)MathLog(close_price / previous_close);
input_data[index++] = (float)(range / close_price);

6.2. Orden temporal

La ventana se carga desde la vela más antigua hacia la más reciente:

IMG_03

Ventana de diez velas cerradas sin utilizar la vela abierta

for(int shift = 10; shift >= 1; shift--)
  {
   // Construir las seis variables de cada vela cerrada.
  }

Usar shift=1 hasta shift=10 invertiría la secuencia. Las dimensiones seguirían siendo [1, 60], pero el modelo recibiría una estructura temporal diferente.

6.3. Evitar información futura

La vela 0 está abierta y puede cambiar hasta su cierre. Si el entrenamiento se realizó con velas cerradas, la inferencia debe usar también velas cerradas. Por eso la ventana termina en shift=1.

Esta correspondencia es indispensable para que el comportamiento histórico pueda reproducirse en tiempo real.


7. Preparar los búferes de salida

Los búferes deben coincidir tanto en tamaño como en tipo:

float input_data[];
ArrayResize(input_data, 60);

long label_output[];
ArrayResize(label_output, 1);

float probability_output[];
ArrayResize(probability_output, 2);

El modelo devuelve:

probability_output[0] = probabilidad de la clase 0 probability_output[1] = probabilidad de la clase 1

La inferencia se ejecuta respetando el orden de los parámetros ONNX: entrada 0, salida 0 y salida 1.

ResetLastError();

if(!OnnxRun(g_onnx_handle,
            ONNX_DEFAULT,
            input_data,
            label_output,
            probability_output))
  {
   Print("OnnxRun falló. Error: ", GetLastError());
   return;
  }

float probability_class_1 = probability_output[1];

MQL5 puede convertir automáticamente determinados tipos cuando no se usa ONNX_NO_CONVERSION. Durante el diagnóstico es mejor hacer coincidir los tipos del EA con los del modelo: float para tensor(float) y long para tensor(int64).

Cuando esa equivalencia está comprobada, ONNX_NO_CONVERSION puede utilizarse para evitar conversiones adicionales. No debe activarse antes de verificar tipos y tamaños.


8. Validación cruzada entre Python y MetaTrader 5

Una integración no debería considerarse terminada porque OnnxRun devuelve true. Hay que comparar el resultado de ambos entornos usando exactamente la misma muestra.

IMG_05

Validación cruzada de variables y probabilidades entre Python y MetaTrader 5

8.1. Guardar una muestra de referencia en Python

import numpy as np
import onnxruntime as ort

session = ort.InferenceSession(
    "ai_signal_h1_target100.onnx",
    providers=["CPUExecutionProvider"]
)

# X_reference debe contener las mismas 60 variables y el mismo orden.
X_reference = np.asarray(
    X_reference,
    dtype=np.float32
).reshape(1, 60)

label, probabilities = session.run(
    None,
    {"input": X_reference}
)

print("label:", label)
print("probabilities:", probabilities)

Además de las predicciones, conviene exportar las 60 variables de la muestra.

8.2. Registrar la misma muestra en MQL5

for(int i = 0; i < ArraySize(input_data); i++)
   PrintFormat("X[%02d] = %.9f", i, input_data[i]);

PrintFormat("label=%d p0=%.9f p1=%.9f",
            label_output[0],
            probability_output[0],
            probability_output[1]);

La comprobación debe hacerse en dos etapas:

  1. Comparar las 60 variables.
  2. Comparar las dos probabilidades.

Si las variables difieren, todavía no tiene sentido investigar la salida del modelo.


9. Prueba correcta en el Strategy Tester

Un procedimiento práctico es:

  1. Compilar el EA y confirmar 0 errors, 0 warnings.
  2. Ejecutar un intervalo corto con registros ONNX detallados.
  3. Confirmar que OnInit crea la sesión y configura las tres formas.
  4. Verificar que no aparezcan valores NaN o infinitos.
  5. Registrar una muestra y compararla con Python.
  6. Ejecutar una prueba fuera del periodo de entrenamiento.
  7. Reiniciar MT5 y repetir una prueba para confirmar que el recurso está integrado.
  8. Desactivar los registros extensos antes de una simulación larga.

Si el EA toma decisiones únicamente al abrir una nueva vela H1 y usa datos cerrados, el modo de ticks puede no cambiar las señales de inferencia. Sin embargo, sí puede modificar la ejecución de órdenes, el diferencial, los stops y el resultado económico. La elección del modelado debe responder a lo que se intenta validar.


10. Tabla de diagnóstico rápido

Código Significado Primera comprobación
5803 Fallo al ejecutar ONNX Runtime Revisar operadores compatibles y registros detallados
5804 Cantidad incorrecta de parámetros en OnnxRun Contar todas las entradas y salidas
5805 Valor de parámetro incorrecto Revisar índices y formas declaradas
5806 Tipo de parámetro incorrecto Comparar float, double, long, matrices y vectores
5807 Tamaño de parámetro incorrecto Comparar cantidad de elementos de cada buffer
5808 Dimensión no definida o inválida Configurar todas las formas dinámicas

Una práctica útil consiste en separar cada comprobación y registrar el error inmediatamente:

ResetLastError();

bool ok   = OnnxSetOutputShape(handle, 1, probability_shape);
int error = GetLastError();

if(!ok)
   Print("OnnxSetOutputShape(output=1) falló. Error: ", error);

Así se evita que una llamada posterior reemplace el código que se intentaba diagnosticar.


11. Lista de comprobación antes de operar

Modelo:

  • El archivo ONNX inspeccionado es exactamente el incorporado al EA.
  • Se conocen los nombres, tipos y formas de todas las entradas y salidas.
  • Las dimensiones dinámicas se resolvieron explícitamente.
  • Se recompiló el EA después de reemplazar el recurso.

Variables:

  • La cantidad de variables coincide.
  • El orden coincide con el entrenamiento.
  • Las fórmulas y constantes son idénticas.
  • La escala y normalización coinciden.
  • El orden temporal coincide.
  • Solo se usan datos disponibles en el momento de la señal.

Inferencia:

  • Los búferes tienen el tipo correcto.
  • Los búferes tienen el número correcto de elementos.
  • OnnxRun recibe entradas y salidas en el orden del modelo.
  • Una muestra produce resultados equivalentes en Python y MT5.

Validación:

  • Se probó un periodo no utilizado en el entrenamiento.
  • Se incluyeron diferencial, comisión y restricciones reales del símbolo.
  • Los registros extensos pueden desactivarse.
  • El EA libera la sesión mediante OnnxRelease.

Conclusión

El error 5805 fue útil porque detuvo una integración incompatible. El problema más peligroso habría sido otro: que el modelo aceptara 60 valores construidos de manera distinta y devolviera probabilidades plausibles sin advertirlo.

La integración correcta de ONNX en MetaTrader 5 exige mantener un contrato explícito entre entrenamiento e inferencia:

modelo correcto + formas correctas + tipos correctos + variables idénticas + orden temporal idéntico + validación cruzada = inferencia reproducible

Solo después de comprobar ese contrato tiene sentido evaluar umbrales, reglas de entrada, gestión del riesgo o rentabilidad. ONNX transporta el modelo; no corrige automáticamente las diferencias entre el entorno de investigación y el EA.


Referencias oficiales