English Русский Deutsch 日本語 Português
preview
Regresiones espurias en Python

Regresiones espurias en Python

MetaTrader 5Estadística y análisis | 25 septiembre 2024, 13:52
65 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Sinopsis

Antes de sumergirnos en el ámbito del trading algorítmico con aprendizaje automático, es fundamental determinar si existe una relación significativa entre las entradas del modelo y la variable que pretendemos predecir. Este artículo ilustra la utilidad de emplear pruebas de raíz unitaria en los residuos del modelo para validar la presencia de dicha relación en nuestros conjuntos de datos.

Desafortunadamente, es posible construir modelos utilizando conjuntos de datos que no tienen ninguna relación genuina. Lamentablemente, estos modelos pueden producir métricas de error impresionantemente bajas, lo que refuerza una falsa sensación de control y perspectivas excesivamente optimistas. Estos modelos defectuosos se conocen comúnmente como "regresiones espurias".

Este artículo comenzará cultivando una comprensión intuitiva de las regresiones espurias. Después, generaremos datos sintéticos de series temporales para simular una regresión espuria y observar sus efectos característicos. Posteriormente profundizaremos en métodos para identificar regresiones espurias, basándonos en nuestros conocimientos para validar un modelo de aprendizaje automático elaborado en Python. Finalmente, si nuestro modelo está validado lo exportaremos a ONNX e implementaremos una estrategia de trading en MQL5.


Introducción: Las regresiones espurias ocurren todo el tiempo

Durante mediados del siglo XIX, Ignaz Semmelweis era un médico que ejercía en Viena. Estaba profundamente frustrado por las estadísticas que observaba en el hospital en el que trabajaba.

Ignaz Semmelweis

Fig 1: Ignaz Semmelweis


El problema era que ⅕ mujeres sanas, que daban a luz en el hospital, morían de fiebres contraídas durante el parto. Ignaz estaba decidido a entender por qué. La mayoría de los médicos de la época lo atribuían al “mal aire” que, según creían, transportaba espíritus malignos que causaban estos problemas. Aunque hoy parezca cómico, en su época fue ampliamente aceptado. Pero esto no satisfizo a Ignaz. Con el paso del tiempo, Ignaz observó un día que los médicos y estudiantes de medicina que realizaban autopsias en una morgue a un lado del hospital corrían a atender partos en el otro lado del hospital, sin lavarse las manos entre partos. Después de convencer al personal de su hospital local para que practicara la higiene de manos, la tasa de mortalidad materna se redujo del 20% al 1%.

Desafortunadamente, los descubrimientos de Ignaz pasaron desapercibidos. Todos los esfuerzos que hizo para compartir sus hallazgos con otros médicos e instituciones médicas sólo lo alejaron aún más de la comunidad médica de la época y de su creencia concreta en el “aire malo”. Ignaz Semmelweis murió marginado socialmente en un manicomio a los 46 años. ¿Qué podemos aprender de los médicos que ignoraron las sabias palabras de Semmelweis y por qué les resultó tan difícil ver su error?

El problema es que es posible construir un modelo utilizando datos que no tienen ninguna relación; además, este modelo puede por casualidad producir métricas de error bajas y probar falsamente relaciones que no existen. Estos modelos se denominan regresiones espurias.

Una regresión espuria es un modelo que prueba falsamente una relación que no existe. Verá, los médicos podrían haberse dicho a sí mismos: “Hay demasiados espíritus malignos en el aire hoy, por lo tanto, más madres morirán mañana”. Como se predijo, más mujeres murieron al día siguiente, sin embargo, el médico tenía razón por las razones equivocadas. Al crear modelos de aprendizaje automático, nuestros modelos también pueden ser correctos por razones equivocadas.

Si usted es explícitamente consciente de que existe una relación entre sus datos de entrada y los de salida, entonces no tiene por qué preocuparse. Pero ¿qué puedes hacer si no estás seguro? ¿O si nunca lo has comprobado y simplemente asumiste que tenía que haber una relación?

La solución más conocida es realizar pruebas especializadas sobre los residuos de su modelo. Estas pruebas se denominan pruebas de raíz unitaria. No intentaremos definir raíces unitarias en esta discusión, esa es otra discusión en sí misma. Sin embargo, para lograr nuestros objetivos, basta con saber que si podemos encontrar raíces unitarias para nuestros residuos, entonces nuestra regresión es espuria.

La solución de raíz unitaria que estamos considerando hoy tiene solo una limitación material: podemos no encontrar raíces unitarias aunque existan (error de clase 1). Y alternativamente, podemos encontrar falsamente raíces unitarias que no existen, un error de clase 2.

Hay muchas pruebas que podemos utilizar para comprobar si nuestros residuos tienen raíces unitarias, como la prueba de Dickey Fuller aumentada y la prueba de Kwiatkowski-Phillips-Schmidt-Shin. Cada prueba tiene sus fortalezas y debilidades y fallará en diferentes condiciones.m Para ver regresiones espurias en acción, generaremos nuestros propios datos de series de tiempo. Crearemos 2 conjuntos de datos de series de tiempo que no tienen relación entre sí y observaremos qué sucede cuando entrenamos un modelo utilizando esos 2 conjuntos de datos independientes.


Simulación de regresiones espurias

Las regresiones espurias pueden ocurrir por una amplia variedad de razones, pero la razón más común es modelar dos series de tiempo independientes y no estacionarias. Analicemos esa definición técnica. Una serie de tiempo son simplemente observaciones registradas uniformemente de una variable aleatoria. Cuando decimos que una serie temporal es estacionaria, sus propiedades estadísticas, como la media, la varianza y la estructura de autocorrelación, permanecen relativamente constantes a lo largo del tiempo. Una serie temporal no es estacionaria cuando sus propiedades estadísticas fluctúan a lo largo del tiempo.

En nuestra discusión, adoptaremos un enfoque práctico simulando nuestros propios datos para comprender la verdad fundamental en cada paso. Este enfoque nos permite observar los efectos de primera mano. Comenzamos importando los paquetes necesarios.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller , kpss
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

A continuación definiremos propiedades estadísticas para nuestros dos conjuntos de datos, uno simulará nuestros datos de entrada y el otro nuestros datos de salida. Ambos conjuntos de datos contendrán números aleatorios y distribuidos normalmente.

size = 1000
mu , sigma = 0 , 1
mu_y , sigma_y = 2 , 4

El siguiente código genera dos series temporales, 'x_non_stationary' e 'y_non_stationary', que representan paseos aleatorios. La aleatoriedad se introduce a través de la distribución normal, y la suma acumulativa asegura que cada valor de la serie dependa de los valores anteriores, creando un comportamiento dinámico y no estacionario.

Los conjuntos de datos 'x_non_stationary' e 'y_non_stationary' se generan aleatoriamente mediante la función numpy.random.normal. Por lo tanto, no existe relación entre los dos conjuntos de datos.

steps = np.random.normal(mu,sigma,size)
steps_y = np.random.normal(mu_y,sigma_y,size)
x_non_stationary =  pd.DataFrame(100 + np.cumsum(steps),index= np.arange(0,1000))
x_non_stationary_lagged = x_non_stationary.shift(1)
x_non_stationary_lagged.dropna(axis=0,inplace=True)
y_non_stationary =   pd.DataFrame(100 + np.cumsum(steps_y),index= np.arange(0,1000))

Veamos nuestros dos conjuntos de datos aleatorios.

plt.plot(x_non_stationary)

Paseo aleatorio x

Fig. 2: Paseo aleatorio x.




Grafiquemos nuestra serie temporal y no estacionaria.

plt.plot(y_non_stationary)

Paseo aleatorio y

Fig. 3: Paseo aleatorio y.

Observemos ahora los resultados de la regresión de las dos series de tiempo independientes y no estacionarias.

ols = sm.OLS(y_non_stationary,x_non_stationary)
lm = ols.fit()
print(lm.summary())

x_y_non_stationary_regression

Fig. 4: Resultados resumidos de la regresión de dos variables independientes no estacionarias.

En muchas series temporales, existe un componente de tendencia aleatorio conocido como tendencia estocástica. Incluso cuando dos series temporales son independientes, sus tendencias estocásticas pueden exhibir correlaciones locales breves. Desafortunadamente, estas correlaciones momentáneas a veces pueden inducir a error a nuestros modelos y hacerles concluir que existe una relación entre dos series de tiempo independientes. Las regresiones espurias que surgen de tales casos a menudo producen métricas R-cuadrado (R2) altas. Es fundamental recordar que R-cuadrado mide la proporción de varianza en la respuesta explicada por la varianza en las variables independientes. Por lo tanto, si dos variables tienen tendencias estocásticas correlacionadas, la confiabilidad de la métrica R-cuadrado puede verse comprometida. La cuestión de la regresión espuria es particularmente relevante en el contexto de los datos de series temporales y merece una consideración cuidadosa.

Nuestro modelo tiene una métrica R-cuadrado ajustada que casi llega a 1, lo que significa que el modelo percibe el ajuste como casi perfecto. El modelo afirma que aproximadamente el 90% de la variación en la variable respuesta puede explicarse por la varianza en los predictores. Sin embargo, esta demostración sirve como un recordatorio crucial de los peligros asociados con las regresiones espurias. Sabemos que no existe relación entre entradas y salidas, ambas son aleatorias y no tienen nada en común.

