English Русский 中文 Deutsch 日本語
preview
Superando las limitaciones del aprendizaje automático (Parte 1): Falta de métricas interoperables

Superando las limitaciones del aprendizaje automático (Parte 1): Falta de métricas interoperables

MetaTrader 5Ejemplos |
23 5
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana
En nuestra serie de artículos relacionados, como Asesores expertos autooptimizables, abordamos una verdad inquietante: incluso cuando se siguen todas las «mejores prácticas» en el desarrollo del trading algorítmico, las cosas pueden salir terriblemente mal. En resumen, observamos que los profesionales que utilizan el RSI según sus reglas estandarizadas pueden esperar varios meses sin que el indicador genere ninguno de los resultados esperados. Lo que da lugar a cuentas de negociación que están expuestas a un riesgo de mercado mayor del previsto. 


Lamentablemente, los peligros de seguir ciegamente las «mejores prácticas» corrompen silenciosamente otras herramientas en las que confiamos en nuestras estrategias comerciales, por lo que los profesionales no deben pensar que este problema es exclusivo de los indicadores técnicos.

En esta serie de artículos, exploraremos los problemas críticos a los que se enfrentan a diario los operadores algorítmicos, según las propias directrices y prácticas destinadas a garantizar su seguridad al utilizar modelos de aprendizaje automático. En resumen, si los modelos de aprendizaje automático que se implementan a diario en la nube MQL5 siguen siendo conscientes de los hechos presentados en este debate, por delante de los profesionales responsables, los problemas son inminentes. Los inversores pueden verse rápidamente expuestos a más riesgos de los que habían previsto.

Sinceramente, estos problemas no se tratan con suficiente profundidad, ni siquiera en los libros más importantes del mundo sobre aprendizaje estadístico. El tema de nuestro debate es una simple verdad que todos los profesionales de nuestra comunidad deberían conocer:

«Se puede demostrar analíticamente que la primera derivada de las métricas de dispersión euclidiana, como RMSE, MAE o MSE, se puede resolver mediante la media del objetivo».

Los profesionales que ya conocen este hecho y sus implicaciones no necesitan seguir leyendo a partir de este punto.

Sin embargo, es precisamente a aquellos profesionales que no comprenden lo que esto significa a quienes necesito llegar urgentemente a través de este artículo. En resumen, las métricas de regresión que utilizamos para construir nuestros modelos no son adecuadas para modelar los rendimientos de los activos. 

Este artículo le explicará cómo ocurre esto, los peligros que supone para usted y los cambios que puede implementar para empezar a utilizar este principio como brújula para filtrar los mercados, dada la posibilidad de elegir entre cientos de mercados potenciales en los que podría operar.

Los profesionales que deseen obtener pruebas más sólidas pueden consultar la bibliografía de la Universidad de Harvard, donde se analizan las limitaciones de métricas como el RMSE. En concreto, el artículo de Harvard ofrece una prueba analítica de que la media de la muestra minimiza el RMSE. El lector puede encontrar el artículo, enlazado, aquí.

Otras instituciones, como la Red de Investigación en Ciencias Sociales (Social Science Research Network, SSRN), mantienen una revista de artículos publicados y revisados por pares de diversos ámbitos, entre los que se incluye un artículo útil para nuestro debate, en el que se exploran funciones de pérdida alternativas para sustituir el RMSE en las tareas de valoración de activos. El artículo que he seleccionado para el lector revisa otros artículos en este campo y ofrece un resumen de la bibliografía actual antes de presentar un enfoque novedoso. Este artículo está disponible para el lector, aquí.


Un experimento mental (todo el mundo debería entenderlo)

Imagina que estás en una competición tipo lotería. Tú y otras 99 personas habéis sido seleccionados al azar para jugar por un bote de 1.000.000 $. Las reglas son sencillas: debes adivinar la estatura de los otros 99 participantes. El ganador es la persona con el menor error total en sus 99 intentos.

Ahora, aquí está el giro: para este ejemplo, imagina que la altura media global de los seres humanos es de 1,1 metros. Si simplemente adivinas 1,1 metros para todos, podrías ganar el premio gordo, aunque técnicamente todas las predicciones sean incorrectas. ¿Por qué? Porque en entornos ruidosos e inciertos, adivinar el promedio tiende a producir el error general más pequeño.


El paralelo comercial

Este experimento mental tuvo como objetivo dramatizar para el lector exactamente cómo se seleccionan la mayoría de los modelos de aprendizaje automático para su uso en los mercados financieros. 

Por ejemplo, supongamos que está construyendo un modelo para pronosticar el rendimiento del S&P 500. Un modelo que siempre predice el promedio histórico del índice, aproximadamente un 7% anual, podría en realidad superar a modelos más complejos cuando se juzga por métricas como RMSE, MAE o MSE. Pero aquí está la trampa: ese modelo no aprendió nada útil. Simplemente se agrupó alrededor de la media estadística. Y lo que es peor, las métricas y los procedimientos de validación en los que confiaba lo recompensaron por hacerlo.

Nota para principiantes: El RMSE (error cuadrático medio) es una «unidad» que se utiliza para evaluar la calidad de los modelos de aprendizaje automático que aprenden a pronosticar objetivos con valores reales. 

Castiga las grandes desviaciones pero no le importa por qué el modelo cometió el error, pero recuerda que algunas de estas desviaciones por las que se castiga al modelo en realidad fueron ganancias. 

Por lo tanto, un modelo que siempre predice el promedio (incluso si no tiene conocimiento del mercado) puede verse muy bien en el papel cuando se lo juzga con RMSE. Desafortunadamente esto nos permite crear modelos que son matemáticamente sólidos, pero prácticamente inútiles.


¿Qué está pasando realmente?

Los datos financieros son ruidosos. No podemos observar variables clave como la verdadera oferta y demanda global, el sentimiento de los inversores o la profundidad de la cartera de pedidos institucionales. Entonces, para minimizar el error, su modelo hará lo más lógico estadísticamente: predecir el promedio.

Y el lector debe entender que, matemáticamente, es una buena práctica. Predecir la media minimiza los errores de regresión más comunes. Pero el trading no consiste en minimizar el error estadístico, sino en tomar decisiones rentables en condiciones de incertidumbre. Y esa distinción importa. En nuestra comunidad, tal conducta es comparable al sobreajuste, pero los estadísticos que construyeron estos modelos no vieron las cosas del mismo modo que nosotros.

Sería ingenuo pensar que este problema es exclusivo de los modelos de regresión. En las tareas de clasificación, un modelo puede minimizar los errores simplemente prediciendo siempre la clase más común en la muestra de entrenamiento. Y si la clase más grande en el conjunto de entrenamiento corresponde a la clase más grande de toda la población, creo que el profesional puede ver rápidamente cómo un modelo puede fingir fácilmente su habilidad.


Reward Hacking: Cuando los modelos ganan haciendo trampa

Este fenómeno se llama Reward Hacking: cuando un modelo alcanza niveles de rendimiento deseables al aprender un comportamiento indeseable. En el caso del trading, la piratería de recompensas (Reward Hacking) da como resultado que los profesionales elijan un modelo que parece hábil, pero en realidad el modelo simplemente está jugando un juego de promedios, un castillo de naipes estadístico. Crees que ha aprendido algo, pero en realidad ha hecho el equivalente estadístico de decir "todos miden 1,1 metros". Y RMSE lo acepta siempre, sin cuestionarlo.


Evidencia real

Ahora que nuestra motivación está clara, dejemos de usar la alegoría y, en cambio, consideremos datos del mercado del mundo real. Usando la API Python de MetaTrader 5, accedí a 333 mercados desde mi bróker. Filtramos los mercados con al menos cuatro años de datos históricos del mundo real. Este filtro redujo nuestro universo de mercados a 119.

