English Русский 中文 Deutsch 日本語 Português
preview
Superando las limitaciones del aprendizaje automático (Parte 2): Falta de reproducibilidad

Superando las limitaciones del aprendizaje automático (Parte 2): Falta de reproducibilidad

MetaTrader 5Ejemplos |
30 2
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Con frecuencia recibo comentarios alentadores de nuestros lectores, pero un tema recurrente en los mensajes privados y los comentarios es la dificultad que algunos encuentran al intentar replicar los resultados presentados en nuestros artículos. Al principio, esto me desconcertó, pero tras reflexionar un poco, surgió una posible explicación.

El mercado financiero global funciona como una red masiva y descentralizada. Existen numerosos brókers en el mundo, y cada día se registran nuevos, pero no existe una autoridad internacional única que los regule o coordine sus flujos de precios. Cada bróker tiene la libertad de obtener los precios de sus fuentes de datos o servicios propios preferidos, como Reuters. 

Por lo tanto, si comparas la rentabilidad del par EURUSD en dos brókeres —llamémoslos «Bróker A» y «Bróker B»—, es posible que observes que ese mismo par evoluciona en direcciones opuestas en el mismo momento. Por ejemplo, el bróker A podría informar que el EURUSD se apreció un 0,12% en un día, mientras que el bróker B registra una depreciación del -0,65% para ese mismo día.


El meollo del asunto: discrepancia de datos entre brókers

Para este análisis, seleccioné al azar dos brókers que yo, personalmente, utilizo para operar de forma independiente. De acuerdo con nuestras normas de la comunidad, que prohíben la promoción de brókeres, sus nombres han sido omitidos y sustituidos por «Bróker A» y «Bróker B».

Utilizando la biblioteca MetaTrader 5 de Python, solicité cuatro años de datos históricos diarios del par EURUSD a ambos brókeres. Tras revisar los datos, observé que las marcas de tiempo no coincidían: los datos de un bróker se remontaban a septiembre de 2019, mientras que los del otro solo llegaban hasta agosto de 2020. Sin embargo, ambos devolvieron exactamente 1.460 filas de datos diarios, cumpliendo correctamente con nuestra solicitud.

Dada la naturaleza descentralizada de los brókers, es de esperar que sus zonas horarias de funcionamiento difieran. Sin embargo, resultan menos evidentes los efectos del horario de verano, los días festivos oficiales y otras discrepancias sutiles, que pueden distorsionar aún más la sincronización de las marcas de tiempo.

A continuación, calculamos la rentabilidad del EURUSD a 10 días en ambos brókers y observamos que las propiedades numéricas del símbolo EURUSD no coincidían entre sí. La rentabilidad media del EURUSD a 10 días con el bróker A fue de 0,000267, mientras que con el bróker B la rentabilidad media a 10 días fue de -0,000352. Esto representa una diferencia de aproximadamente el 232% en la rentabilidad esperada del mismo activo subyacente.

Para empeorar las cosas, parece que la rentabilidad esperada del bróker A conlleva un 21 % más de riesgo que la rentabilidad esperada del bróker B. Así lo sugiere el hecho de que la varianza en la rentabilidad entre ambos brókeres aumentó en la misma proporción, un 21 %. 

Nota para principiantes: Aquí basta con recordar que la varianza de los rendimientos se considera una medida del riesgo financiero. Cualquier libro de texto introductorio sobre la teoría de la cartera financiera puede demostrar esto a los lectores que tal vez no estuvieran familiarizados con este principio de antemano.

En estadística, podemos averiguar si dos variables varían al unísono o si lo hacen de forma independiente entre sí midiendo sus niveles de correlación. Las medidas de correlación estandarizadas van de 1 a -1. Una puntuación de 1 implica que las variables se mueven perfectamente en la misma dirección, mientras que una puntuación de -1 implica que las variables se mueven perfectamente en direcciones opuestas. Cuando comparamos los coeficientes de correlación de Pearson entre los dos brókers, yo, el autor, esperaba sinceramente coeficientes de correlación cercanos a 1. Sin embargo, los datos mostraron niveles de correlación de tan solo 0,41. 

Esto sugiere que cualquier creencia de que los niveles de precios del símbolo EURUSD se moverán en armonía entre diferentes brókeres parece carecer de fundamento matemático. Más bien los resultados de nuestra prueba sugieren que más de la mitad del tiempo, el mercado EURUSD parece moverse en direcciones distintas entre diferentes brókeres.

Otras características numéricas importantes de las cotizaciones de los dos brókeres no hicieron sino reforzar la gravedad de los problemas que este artículo pone de manifiesto. En nuestra anterior discusión sobre las limitaciones de la IA, mostramos al lector algunos de los inconvenientes asociados con las métricas comúnmente utilizadas para construir modelos de regresión, como el RMSE. El lector puede encontrar el enlace a ese artículo aquí.