Los desafíos persisten ya que el valor P parece significativo y 0 está notoriamente ausente de nuestros intervalos de confianza. Si bien esto podría interpretarse normalmente como un sello distintivo de un ajuste excelente, es necesario tener precaución. El modelo, en este caso, indica erróneamente una relación fuerte que en realidad no existe. Nosotros mismos creamos los datos de entrada y salida, por lo tanto sabemos que no existe ninguna relación.


Retraso del predictor

Un signo revelador de una regresión espuria surge cuando incluimos una versión retardada del predictor. En tales casos, el coeficiente que alguna vez fue significativo de repente se vuelve insignificante, como observaremos pronto. Este fenómeno sirve como un indicador crucial, que nos aleja de conclusiones erróneas y resalta la importancia de comprender y abordar las regresiones espurias en el análisis de series de tiempo.

Repetiremos el mismo procedimiento anterior, pero esta vez incluiremos también una versión retardada de los datos de entrada.

ols = sm.OLS(y_non_stationary.iloc[0:998,0],x_matrix.loc[0:998,['current_x','lagged_x']])
lm = ols.fit()
print(lm.summary())

Regresión X_y no estacionaria

Fig. 5: Resumen estadístico de nuestra regresión espuria.

Añadiendo otra capa de precaución, observe que nuestro valor R-cuadrado supera nuestro valor de Durbin-Watson. Esta observación puede considerarse una señal de alerta adicional que apunta hacia una posible regresión espuria. La estadística de Durbin-Watson, empleada en el análisis de regresión, sirve para detectar la presencia de autocorrelación en los residuos de un modelo de regresión. La autocorrelación se manifiesta cuando los residuos de una serie temporal o un modelo de regresión muestran correlación entre sí. En el contexto de datos de series temporales, donde las observaciones dependen de las anteriores, la prueba de Durbin-Watson se vuelve particularmente relevante. Sus resultados proporcionan información valiosa sobre la presencia de autocorrelación, lo que nos orienta aún más en nuestra interpretación del rendimiento del modelo.


Estadísticas de Durbin-Watson

  •     Rango: La estadística de Durbin-Watson tiene valores entre 0 y 4.
  •     Interpretación: Un valor cercano a 2 indica que no hay autocorrelación significativa. Valores significativamente inferiores a 2 sugieren autocorrelación positiva (los residuos están correlacionados positivamente). Valores significativamente superiores a 2 sugieren autocorrelación negativa (los residuos están correlacionados negativamente).

Si bien un R-cuadrado alto y un valor de Durbin-Watson bajo pueden generar sospechas de regresión espuria, es importante señalar que estos indicadores por sí solos no confirman de manera concluyente su presencia. En el ámbito del análisis de series de tiempo, las pruebas de diagnóstico adicionales y el conocimiento del dominio se convierten en componentes esenciales de una evaluación exhaustiva. 


Pruebas de raíz unitaria

El método más confiable para identificar una regresión espuria radica en el análisis de los residuos. Si los residuos de nuestro modelo carecen de estacionariedad, esto proporciona un fuerte indicio de que la regresión es de hecho espuria. Sin embargo, determinar si una serie temporal dada es estacionaria no es una tarea sencilla. En nuestro caso, sabiendo que la regresión es espuria y, por lo tanto, los residuos no son estacionarios, la prueba de Dickey-Fuller aumentada puede no rechazar la hipótesis nula. En otras palabras, puede no demostrar que los datos no son estacionarios aun cuando la regresión sea espuria, lo que ilustra las sutilezas y los desafíos que implica identificar regresiones espurias. Esto subraya la importancia de un enfoque matizado, que combine pruebas estadísticas y conocimiento del dominio para navegar eficazmente por las complejidades del análisis de series de tiempo.

Ahora utilizaremos sklearn para ajustar un modelo a nuestro conjunto de entrenamiento.

lm = LinearRegression()
lm.fit(x[train_start:train_end],y[train_start:train_end])

Luego calcularemos los residuos.

residuals = y[test_start:test_end] - lm.predict(x[test_start:test_end])

Grafiquemos los residuos.

residuals.plot()

Gráfica de residuos

Fig. 6: Los residuos de nuestro modelo de regresión construido con Scikit-Learn.


Ahora, someteremos nuestros residuos a la prueba Dickey-Fuller aumentada para determinar su estacionariedad.


Prueba de Dickey-Fuller aumentada

La prueba Dickey-Fuller Aumentada (ADF, Augmented Dickey-Fuller) sirve como un instrumento estadístico fundamental diseñado para la evaluación de la estacionariedad de series de tiempo o la identificación de una raíz unitaria, indicativa de no estacionariedad. Dentro del dominio del análisis de series de tiempo, la estacionariedad asume un papel primordial, lo que significa la constancia de atributos estadísticos como la media y la varianza a lo largo del tiempo. La presencia de una raíz unitaria en una serie temporal, que denota no estacionariedad, genera una sospecha razonable de que las observaciones dentro de la serie temporal pueden caracterizarse como de naturaleza estocástica o aleatoria. Por lo tanto, la prueba ADF ofrece una metodología sólida para analizar el comportamiento temporal de un conjunto de datos, contribuyendo a una comprensión matizada de sus características inherentes y sus posibles implicaciones para análisis posteriores.

  1. Hipótesis nula: La hipótesis nula de la prueba ADF postula que la serie temporal posee una raíz unitaria, lo que indica no estacionariedad.
  2. Hipótesis alternativa: La hipótesis alternativa de la prueba Dickey-Fuller Aumentada (ADF) es que la serie temporal no tiene raíz unitaria y es estacionaria.
  3. La regla de decisión: La regla de decisión en la prueba ADF implica comparar la estadística de prueba con valores críticos. Si el estadístico de prueba es menor que el valor crítico, se rechaza la hipótesis nula (presencia de una raíz unitaria), lo que indica estacionariedad. Por el contrario, si la estadística de prueba es mayor que el valor crítico, no hay evidencia suficiente para rechazar la hipótesis nula, lo que sugiere no estacionariedad.

A medida que realizamos la prueba Dickey-Fuller aumentada (ADF) en los residuos generados a partir de nuestro modelo, los hallazgos resultantes servirán como un determinante crucial para discernir la presencia de características estacionarias o no estacionarias dentro de los residuos. La prueba ADF tiene una importancia significativa en el proceso de validación de nuestros resultados de regresión y desempeña un papel fundamental para garantizar la confiabilidad de nuestro marco analítico. Al dilucidar las propiedades de estacionariedad de los residuos, esta prueba contribuye sustancialmente a mejorar la solidez interpretativa de nuestro análisis de series de tiempo.

adfuller(residuals)

(-12.753804093890963, 8.423533501802878e-24, 2, 497, {'1%': -3.4435761493506294, '5%': -2.867372960189225, '10%': -2.5698767442886696}, 1366.9343966932422)

Nuestro enfoque principal se centra en el valor p derivado de este experimento, específicamente el segundo valor de la lista proporcionada, que es 8.423533501802878e-24. En particular, este valor p se aproxima a cero y supera significativamente cualquier valor crítico razonable. En el contexto de la prueba Dickey-Fuller aumentada (ADF), si el estadístico ADF es menor que el valor crítico, rechazar la hipótesis nula se vuelve pertinente, lo que significa la presencia de estacionariedad.

Es imperativo reconocer que la prueba ADF, al igual que cualquier prueba estadística, está acompañada de limitaciones y suposiciones inherentes. Existen varios factores que pueden contribuir al fracaso de la prueba ADF al aceptar la hipótesis nula, lo que lleva al rechazo de la presencia de una raíz unitaria, incluso cuando los datos subyacentes no son estacionarios. Comprender estos matices es fundamental para una interpretación integral de los resultados de la prueba.

  1. Tamaño de muestra pequeño: El rendimiento de la prueba ADF puede verse afectado por tamaños de muestra pequeños. En tales casos, la prueba podría carecer de potencia suficiente para detectar la no estacionariedad.
  2. Orden de retraso deficiente: La elección del orden de retardo en la prueba ADF es crucial. Si el orden de retraso se especifica incorrectamente, puede generar resultados inexactos. El uso de muy pocos o demasiados rezagos puede afectar la capacidad de la prueba para capturar la estructura subyacente de los datos.
  3. Presencia de tendencias deterministas: Si los datos contienen tendencias deterministas (por ejemplo, tendencias lineales, tendencias cuadráticas) que no se tienen en cuenta en el modelo de prueba, la prueba ADF podría no rechazar la hipótesis nula. En tales casos, podrían ser necesarios pasos de preprocesamiento como la eliminación de tendencias.
  4. Diferenciación inadecuada: Si el orden de diferenciación utilizado en la prueba ADF es insuficiente para hacer que los datos sean estacionarios, la prueba podría no rechazar la hipótesis nula.


Kwiatkowski-Phillips-Schmidt-Shin