Luego construimos dos modelos para cada mercado:

  • Modelo de control: Siempre predice el rendimiento promedio de 10 días.
  • Modelo de previsión: Intenta aprender y predecir rendimientos futuros.


Nuestros resultados

En el 91,6% de los mercados que probamos, el modelo de control ganó. Es decir, ¡el modelo que siempre predice el retorno promedio histórico de 10 días produjo un error menor durante 4 años, el 91% del tiempo! Como el profesional verá pronto, incluso cuando probamos redes neuronales más profundas, la mejora fue insignificante.

Por lo tanto, ¿debería el profesional obedecer las "mejores prácticas" del aprendizaje automático y elegir siempre el modelo que produzca el menor error y, recuerde, el modelo siempre predice el rendimiento promedio?

Nota para principiantes: Esto no significa que el aprendizaje automático no pueda funcionar para usted. Esto significa que debes ser extremadamente cauteloso acerca de cómo debe definirse el “buen desempeño” en tu contexto comercial. Si no tiene cuidado, sería razonable esperar que, sin saberlo, seleccione un modelo que sea recompensado por predecir el rendimiento promedio del mercado. 


¿Y ahora qué?

La conclusión es clara: las métricas de evaluación que utilizamos actualmente (RMSE, MSE, MAE) no están diseñadas para el trading. Nacieron en la estadística, la medicina y otras ciencias naturales donde predecir la media tiene sentido. Pero en nuestra comunidad de trading algorítmico, predecir el retorno promedio podría ser a veces incluso peor que no tener ninguna predicción; no tener IA en absoluto a veces es más seguro. ¡Pero la IA sin comprensión nunca está segura!

Necesitamos marcos de evaluación que recompensen las habilidades útiles, no métricas de cooperación estadística. Necesitamos métricas que entiendan ganancias y pérdidas. Además, necesitamos protocolos de capacitación que penalicen el abrazo cruel, procedimientos que animen al modelo a reflejar los valores de nuestra comunidad mientras aprende las realidades del trading. Y en eso se centra esta serie. Un conjunto único de problemas, que no son generados directamente por el mercado. Más bien, surgen de las mismas herramientas que deseamos emplear y rara vez se analizan en otros artículos, libros de aprendizaje estadístico académico o incluso en nuestras propias discusiones comunitarias. 

Los profesionales deben estar bien informados sobre estos hechos, por su propia seguridad. Lamentablemente, el conocimiento básico de estas limitaciones no es un conocimiento común que todos los miembros de nuestra comunidad tengan a su alcance. Los profesionales cotidianos repiten sin dudarlo prácticas estadísticas muy peligrosas.


Empezando

Veamos si hay algún mérito en lo que hemos discutido hasta ahora. Cargue nuestras bibliotecas estándar.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import MetaTrader5 as mt5
import multiprocessing as mp

Definir hasta qué punto en el futuro deseamos pronosticar.

HORIZON = 10

Cargue la terminal MetaTrader 5 para que podamos obtener datos reales del mercado.

if(not mt5.initialize()):
   print("Failed to load MT5")

Obtenga una lista completa de todos los símbolos disponibles.

symbols = mt5.symbols_get()

Simplemente extraiga los nombres de los símbolos.

symbols[0].name
symbol_names = []

for i in range(len(symbols)):
    symbol_names.append(symbols[i].name)

Ahora, prepárense para ver si podemos superar a un modelo que siempre predice la media.

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

Crear un divisor de validación de series de tiempo.

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

Necesitamos un método que nos devuelva nuestros niveles de error al predecir siempre el rendimiento promedio del mercado y nuestros niveles de error al intentar predecir el rendimiento futuro del mercado.

def null_test(name):
    
    data_amount = 365 * 4
    
    f_data = pd.DataFrame(mt5.copy_rates_from_pos(name,mt5.TIMEFRAME_D1,0,data_amount))
    
    if(f_data.shape[0] < data_amount):
        print(f"{symbol_names[i]} did not have enough data!")
        return(None)
    
    f_data['time'] =pd.to_datetime(f_data['time'],unit='s')
    
    f_data['target'] = f_data['close'].shift(-HORIZON) - f_data['close']
    
    f_data.dropna(inplace=True)

    model = Ridge()
    
    res = []

    res.append(np.mean(np.abs(cross_val_score(model,f_data.loc[:,['open']] * 0,f_data['target'],cv=tscv))))
    res.append(np.mean(np.abs(cross_val_score(model,f_data.loc[:,['open','high','low','close']],f_data['target'],cv=tscv))))

    return(res)

Ahora realizaremos la prueba en todos los mercados que tenemos disponibles.

res = pd.DataFrame(columns=['Mean Forecast','Direct Forecast'],index=symbol_names)

for i in range(len(symbol_names)):
    test_score = null_test(symbol_names[i])

    if(test_score is None):
        print(f"{symbol_names[i]} does not have enough data!")
        res.iloc[i,:] = [np.nan,np.nan]
        continue

    res.iloc[i,0] = test_score[0]
    res.iloc[i,1] = test_score[1]
    print(f"{i/len(symbol_names)}% complete.")

res['Score'] = ((res.iloc[:,1] / res.iloc[:,0]))
res.to_csv("Deriv Null Model Test.csv")

0.06606606606606606% complete.

0.06906906906906907% complete.

0.07207207207207207% complete.

...

¡El índice de tendencia bajista del GBPUSD RSI no tenía suficientes datos!

¡El índice de tendencia bajista del GBPUSD RSI no tiene suficientes datos!

De todos los mercados que teníamos disponibles, ¿cuántos mercados pudimos analizar?

#How many markets did we manage to investigate?
test = pd.read_csv("Null Model Test.csv")
print(f"{(test.dropna().shape[0] / test.shape[0]) * 100}% of Markets Were Evaluated")

35.73573573573574% of Markets Were Evaluated

Ahora agruparemos nuestros 119 mercados en función de una puntuación asociada a cada mercado. La puntuación será la relación entre nuestro error al predecir el mercado y nuestro error al predecir siempre el rendimiento medio. Las puntuaciones menores a 1 son impresionantes porque implican que superamos a un modelo que siempre predice el rendimiento promedio. De lo contrario, puntuaciones mayores a 1 validan nuestra motivación para realizar este ejercicio a la luz de lo que compartimos en la introducción de este artículo.

Nota para principiantes: El método de puntuación que hemos descrito brevemente no es nada nuevo dentro del aprendizaje automático. Es una métrica comúnmente conocida como r-cuadrado. Nos informa qué parte de la varianza en el objetivo estamos explicando con nuestro modelo propuesto. No utilizamos la fórmula exacta de R-cuadrado con la que quizás esté familiarizado gracias a sus estudios independientes. 

Agrupemos primero todos los mercados donde obtuvimos una puntuación menor a 1.

res.loc[res['Score'] < 1]
Nombre del mercadoPronóstico medio  Pronóstico directoPuntuación
AUDCAD 0.022793 0.0185660.814532
EURCAD 0.037192 0.0272090.731587
NZDCAD 0.019124 0.0151170.790466
USDCNH  0.125586 0.1128140.898297

¿En qué porcentaje de todos los mercados que probamos superamos a un modelo que siempre predice el rendimiento medio del mercado, durante 4 años? Aproximadamente el 8%.

res.loc[res['Score'] < 1].shape[0] / res.shape[0]

0.08403361344537816

Por lo tanto, eso también implica que a lo largo de 4 años no podríamos superar a un modelo que siempre predice el rendimiento promedio de aproximadamente el 91,6 % de todos los mercados que probamos.

res.loc[res['Score'] > 1].shape[0] / res.shape[0]