En resumen, hemos aconsejado al lector que no considere el RMSE como una métrica aislada, sino que lo interprete con cierta cautela, comparando la relación entre el rendimiento del modelo que pretende utilizar (suma de cuadrados residuales, RSS) y el error generado por un modelo sencillo que siempre predice la rentabilidad media del mercado (suma total de cuadrados, TSS). La cuestión es que a los lectores les puede sorprender lo difícil que puede resultar superar los resultados del modelo más sencillo. La relación RSS/TSS nos indica con qué eficiencia estamos superando al modelo simple.

Cabría esperar que, para un mismo valor, esta ratio se mantuviera prácticamente constante, incluso entre diferentes brókers. Sin embargo, nuestra capacidad para superar un modelo que predice la rentabilidad media del mercado mejoró en un 7 % simplemente cambiando de bróker. Esto significa que la rentabilidad del EURUSD a 10 días es aproximadamente un 7 % más fácil de predecir directamente con el bróker B que con el bróker A.

Los estadísticos suelen comparar el centro de una distribución con su desviación típica para conocer mejor las características de la cola de dicha distribución. Si reinterpretamos esta operación para aplicarla a la rentabilidad del EURUSD en un horizonte de 10 días, aprenderíamos un método numérico para comparar qué bróker tiende a generar rentabilidades superiores a la media. Siguiendo este razonamiento, la rentabilidad a 10 días del par EURUSD del bróker B parecía estar inflada en un 147%.

A estas alturas, el problema al que nos enfrentamos debería estar claro: no se garantiza que las características numéricas importantes de un mismo símbolo sean consistentes entre los distintos brókers. En consecuencia, la rentabilidad de una estrategia de trading determinada no siempre puede reproducirse de forma fiable entre diferentes brókeres.

Las estrategias de trading que integran modelos de IA creados mediante la API de ONNX, o incluso de forma nativa en MQL5, pueden no cumplir sistemáticamente con las expectativas de los inversores, a menos que el tiempo adicional necesario para adaptar la IA de forma específica al bróker previsto se convierta en una práctica ampliamente adoptada. Si bien este trabajo requiere mucho tiempo, es evidente que es un trabajo fundamental.

A medida que lea este artículo, recrearemos paso a paso el ciclo de producción que suelen seguir la mayoría de los desarrolladores de MQL5. Nuestro objetivo con este artículo es ilustrar que cuando un desarrollador crea y optimiza su aplicación utilizando su bróker privado, en nuestro caso el Bróker B, pero su cliente implementa esa aplicación con un bróker diferente, el Bróker A, entonces el problema no está tan lejos del desarrollador y su cliente. Cualquier desarrollador que siga un ciclo de producción de este tipo probablemente recibirá opiniones encontradas sobre sus productos.

Para evitar niveles de rendimiento tan insatisfactorios, los desarrolladores de MQL5 que deseen ofrecer servicios fiables deben tener en cuenta que sus clientes podrían beneficiarse más si contaran con estrategias y aplicaciones adaptadas a brókers específicos para la seguridad de los consumidores a los que deseamos servir en el Marketplace.


Empezando

Primero necesitamos importar nuestras bibliotecas numéricas estándar.

#Load our libraries
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import MetaTrader5 as mt5

Defina en qué marco temporal y par de divisas nos centraremos, así como cuántas filas de datos necesitamos.

#Let us define certain constants
TF = mt5.TIMEFRAME_D1
DATA = (365 * 4)
START = 1
PAIR = "EURUSD"

Inicia tu terminal.

#Log in to the terminal
if mt5.initialize():
   print('Logged in successfully')

else:
   print('Failed To Log In')

Sesión iniciada correctamente

Analicemos la dificultad de pronosticar el EURUSD con el Bróker A.

EURUSD_BROKER_A = pd.DataFrame(mt5.copy_rates_from_pos(PAIR,TF,START,DATA))
#Store the data we retrieved from broker A
EURUSD_BROKER_A.to_csv("EURUSD BROKER A.csv")

Ahora repetiremos el mismo procedimiento para el bróker B.

#I have manually changed brokers using the MT5 terminal, you should also do the same on your side
EURUSD_BROKER_B = pd.DataFrame(mt5.copy_rates_from_pos(PAIR,TF,START,DATA))
#Store the data we retrieved from broker B
EURUSD_BROKER_B.to_csv("EURUSD BROKER B.csv")

¡Excelente! Ahora que hemos recopilado datos históricos del par EURUSD de ambos brókeres, comencemos a analizar las propiedades empíricas de estos conjuntos de datos para ver si el símbolo EURUSD es consistente en los diferentes brókeres. Necesitamos definir con qué antelación vamos a realizar nuestras previsiones.

#Our forecasting horizon
HORIZON = 10

Leer ambos conjuntos de datos.

EURUSD_BROKER_A = pd.read_csv("EURUSD BROKER A.csv")
EURUSD_BROKER_B = pd.read_csv("EURUSD BROKER B.csv")

Actualmente, la columna de tiempo de los conjuntos de datos se registra en segundos; preferiríamos tener columnas de tiempo legibles para humanos en formato de fecha-mes-año. Vamos a construir un método para hacerlo.

def format_data(f_data):
    #First make a copy of the data, so we always preserve the original data
    f_data_copy = f_data.copy()
    #Format the time correctly, form seconds to human readable formats
    f_data_copy['time'] = pd.to_datetime(f_data_copy['time'],unit='s')
    return(f_data_copy)