La prueba Kwiatkowski-Phillips-Schmidt-Shin (KPSS) se presenta como una alternativa viable a la prueba Dickey-Fuller aumentada (ADF) para evaluar la estacionariedad de datos de series de tiempo. Si bien ambas pruebas prevalecen en el análisis de series de tiempo, divergen en sus hipótesis nulas y alternativas, así como en sus modelos subyacentes. La selección entre las pruebas ADF y KPSS depende de las características específicas de la serie temporal en examen y de la investigación general. El uso conjunto de ambas pruebas suele proporcionar un análisis más completo de la estacionariedad, ofreciendo a los investigadores una comprensión matizada de la dinámica de las series temporales.

  1. Hipótesis nula: La hipótesis nula de la prueba KPSS es que la serie temporal es estacionaria en cuanto a tendencia. La estacionariedad de tendencia implica que la serie exhibe una raíz unitaria, lo que sugiere la presencia de una tendencia determinista.
  2. Hipótesis alternativa: La hipótesis alternativa de la prueba KPSS es que la serie temporal no es estacionaria en cuanto a la tendencia, lo que indica que es estacionaria en cuanto a la diferencia o estacionaria alrededor de una tendencia estocástica.
  3. La regla de decisión: La regla de decisión para la prueba KPSS implica comparar la estadística de prueba con valores críticos en un nivel de significancia elegido (por ejemplo, 1%, 5% o 10%). Si la estadística de prueba es mayor que el valor crítico, se rechaza la hipótesis nula, lo que sugiere que la serie temporal no es estacionaria en cuanto a tendencia. Por otro lado, si el estadístico de prueba es menor que el valor crítico, no se puede rechazar la hipótesis nula, lo que implica estacionariedad de tendencia.

En el caso de la prueba KPSS, un umbral comúnmente adoptado es un nivel de significancia de 0.05. Si la estadística KPSS cae por debajo de este umbral, sugiere no estacionariedad en los datos. En nuestro análisis, la estadística KPSS arrojó un valor de 0.016, lo que confirma su desviación del umbral crítico e indica una tendencia hacia la no estacionariedad en el conjunto de datos. Este resultado subraya aún más la importancia de considerar múltiples herramientas de diagnóstico, como las pruebas ADF y KPSS, para garantizar una evaluación exhaustiva y precisa de las características de las series de tiempo.

kpss(residuals)

(0.6709994557854182, 0.016181867655871072, 1, {'10%': 0.347, '5%': 0.463, '2.5%': 0.574, '1%': 0.739})

La prueba KPSS puede rechazar falsamente la hipótesis nula (H0) en determinadas circunstancias, lo que genera un error tipo I. Un error tipo I ocurre cuando la prueba concluye incorrectamente que la serie temporal no es estacionaria en cuanto a tendencia cuando, en realidad, lo es.

A continuación se presentan algunas situaciones en las que la prueba KPSS podría rechazar incorrectamente la hipótesis nula:

  1. Patrones estacionales: La prueba KPSS es sensible tanto a la tendencia como a la estacionalidad. Si una serie temporal muestra un fuerte patrón estacional, la prueba puede interpretarlo como una tendencia no estacionaria. En tales casos, podría ser necesario establecer una diferenciación para abordar la estacionalidad.
  2. Rupturas estructurales: Si hay rupturas estructurales en las series temporales, como cambios repentinos y significativos en el proceso subyacente de generación de datos, la prueba KPSS puede detectarlos como tendencias no estacionarias. Las rupturas estructurales pueden llevar al rechazo de la hipótesis nula.
  3. Valores atípicos: La presencia de valores atípicos en los datos puede influir en el rendimiento de la prueba KPSS. Los valores atípicos podrían percibirse como desviaciones de tendencia, lo que lleva a un rechazo de la estacionariedad de la tendencia. La robustez ante valores atípicos es una consideración importante al interpretar los resultados de la prueba KPSS.
  4. Tendencias no lineales: La prueba KPSS supone una tendencia lineal. Si la tendencia subyacente en la serie temporal no es lineal, la prueba puede producir resultados engañosos. Es posible que la prueba no capture adecuadamente las tendencias no lineales, lo que genera un falso rechazo de la estacionariedad.

Es fundamental interpretar los resultados de la prueba KPSS con cautela y considerar las características específicas de la serie temporal que se analiza. Además, la combinación de la prueba KPSS con otras pruebas de estacionariedad, como la prueba Dickey-Fuller aumentada (ADF), puede proporcionar una evaluación más completa de las propiedades de estacionariedad de la serie temporal.


Poniéndolo todo junto

Habiendo establecido una base de confianza, ahora es el momento de dejar de centrarnos en los datos de control sintéticos y redirigir nuestros esfuerzos hacia el análisis de datos de mercado auténticos obtenidos directamente de nuestro terminal MetaTrader 5. Para facilitar esta transición, proponemos el desarrollo de un script en MetaQuotes Language 5 (MQL5). Este script estará diseñado específicamente para recuperar datos de nuestra terminal comercial, formateándolos y exportándolos en formato CSV.

Al comenzar nuestro script, el paso inicial implica la declaración de variables globales, con el primer conjunto dedicado a albergar los controladores de nuestros indicadores técnicos. Estas variables desempeñarán un papel fundamental en la gestión y el acceso eficientes a los indicadores relevantes durante la ejecución del script, contribuyendo a la coherencia y organización general de nuestro programa MetaQuotes Language 5 (MQL5).

//---Our handlers for our indicators
int ma_handle;
int rsi_handle;
int cci_handle;
int ao_handle;

Posteriormente, necesitamos estructuras de datos meticulosamente diseñadas para acomodar y organizar las lecturas de nuestros indicadores técnicos. Estas estructuras de datos se utilizarán durante toda la ejecución del script. 

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

Naturalmente después creamos un nombre para el archivo.

//---File name
string file_name = "Market Data.csv";

Ahora definiremos cuántos datos recuperar.

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

Al comenzar el desarrollo de nuestro controlador de eventos OnStart, la primera llamada al orden implica la inicialización de nuestros indicadores técnicos designados.

//---Setup our technical indicators
ma_handle = iMA(_Symbol,PERIOD_CURRENT,20,0,MODE_EMA,PRICE_CLOSE);
rsi_handle = iRSI(_Symbol,PERIOD_CURRENT,60,PRICE_CLOSE);
cci_handle = iCCI(_Symbol,PERIOD_CURRENT,10,PRICE_CLOSE);
ao_handle = iAO(_Symbol,PERIOD_CURRENT);

Continuando con la ejecución del script, la siguiente tarea consiste en transferir valores desde nuestros indicadores a las estructuras de datos correspondientes. Este proceso esencial implica un mapeo meticuloso de los resultados de los indicadores con las estructuras de datos preestablecidas.

//---Set the values as series
CopyBuffer(ma_handle,0,0,size,ma_reading);
ArraySetAsSeries(ma_reading,true);
CopyBuffer(rsi_handle,0,0,size,rsi_reading);
ArraySetAsSeries(rsi_reading,true);
CopyBuffer(cci_handle,0,0,size,cci_reading);
ArraySetAsSeries(cci_reading,true);
CopyBuffer(ao_handle,0,0,size,ao_reading);
ArraySetAsSeries(ao_reading,true);

A medida que nos preparamos para iniciar el proceso de escritura de archivos, un precursor crítico implica el establecimiento de un controlador de archivos dentro de nuestro script.

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

Posteriormente, se desarrolla una fase crucial en nuestro script a medida que recorremos sistemáticamente el conjunto de datos, orquestando el meticuloso proceso de escritura de datos en nuestro archivo CSV designado. Este procedimiento iterativo implica un examen detallado y la extracción de cada punto de datos, siguiendo una secuencia cuidadosamente orquestada que se alinea con los parámetros establecidos.

for(int i=-1;i<=size;i++){
      if(i == -1){
            FileWrite(file_handle,"Open","High","Low","Close","MA 20","RSI 60","CCI 10","AO");
      }
      
        else{
                FileWrite(file_handle,iOpen(_Symbol,PERIOD_CURRENT,i),
                                        iHigh(_Symbol,PERIOD_CURRENT,i),
                                        iLow(_Symbol,PERIOD_CURRENT,i),
                                        iClose(_Symbol,PERIOD_CURRENT,i),
                                        ma_reading[i],
                                        rsi_reading[i],
                                        cci_reading[i],
                                        ao_reading[i]);
      } 
}

Una vez finalizada la configuración del script, proceda a ejecutarlo en el símbolo de su preferencia. Posteriormente, el script generará un archivo CSV, reflejando el formato ejemplificado a continuación, proporcionando así una representación completa y estructurada de los datos pertinentes asociados con el símbolo seleccionado.

Datos comerciales

Fig. 7: Nuestro archivo CSV de datos de mercado.

Ahora que nuestro enfoque se centra en el análisis de datos auténticos del mercado, nuestro objetivo es construir un modelo de regresión diseñado para predecir el crecimiento de precios previsto en los siguientes 15 minutos. El criterio fundamental de nuestra búsqueda analítica es la validación de la autenticidad del modelo de regresión. Una vez establecida esta autenticidad, pretendemos exportar el modelo validado al formato ONNX, aprovechándolo posteriormente para el desarrollo de un Asesor Experto.