0.9159663865546218

Ahora bien, en este punto, algunos lectores pueden estar pensando que "El autor utilizó modelos lineales simples, si nos tomáramos nuestro tiempo y construyéramos modelos más flexibles, siempre podríamos superar al modelo que predice el rendimiento promedio. "Esto es una tontería". Lo cual es en parte cierto. Construyamos una red neuronal profunda en el mercado EURUSD para superar el modelo que predice el rendimiento promedio del mercado.

Primero, necesitaremos un script MQL5 para capturar información detallada del mercado sobre el tipo de cambio EURUSD. Registraremos los crecimientos en cada uno de los cuatro niveles de precios, y también sus crecimientos entre sí.

//+------------------------------------------------------------------+
//|                                                      ProjectName |
//|                                      Copyright 2020, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+


//+------------------------------------------------------------------+
//| File name                                                        |
//+------------------------------------------------------------------+
string file_name = Symbol() + " IID Candlestick Recognition.csv";

//+------------------------------------------------------------------+
//| User inputs                                                      |
//+------------------------------------------------------------------+
input int size = 3000;
input int HORIZON = 10;

//+------------------------------------------------------------------+
//| Our script execution                                             |
//+------------------------------------------------------------------+
void OnStart()
  {

//---Write to file
   int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");

   for(int i=size;i>=1;i--)
     {
      if(i == size)
        {
         FileWrite(file_handle,"Time","Open","High","Low","Close","Delta Open","Delta High","Delta Low","Delta Close","O - H","O - L","O - C","H - L","H - C","L - C");
        }

      else
        {
         FileWrite(file_handle,
                   iTime(_Symbol,PERIOD_CURRENT,i),
                   iOpen(_Symbol,PERIOD_CURRENT,i),
                   iHigh(_Symbol,PERIOD_CURRENT,i),
                   iLow(_Symbol,PERIOD_CURRENT,i),
                   iClose(_Symbol,PERIOD_CURRENT,i),  
                   iOpen(_Symbol,PERIOD_CURRENT,i)  - iOpen(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iHigh(_Symbol,PERIOD_CURRENT,i)  - iHigh(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iLow(_Symbol,PERIOD_CURRENT,i)   - iLow(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iClose(_Symbol,PERIOD_CURRENT,i) - iClose(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iOpen(_Symbol,PERIOD_CURRENT,i)  - iHigh(_Symbol,PERIOD_CURRENT,i),
                   iOpen(_Symbol,PERIOD_CURRENT,i)  - iLow(_Symbol,PERIOD_CURRENT,i),
                   iOpen(_Symbol,PERIOD_CURRENT,i)  - iClose(_Symbol,PERIOD_CURRENT,i),
                   iHigh(_Symbol,PERIOD_CURRENT,i)  - iLow(_Symbol,PERIOD_CURRENT,i),
                   iHigh(_Symbol,PERIOD_CURRENT,i)  - iClose(_Symbol,PERIOD_CURRENT,i),
                   iLow(_Symbol,PERIOD_CURRENT,i)   - iClose(_Symbol,PERIOD_CURRENT,i)
                  );
        }
     }
//--- Close the file
   FileClose(file_handle);
  }

//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef HORIZON

Ahora arrastre y suelte su script en el gráfico para obtener datos históricos sobre el mercado y luego podremos comenzar.

#Read in market data
HORIZON = 10
data = pd.read_csv('EURUSD IID Candlestick Recognition.csv')

#Label the data
data['Null'] = 0
data['Target'] = data['Close'].shift(-HORIZON) - data['Close']

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

data

Los lectores que piensan que las redes neuronales profundas y los modelos sofisticados resolverán el problema de superar a un modelo que predice el retorno promedio del mercado, se sorprenderán al leer lo que sigue en el resto de este artículo.

Te animo a que repitas este experimento con tu broker, antes de implementar tus modelos con tu capital. Ahora carguemos las herramientas de aprendizaje de scikit para comparar el rendimiento de nuestra red neuronal profunda frente a nuestro modelo lineal simple.

#Load our models
from sklearn.neural_network import MLPRegressor
from sklearn.linear_model       import Ridge
from sklearn.model_selection import TimeSeriesSplit,train_test_split,cross_val_score

Dividir los datos.

#Split the data into half
train,test = train_test_split(data,test_size=0.5,shuffle=False)

Crear un objeto de validación de series de tiempo.

#Create a time series validation object
tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)

Ajuste el modelo prediciendo siempre la rentabilidad media del mercado. El error causado por predecir siempre la media se denomina "suma total de cuadrados". El TSS es un punto de referencia de error crítico en el aprendizaje automático; nos dice dónde está el norte.

#Fit the model predicting the mean on the train set
null_model = Ridge()

null_model.fit(train[['Null']],train['Target'])

tss = np.mean(np.abs(cross_val_score(null_model,test[['Null']],test['Target'],cv=tscv,scoring='neg_mean_absolute_error')))

tss

np.float64(0.011172426520738554)

Definir nuestras entradas y objetivos.

X = data.iloc[:,4:-2].columns
y = 'Target'

Ajuste su red neuronal profunda. Animo al lector a que se sienta bienvenido a ajustar esta red neuronal como crea conveniente para ver si tiene lo necesario para superar la media.

#Let us now try to outperform the null model
model = MLPRegressor(activation='logistic',solver='lbfgs',random_state=0,shuffle=False,hidden_layer_sizes=(len(X),200,50),max_iter=1000,early_stopping=False)

model.fit(train.loc[:,X],train['Target'])

rss = np.mean(np.abs(cross_val_score(model,test.loc[:,X],test['Target'],cv=tscv,scoring='neg_mean_absolute_error')))

print(f"Test Passed: {rss < tss} || New R-Squared Metric {rss/tss}")

res = []

res.append(rss)
res.append(tss)

sns.barplot(res,color='black')
plt.axhline(tss,color='red',linestyle=':')
plt.grid()
plt.ylabel("RMSE")
plt.xticks([0,1],['Deep Neural Network','Null Model'])
plt.title("Our Deep Neural Network vs The Null Model")
plt.show()

Después de mucha configuración, ajustes y optimización, pude derrotar al modelo que predecía el retorno promedio, usando una red neuronal profunda. Sin embargo, veamos más de cerca lo que está sucediendo aquí.

Figura 1: Superar un modelo que predice el rendimiento promedio del mercado es un desafío material.

Visualicemos las mejoras que está realizando nuestra Red Neuronal Profunda. Primero, configuraré una cuadrícula para que podamos evaluar la distribución de los rendimientos del mercado en el conjunto de prueba y las distribuciones de los rendimientos que predijo nuestro modelo. Almacene los retornos que el modelo predice para el conjunto de prueba.

predictions = model.predict(test.loc[:,X])

Comenzaremos marcando primero el rendimiento promedio del mercado que observamos en el conjunto de entrenamiento como la línea punteada roja en el medio del gráfico.

plt.title("Visualizing Our Improvements")
plt.plot()
plt.grid()
plt.xlabel("Return")
plt.axvline(train['Target'].mean(),color='red',linestyle=':')
legend = ['Train Mean']
plt.legend(legend)

Figura 2: Visualización del rendimiento promedio del mercado a partir del conjunto de entrenamiento.

Ahora, superpongamos la predicción del modelo para el conjunto de prueba, sobre el rendimiento promedio del conjunto de entrenamiento. Como puede ver el lector, las predicciones del modelo se distribuyen alrededor de la media del conjunto de entrenamiento. Sin embargo, el problema se vuelve claro cuando finalmente superponemos la distribución real que siguió el mercado como un gráfico negro debajo.

plt.title("Visualizing Our Improvements")
plt.plot()
plt.grid()
plt.xlabel("Return")
plt.axvline(train['Target'].mean(),color='red',linestyle=':')
sns.histplot(predictions,color='blue')
legend = ['Train Mean','Model Predictions']
plt.legend(legend)

Figura 3: Visualización de las predicciones de nuestro modelo con respecto al rendimiento promedio que el modelo observó en el conjunto de entrenamiento.

La región azul de las predicciones realizadas por nuestro modelo parecía razonable en la Figura 3, pero cuando finalmente consideramos la distribución real que siguió el mercado en la Figura 4, se hace evidente que este modelo no es sólido para la tarea en cuestión. El modelo no logra capturar la amplitud de la distribución real del mercado, donde algunas de las mayores ganancias y pérdidas quedan ocultas a la percepción de nuestro modelo. Lamentablemente, RMSE frecuentemente remitirá a los profesionales a dichos modelos, si el profesional a cargo no comprende, respeta e interpreta cuidadosamente la métrica de RMSE. Utilizar un modelo así en el trading real sería catastrófico para su experiencia de trading en vivo.

plt.title("Visualizing Our Improvements")
plt.plot()
plt.grid()
plt.xlabel("Return")
plt.axvline(train['Target'].mean(),color='red',linestyle=':')
sns.histplot(test['Target'],color='black')
sns.histplot(predictions,color='blue')

legend = ['Train Mean','Test Distribution','Model Prediction']
plt.legend(legend)

Figura 4: Visualización de la distribución de las predicciones de nuestros modelos con respecto a la distribución real del mercado.


Nuestra solución propuesta

En este punto, hemos demostrado al lector que métricas como el RMSE pueden optimizarse fácilmente prediciendo siempre el rendimiento medio del mercado, y hemos demostrado por qué esto no es atractivo, ya que el RMSE podría indicarnos con frecuencia que un modelo tan inútil es lo mejor que podemos hacer. Expresamos que, claramente, existe la necesidad de procedimientos y nuevas técnicas que explícitamente:

  1. Prueba tu comprensión del mercado real.
  2. Comprender la diferencia entre ganancias y pérdidas.
  3. Desaliente los comportamientos agresivos.

Me gustaría proponer una arquitectura de modelo única que podría ser una posible solución candidata que el lector puede considerar. Me refiero a la estrategia como «Modelos dinámicos de cambio de régimen» o DRS, por sus siglas en inglés. En un debate aparte sobre las configuraciones de alta probabilidad, observamos que modelar las ganancias/pérdidas generadas por una estrategia comercial puede ser más fácil que intentar pronosticar el mercado directamente. Los lectores que aún no hayan leído ese artículo pueden encontrar el enlace al mismo aquí.

Ahora procederemos a explotar esa observación de una manera interesante. Construiremos dos modelos idénticos para simular versiones opuestas de una estrategia comercial. Un modelo siempre asumirá que el mercado está en un estado de seguimiento de tendencia, mientras que el último siempre asumirá que el mercado está en un estado de reversión a la media. Cada modelo se entrenará por separado y no tendrá conciencia ni medios para coordinar sus predicciones con el otro modelo.

El lector debe recordar que la Hipótesis del Mercado Eficiente enseña a los inversores que comprar y vender los mismos volúmenes del mismo activo dejará al inversor perfectamente cubierto, y si ambas posiciones se abren y cierran simultáneamente, el beneficio total es 0, sin incluir ninguna comisión de transacción. Por lo tanto, debemos esperar que nuestros modelos siempre estén de acuerdo con este hecho. De hecho, podemos probar si nuestros modelos están de acuerdo con esta verdad; los modelos que no están de acuerdo con esta verdad no tienen una verdadera comprensión del mercado.

Por lo tanto, podemos sustituir la necesidad de confiar en métricas como RMSE probando si nuestro modelo DVM demuestra una comprensión de este principio que construye la estructura del mercado. Aquí es donde nuestra prueba de comprensión del mercado del mundo real entra en juego. Si ambos modelos comprenden verdaderamente las realidades del trading, entonces la suma de sus pronósticos debería ser siempre 0, en todo momento. Ajustaremos nuestros modelos al conjunto de entrenamiento y luego los probaremos fuera de la muestra, para ver si las predicciones del modelo siempre suman 0, incluso cuando los modelos están subestresados.

Recordemos que las predicciones de los modelos no están coordinadas de ningún modo. Los modelos se entrenan por separado y no tienen conciencia de los demás. Por lo tanto, si los modelos no están "pirateando" métricas de error deseables sino que realmente están aprendiendo la estructura subyacente del mercado, entonces demostrarán que han llegado a sus predicciones a través de una conducta ética si ambas predicciones del modelo suman 0.

Sólo uno de estos modelos puede esperar una recompensa positiva en cualquier momento. Si la suma de las predicciones de nuestro modelo no es 0, entonces los modelos pueden haber aprendido involuntariamente un sesgo direccional que viola la hipótesis del mercado eficiente. De lo contrario, en el mejor de los casos, podemos alternar dinámicamente entre estos dos estados del mercado con un nivel de confianza con el que no estábamos familiarizados en el pasado. Solo un modelo debe esperar una recompensa positiva en cualquier momento, y nuestra filosofía comercial es que creemos que ese modelo corresponde al estado oculto en el que se encuentra actualmente el mercado. Y esto podría potencialmente tener más valor para nosotros que unas lecturas RMSE bajas.

Figura 5: Comprensión de la arquitectura general de nuestros modelos DRS.


Obtención de datos desde nuestro terminal

Nuestros datos deben ser lo más detallados posible para obtener los mejores resultados. Por lo tanto, estamos realizando un seguimiento de los valores actuales de nuestros indicadores técnicos y niveles de precios, así como del crecimiento que se ha producido en estas dinámicas de mercado al mismo tiempo.

//+------------------------------------------------------------------+
//|                                                      ProjectName |
//|                                      Copyright 2020, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs

//--- Define our moving average indicator
#define MA_PERIOD 5                //--- Moving Average Period
#define MA_TYPE   MODE_SMA          //--- Type of moving average we have
#define HORIZON 10

//--- Our handlers for our indicators
int ma_handle;

//--- Data structures to store the readings from our indicators
double ma_reading[];

//--- File name
string file_name = Symbol() + " DRS Modelling.csv";

//--- Amount of data requested
input int size = 3000;

//+------------------------------------------------------------------+
//| Our script execution                                             |
//+------------------------------------------------------------------+
void OnStart()
  {
  int fetch = size + (HORIZON * 2);
//---Setup our technical indicators
   ma_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE);

//---Set the values as series
   CopyBuffer(ma_handle,0,0,fetch,ma_reading);
   ArraySetAsSeries(ma_reading,true);

//---Write to file
   int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");

   for(int i=size;i>=1;i--)
     {
      if(i == size)
        {
         FileWrite(file_handle,"Time","Open","High","Low","Close","Delta O","Delta H","Delta Low","Delta Close","SMA 5","Delta SMA 5");
        }

      else
        {
         FileWrite(file_handle,
                   iTime(_Symbol,PERIOD_CURRENT,i), 
                   iOpen(_Symbol,PERIOD_CURRENT,i),
                   iHigh(_Symbol,PERIOD_CURRENT,i), 
                   iLow(_Symbol,PERIOD_CURRENT,i),
                   iClose(_Symbol,PERIOD_CURRENT,i),
                   iOpen(_Symbol,PERIOD_CURRENT,i)   - iOpen(_Symbol,PERIOD_CURRENT,(i + HORIZON)),
                   iHigh(_Symbol,PERIOD_CURRENT,i)   - iHigh(_Symbol,PERIOD_CURRENT,(i + HORIZON)),
                   iLow(_Symbol,PERIOD_CURRENT,i)    - iLow(_Symbol,PERIOD_CURRENT,(i + HORIZON)),
                   iClose(_Symbol,PERIOD_CURRENT,i)  - iClose(_Symbol,PERIOD_CURRENT,(i + HORIZON)),
                   ma_reading[i],
                   ma_reading[i] - ma_reading[(i + HORIZON)]
                   );
        }
     }
//--- Close the file
   FileClose(file_handle);
  }
//+------------------------------------------------------------------+


Empezando

Para empezar, primero importaremos nuestras bibliotecas estándar.

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

Ahora podemos leer los datos que escribimos anteriormente en el archivo CSV.

#Read in the data
data = pd.read_csv("/content/drive/MyDrive/Colab Data/Financial Data/FX/EUR USD/DRS Modelling/EURUSD DRS Modelling.csv")
data

Cuando creamos nuestro script MQL5, pronosticábamos 10 pasos hacia el futuro. Debemos mantener eso.

#Recall that in our MQL5 Script our forecast horizon was 10
HORIZON = 10

#Calculate the returns generated by the market
data['Return'] = data['Close'].shift(-HORIZON) - data['Close']

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

Ahora etiqueta los datos. Recordemos que tendremos dos etiquetas, una que siempre asume que el mercado continuará con una tendencia y otra que siempre asume que el mercado se encuentra estancado en un comportamiento de reversión a la media.

#Now let us define the signals being generated by the moving average, in the DRS framework there are always at least n signals depending on the n states the market could be in
#Our simple DRS model assumes only 2 states
#First we will define the actions you should take assuming the market is in a trending state
#Therefore if price crosses above the moving average, buy. Otherwise, sell.
data['Trend Action'] = 0

data.loc[data['Close'] > data['SMA 5'], 'Trend Action'] = 1
data.loc[data['Close'] < data['SMA 5'], 'Trend Action'] = -1

#Now calculate the returns generated by the strategy
data['Trend Profit'] = data['Trend Action'] * data['Return']

Después de etiquetar las acciones que siguen la tendencia, inserte las acciones de reversión a la media.

#Now we will repeat the procedure assuming the market was mean reverting
data['Mean Reverting Action'] = 0

data.loc[data['Close'] > data['SMA 5'], 'Mean Reverting Action'] = -1
data.loc[data['Close'] < data['SMA 5'], 'Mean Reverting Action'] = 1

#Now calculate the returns generated by the strategy
data['Mean Reverting Profit'] = data['Mean Reverting Action'] * data['Return']

Al etiquetar los datos de esta manera, esperamos que el ordenador detecte las condiciones en las que cada estrategia pierde dinero y cuándo seguir cada estrategia. Si trazamos el valor objetivo acumulativo, podemos ver claramente que, durante el periodo de tiempo seleccionado en nuestros datos, el mercado EURUSD pasó más tiempo mostrando un comportamiento de reversión a la media que siguiendo una tendencia. Sin embargo, hay que tener en cuenta que ambas líneas presentan sacudidas repentinas. Creo que estas perturbaciones pueden corresponder a cambios repentinos en el régimen del mercado.

#If we plot our cumulative profit sums, we can see the profit and losses aren't evenly distributed between the two states
plt.plot(data['Trend Profit'].cumsum(),color='black')
plt.plot(data['Mean Reverting Profit'].cumsum(),color='red')

#The mean reverting strategy appears to have been making outsized profits with respect to the trending stratetefgy
#However, closer inspection reveals, that both strategies are profitable, but never at the same time!
#The profit profiles of both strategies show abrupt shocks, when the opposite strategy become more profitable.
plt.legend(['Trend Profit','Mean Reverting Profit'])
plt.xlabel('Time')
plt.ylabel('Profit/Loss')
plt.title('A DRS Model Visualizes The Market As Being in 2 Possible States')
plt.grid()
plt.axhline(0,color='black',linestyle=':')

Figura 6: Visualización de la distribución de ganancias entre dos estrategias opuestas.

Define tus entradas y objetivos.

#Let's define the inputs and target
X = data.iloc[:,1:-5].columns
y = ['Trend Profit','Mean Reverting Profit']

Seleccione nuestras herramientas para modelar el mercado.

#Import the modelling tools
from sklearn.model_selection import train_test_split,TimeSeriesSplit
from sklearn.ensemble import RandomForestRegressor

Dividir los datos dos veces. De esta manera tenemos un conjunto de entrenamiento, un conjunto de validación y un conjunto de prueba final.

#Split the data
train , test = train_test_split(data,test_size=0.5,shuffle=False)

f_train , f_validation = train_test_split(train,test_size=0.5,shuffle=False)

Preparemos nuestros 2 modelos ahora. Recordemos que ambos modelos nos darán una doble visión del mundo, pero no deben coordinarse de ninguna manera.

#The trend model
trend_model = RandomForestRegressor()

#The mean reverting model
mean_model = RandomForestRegressor()

Se adapta a nuestros modelos.

trend_model.fit(f_train.loc[:,X],f_train.loc[:,y[0]])
mean_model.fit(f_train.loc[:,X],f_train.loc[:,y[1]])

Probando la validez de nuestros modelos. Registraremos las predicciones de ambos modelos sobre qué valores tomarán sus objetivos en el conjunto de prueba. Recuerde, los modelos no están aprendiendo el mismo objetivo. Cada modelo aprendió independientemente su propio objetivo y trabajó para reducir su error independientemente del otro modelo.

pred_1 = trend_model.predict(f_validation.loc[:,X])
pred_2 = mean_model.predict(f_validation.loc[:,X])

El contenido del conjunto de validación está fuera de la muestra para nuestros modelos. En esencia, estamos poniendo a prueba los modelos utilizando datos que no han visto antes, para observar si nuestros modelos se comportarán de manera ética bajo estrés material. 

Nuestra prueba se realiza sobre la suma de las predicciones de ambos modelos. Si el valor máximo de la suma de las predicciones del modelo es igual a 0,0, entonces nuestro modelo ha pasado nuestra prueba. Porque nuestro modelo concuerda esencialmente con la hipótesis del mercado eficiente de que al seguir ambos modelos al mismo tiempo el inversor no ganará nada. Nuestra intención es seguir sólo un modelo a la vez. Por lo tanto, cambiaremos dinámicamente entre regímenes. Es decir, nuestra estrategia, tiene la capacidad de cambiar de estrategia sin intervención humana.

test_result = pred_1 + pred_2

print(f" Test Passed: {np.linalg.norm(test_result,ord=2) == 0.0}")
Test Passed: True

El paquete numpy contiene muchas bibliotecas útiles, como el paquete de álgebra lineal que usamos anteriormente. La función norma que llamamos simplemente devuelve la suma total del contenido de un vector, o el valor más grande de un vector, dependiendo de cómo se llama el método. Lógicamente, esto es lo mismo que comprobar manualmente el contenido de la matriz por uno mismo, para asegurarse de que todos los números en la matriz sean 0. Tenga en cuenta que he truncado las salidas de la matriz, pero el lector puede estar seguro de que todas eran 0.

array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,

       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,

Cuando graficamos las ganancias reales obtenidas con la estrategia de seguimiento de tendencias y las comparamos con las predicciones realizadas por el modelo de seguimiento de tendencias, nos damos cuenta de que, al retener algunas oscilaciones masivas en la rentabilidad, como durante el intervalo de 600 y 700 días, cuando el mercado ERUUSD osciló con una volatilidad significativa, nuestro modelo pudo mantenerse al día con las otras ganancias de tamaño normal.

plt.plot(f_validation.loc[:,y[0]],color='black')
plt.plot(pred_1,color='red',linestyle=':')
plt.legend(['Actual Profit','Predicted Profit'])
plt.grid()
plt.ylabel('Loss / Profit')
plt.xlabel('Out Of Sample Days')
plt.title('Visualizing Our Model Performance Out Of Sample on the EURUSD 10 Day Return')

Figura 7: Nuestro modelo DRS no logró capturar la verdadera volatilidad del mercado.

Ahora estamos listos para exportar nuestros modelos de aprendizaje automático al formato ONNX y comenzar a guiar nuestras aplicaciones comerciales en nuevas direcciones. ONNX significa Open Neural Network Exchange y nos permite construir e implementar modelos de aprendizaje automático a través de un conjunto de API ampliamente adoptadas. Esta adopción generalizada permite que diferentes lenguajes de programación trabajen con el mismo modelo ONNX. Y recuerda, cada modelo ONNX es solo una representación de tu modelo de aprendizaje automático. Si aún no tiene instaladas las bibliotecas skl2onnx y ONNX, comience por instalarlas.

!pip install skl2onnx onnx

Ahora cargamos las librerías que necesitamos exportar.

import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

Define la forma de E/S de tus modelos ONNX.

eurusd_drs_shape = [("float_input",FloatTensorType([1,len(X)]))]
eurusd_drs_output_shape = [("float_output",FloatTensorType([1,1]))]

Prepare prototipos ONNX de su modelo DRS.

trend_drs_model_proto = convert_sklearn(trend_model,initial_types=eurusd_drs_shape,final_types=eurusd_drs_output_shape,target_opset=12)
mean_drs_model_proto = convert_sklearn(mean_model,initial_types=eurusd_drs_shape,final_types=eurusd_drs_output_shape,target_opset=12)

Guardar los modelos.

onnx.save(trend_drs_model_proto,"EURUSD RF D1 T LBFGSB DRS.onnx")
onnx.save(mean_drs_model_proto,"EURUSD RF D1 M LBFGSB DRS.onnx")

Felicitaciones, ha creado su primera arquitectura de modelo DRS. Ahora preparémonos para realizar pruebas retrospectivas de los modelos y ver si hacen una diferencia y si pudimos sustituir de manera significativa el RMSE de nuestro proceso de diseño.


Introducción a MQL5

Para comenzar, primero definiremos las constantes del sistema que son importantes para nuestras actividades comerciales.

//+------------------------------------------------------------------+
//|                                                   EURUSD DRS.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define HORIZON 10
#define MA_PERIOD 5
#define MA_SHIFT 0
#define MA_MODE MODE_SMA
#define TRADING_VOLUME SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN)

Ahora podemos cargar los recursos de nuestro sistema.

//+------------------------------------------------------------------+
//| System dependencies                                              |
//+------------------------------------------------------------------+
#resource "\\Files\\DRS\\EURUSD RF D1 T DRS.onnx" as uchar onnx_proto[]   //Our Trend Model
#resource "\\Files\\DRS\\EURUSD RF D1 M DRS.onnx" as uchar onnx_proto_2[] //Our Mean Reverting Mode

Cargue la biblioteca comercial.

//+------------------------------------------------------------------+
//| System libraries                                                 |
//+------------------------------------------------------------------+
#include <Trade/Trade.mqh>
CTrade Trade;

También necesitaremos variables para nuestros indicadores técnicos.

//+------------------------------------------------------------------+
//| Technical Indicators                                             |
//+------------------------------------------------------------------+
int    ma_o_handle,ma_c_handle,atr_handle;
double ma_o[],ma_c[],atr[];
double bid,ask;
int    holding_period;

Especificar variables globales.

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
long onnx_model,onnx_model_2;

Cuando nuestra aplicación se carga inicialmente, llamaremos a un método responsable de cargar nuestros indicadores técnicos y modelos ONNX.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(!setup()) return(INIT_FAILED);
//---
   return(INIT_SUCCEEDED);
  }

Si no estamos usando la aplicación, limpiemos los recursos que ya no necesitamos.

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

Finalmente, cuando recibamos niveles de precios actualizados, buscaremos una oportunidad comercial o gestionaremos las posiciones abiertas que tengamos.

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

   if(time_stamp != current_time)
     {
      time_stamp = current_time;
      update_variables();

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

      else
         if(PositionsTotal() > 0)
           {
            manage_setup();
           }
     }
  }