Formatear nuestros conjuntos de datos.

A = format_data(EURUSD_BROKER_A)
B = format_data(EURUSD_BROKER_B)

Cambie el nombre de todas las columnas en consecuencia, de modo que el nombre de cada columna vaya seguido de la letra del bróker que nos proporcionó los datos. Todas las columnas del Bróker A o B terminarán con una A o una B respectivamente. Analicemos ahora detenidamente los datos históricos del par EURUSD que hemos recibido de ambos brókeres. Presta atención al hecho de que ambos conjuntos tienen exactamente 1460 filas de datos diarios, lo que significa que cada bróker devolvió correctamente exactamente 4 años de datos diarios. ¿Qué otras diferencias puede observar el lector? ¿Has echado un vistazo al volumen de ticks?

# Rename all columns (except the join key)
B = B.rename(columns=lambda col: col + ' B' if col != 'id' else col)
A = A.rename(columns=lambda col: col + ' A' if col != 'id' else col)

Figura 1: Datos históricos diarios del par EURUSD que recibimos del bróker A, con marcas de tiempo que datan de septiembre de 2019.


Figura 2: Los datos históricos diarios del EURUSD que recibimos del Bróker B no coinciden con las marcas de tiempo de la Figura 1, pero ambos son exactamente de 4 años.

Ahora vamos a unir los dos conjuntos de datos.

combined = pd.concat([A,B],axis=1)

Crea una columna llena solo de 0.

combined['Null'] = 0

Defina las entradas.

inputs = ['open A','high A','low A','close A','tick_volume A','spread A','open B','high B','low B','close B','tick_volume B','spread B']

Calcula la rentabilidad del EURUSD a 10 días.

#Label the data
combined['A Target'] = combined['close A'].shift(-HORIZON) - combined['close A']
combined['B Target'] = combined['close B'].shift(-HORIZON) - combined['close B']

#Drop the last HORIZON rows of data
combined = combined.iloc[:-HORIZON,:]

El volumen de ticks nos indica cuántos cambios de precio hemos observado periódicamente. Los períodos de intensa actividad comercial se evidenciarán por un alto volumen de transacciones, lo que indica que hubo mucha actividad en el mercado, mientras que lo contrario implica que el mercado estuvo relativamente tranquilo con poca actividad. El bróker A parece tener una tendencia alcista a largo plazo en sus datos de volumen de ticks, lo que sugiere un aumento gradual del interés del mercado con el tiempo. En el gráfico se observan picos importantes de forma ocasional, que pueden corresponder a períodos de actividad particularmente alta en los que el interés abierto en el par EURUSD se acerca a un máximo.

plt.title('Bróker A Daily EURUSD Tick Volume')
plt.plot(combined['tick_volume A'],color='black')
plt.ylabel('Tick Volume')
plt.xlabel('Historical Day')
plt.grid()

Figura 3: Volumen de ticks del EURUSD que recibimos del Bróker A.

Cuando comparamos el volumen de ticks que nos presenta el Bróker B en la Figura 4 con la Figura 3, podemos ver claramente grandes diferencias en los niveles de actividad reportados. El Bróker B prácticamente no muestra ninguna tendencia en su volumen de ticks, en comparación con el broker A. La figura 4 es densa, con picos aleatorios que no parecen tan periódicos como los picos que observamos en la figura 3. 

plt.title('Bróker B Daily EURUSD Tick Volume')
plt.plot(combined['tick_volume B'],color='black')
plt.ylabel('Tick Volume')
plt.xlabel('Historical Day')
plt.grid()

Figura 4: Volumen diario de ticks del EURUSD que recibimos del Bróker B.

Si tenemos en cuenta la rentabilidad media que un inversor podría esperar al mantener una posición en EURUSD con cada bróker, descubrimos que ambos ofrecen diferentes variaciones del mismo símbolo. De lo contrario, si estos dos brókers nos ofrecieran versiones idénticas del mismo símbolo, ¿no deberíamos tener niveles de rentabilidad esperada similares?

#What's the average 10-Day EURUSD return from both brokers
delta_return = str(((combined.iloc[:,-2:].mean()[0]-combined.iloc[:,-2:].mean()[1]) / combined.iloc[:,-2:].mean()[0]) * 100)

t = 'The Expected 10-Day EURUSD Return Differes by ' + delta_return[:5] + '% Between Our Brokers'

sns.barplot(combined.iloc[:,-2:].mean(),color='black')
plt.axhline(0,color='grey',linestyle='--')
plt.title(t)
plt.ylabel('Return')

Figura 5: La rentabilidad media de mercado entre los brókers se sitúa a ambos lados del 0.

Trazamos en un gráfico las rentabilidades generadas por los dos mercados, una encima de la otra. Escalaré cada una de las rentabilidades de manera que ambas líneas estén centradas en 0, y el desplazamiento desde 0 representará cuántas desviaciones estándar nos hemos desviado de la rentabilidad media del mercado. Podemos observar de inmediato que hay muchos momentos en que las dos líneas se encuentran en lados opuestos de la línea 0, y sin embargo, hay otros momentos en que ambas líneas sí se mueven de forma similar. Recordemos que, informalmente, solemos asumir que estas dos líneas siempre van una tras otra, pero la figura 6 nos muestra que esto solo es cierto en algunos casos.