Al comenzar esta fase, nuestro paso inicial implica la carga de dependencias esenciales. Entre estas dependencias, una adición notable es el paquete 'Arch', reconocido por su conjunto completo de herramientas de análisis estadístico. La integración de 'Arch' nos proporciona una variedad de recursos invaluables para emplear en nuestros esfuerzos analíticos, mejorando la profundidad y sofisticación de nuestro enfoque en el análisis de datos de mercado.

import pandas as pd
import numpy as np
import statsmodels.api as sm
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from arch.unitroot import PhillipsPerron , ADF , KPSS
import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import DoubleTensorType

Luego leemos el CSV que creamos usando nuestro script MQL5.

csv = pd.read_csv("/enter/your/path/here")

Luego preparamos nuestro objetivo, nuestro objetivo es el crecimiento del precio de cierre.

csv["Target"] = csv["Close"] / csv["Close"].shift(-15)
csv.dropna(axis=0,inplace=True)

Podemos examinar nuestro marco de datos.

marco de datos

Fig. 8: Lectura de nuestro archivo CSV de datos de mercado con Pandas

Desde allí prepararemos nuestra división de entrenamiento y prueba.

train = np.arange(0,20000)
test = np.arange(20020,89984)


Ahora ajustaremos una regresión lineal múltiple utilizando statsmodels.

ols = sm.OLS(csv.loc[:,"Target"],csv.loc[:,["Open","High","Low","Close","MA 20","RSI 60","CCI 10","AO"]])
lm = ols.fit()
print(lm.summary())

Datos de mercado OLS

Fig. 9: Resultados de la regresión de todas las variables para predecir el crecimiento del precio.

Permítanos profundizar colectivamente en la interpretación de los resultados de nuestro modelo. El intervalo de confianza para el coeficiente asociado con la característica del precio de apertura (Open) incluye el 0, lo que implica una falta de significancia estadística. En consecuencia, la decisión de excluir esta característica de nuestro modelo está fundamentada en la potencial insignificancia de su contribución. Un análisis más detallado revela que tanto el Índice de Fuerza Relativa (RSI, Relative Strength Index) como el Índice de Canal de Commodities (CCI, Commodity Channel Index) presentan coeficientes cercanos a 0, con sus respectivos intervalos de confianza mostrando una proximidad similar a este valor. Teniendo en cuenta esto, actuamos con prudencia al optar por eliminar estas características, postulando que su contribución marginal a la informatividad puede ser limitada.

Aunque nuestro modelo cuenta con un valor R-cuadrado notablemente alto, lo que sugiere que una proporción sustancial de la varianza se explica por las características incluidas, un examen concomitante de la estadística de Durbin-Watson (DW) revela un valor bajo. Esto requiere un enfoque cauto, ya que la baja estadística DW indica la posibilidad de correlación residual, lo que podría comprometer la validez de nuestro modelo. Por consiguiente, abogamos por un análisis meticuloso de los residuos, centrándonos específicamente en su estacionariedad. Esta capa adicional de escrutinio es imperativa para garantizar la solidez y confiabilidad de nuestro modelo al capturar los patrones subyacentes en los datos.

Vamos a crear una estructura de datos para almacenar las características que creemos que pueden ser importantes.

predictors = ["High","Low","Close","MA 20","AO"]

Registremos los residuos.

residuals = csv.loc[test[0]:test[-1],"Target"] - lm.predict(csv.loc[test[0]:test[-1],predictors])

Procedamos a la generación de gráficos residuales, un paso crítico en el proceso de evaluación del modelo. 

plt.plot(residuals)

Residuos de datos de mercado

Fig. 10: Trazado de los residuos a partir de la previsión de datos de mercado.

El análisis de los residuos es un paso crucial para comprender qué tan bien nuestro modelo se ajusta a los datos. Los residuos pueden revelar si nuestro modelo tiene algún error sistemático o si está cometiendo errores consistentes. Esto es importante porque nos ayuda a comprobar si nuestro modelo sigue ciertas reglas básicas, como tener una variación consistente en sus predicciones y no depender de errores anteriores.

Una cosa que debemos tener en cuenta al analizar los residuos es algo llamado "raíz unitaria". Esto básicamente significa que los residuos muestran un patrón de cambio a lo largo del tiempo que no desaparece. Es como tener una tendencia persistente en nuestros errores. Encontrar una raíz unitaria en los residuos es una cuestión importante porque altera algunas de las suposiciones básicas que hacemos sobre nuestro modelo, como que cada predicción es independiente de las demás.

Tratar con raíces unitarias es importante porque si no lo hacemos, podemos alterar nuestras estimaciones de cuán bueno es nuestro modelo y hacer más difícil confiar en las conclusiones que extraemos de él.

Entonces, cuando analizamos los residuos, no solo marcamos casillas. Nos estamos asegurando de que nuestro modelo resista el escrutinio y solucionando cualquier problema, como las raíces unitarias, para que nuestras predicciones sean más confiables.

Tal vez sea útil pensarlo de esta manera: imaginemos la tarea de clasificar un gato, una tarea aparentemente sencilla. En un escenario ideal, donde nuestra comprensión de un gato simboliza una verdad inmutable, cada clasificación sería impecable, si graficamos el error de dicho modelo ideal obtendríamos una línea recta y estacionaria, representada por y = una constante. Esta línea simboliza la constancia de la verdad de que un gato es siempre un gato en todo momento y si lo llamáramos gato en cualquier momento nuestro error sería 0.

Sin embargo, si la competencia del clasificador disminuye, se introducen clasificaciones erróneas que hacen que los residuos se desvíen de este estado de estacionariedad perfecta. Esta desviación corresponde a la divergencia del clasificador con respecto a la verdad y se manifiesta como fluctuaciones en los residuos. La no estacionariedad surge en los días en que un perro podría ser etiquetado erróneamente como gato o viceversa, lo que refleja la comprensión incierta del clasificador sobre las características definitorias de un gato.

Para profundizar en el rigor estadístico, considere el vínculo entre una raíz unitaria en los residuos y la no estacionariedad en el análisis de series de tiempo. La presencia de una raíz unitaria en los residuos, análoga a la falta de conocimiento de un clasificador, significa no estacionariedad, lo que lleva a una mayor volatilidad en los residuos. Cuanto más profunda es la falta de conocimiento, más pronunciadas se vuelven las fluctuaciones, algo similar a clasificar erróneamente a los perros como gatos y viceversa.

Comprender estos conceptos es importante porque nos ayuda a ajustar nuestro modelo para hacer mejores predicciones. Entonces, incluso si las cosas parecen bien a primera vista, vale la pena volver a verificar para asegurarnos de que nuestros residuos se estén comportando como deberían. Recuerde que no existe una prueba única para esto, por lo que es mejor utilizar múltiples métodos y observar las tendencias generales en nuestros residuos.


Prueba de Phillip-Perron

La prueba de Phillips-Perron es una prueba estadística utilizada en econometría y análisis de series de tiempo para evaluar la presencia de una raíz unitaria en un conjunto de datos de series de tiempo. Una raíz unitaria implica que una variable de serie temporal tiene una tendencia estocástica, lo que la hace no estacionaria. La estacionariedad es un supuesto crucial en muchos análisis estadísticos, y los datos de series de tiempo no estacionarias pueden conducir a resultados de regresión espurios.

La prueba de Phillips-Perron es una variación de la prueba de Dickey-Fuller y fue propuesta por Peter C.B. Phillips y Pierre Perron en 1988. Al igual que la prueba de Dickey-Fuller, la prueba de Phillips-Perron está diseñada para detectar la presencia de una raíz unitaria examinando el comportamiento de una variable de series de tiempo a lo largo del tiempo.

La idea básica de la prueba de Phillips-Perron es hacer una regresión de la variable de la serie temporal diferenciada sobre sus valores retardados. A continuación, se utiliza la estadística de prueba para evaluar si el coeficiente de la variable retardada es significativamente distinto de cero. Si el coeficiente es significativamente distinto de cero, sugiere pruebas contra la presencia de una raíz unitaria e implica que la serie temporal es estacionaria.

Una característica notable de la prueba Phillips-Perron es que permite ciertas formas de correlación serial y heteroscedasticidad en los datos, que la prueba Dickey-Fuller original no tiene en cuenta. Esto hace que la prueba Phillips-Perron sea robusta frente a ciertas violaciones de los supuestos que podrían estar presentes en los datos de series temporales del mundo real.

  1. Hipótesis nula: La hipótesis nula de la prueba Phillips-Perron es que la variable de la serie temporal contiene una raíz unitaria, lo que implica que no es estacionaria.
  2. Hipótesis alternativa: La hipótesis alternativa es que la variable de la serie temporal no contiene una raíz unitaria, lo que indica que es estacionaria.
  3. Regla de decisión: Si el estadístico de prueba calculado es menor que el valor crítico, rechace la hipótesis nula de que existe una raíz unitaria, lo que sugiere pruebas de que la serie temporal es estacionaria. Si la estadística de prueba calculada es mayor que el valor crítico, no rechace la hipótesis nula, lo que indica que no hay pruebas suficientes para concluir que la serie temporal es estacionaria.