//+------------------------------------------------------------------+

Esta es la implementación de la función que escribimos para configurar todo el sistema.

//+------------------------------------------------------------------+
//| Attempt To Setup Our System Variables                            |
//+------------------------------------------------------------------+
bool setup(void)
  {

   atr_handle = iATR(Symbol(),PERIOD_CURRENT,14);
   ma_c_handle = iMA(Symbol(),PERIOD_CURRENT,MA_PERIOD,MA_SHIFT,MA_MODE,PRICE_CLOSE);
   ma_o_handle = iMA(Symbol(),PERIOD_CURRENT,MA_PERIOD,MA_SHIFT,MA_MODE,PRICE_OPEN);
   holding_period = 0;

   onnx_model = OnnxCreateFromBuffer(onnx_proto,ONNX_DEFAULT);
   onnx_model_2 = OnnxCreateFromBuffer(onnx_proto_2,ONNX_DEFAULT);

   if(onnx_model == INVALID_HANDLE)
     {
      Comment("Failed to load Trend DRS model");
      return(false);
     }

   if(onnx_model_2 == INVALID_HANDLE)
     {
      Comment("Failed to load Mean Reverting DRS model");
      return(false);
     }


   ulong input_shape[] = {1,10};
   ulong output_shape[] = {1,1};

   if(!OnnxSetInputShape(onnx_model,0,input_shape))
     {
      Comment("Failed to set Trend DRS Model input shape");
      return(false);
     }

   if(!OnnxSetInputShape(onnx_model_2,0,input_shape))
     {
      Comment("Failed to set Mean Reverting DRS Model input shape");
      return(false);
     }

   if(!OnnxSetOutputShape(onnx_model,0,output_shape))
     {
      Comment("Failed to set Trend DRS Model output shape");
      return(false);
     }

   if(!OnnxSetOutputShape(onnx_model_2,0,output_shape))
     {
      Comment("Failed to set Mean Reverting DRS Model output shape");
      return(false);
     }
   return(true);
  }