plt.plot(((combined.iloc[:,-1]-combined.iloc[:,-1].mean())/combined.iloc[:,-1].std()),color='red')
plt.plot(((combined.iloc[:,-2]-combined.iloc[:,-2].mean())/combined.iloc[:,-2].std()),color='black')
plt.grid()
plt.axhline(0,color='black',linestyle='--')
plt.ylabel('Std. Deviations From Expected 10-Day EURUSD Return')
plt.xlabel('Historical Days')
plt.title('EURUSD Returns from Different Brokers May Not Always Allign')
plt.legend(['Broker A','Broker B'])

Figura 6: Visualización de la rentabilidad del par EURUSD a 10 días generada por dos brókeres diferentes.

Comparar la variabilidad en las rentabilidades que ofrecen los brókeres nos permite evaluar qué bróker es más arriesgado y cuál nos ofrece rendimientos más estables. Según este criterio, la cotización del EURUSD del Bróker A conlleva un mayor riesgo asociado a su rentabilidad en comparación con la del Bróker B.

#The variance of returns is not the same across both brokers, broker A is riskier
delta_var = str(((combined.iloc[:,-2:].var()[0]-combined.iloc[:,-2:].var()[1]) / combined.iloc[:,-2:].var()[0]) * 100)

t = 'Bróker A EURUSD Returns Appear to Carry '+ delta_var[:5]+'% Additional Risk.'

sns.barplot(combined.iloc[:,-2:].var(),color='black')
plt.axhline(np.min(combined.iloc[:,-2:].var()),color='red',linestyle=':')
plt.title(t)
plt.ylabel('Vriance of Returns')

Figura 7: Las rentabilidades del Bróker A conllevan un 21 % más de riesgo que las del Bróker B. Llegados a este punto, ¿sigues considerando que estos valores son «iguales»?

Cuando nos centramos en considerar la mayor caída registrada por ambos brókeres, seguimos sin poder obtener observaciones coherentes. La mayor caída registrada por ambos mercados difirió en aproximadamente un 37% entre nuestros dos brókeres. Todo esto parece indicar que el bróker B está protegiendo inteligentemente a sus clientes de la volatilidad del mercado EURUSD al ofrecer una visión truncada.

#Bróker A also demonstrated the largest drawdown ever in our 4 year sample window
delta = (((combined.iloc[:,-2:].min()[0]-combined.iloc[:,-2:].min()[1]) / combined.iloc[:,-2:].min()[0]) *100)
delta_s = str(delta)

t = 'The Largest Negative 10-Day EURUSD Return Grew By: ' + delta_s[:5] + ' %'

sns.barplot(combined.iloc[:,-2:].min(),color='black')
plt.axhline(np.max(combined.iloc[:,-2:].min()),color='red',linestyle=':')
plt.title(t)
plt.ylabel('Return')

Figura 8: El Bróker A registró la mayor caída en la rentabilidad, con un 36,79 %, muy por encima de la mayor caída del Bróker B.

Al superponer la distribución de la rentabilidad del EURUSD a 10 días generada por ambos brókeres, se observa que ambos no ofrecen la misma visión del mercado. Como explicamos al inicio de nuestra discusión, cada bróker es libre de obtener sus datos de precios de la fuente que elija. Este esquema de precios descentralizado significa que cada bróker puede ofrecer perspectivas arbitrariamente diferentes sobre cualquier mercado en particular.

sns.histplot(((combined.iloc[:,-2]-combined.iloc[:,-2].mean())/combined.iloc[:,-2].std()),color='black')
sns.histplot(((combined.iloc[:,-1]-combined.iloc[:,-1].mean())/combined.iloc[:,-1].std()),color='red')
plt.xlabel('Std. Deviations From The Expected Return')
plt.ylabel('Frequency')
plt.title('Comparing The Distribution of 10-Day EURUSD Returns Between 2 Brokers')
plt.grid()
plt.legend(['Broker A','Broker B'])

Figura 9: Comparación de la distribución de los rendimientos generados por los dos mercados.

Además, al analizar los niveles de correlación entre los brókers, observamos que los precios del mercado presentan una baja correlación entre sí. Es decir, como ya hemos mencionado, en más de la mitad de los casos, los niveles de precios entre estos dos brókeres en particular podrían estar evolucionando en direcciones opuestas.

sns.heatmap(combined.loc[:,inputs].corr(),annot=True)

Figura 10: La visualización de los niveles de correlación nos muestra que los símbolos de ambos brókers se mueven casi independientemente la mayor parte del tiempo.

Veamos ahora si nuestra capacidad predictiva se mantiene constante entre los distintos brókers.

from sklearn.model_selection import train_test_split,TimeSeriesSplit,cross_val_score
from sklearn.linear_model import Ridge

Cree un objeto de validación de series temporales.

tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)

Escribe un método que nos devuelva un nuevo modelo para usar.