Utilizaremos la librería 'arch' para realizar la prueba de Phillip-Perron.

pp = PhillipsPerron(residuals)

Podemos obtener resultados resumidos. Resumen estadístico de la prueba de Phillips-Perron:

pp.summary()

Phillips-Perron Test
(Z-tau)

Test Statistic -73.916

P-value 0.000

Lags 62

Trend: Constant
Critical Values: -3.43 (1%), -2.86 (5%), -2.57 (10%)

Null Hypothesis: The process contains a unit root.
Hipótesis alternativa: El proceso es débilmente estacionario.

Interpretemos juntos los resultados. La estadística de prueba es -73.916. Este valor representa a cuántas desviaciones estándar se encuentra el coeficiente estimado del valor hipotético de 1 (lo que indica la presencia de una raíz unitaria). En este caso, una estadística de prueba negativa muy grande sugiere una fuerte evidencia contra la presencia de una raíz unitaria, lo que respalda la estacionariedad de la serie temporal.

El valor p asociado a la estadística de la prueba es 0.000. El valor p es una medida de la evidencia contra la hipótesis nula. Un valor p de 0.000 significa que la estadística de prueba observada es extremadamente improbable bajo el supuesto de que la hipótesis nula sea verdadera. En términos prácticos, este valor p extremadamente pequeño proporciona una fuerte evidencia contra la presencia de una raíz unitaria.

Dado el valor p muy bajo (0.000), normalmente se rechazaría la hipótesis nula en niveles de significancia convencionales (por ejemplo, 0.05). La evidencia sugiere que la serie temporal probablemente sea estacionaria, ya que el valor p está por debajo del nivel de significancia elegido. En resumen, con base en los resultados proporcionados, usted tiene evidencia sólida para rechazar la hipótesis nula de una raíz unitaria, lo que indica que es probable que la serie temporal sea estacionaria. Sin embargo, no podemos basar nuestras decisiones en pruebas, queremos observar medidas de tendencia central de diferentes pruebas.

Prueba de Dickey-Fuller aumentada.

Utilizaremos la librería de arcos para realizar la prueba Dickey-Fuller aumentada.

adf = ADF(residuals)

Podemos obtener estadísticas resumidas. Resumen de estadísticas de la prueba de Dickey Fuller aumentada:

adf.summary()
Augmented Dickey-Fuller Results

Test Statistic -31.300
P-value 0.000
Lags 60

Trend: Constant
Critical Values: -3.43 (1%), -2.86 (5%), -2.57 (10%)

Null Hypothesis: The process contains a unit root.
Hipótesis alternativa: El proceso es débilmente estacionario.


Ahora interpretemos los resultados. La estadística de prueba es -31.300. Al igual que la prueba de Phillips-Perron, la estadística de prueba ADF se utiliza para evaluar la presencia de una raíz unitaria en la serie de tiempo. En este caso, el valor negativo muy grande indica una fuerte evidencia contra la hipótesis nula de una raíz unitaria, lo que respalda la idea de que la serie temporal es estacionaria.

El valor p asociado es 0.000. De manera similar a la prueba de Phillips-Perron, un valor p de 0.000 significa que la estadística de prueba observada es extremadamente improbable bajo el supuesto de que la hipótesis nula (presencia de una raíz unitaria) sea verdadera. El valor p muy pequeño proporciona una fuerte evidencia contra la hipótesis nula.

Dado el bajo valor p (0.000), normalmente se rechazaría la hipótesis nula en niveles de significancia convencionales. La evidencia de la prueba Dickey-Fuller aumentada respalda la conclusión de que la serie temporal probablemente sea estacionaria.

Tanto la prueba de Phillips-Perron como la prueba de Dickey-Fuller aumentada han proporcionado evidencia sólida contra la presencia de una raíz unitaria, lo que indica que es probable que la serie temporal sea estacionaria. Se espera la similitud en los resultados entre estas dos pruebas, ya que ambas están diseñadas para evaluar la estacionariedad en datos de series de tiempo. La elección entre ellos a menudo depende de las características específicas de los datos y de los supuestos de las pruebas. En su caso, ambas pruebas sugieren que la serie temporal es estacionaria.


Exportemos nuestro modelo al formato Open Neural Network Exchange

ONNX, o Open Neural Network Exchange, es un formato abierto e interoperable para representar modelos de aprendizaje automático. Desarrollado por una comunidad colaborativa, ONNX permite el intercambio fluido de modelos entre varios marcos y herramientas, fomentando la interoperabilidad en el ecosistema de aprendizaje automático. Proporciona una forma estandarizada de representar y transferir modelos entrenados, lo que facilita a los desarrolladores implementar modelos en diferentes plataformas e integrarlos en diversas aplicaciones. ONNX admite una amplia gama de modelos y marcos de aprendizaje automático, lo que promueve la flexibilidad y la eficiencia en los flujos de trabajo de desarrollo e implementación de modelos.

Este código define un tipo inicial para una variable denominada 'double_input' que se utilizará en el contexto de la generación de un archivo ONNX (Open Neural Network Exchange). El tipo especificado es 'DoubleTensorType', lo que indica que se espera que los datos de entrada tengan doble precisión. La forma del tensor de entrada está determinada por la cantidad de columnas en un DataFrame (presumiblemente llamado 'csv') correspondiente a las características utilizadas para la predicción (recuperadas usando 'csv.loc[:, predictors].shape[1]'). 'None' en la forma indica que el tamaño de la primera dimensión (probablemente representando el número de instancias o muestras) no está fijo en esta etapa.

initial_type_double = [('double_input', DoubleTensorType([None, csv.loc[:,predictors].shape[1]]))]

Este código utiliza la función 'convert_sklearn' para convertir nuestro modelo de regresión lineal entrenado ('lm') en su representación ONNX. La variable 'initial_type_double', definida en el fragmento de código anterior, especifica el tipo esperado para los datos de entrada como precisión doble. Además, el parámetro 'target_opset' se establece en 12, lo que indica la versión deseada del conjunto de operadores ONNX. El 'onnx_model_double' resultante será una representación del modelo ONNX del modelo de regresión lineal proporcionado, adecuado para la implementación y la interoperabilidad con otros marcos que admiten el formato ONNX.

onnx_model_double = convert_sklearn(lm, initial_types=initial_type_double, target_opset=12)

Este código especifica el nombre de archivo ("EURUSD_ONNX") para guardar la representación del modelo ONNX de un modelo de regresión lineal. El modelo ONNX resultante, convertido mediante la función 'convert_sklearn' mencionada anteriormente, se almacenará con este nombre de archivo, lo que lo hará fácilmente identificable y accesible para uso o implementación futuros.

onnx_model_filename = "EURUSD_ONNX"

Este código combina el nombre de archivo definido previamente ("EURUSD_ONNX") con el sufijo "_Double.onnx" para crear un nuevo nombre de archivo ("EURUSD_ONNX_Double.onnx"). Posteriormente, se emplea la función 'onnx.save_model' para guardar el modelo ONNX ('onnx_model_double') en un archivo con el nombre de archivo construido. Este proceso garantiza que el modelo ONNX, que representa el modelo de regresión lineal con doble precisión, se almacene y se pueda referenciar fácilmente mediante el nombre de archivo especificado.

onnx_filename=onnx_model_filename+"_Double.onnx"
onnx.save_model(onnx_model_double, onnx_filename)


Construcción de un asesor experto para nuestro archivo ONNX en lenguaje MetaQuotes 5 (MQL5) utilizando el MetaEditor integrado y versátil.

El desarrollo de un Asesor Experto (EA) para nuestro archivo ONNX en MetaQuotes Language 5 (MQL5) implica aprovechar las capacidades del MetaEditor integrado y versátil. Un asesor experto es un script escrito en MQL5 que permite el trading automatizado dentro de la plataforma MetaTrader 5. En este contexto, el EA interactuará con el archivo ONNX, facilitando la integración perfecta de los modelos de aprendizaje automático en las estrategias comerciales, mejorando así los procesos de toma de decisiones basados en análisis predictivos. MetaEditor proporciona un entorno integral para codificar, probar y optimizar EA, garantizando una implementación y ejecución eficientes dentro del marco MetaTrader 5.

Primero incluimos la biblioteca Trade en MQL5, que es una biblioteca estándar para manejar operaciones comerciales dentro de MetaTrader 5 (MT5). La biblioteca de comercio proporciona funciones y estructuras predefinidas que facilitan la ejecución de diversas actividades comerciales, como abrir y cerrar posiciones, gestionar órdenes y manejar eventos relacionados con el comercio. La inclusión de esta biblioteca en un Asesor Experto (EA) permite una implementación optimizada y eficiente de la lógica y las operaciones comerciales dentro del código MQL5.

//Our trade class helps us open trades
#include <Trade\Trade.mqh>
CTrade Trade;