Cuando desinicialicemos el sistema, liberaremos manualmente los indicadores y modelos ONNX que ya no utilizamos. 

//+------------------------------------------------------------------+
//| Free up system resources                                         |
//+------------------------------------------------------------------+
void release(void)
  {
   IndicatorRelease(ma_c_handle);
   IndicatorRelease(ma_o_handle);
   OnnxRelease(onnx_model);
   OnnxRelease(onnx_model_2);
  }

Actualizar las variables del sistema siempre que tengamos nueva información de precios. 

//+------------------------------------------------------------------+
//| Update our system variables                                      |
//+------------------------------------------------------------------+
void update_variables(void)
  {
   bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
   ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
   CopyBuffer(ma_c_handle,0,1,(HORIZON*2),ma_c);
   CopyBuffer(ma_o_handle,0,1,(HORIZON*2),ma_o);
   CopyBuffer(atr_handle,0,0,1,atr);
   ArraySetAsSeries(ma_c,true);
   ArraySetAsSeries(ma_o,true);
  }

Gestionar operaciones abiertas, básicamente realizando una cuenta regresiva hasta nuestro retorno de 10 días.

//+------------------------------------------------------------------+
//| Manage The Trade We Have Open                                    |
//+------------------------------------------------------------------+
void manage_setup(void)
  {
   if((PositionsTotal() > 0) && (holding_period < (HORIZON-1)))
      holding_period +=1;

   else
      if((PositionsTotal() > 0) && (holding_period == (HORIZON - 1)))
         Trade.PositionClose(Symbol());
  }