def get_model():
    return(Ridge())

Divide los datos y asegúrate de no mezclarlos.
train , test = train_test_split(combined,shuffle=False,test_size=0.5)

Registra nuestros niveles de error al usar la columna que llenamos intencionalmente solo con ceros. Esto obligará al modelo a predecir siempre el valor promedio del objetivo. Recordemos que cuando todas las entradas son 0, un modelo lineal predecirá la intersección. O, dicho de forma más sencilla, este modelo nos indica qué resultados podríamos obtener en este mercado si siempre prediciéramos la rentabilidad media del mercado. Si no logramos superar este modelo, nos damos cuenta de que no tenemos ninguna habilidad.

Este valor de referencia se denomina TSS. Definimos el TSS en la introducción de nuestra discusión. Nuestro objetivo aquí es medir el TSS en ambos brokers y luego comparar nuestra capacidad para superar este índice de referencia en ambos brokers.

broker_a_tss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,['Null']],train.loc[:,'A Target'],scoring='neg_mean_squared_error',n_jobs=-1,cv=tscv)))
broker_a_rss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,inputs[0:(len(inputs)//2)]],train.loc[:,'A Target'],scoring='neg_mean_squared_error',n_jobs=-1,cv=tscv)))

broker_b_tss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,['Null']],train.loc[:,'B Target'],scoring='neg_mean_squared_error',n_jobs=-1,cv=tscv)))
broker_b_rss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,inputs[(len(inputs)//2):]],train.loc[:,'B Target'],scoring='neg_mean_squared_error',n_jobs=-1,cv=tscv)))

¡Sorprendentemente, nos resulta más fácil superar el TSS con el Bróker B que con el Bróker A! Esto significa que la rentabilidad futura del EURUSD a 10 días no siempre es eficiente a medida que cambiamos de bróker.

res = [(broker_a_rss/broker_a_tss),(broker_b_rss/broker_b_tss)]

eff = str(((res[0] - res[1])/res[1]) * 100)

t = 'The EURUSD Appears ' + eff[0:4] + '% Easier To Forecast With Bróker B'

sns.barplot(res,color='black')
plt.axhline(np.min(res),color='red',linestyle=':')
plt.ylabel('5-Fold Cross Valiated Ratio of RSS/TSS ')
plt.title(t)
plt.xticks([0,1],['Bróker A','Bróker B'])

Figura 11: La rentabilidad futura del EURUSD a 10 días es más fácil de pronosticar utilizando el Bróker B.

Ahora que hemos decidido en qué bróker queremos centrarnos, seleccionemos los datos de entrada que recibimos del Corredor B.

b_inputs = inputs[len(inputs)//2:]

Ahora vamos a construir un modelo completamente nuevo.

from sklearn.ensemble import GradientBoostingRegressor

model = GradientBoostingRegressor()

Ajusta el modelo a todos los datos que tenemos del Bróker B.

model.fit(train.loc[:,b_inputs[:-2]],train['B Target'])
Ahora vamos a prepararnos para exportar nuestro modelo al formato ONNX para poder integrar fácilmente nuestro modelo de IA en nuestra aplicación MQL5.
import skl2onnx,onnx
Defina el número de entradas que acepta nuestro modelo ONNX.
initial_types = [('float_input',skl2onnx.common.data_types.FloatTensorType([1,4]))]
Convierta el modelo ONNX en un prototipo ONNX.
onnx_proto = skl2onnx.convert_sklearn(model,initial_types=initial_types,target_opset=12)

Guarda el prototipo de ONNX en el disco.

onnx.save(onnx_proto,"EURUSD GBR D1.onnx")


Primeros pasos en MQL5

Ahora que tenemos listo nuestro modelo ONNX, podemos comenzar a construir nuestra aplicación MQL5. Primero, cargaremos las bibliotecas que necesitamos.
//+------------------------------------------------------------------+
//|                                                       EURUSD.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System Constants Definitions                                     |
//+------------------------------------------------------------------+
#include  <Trade\Trade.mqh>
CTrade Trade;

También necesitaremos constantes del sistema para garantizar que nuestra aplicación refleje los parámetros importantes que definimos anteriormente en nuestra discusión, como el período de rendimiento a 10 días.

//+------------------------------------------------------------------+
//| System Constants Definitions                                     |
//+------------------------------------------------------------------+
#define  ONNX_INPUT_SHAPE 4
#define  ONNX_OUTPUT_SHAPE 1
#define  SYSTEM_TIME_FRAME PERIOD_D1
#define  RETURN_PERIOD 10
#define  TRADING_VOLUME SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN)
Cargue el archivo ONNX como un recurso del sistema para que se compile con nuestra aplicación.
//+------------------------------------------------------------------+
//| System Resources                                                 |
//+------------------------------------------------------------------+
#resource "\\Files\\Bróker Manipulation\\EURUSD GBR D1.onnx" as const uchar onnx_proto[];

Para implementar nuestra estrategia de trading, necesitaremos tener en cuenta algunas variables globales.

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
long   model;
int    position_timer;
double bid,ask;
double o,h,l,c;
bool   bullish;
double sl_width;

Cuando nuestro sistema se inicialice por primera vez, configuraremos nuestro modelo ONNX y luego restableceremos las variables globales importantes para nuestra estrategia de trading.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   model = OnnxCreateFromBuffer(onnx_proto,ONNX_DATA_TYPE_FLOAT);

   ulong input_shape[] = {1,ONNX_INPUT_SHAPE};
   ulong output_shape[] = {1,ONNX_OUTPUT_SHAPE};

   if(model == INVALID_HANDLE)
     {
      Comment("Failed To Load EURUSD Auto-Encoder-Decoder: ",GetLastError());
      return(INIT_FAILED);
     }

   if(!OnnxSetInputShape(model,0,input_shape))
     {
      Comment("Failed To Set EURUSD Auto-Encoder-Decoder Input Shape: ",GetLastError());
      return(INIT_FAILED);
     }

   else
      if(!OnnxSetOutputShape(model,0,output_shape))
        {
         Comment("Failed To Set EURUSD Auto-Encoder-Decoder Output Shape: ",GetLastError());
         return(INIT_FAILED);
        }

   position_timer = 0;
   sl_width = 30;
//---
   return(INIT_SUCCEEDED);
  }

Si ya no utilizamos nuestra estrategia de trading, liberemos los recursos que estaban siendo consumidos por nuestro modelo ONNX.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   OnnxRelease(model);
  }

Cada vez que recibamos precios actualizados, almacenaremos los nuevos niveles de precios una vez al día y, si no tenemos posiciones abiertas, obtendremos una previsión de nuestro modelo y operaremos en consecuencia. De lo contrario, si ya tenemos una operación abierta, intentaremos ajustar nuestro stop-loss si es posible mientras contamos los 10 días que dura el período de tenencia de cada operación.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   static datetime time_stamp;
   datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0);

   if(time_stamp != current_time)
     {
      time_stamp = current_time;
      ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
      bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
      o = iOpen(Symbol(),SYSTEM_TIME_FRAME,1);
      h = iHigh(Symbol(),SYSTEM_TIME_FRAME,1);
      l = iLow(Symbol(),SYSTEM_TIME_FRAME,1);
      c = iClose(Symbol(),SYSTEM_TIME_FRAME,1);
      bullish = (o < c) && (c > iClose(Symbol(),SYSTEM_TIME_FRAME,2));

      if(PositionsTotal() == 0)
        {
         position_timer = 0;
         find_setup();
        }

      else
         if(PositionsTotal() > 0)
           {
            if(PositionSelect(Symbol()))
              {
               long position_type = PositionGetInteger(POSITION_TYPE);
               double current_sl = PositionGetDouble(POSITION_SL);
               double new_sl;

               //--- Buy Trades
               if(position_type == POSITION_TYPE_BUY)
                 {
                  new_sl = bid - ((h-l)*sl_width);
                  if(new_sl > current_sl)
                     Trade.PositionModify(Symbol(),new_sl,0);
                 }

               //--- Sell Trades
               else
                  if(position_type == POSITION_TYPE_SELL)
                    {
                     new_sl = ask + ((h-l)*sl_width);
                     if(new_sl < current_sl)
                        Trade.PositionModify(Symbol(),new_sl,0);
                    }
              }

            if(position_timer < RETURN_PERIOD)
               position_timer+=1;

            else
               Trade.PositionClose(Symbol());
           }
     }
  }