Este fragmento de código en MQL5 implica el uso de un Asesor Experto (EA) e incorpora un modelo ONNX para análisis predictivo. La directiva #resource se emplea para integrar el archivo del modelo ONNX, "EURUSD_ONNX_Double.onnx", en los recursos del EA como una matriz de bytes denominada ONNXModel. Esto facilita el acceso y la utilización del modelo de aprendizaje automático dentro del EA.

La variable ONNXHandle se inicializa como INVALID_HANDLE, lo que indica que se utilizará para almacenar el identificador asociado con el modelo ONNX una vez que se cargue durante la ejecución del EA.

Además, PredictedMove se inicializa en -1, lo que sugiere que el movimiento o resultado previsto según el modelo ONNX aún no está determinado. Es probable que esta variable se actualice con el valor previsto una vez que el EA procese datos relevantes a través del modelo ONNX durante su ejecución. Los detalles de la lógica predictiva y el procesamiento posterior dependerán de las secciones posteriores del código EA.

//Loading our ONNX model
#resource "ONNX\\EURUSD_ONNX_Double.onnx" as uchar EURUSD_ONNX_MODEL[]
long     ONNXHandle=INVALID_HANDLE;

En esta sección del código MQL5 para un Asesor Experto, se declaran dos conjuntos de variables: ma_handle y ma_reading[] para una media móvil, y ao_handle y ao_reading[] para un Awesome Oscillator.

La variable ma_handle sirve como referencia o identificador para el indicador de promedio móvil, lo que permite al EA interactuar con esta herramienta de análisis técnico específica y recuperar información sobre ella. La matriz ma_reading[] está destinada a almacenar los valores calculados del promedio móvil, lo que permite al EA acceder y analizar sus valores históricos para la toma de decisiones.

De manera similar, se espera que la variable ao_handle represente un identificador para el indicador Awesome Oscillator, mientras que la matriz ao_reading[] está designada para almacenar los valores calculados correspondientes. 

//Handles for our technical indicators and dynamic arrays to store their readings
int ma_handle;
double ma_reading[];
int ao_handle;
double ao_reading[];

//Inputs
int input sl_width = 150; //How wide should the stoploss be?
int input  positions = 1; //How many positions should the we open?
int input lot_multiple = 1; //How many times greater than minimum lot should each position be?

//Symbol variables
double min_volume;
double bid,ask;

//We'll use this time stamp to keep track of the number of candles passing
static datetime time_stamp;

//Our model's forecast will be stored here
vector model_forecast(1);


Función OnInit

La función OnInit() es una parte crucial de un Asesor Experto (EA) en MQL5 y sirve como función de inicialización que se ejecuta automáticamente cuando el EA se adjunta a un gráfico en MetaTrader 5. En esta función se suelen realizar diversas tareas relacionadas con la configuración y preparación del EA. El resto del código que examinaremos está anidado dentro de nuestro controlador OnInit().

Esta declaración condicional dentro de la función OnInit() del Asesor Experto verifica si el símbolo comercial es "EURUSD" y si el marco de tiempo del gráfico está establecido en M1 (intervalos de un minuto). Si no se cumplen las condiciones, el EA imprime un mensaje en la consola mediante la función Print(), indicando que el modelo debe operar específicamente con el par de divisas "EURUSD" en el marco temporal de un minuto. Posteriormente, se emplea la instrucción return(INIT_FAILED) para finalizar el proceso de inicialización del EA, indicando una falla en la inicialización si no se cumplen las condiciones especificadas.

int OnInit(){
    //Validating trading conditions
   if(_Symbol!="EURUSD" || _Period!=PERIOD_M1)
     {
      Print("Model must work with EURUSD on the M1 timeframe");
      return(INIT_FAILED);
     }

Este segmento de código dentro del Asesor Experto (EA) en MQL5 es responsable de crear un modelo ONNX a partir de un búfer estático. Para este propósito se utiliza la función OnnxCreateFromBuffer, que toma como parámetros los datos del modelo ONNX almacenados en el búfer ONNXModel y utiliza la configuración predeterminada para la creación del modelo.

Al ejecutarse, a la variable ONNXHandle se le asigna el identificador asociado con el modelo ONNX creado. Subsequently, a conditional statement checks whether the ONNXHandle is a valid handle (not equal to INVALID_HANDLE). Si el identificador no es válido, el EA imprime un mensaje de error en la consola utilizando la función Print(), proporcionando información sobre el error encontrado (a través de GetLastError()) y luego señala una falla de inicialización devolviendo INIT_FAILED.

Esta sección de código es crucial para inicializar el EA con un modelo ONNX funcional, lo que garantiza que el modelo se cree correctamente a partir del búfer proporcionado. Cualquier fallo en este proceso se comunica oportunamente.

ONNXHandle=OnnxCreateFromBuffer(ONNXModel,ONNX_DEFAULT);

if(ONNXHandle==INVALID_HANDLE)
{
   Print("OnnxCreateFromBuffer error ",GetLastError());
   return(INIT_FAILED);
}

La línea de código declara una matriz constante llamada input_shape con dos elementos. Esta matriz se denota como long y contiene números enteros. Los elementos de la matriz representan la forma de los datos de entrada para un modelo o algoritmo de aprendizaje automático. En este caso específico, la matriz input_shape se inicializa con los valores {1, 5}, lo que indica que se espera que los datos de entrada tengan dimensiones de una fila y cinco columnas.

//Defining the model's input shape
const long input_shape[] = {1,5};

Este bloque de código, dentro del Asesor Experto en MQL5, verifica si la configuración de la forma de entrada para el modelo ONNX es exitosa utilizando la función OnnxSetInputShape. La forma de entrada se especifica mediante la matriz input_shape, que denota las dimensiones esperadas de los datos de entrada.

La declaración if evalúa si se cumple la negación de la condición OnnxSetInputShape (devolviendo verdadero si la configuración no es exitosa). Si la condición es verdadera, el EA imprime un mensaje de error en la consola utilizando la función Print(), transmitiendo detalles sobre el error encontrado obtenido a través de GetLastError(). Posteriormente, la función devuelve INIT_FAILED, indicando un error de inicialización.

if(!OnnxSetInputShape(ONNXHandle,ONNX_DEFAULT,input_shape)) 
{
    Print("OnnxSetInputShape error ",GetLastError());
    return(INIT_FAILED); 
}

Esta línea de código declara una matriz constante llamada output_shape con dos elementos. Similar a la declaración anterior para input_shape, esta matriz es de tipo long y contiene números enteros. En este caso, los valores {1, 1} se asignan a output_shape, lo que indica que la forma esperada de los datos de salida del modelo de aprendizaje automático es una matriz unidimensional con un solo elemento.

La especificación de la forma de salida es crucial para manejar e interpretar adecuadamente los resultados generados por el modelo de aprendizaje automático. 

//Defining the model's output shape
const long output_shape[] = {1,1};

Este bloque de código, dentro del Asesor Experto en MQL5, verifica si la configuración de la forma de salida para el modelo ONNX es exitosa utilizando la función OnnxSetOutputShape. La forma de salida se especifica mediante la matriz output_shape, que denota las dimensiones esperadas de los datos de salida.

La declaración if evalúa si se cumple la negación de la condición OnnxSetOutputShape (devolviendo verdadero si la configuración no es exitosa). Si la condición es verdadera, el EA imprime un mensaje de error en la consola utilizando la función Print(), transmitiendo detalles sobre el error encontrado obtenido a través de GetLastError(). Posteriormente, la función devuelve INIT_FAILED, indicando un error de inicialización.

De manera similar a configurar la forma de entrada, configurar la forma de salida es esencial para alinear el formato esperado de la salida del modelo de aprendizaje automático con los requisitos de procesamiento posteriores en el EA. 

if(!OnnxSetOutputShape(ONNXHandle,0,output_shape))
{
    Print("OnnxSetOutputShape error ",GetLastError());
    return(INIT_FAILED);
}

En esta parte de la función OnInit() del Asesor Experto en MQL5, se inicializan dos indicadores técnicos.

  1. ma_handle se asigna el handle o identificador de una Media Móvil Exponencial (EMA) de 20 periodos calculada a partir de los precios de cierre (PRICE_CLOSE) del gráfico de un minuto (PERIOD_M1) para el símbolo especificado por _Symbol.
  2. ao_handle se asigna el handle del indicador Awesome Oscillator (AO) calculado en el gráfico de un minuto para el mismo símbolo.
  3. min_volume es el tamaño de contrato más pequeño permitido por el broker.
//Setting up our technical indicators
ma_handle = iMA(_Symbol,PERIOD_M1,20,0,MODE_EMA,PRICE_CLOSE);
ao_handle = iAO(_Symbol,PERIOD_M1);
min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
return(INIT_SUCCEEDED);
}


Función OnDeinit

La función OnDeinit en este Asesor Experto MQL5 sirve como controlador para el proceso de desinicialización del EA. Se ejecuta automáticamente cuando se elimina el EA o cuando se cierra la plataforma de negociación.