Encuentre una configuración comercial obteniendo información detallada sobre el estado actual del mercado y alimentándola a nuestro modelo. Nuestra estrategia se basa en el uso de medias móviles como indicadores del sentimiento de los inversores. Cuando las medias móviles dan señales de venta, asumiremos que la mayoría de los inversores quieren ir en corto, pero creemos que la mayoría tiende a estar equivocada en el mercado de divisas.

//+------------------------------------------------------------------+
//| Find A Trading Oppurtunity For Our Strategy                      |
//+------------------------------------------------------------------+
void find_setup(void)
  {
   vectorf model_inputs   = vectorf::Zeros(10);
   vectorf model_outputs  = vectorf::Zeros(1);
   vectorf model_2_outputs  = vectorf::Zeros(1);
   holding_period = 0;
   int i = 0;

   model_inputs[0] = (float) iOpen(Symbol(),PERIOD_CURRENT,0);
   model_inputs[1] = (float) iHigh(Symbol(),PERIOD_CURRENT,0);
   model_inputs[2] = (float) iLow(Symbol(),PERIOD_CURRENT,0);
   model_inputs[3] = (float) iClose(Symbol(),PERIOD_CURRENT,0);
   model_inputs[4] = (float)(iOpen(Symbol(),PERIOD_CURRENT,0) - iOpen(Symbol(),PERIOD_CURRENT,HORIZON));
   model_inputs[5] = (float)(iHigh(Symbol(),PERIOD_CURRENT,0) - iHigh(Symbol(),PERIOD_CURRENT,HORIZON));
   model_inputs[6] = (float)(iLow(Symbol(),PERIOD_CURRENT,0) - iLow(Symbol(),PERIOD_CURRENT,HORIZON));
   model_inputs[7] = (float)(iClose(Symbol(),PERIOD_CURRENT,0) - iClose(Symbol(),PERIOD_CURRENT,HORIZON));
   model_inputs[8] = (float) ma_c[0];
   model_inputs[9] = (float)(ma_c[0] - ma_c[HORIZON]);

   if(!OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_outputs))
     {
      Comment("Failed to run the ONNX model correctly.");
     }

   if(!OnnxRun(onnx_model_2,ONNX_DEFAULT,model_inputs,model_2_outputs))
     {
      Comment("Failed to run the ONNX model correctly.");
     }

   if(model_outputs[0] > 0)
     {
      if(ma_c[0] < ma_o[0])
        {
         if(iClose(Symbol(),PERIOD_CURRENT,0) > ma_c[0])
            Trade.Buy(TRADING_VOLUME,Symbol(),ask,0,0,"");
        }

      else
         if(ma_c[0] > ma_o[0])
           {
            if(iClose(Symbol(),PERIOD_CURRENT,0) < ma_c[0])
               Trade.Sell(TRADING_VOLUME,Symbol(),bid,0,0,"");
           }
     }

   else
      if(model_2_outputs[0] > 0)
        {
         if(ma_c[0] < ma_o[0])
           {
            if(iClose(Symbol(),PERIOD_CURRENT,0) < ma_c[0])
               Trade.Buy(TRADING_VOLUME,Symbol(),ask,0,0,"");
           }

         if(ma_c[0] > ma_o[0])
           {
            if(iClose(Symbol(),PERIOD_CURRENT,0) > ma_c[0])
               Trade.Sell(TRADING_VOLUME,Symbol(),bid,0,0,"");
           }
        }

   Comment("0: ",model_outputs[0],"1: ",model_2_outputs[0]);
  }