Finalmente, esta función obtendrá una previsión de nuestro modelo y, a continuación, comprobará si tenemos una oportunidad de trading válida.

//+------------------------------------------------------------------+
//| Find A Trading Setup                                             |
//+------------------------------------------------------------------+
void find_setup(void)
  {
   vectorf model_inputs(ONNX_INPUT_SHAPE);

   model_inputs[0] = (float) iOpen(Symbol(),SYSTEM_TIME_FRAME,0);
   model_inputs[1] = (float) iHigh(Symbol(),SYSTEM_TIME_FRAME,0);
   model_inputs[2] = (float) iLow(Symbol(),SYSTEM_TIME_FRAME,0);
   model_inputs[3] = (float) iClose(Symbol(),SYSTEM_TIME_FRAME,0);

   vectorf model_output(ONNX_OUTPUT_SHAPE);

   if(!OnnxRun(model,ONNX_DATA_TYPE_FLOAT,model_inputs,model_output))
     {
      Comment("Failed To Get A Prediction From Our Model: ",GetLastError());
      return;
     }

   else
     {
      Comment("Prediction: ",model_output[0]);

      vector open,close;

      open.CopyRates(Symbol(),SYSTEM_TIME_FRAME,COPY_RATES_OPEN,1,2);
      close.CopyRates(Symbol(),SYSTEM_TIME_FRAME,COPY_RATES_CLOSE,1,2);

      if(open.Mean() < close.Mean())
        {
         if((model_output[0] > 0) && (bullish))
            Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid - ((h-l) * sl_width)),0);
        }


      else
         if(open.Mean() > close.Mean())
           {
            if((model_output[0] < 0) && (!bullish))
               Trade.Sell(TRADING_VOLUME,Symbol(),bid,(ask + ((h-l) * sl_width)),0);
           }
     }
  }