Dentro de esta función, una declaración condicional verifica si la variable ONNXHandle contiene un identificador válido (no igual a INVALID_HANDLE). Si la condición es verdadera, significa que el modelo ONNX se ha inicializado durante la vida útil del EA. En tales casos, se llama a la función OnnxRelease para liberar recursos asociados con el modelo ONNX y, posteriormente, ONNXHandle se establece en INVALID_HANDLE.

Esta rutina de desinicialización garantiza la liberación adecuada de recursos, evitando fugas de memoria y contribuyendo a la eficiencia general y la limpieza del ciclo de vida del EA. Refleja una práctica de codificación responsable para administrar y liberar recursos adquiridos durante la ejecución del EA, mejorando la robustez y confiabilidad del sistema comercial.

void OnDeinit(const int reason)
  {
      //OnDeinit we should freeup resources we don't require
      //We'll begin by releasing the ONNX models then removing the Expert Advisor
      if(ONNXHandle!=INVALID_HANDLE)
     {
      OnnxRelease(ONNXHandle);
      ONNXHandle=INVALID_HANDLE;
     }
   //Lastly remove the Expert Advisor
   ExpertRemove();
  }


Función OnTick

La función OnTick es un componente vital de este Asesor Experto MQL5, que representa el código ejecutado en cada tick entrante. En esta función:

Los datos históricos de los indicadores Media Móvil (MA) y Awesome Oscillator (AO) se obtienen mediante la función CopyBuffer. Los 10 valores más recientes se copian en matrices (ma_reading y ao_reading) y se aplica ArraySetAsSeries para organizar las matrices de manera similar a una serie.

La marca de tiempo actual (current_time) se obtiene utilizando la función iTime para el gráfico de un minuto (PERIOD_M1) y el símbolo especificado (_Symbol).

Una declaración condicional verifica si la marca de tiempo actual difiere de la marca de tiempo almacenada (time_stamp). Si hay una diferencia que indica una nueva marca, se llama a la función ModelForecast.

Esta estructura de código permite al EA capturar y organizar valores de indicadores recientes en cada tick, facilitando la evaluación periódica de los pronósticos del modelo de aprendizaje automático a través de la función ModelForecast. Por lo tanto, la función OnTick establece las bases para la toma de decisiones en tiempo real basada en las últimas condiciones del mercado y las predicciones del modelo, contribuyendo a la naturaleza dinámica y adaptativa del Asesor Experto.

void OnTick()
{
   //Update the arrays storing the technical indicator values to their current values
   //Then set the arrays as series so that the current reading is at the top and the oldest reading is last
   CopyBuffer(ma_handle,0,0,10,ma_reading);
   ArraySetAsSeries(ma_reading,true);
   CopyBuffer(ao_handle,0,0,10,ao_reading);
   ArraySetAsSeries(ao_reading,true);
   ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
//Update time marker
   datetime current_time = iTime(_Symbol,PERIOD_M1,0);
   //Periodically make forecasts if we have no open positions
   if(time_stamp != current_time){
      //No open positions
      if(PositionsTotal() == 0){
               ModelForecast();
      }  
      //We have open positions to manage
      else if(PositionsTotal() > 0){
               ManageTrade();
      }
   }
  }


Inferencia de modelos

La función ModelForecast dentro de este Asesor Experto MQL5 está diseñada para ejecutar el modelo de aprendizaje automático para realizar predicciones basadas en las condiciones actuales del mercado. A continuación se muestra una elaboración del código:

Se declaran dos vectores dentro de la función:

  1. model_forecast: Vector utilizado para almacenar la salida del modelo de aprendizaje automático, que representa su previsión o predicción.
  2. model_input: Un vector que contiene las características de entrada necesarias para el modelo. Estas características incluyen los precios máximo, mínimo y de cierre de la vela actual, el valor de la media móvil (ma_reading[0]) y el valor del Awesome Oscillator (ao_reading[0]).

Se llama a la función OnnxRun para realizar la inferencia utilizando el modelo ONNX (ONNXHandle). La bandera ONNX_NO_CONVERSION indica que no se aplica ninguna conversión de tipo de datos durante el proceso de inferencia. Se proporcionan las características de entrada (model_input) y el pronóstico resultante se almacena en el vector model_forecast.

Una declaración condicional verifica si el proceso de inferencia fue exitoso. En caso contrario, se imprime un mensaje de error en la consola mediante la función Print(), que transmite detalles sobre el error encontrado obtenido a través de GetLastError().

Si la inferencia es exitosa, el pronóstico almacenado en el vector model_forecast se imprime en la consola.

Esta función encapsula los pasos esenciales para obtener predicciones del modelo basadas en las condiciones actuales del mercado, fomentando una estrategia comercial dinámica y adaptativa dentro del Asesor Experto. La inclusión de mecanismos de manejo de errores mejora la robustez del sistema al proporcionar información sobre posibles problemas durante el proceso de inferencia. Una vez completada la función llama a NextMove().

//This function provides these utilities:
//          1) Inferencing using our ONNX model 
//          2) Calling the next function responsible for intepreting model forecasts and other subroutines
void ModelForecast(void){
      //These are the inputs for our ONNX model:
      //          1)High 
      //          2)Low
      //          3)Close
      //          4)20 PERIOD MA MODE: EMA  APPLIED_PRICE:PRICE CLOSE 
      //          5)Awesome Oscilator
      vector model_input{iHigh(_Symbol,PERIOD_M1,0),iLow(_Symbol,PERIOD_M1,0),iClose(_Symbol,PERIOD_M1,0),ma_reading[0],ao_reading[0]};
      //Inferencing with our model
      if(!OnnxRun(ONNXHandle,ONNX_NO_CONVERSION,model_input,model_forecast)){
            Print("Error performing inference: ",GetLastError());
      }
      //Pring model forecast to the terminal
      else{
               Print(model_forecast);
                NextMove();
      }
}


Interpretación del modelo

Para interpretar nuestro modelo hay que realizar dos trabajos:

  1. Obtener la interpretación de la salida del modelo: la función utiliza la función InterpretForecast para interpretar la salida de un modelo predictivo. La función InterpretForecast se llama dos veces, primero con un argumento de 1 y luego con un argumento de -1. El propósito de estas llamadas es verificar si la salida del modelo indica una dirección específica: 1 para un pronóstico positivo y -1 para un pronóstico negativo.
  2. Actuar según las interpretaciones: Dependiendo de la interpretación de la salida del modelo, la función realiza acciones específicas. Si el modelo predice un resultado positivo (1), llama a la función CheckOrder con un argumento de 1 y luego regresa. Si el modelo predice un resultado negativo (-1), llama a la función CheckOrder con un argumento de -1.

En resumen, la función NextMove está diseñada para procesar la salida de un modelo predictivo, interpretarlo en función de valores específicos (1 o -1) y tomar las acciones correspondientes llamando a la función CheckOrder con los valores interpretados.

//This function provides these utilities:
//          1) Getting the model output intepreted
//          2) Acting on the intepretations
void NextMove(){
      if(InterpretForecast(1)){
            CheckOrder(1);
            return;
      }     
      else if(InterpretForecast(-1)){
            CheckOrder(-1);
      }
}


La función InterpretForecast sirve para interpretar la salida de un modelo predictivo en función de una dirección específica. La función toma un argumento dirección que puede ser 1 o -1. La interpretación depende de si el modelo pronosticó una lectura mayor que 1 o menor que 1.

A continuación se muestra un desglose del código:

Si la dirección es igual a 1, la función verifica si el primer elemento (model_forecast[0]) de la matriz model_forecast es mayor que 1. Si es así, la función devuelve verdadero, lo que indica que el modelo prevé un crecimiento mayor que el precio actual.

Si la dirección es igual a -1, la función verifica si el primer elemento (model_forecast[0]) de la matriz model_forecast es menor que 1. Si es así, la función devuelve verdadero, lo que indica que el modelo pronostica un crecimiento menor que el precio actual.

Si la dirección no es 1 ni -1, la función devuelve falso. Este sirve como un caso predeterminado, que indica que no se reconoce la dirección especificada y la función no puede proporcionar una interpretación significativa.

En resumen, la función InterpretForecast verifica el valor pronosticado del modelo según la dirección especificada y devuelve verdadero si se cumple la condición; de lo contrario, devuelve falso.

//This function provides these utilities:
//          1) Check whether the model forecasted a reading greater than or less than 1.
bool InterpretForecast(int direction){
      //1 means check if the model is forecasting growth greater than the current price
      if(direction == 1){
            return(model_forecast[0] > 1);
      }
      //-1 means check if the model is forecasting growth less than the current price
      if(direction == -1){
            return(model_forecast[0] < 1);
      }
      //Otherwise return false.
      return false;
}


Ejecución de órdenes

El siguiente código define la función CheckOrder. Esta función tiene la responsabilidad de iniciar posiciones comerciales basadas en una dirección de orden específica.

Verificación de posiciones abiertas: La declaración condicional inicial (PositionsTotal() == 0) sirve como verificación previa, garantizando que las nuevas posiciones se abran exclusivamente cuando no haya operaciones activas en la cartera.