Indefinir constantes del sistema.

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef HORIZON
#undef MA_MODE
#undef MA_PERIOD
#undef MA_SHIFT
#undef TRADING_VOLUME
//+------------------------------------------------------------------+

Primero, seleccionaremos las fechas de nuestras pruebas retrospectivas. Recuerde que siempre elegimos fechas que están más allá del conjunto de entrenamiento del modelo para obtener una idea confiable de qué tan bien puede funcionar el modelo en el futuro. 

Figura 8: Asegúrese de haber seleccionado fechas que estén más allá del conjunto de entrenamiento del modelo.

Generalmente queremos poner a prueba nuestro modelo, así que seleccionamos "Retraso aleatorio" y "Cada tick basado en ticks reales" para probar nuestra estrategia en condiciones de mercado realistas y desafiantes. 

Figura 9: Usar "Cada tick basado en ticks reales" es la opción de modelado más realista que podemos seleccionar para probar nuestra arquitectura DRS.

A continuación podemos encontrar un resumen detallado del desempeño de nuestra nueva estrategia DRS. Es interesante darse cuenta de que logramos sustituir RMSE por DRS y aún así produjimos una estrategia rentable, a pesar de que este fue nuestro primer intento de sustituir RMSE en nuestro proceso de construcción de modelos de manera formal.

Figura 10: Resultados detallados del desempeño de nuestra estrategia con datos que no había visto antes.

Cuando observamos la curva de capital producida por nuestra estrategia, podemos empezar a ver los problemas causados por nuestro modelo DRS al no anticipar la volatilidad del mercado como comentamos anteriormente. Esto hace que nuestra estrategia oscile entre períodos rentables y períodos no rentables. Sin embargo, nuestra estrategia está demostrando capacidad para recuperarnos y mantener el rumbo, que es lo que deseamos.

Figura 11: La curva de capital producida por nuestra nueva estrategia comercial.



Conclusión

Después de leer este artículo, los lectores que no eran conscientes de las limitaciones de estas métricas se sienten más fuertes que cuando comenzaron a leerlo. Conocer las limitaciones de tus herramientas es tan importante como conocer sus fortalezas. Los métodos de optimización que utilizamos para desarrollar IA pueden quedar “atascados” cuando intentamos resolver problemas particularmente desafiantes. Y cuando los profesionales utilizan estas herramientas para modelar el rendimiento de los activos, deben ser plenamente conscientes de que sus modelos pueden mostrar una tendencia a estancarse en el rendimiento promedio del mercado.

El lector también ha adquirido conocimientos útiles y ahora entiende los beneficios de filtrar los mercados en función de qué tan bien pueden superar a un modelo que predice el rendimiento promedio en ese mercado porque esto implica que el mercado tiene ineficiencias que el profesional debe aprovechar.

Al filtrar los mercados en función de qué tan bien el profesional puede superar la media, el lector ha aprendido a desistir de interpretar ciegamente el RMSE como una métrica independiente y, en cambio, leer el RMSE en relación con el TSS en todo momento. El lector se ha beneficiado de una comprensión práctica de las limitaciones que estas métricas imponen en su trabajo diario, una característica que no es común en la mayoría de la literatura que cubre este tema o en los artículos de investigación vinculados aquí.

Y por último, si el lector tenía la intención de implementar un modelo de aprendizaje automático para negociar su capital privado pronto, pero no estaba al tanto de estas limitaciones, entonces le advertiría que primero repitiera el ejercicio que demostré en este artículo, para estar seguro de que no estaba a punto de dispararse silenciosamente en el pie. RMSE permite que nuestros modelos hagan trampa en nuestras pruebas, pero tengo confianza en que los lectores que han leído hasta aquí no serán engañados fácilmente por las limitaciones de la IA.

Nombre del archivoDescripción del archivo
DRS Models.mq5El script MQL5 que construimos para obtener los datos de mercado detallados que necesitábamos para construir nuestro modelo DRS.
Dynamic_Regime_Switching_Models_(DRS_Modelling).ipynbEl notebook Jupyter que escribimos para diseñar nuestro modelo DRS.
EURUSD DRS.mq5Nuestro asesor experto EURUSD que empleó nuestro modelo DRS.
EURUSD RF D1 T DRS.onnxEl modelo DRS de seguimiento de tendencias siempre supone que el mercado está en tendencia.
EURUSD RF D1 M DRS.onnxEl modelo DRS de reversión a la media siempre supone que el mercado está en un estado de reversión a la media.

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