No olvide anular la definición de cualquier constante del sistema que cree en su aplicación.

//+------------------------------------------------------------------+
//| Undefine System Constants                                        |
//+------------------------------------------------------------------+
#undef  ONNX_INPUT_SHAPE
#undef  ONNX_OUTPUT_SHAPE
#undef  SYSTEM_TIME_FRAME
#undef  TRADING_VOLUME
#undef  RETURN_PERIOD
//+------------------------------------------------------------------+

Los periodos de fechas que utilizaremos para nuestra prueba retrospectiva están fuera de la muestra del periodo de entrenamiento de nuestro modelo. Estas fechas se mantendrán constantes para nuestras pruebas tanto en el Bróker A como en el Bróker B. Recordemos que el Bróker B simboliza el Bróker que un desarrollador de MQL5 utiliza para crear su aplicación, mientras que el Bróker A simboliza el Bróker con el que sus clientes podrían terminar implementando la aplicación.

Figura 12: Seleccione las fechas de entrada para nuestro período de prueba.

Ambas configuraciones especificadas en la Figura 12 anterior y en la Figura 13 siguiente permanecerán fijas en ambas pruebas que realicemos.

Figura 13: También seleccionaremos configuraciones de modelado desafiantes para obtener una expectativa realista de la capacidad de nuestra estrategia.

Como podemos ver en la figura 14, nuestra estrategia parece prometedora cuando la probamos con el Bróker B. Gestiona bien los datos fuera de la muestra y nos anima a dedicar más tiempo a perfeccionarla para obtener el máximo rendimiento. Sin embargo, lo que intentamos dejar claro al lector es que puede ser ingenuo pensar que las mejoras realizadas con un bróker se corresponderán de forma significativa con las de cualquier otro bróker.

Figura 14: La curva de capital generada por nuestra estrategia presenta una tendencia positiva al aplicarse al bróker previsto.

Sin embargo, tras aplicar la misma estrategia con el bróker A, ya no pudimos observar la tendencia alcista positiva en el saldo de la cuenta que sí habíamos observado con el bróker B. Es evidente que la estrategia nos aporta muy poco valor si cambiamos de bróker sin modificar el modelo subyacente. Los desarrolladores deben comprender que esto no siempre es culpa suya. Para cualquier desarrollador, resulta todo un reto personalizar sus modelos para cada bróker de bolsa que existe.

Sin embargo, esta es una forma visual de concebir el problema. Los desarrolladores y sus clientes pueden tener puntos de vista completamente diferentes si sus relaciones no se definen con suficiente precisión. 

Figura 15: Al implementar nuestra estrategia con el Bróker A, no logramos reproducir la tendencia alcista en el saldo de la cuenta por la que tanto trabajamos.

También podemos examinar más detenidamente un análisis más detallado de nuestro rendimiento con el Bróker B en la figura 16 y compararlo con el rendimiento de nuestro modelo con el Bróker A en la figura 17. 

Figura 16: Análisis detallado de nuestro rendimiento en las operaciones cuando nos centramos en el bróker elegido.

Podemos ver claramente que elaborar una estrategia que funcione de manera significativa en múltiples brókers no es, sin duda, una tarea trivial. A medida que los modelos de aprendizaje automático se vuelven más complejos, también se vuelven más sensibles a los pequeños cambios en sus datos de entrada. Estas variaciones en las propiedades numéricas del símbolo pueden tener efectos devastadores en nuestra capacidad para compartir y reproducir de manera significativa las estrategias de trading. 

Figura 17: Un análisis detallado de la estrategia que intenta funcionar en un Bróker con el que no ha sido entrenado.


Conclusión

La naturaleza descentralizada de los mercados financieros globales impone limitaciones en el mundo real que dificultan que nuestra comunidad reproduzca los hallazgos de los demás. Los brókeres no ofrecen garantías de que sus precios coincidan, lo que significa que las ineficiencias que usted aprovecha con su bróker pueden no existir con el mío, incluso utilizando la misma estrategia en el mismo símbolo.

Dependiendo del rol que prefieras desempeñar en nuestra comunidad, estas ideas tienen implicaciones prácticas:

  • Si le gusta utilizar la sección "Freelance" del sitio web de MQL5, especifique su bróker al solicitar aplicaciones y pida a los desarrolladores que creen cuentas demo con su bróker para asegurarse de recibir soluciones personalizadas. Evite hacer solicitudes informales y generales como "Se busca aplicación para operar con EURUSD", ya que, como hemos visto, puede ser más seguro que proporcione la mayor cantidad de detalles posible.
  • Los usuarios que compran aplicaciones con frecuencia en el Market ahora entienden por qué los productos específicos de cada bróker pueden ofrecer mayor valor que aquellos que afirman tener una utilidad universal.
  • Los suscriptores de señales pueden maximizar su satisfacción seleccionando de forma selectiva a aquellos proveedores de señales que utilicen el mismo bróker, lo que garantiza que los rendimientos reportados y los realmente obtenidos coincidan.
  • Finalmente, mis compañeros desarrolladores de MQL5 comprenden mejor lo que se necesita para ofrecer productos consistentes y servicios confiables que mantengan satisfechos a nuestros clientes.