Ejecución de órdenes de compra: En el caso de que el parámetro order_direction sea igual a 1 (indicando una orden de compra), la función emplea un bucle for para ejecutar iterativamente el número deseado de posiciones (posiciones). Dentro de este bucle, se invoca la función Trade.PositionOpen para inicializar posiciones de compra. Los parámetros relevantes como el símbolo (_Symbol), el tipo de orden (ORDER_TYPE_BUY), el volumen (min_volume * lot_multiple) y el precio de ejecución (ask) se proporcionan como argumentos para esta función.

Ejecución de órdenes de venta: por el contrario, si el parámetro order_direction es igual a -1 (lo que indica una orden de venta) y no hay posiciones activas actualmente (se vuelve a evaluar PositionsTotal() == 0), la función procede a abrir posiciones de venta a través de un proceso iterativo similar. La función Trade.PositionOpen se emplea nuevamente con parámetros adaptados a las posiciones de venta.

En resumen, la función CheckOrder asegura el inicio de posiciones comerciales de manera disciplinada, considerando la ausencia de posiciones existentes y adhiriéndose a la dirección de orden especificada. El código encapsula la lógica comercial dentro del contexto de las estrategias comerciales algorítmicas.

//This function is responsible for opening positions
void CheckOrder(int order_direction){
      //Only open new positions if we have no open positions
     if(PositionsTotal() == 0){
            //Buy
            if(order_direction == 1){
                  //Iterate over the desired number of positions
                  for(int i = 0; i < positions; i++){
                           Trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,min_volume * lot_multiple,ask,0,0,"Volatitlity Doctor AI");
                  }
            }
            //Sell
            else if(order_direction == -1 && PositionsTotal() == 0){
                  //Iterate over the desired number of positions
                  for(int i = 0; i < positions; i++){
                        Trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,min_volume  * lot_multiple ,bid,0,0,"Volatitlity Doctor AI");
            }
         }
   }
}


Gestión comercial

La función ManageTrade, actúa como módulo central de gestión comercial, delega la responsabilidad de ajustar los niveles de Stop Loss y Take Profit a la función CheckStop.

La función CheckStop itera sobre todas las posiciones abiertas, extrayendo información relevante como símbolo, ticket, tipo de posición, Stop Loss actual y precio de apertura de la posición. Asegura que el símbolo de la posición coincida con el símbolo que se está negociando activamente (_Symbol). Para cada posición válida, el código calcula nuevos niveles de Stop Loss y Take Profit según parámetros predefinidos, como los precios de oferta y demanda, el ancho del Stop Loss (sl_width) y el valor del punto (_Point).

La función luego distingue entre posiciones de compra y venta. Para las posiciones de compra, calcula los nuevos niveles de Stop Loss y Take Profit en función del precio de venta, ajustándolos solo si los nuevos niveles son más favorables. De manera similar, para las posiciones de venta, el cálculo se basa en el precio de oferta y se realizan ajustes si los nuevos niveles son más favorables.

El uso de NormalizeDouble garantiza que los niveles calculados se ajusten al número especificado de dígitos (_Digits). La función Trade.PositionModify se utiliza para modificar la operación existente con los niveles de Stop Loss y Take Profit actualizados, solo si es necesario.

//This function handles our trade management
void ManageTrade(){
   CheckStop();
}

//This funciton will update our S/L & T/P 
void CheckStop(){
      //First we iterate over the total number of open positions                      
      for(int i = PositionsTotal() -1; i >= 0; i--){
            //Then we fetch the name of the symbol of the open position
            string symbol = PositionGetSymbol(i);
            //Before going any furhter we need to ensure that the symbol of the position matches the symbol we're trading
                  if(_Symbol == symbol){
                           //Now we get information about the position
                           ulong ticket = PositionGetInteger(POSITION_TICKET); //Position Ticket
                           double position_price = PositionGetDouble(POSITION_PRICE_OPEN); //Position Open Price
                           long type = PositionGetInteger(POSITION_TYPE); //Position Type
                           double current_stop_loss = PositionGetDouble(POSITION_SL); //Current Stop loss value
                           //If the position is a buy
                           if(type == POSITION_TYPE_BUY){
                                  //The new stop loss value is just the ask price minus the stop we calculated above
                                  double new_stop_loss = NormalizeDouble(ask - ((sl_width * _Point) / 2) ,_Digits);
                                  //The new take profit is just the ask price plus the stop we calculated above
                                  double new_take_profit = NormalizeDouble(ask + (sl_width * _Point),_Digits);
                                  //If our current stop loss is less than our calculated stop loss 
                                  //Or if our current stop loss is 0 then we will modify the stop loss and take profit
                                 if((current_stop_loss < new_stop_loss) || (current_stop_loss == 0)){
                                       Trade.PositionModify(ticket,new_stop_loss,new_take_profit);
                                 }  
                           }
                            //If the position is a sell
                           else if(type == POSITION_TYPE_SELL){
                                  //The new stop loss value is just the ask price minus the stop we calculated above
                                  double new_stop_loss = NormalizeDouble(bid + ((sl_width * _Point)/2),_Digits);
                                  //The new take profit is just the ask price plus the stop we calculated above
                                  double new_take_profit = NormalizeDouble(bid - (sl_width * _Point),_Digits);
                                 //If our current stop loss is greater than our calculated stop loss 
                                 //Or if our current stop loss is 0 then we will modify the stop loss and take profit 
                                 if((current_stop_loss > new_stop_loss) || (current_stop_loss == 0)){
                                       Trade.PositionModify(ticket,new_stop_loss,new_take_profit);
                                 }
                           }  
                  }  
            }
}


Nuestro Asesor Experto ahora contendrá estos parámetros en su menú.


EA

Fig. 11: Nuestro Asesor Experto.


Y debería colocar automáticamente niveles de Stop Loss y Take Profit para cada operación que abra.

EA en acción

Fig. 12: Nuestro Asesor Experto en acción.


Conclusión

En conclusión, las regresiones espurias plantean un desafío importante al modelar datos de series de tiempo y a menudo conducen a resultados engañosos y poco confiables. Los investigadores y profesionales deben tener cuidado al interpretar los resultados de la regresión, en particular cuando trabajan con datos de series de tiempo no estacionarias. Para mitigar el riesgo de regresiones espurias, es fundamental emplear técnicas estadísticas apropiadas, como pruebas de raíz unitaria, análisis de cointegración y utilizar variables estacionarias. Además, la adopción de métodos avanzados de series temporales, como los modelos de corrección de errores, puede mejorar la solidez de los análisis de regresión y contribuir a interpretaciones económicas más precisas y significativas. En última instancia, una comprensión matizada de las propiedades subyacentes de los datos y una aplicación rigurosa de métodos estadísticos son esenciales para los investigadores que buscan producir resultados de regresión confiables y válidos frente a posibles relaciones espurias.

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

Procesos no estacionarios y regresión espuria Procesos no estacionarios y regresión espuria
El presente artículo pretende demostrar la aparición de regresiones espurias cuando se intenta aplicar el análisis de regresión a procesos no estacionarios utilizando la simulación de Montecarlo.
Red neuronal en la práctica: Pseudo inversa (II) Red neuronal en la práctica: Pseudo inversa (II)
Por esta razón, dado que estos artículos tienen un propósito didáctico y no están enfocados en mostrar cómo implementar una funcionalidad específica, haremos algo un poco diferente aquí. En lugar de mostrar cómo implementar la factorización para obtener la inversa de una matriz, nos centraremos en cómo factorizar la pseudo inversa. El motivo es que no tiene sentido mostrar cómo factorizar algo de forma genérica si podemos hacerlo de manera especializada. Y mejor aún, será algo que podrás entender mucho más profundamente, comprendiendo por qué las cosas son como son. Así que veamos por qué, con el tiempo, un hardware sustituye a un software.
Asesor Experto Grid-Hedge Modificado en MQL5 (Parte IV): Optimización de la estrategia de cuadrícula simple (I) Asesor Experto Grid-Hedge Modificado en MQL5 (Parte IV): Optimización de la estrategia de cuadrícula simple (I)
En esta cuarta parte, revisamos los asesores expertos (EA) Simple Hedge y Simple Grid desarrollados anteriormente. Nuestro enfoque se centra en perfeccionar Simple Grid EA a través del análisis matemático y un enfoque de fuerza bruta, apuntando al uso óptimo de la estrategia. Este artículo profundiza en la optimización matemática de la estrategia, preparando el escenario para la futura exploración de la optimización basada en codificación en entregas posteriores.
Algoritmos de optimización de la población: Algoritmo de enjambre de aves (Bird Swarm Algorithm, BSA) Algoritmos de optimización de la población: Algoritmo de enjambre de aves (Bird Swarm Algorithm, BSA)
El artículo analiza un algoritmo BSA basado en el comportamiento de las aves, que se inspira en las interacciones colectivas de bandadas de aves en la naturaleza. Las diferentes estrategias de búsqueda de individuos en el BSA, que incluyen el cambio entre el comportamiento de vuelo, la vigilancia y la búsqueda de alimento, hacen que este algoritmo sea multidimensional. El algoritmo usa los principios del comportamiento de las bandadas, la comunicación, la adaptabilidad, el liderazgo y el seguimiento de las aves para encontrar con eficacia soluciones óptimas.