[Eliminado] | 26 jul 2025 en 18:02

"Sin embargo, nuestra estrategia demuestra la capacidad de recuperarse y mantener el rumbo, que es exactamente lo que pretendemos".

Siempre he pensado que por lo que hay que esforzarse es porque una estrategia genere beneficios :)

Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 29 ago 2025 en 15:11
Maxim Dmitrievsky #:

"Sin embargo, nuestra estrategia muestra la capacidad de recuperarnos y mantenernos en el buen camino, que es exactamente lo que pretendemos".

Siempre he pensado que hay que esforzarse para que una estrategia aporte beneficios :)

Sí, en efecto, pero por desgracia aún no disponemos de métricas estandarizadas de aprendizaje automático que tengan en cuenta la diferencia entre beneficios y pérdidas.
Miguel Angel Vico Alba
Miguel Angel Vico Alba | 30 ago 2025 en 09:28

Gracias por el artículo, @Gamuchirai Zororo Ndawana.

Estoy de acuerdo con @Maxim Dmitrievsky en que el objetivo final es la rentabilidad. La idea de "recuperar y mantener el rumbo" tiene sentido como robustez y control del drawdown, pero no sustituye a la rentabilidad.

Sobre las métricas: es cierto que no hay una métrica estandarizada de ML que tenga en cuenta el PnL, aunque en la práctica se validan modelos con Sharpe, Sortino, Calmar, profit factor, max DD, más pérdidas o recompensas asimétricas en aprendizaje por refuerzo (estilo RL) que sí incorporan PnL y costes (costes y rotación).

Técnicamente, yo revisaría dos puntos clave del artículo:
  • Los ejemplos contienen sesgo look-ahead (características que usan i + HORIZON), lo que invalida la evaluación;
  • La prueba DRS que "suma cero" es tautológica porque las dos etiquetas son antisimétricas por construcción, no prueba la comprensión del mercado.
Aún así, el recordatorio de no seleccionar por RMSE o MAE en los rendimientos es útil.

Sugerencia práctica: prueba de walk-forward, costes y deslizamiento, pérdida asimétrica o cuantílica u objetivos basados en la utilidad, y penalizar la rotación para evitar el abrazo a la media. (Sugerencia pragmática: alinear la pérdida con la forma de ganar dinero).
peter matty
peter matty | 31 ago 2025 en 06:44

Citado: Sí, en efecto, pero por desgracia aún no disponemos de métricas estandarizadas de aprendizaje automático que sean conscientes de la diferencia entre pérdidas y ganancias.

Respuesta: Las columnas de pérdidas y ganancias sólo existirán si su producto de backtesting o el mercado plano es tan bueno como el mercado a plazo que está utilizando contra la cartera subsiguiente o cesta de índice que seguirá esta línea de orden.

Hay algunos índices y ETF's recientemente fundados que están saliendo, o que son producidos sobre una base creciente, para este uso previsto, y producirán estos resultados, márgenes de beneficio tales como el índice dowjones 30 así como muchos otros índices que han sido creados para este uso previsto. Peter Matty

Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 31 ago 2025 en 17:09
Miguel Angel Vico Alba # :

Gracias por el artículo, @Gamuchirai Zororo Ndawana .

Estoy de acuerdo con @Maxim Dmitrievsky en que el objetivo final es la rentabilidad. La idea de recuperar y mantener el rumbo tiene sentido por solidez y para controlar el drawdown, pero no sustituye a la rentabilidad.

Respecto a las métricas: Es cierto que no existe una métrica estandarizada de ML que tenga en cuenta el PnL, aunque en la práctica se validan modelos utilizando Sharpe, Sortino, Calmar, Win Factor, Max DD, así como pérdidas o recompensas asimétricas en aprendizaje por refuerzo (estilo RL) que incluyen PnL y coste (coste e ingresos).

Desde un punto de vista técnico, revisaría dos puntos importantes del artículo:
  • Los ejemplos contienen sesgo de look-ahead (características con i + HORIZON), lo que invalida la evaluación;
  • La prueba DRS, en la que "la suma es cero", es tautológica porque los dos términos son antisimétricos por diseño; no demuestra comprensión del mercado.
No obstante, el consejo de no seleccionar basándose en el RMSE o MAE de los rendimientos es útil. Sugerencias prácticas: Pruebas de avance, costes y desviaciones, objetivos asimétricos o cuantílicos basados en pérdidas o beneficios, y penalización de los ingresos para evitar el abrazo a la media. (Enfoque pragmático: Alinear la pérdida con la forma de ganar dinero.)

A veces me pregunto si las herramientas de traducción que utilizamos no captan el mensaje original. Tu respuesta ofrece muchos más puntos de discusión que lo que yo entendí del mensaje original de @Maxim Dmitrievsky .

Gracias por señalar esos descuidos en el look ahead bias (rasgos con i + HORIZONTE), esos son los peores bugs que odio, necesitan todo un re-test. Pero esta vez con más detenimiento.

También has aportado información valiosa con las medidas de validación utilizadas para validar modelos en la práctica, el Sharpe Ratio debe ser algo así como un Gold Standard universal. Tengo que aprender más sobre Calmar y Sortino para formarme una opinión al respecto, gracias por ello.

Estoy de acuerdo contigo en que los dos términos son antisimétricos por diseño, y la prueba es que los modelos deben permanecer antisimétricos, cualquier desviación de esta expectativa, es fallar la prueba. Si uno o ambos modelos tienen un sesgo inaceptable, sus predicciones no seguirán siendo antisimétricas como esperamos.

Sin embargo, la noción de beneficio no es más que una simple ilustración que he dado para poner de relieve el problema. Ninguna de las métricas que tenemos hoy en día nos informa de cuándo se está produciendo el abrazo a la media. Ninguna de las publicaciones sobre aprendizaje estadístico nos explica por qué se produce este fenómeno. Desgraciadamente, se debe a las mejores prácticas que seguimos, y ésta es sólo una de las muchas maneras en que deseo que se inicien más conversaciones sobre los peligros de las mejores prácticas.

Este artículo era más bien un grito de ayuda, para que nos unamos y diseñemos nuevos protocolos desde cero. Nuevas normas. Nuevos objetivos en los que trabajen directamente nuestros optimizadores, que se adapten a nuestros intereses.

Utilizando redes neuronales en MetaTrader Utilizando redes neuronales en MetaTrader
En el artículo se muestra la aplicación de las redes neuronales en los programas de MQL, usando la biblioteca de libre difusión FANN. Usando como ejemplo una estrategia que utiliza el indicador MACD se ha construido un experto que usa el filtrado con red neuronal de las operaciones. Dicho filtrado ha mejorado las características del sistema comercial.
Algoritmo de optimización caótica — Chaos optimization algorithm (COA): Continuación Algoritmo de optimización caótica — Chaos optimization algorithm (COA): Continuación
Continuamos el estudio del algoritmo de optimización caótica. La segunda parte del artículo está dedicada a los aspectos prácticos de la implementación del algoritmo, sus pruebas y conclusiones.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Del básico al intermedio: Objetos (I) Del básico al intermedio: Objetos (I)
En este artículo, empezaremos a ver cómo podremos trabajar con objetos directamente en el gráfico. Esto usando un código construido especialmente para mostrarnos algo. Trabajar con objetos es algo muy interesante y bastante divertido. Como este será el primer contacto, empezaremos con algo muy simple.