Al reconocer estos desafíos, podemos trabajar para lograr soluciones más reproducibles y específicas para cada bróker, que beneficien a todos los miembros de nuestra comunidad diversa e inclusiva. Este artículo se diseñó para ilustrar los peligros asociados con intentar compartir un modelo ONNX entre diferentes brókers. Como desarrolladores de MQL5, creo que debemos regirnos por estándares de práctica más elevados y evitar exponer a nuestros clientes a tales peligros.

Nombre del archivo Descripción del archivo
Requesting Bróker Data.ipynb El Jupyter Notebook que utilizamos para obtener datos históricos diarios del par EURUSD de nuestros 2 brókers.
Analyzing Bróker Data.ipynb El Jupyter Notebook que utilizamos para comprobar la coherencia de los datos históricos diarios del EURUSD de nuestros dos brókeres.
EURUSD.mq5 El Asesor Experto que desarrollamos se utiliza para evaluar nuestra rentabilidad siguiendo el mismo modelo en dos brókers diferentes.

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/18133

Archivos adjuntos |
EURUSD.mq5 (6.94 KB)
Juan Luis De Frutos Blanco
Juan Luis De Frutos Blanco | 8 sept 2026 en 09:14
Devastating.
Thank you so much for the article.
Miguel Angel Vico Alba
Miguel Angel Vico Alba | 8 sept 2026 en 10:12

Me pareció muy interesante la tesis principal del artículo, sobre todo la idea de que un modelo de aprendizaje automático puede comportarse de forma bastante diferente en función de los datos que proporcione cada bróker.

Sin embargo, al leer el análisis, me he dado cuenta de un importante problema metodológico que podría tener un impacto significativo en las conclusiones.

Los dos conjuntos de datos históricos comienzan en fechas diferentes, pero posteriormente se combinan utilizando:

pd.concat([A, B], axis=1)
Dado que ambos DataFrames tienen índices enteros consecutivos, Pandas los alinea por índice en lugar de por marca de tiempo. Esto significa que muchas filas de A y B pueden corresponder, en realidad, a días de negociación distintos.

Por ejemplo, si A comienza en 2019 y B en 2020, la fila 0 de ambos conjuntos de datos no representa el mismo momento en el tiempo. Esto podría afectar directamente a la correlación obtenida, a las diferencias en las medias y varianzas, y a varias de las comparaciones posteriores entre los dos corredores.

Creo que, antes de extraer conclusiones de estas comparaciones, habría que sincronizar primero los dos conjuntos de datos por «tiempo», utilizando únicamente marcas de tiempo comunes, por ejemplo, mediante una «fusión» o «unión» en la columna de fecha y hora.

La idea principal del artículo parece válida y muy relevante, pero tal y como está diseñado actualmente el experimento, no estoy seguro de que los resultados presentados lo demuestren de forma concluyente.

Sería muy interesante ver cómo cambian los resultados tras repetir el análisis con ambos conjuntos de datos correctamente alineados en el tiempo. 😉
Redes neuronales en el trading: Framework de predicción cruzada de dominios de series temporales (Final) Redes neuronales en el trading: Framework de predicción cruzada de dominios de series temporales (Final)
El artículo trata sobre la construcción práctica del modelo TimeFound para la predicción de series temporales. Asimismo, se analizan las etapas clave de la implementación de los principales enfoques del framework con MQL5.
Redes neuronales en el trading: Extracción eficaz de características para una clasificación precisa (Implementación de objetos) Redes neuronales en el trading: Extracción eficaz de características para una clasificación precisa (Implementación de objetos)
Mantis es una herramienta universal para el análisis profundo de series temporales, escalable de forma flexible para cualquier escenario financiero. Descubre cómo la combinación de patching, convoluciones locales y atención cruzada permite obtener una interpretación de alta precisión de los patrones de mercado.
Redes neuronales en el trading: Modelo de consultas temporales (Final) Redes neuronales en el trading: Modelo de consultas temporales (Final)
Les presentamos la fase final de la implementación y las pruebas del framework TQNet, en la que la teoría se une a la práctica real de trading. Recorreremos todo el proceso, desde el entrenamiento histórico hasta la prueba de estrés con datos recientes del mercado, evaluando la solidez y la precisión del modelo. Los resultados finales no son solo cifras frías, sino también una demostración clara del valor práctico del enfoque propuesto.
Automatización de estrategias de trading en MQL5 (Parte 24): Sistema de ruptura de la sesión de Londres con gestión del riesgo y stops dinámicos Automatización de estrategias de trading en MQL5 (Parte 24): Sistema de ruptura de la sesión de Londres con gestión del riesgo y stops dinámicos
En este artículo, desarrollamos un sistema de ruptura de la sesión de Londres que identifica rupturas del rango previo a la apertura de Londres y coloca órdenes pendientes con direcciones de trading y parámetros de riesgo personalizables. Incorporamos funciones como órdenes de stop dinámico, ratios riesgo-beneficio, límites máximos de pérdidas y un panel de control para la monitorización y gestión en tiempo real.