English Русский 中文 Deutsch 日本語
preview
Características del Wizard MQL5 que debe conocer (Parte 64): Uso de los patrones de DeMarker y los canales de envolvente con el núcleo de ruido blanco

Características del Wizard MQL5 que debe conocer (Parte 64): Uso de los patrones de DeMarker y los canales de envolvente con el núcleo de ruido blanco

MetaTrader 5Integración |
28 0
Stephen Njuki
Stephen Njuki

Introducción

Retomamos nuestro último artículo, en el que combinamos el indicador DeMarker basado en el impulso con las bandas de soporte/resistencia Envelopes, examinando cómo se podrían aprovechar sus señales en el aprendizaje automático. En artículos recientes hemos abordado enfoques similares sobre la combinación de indicadores, y los lectores que busquen una introducción al tema pueden consultarlos. Básicamente, implementamos los indicadores MQL5 en el lenguaje Python, utilizando datos de precios importados con el módulo Python de MetaTrader 5. Este módulo le permite iniciar sesión en el servidor de su bróker y recuperar datos de precios e información de símbolos.


Indicadores en Python

Python cuenta con una gran cantidad de bibliotecas de análisis técnico que se pueden importar y utilizar fácilmente para implementar diversos indicadores. El problema actual es que no todos son estándar y a algunos les faltan algunos indicadores. Por ejemplo, para este artículo, la biblioteca de análisis técnico pandas no incluye el oscilador DeMarker, mientras que este sí está disponible en el módulo 'ta' o de análisis técnico. Por otro lado, aunque los Envelopes están presentes en la biblioteca de análisis técnico de pandas, están ausentes en la biblioteca 'ta' en su forma original. Esto se debe a que 'ta' ofrece indicadores relacionados con las Bandas de Bollinger y el Canal de Donchian.

Y lo curioso es que, hoy en día, implementar tu propio indicador "desde cero" requiere casi el mismo esfuerzo (si no menos) que instalar una de estas bibliotecas para usar estas funciones estándar. Así pues, implementamos nuestras propias funciones para DeMarker y Envelopes, lo que, en teoría, debería permitir que nuestro Python se ejecute un poco más rápido debido a la menor cantidad de referencias a módulos.


El DeMarker

El indicador DeMarker es un oscilador que registra los extremos de las condiciones de compra y venta de un activo durante un período determinado. Como ya se explicó en el artículo anterior, su rango va de 0 a 1, donde los valores superiores a 0,7 implican condiciones de sobrecompra, mientras que los valores inferiores a 0,3 significan que existen condiciones de sobreventa. Hemos decidido implementarlo como una función personalizada en Python de la siguiente manera:

def DeMarker(df, period=14):
    """
    Calculate DeMarker indicator for a DataFrame with OHLC prices
    
    Args:
        df: Pandas DataFrame with columns ['open', 'high', 'low', 'close']
        period: Lookback period (default 14)
        
    Returns:
        Pandas Series with DeMarker values
    """
    # Calculate DeMax and DeMin
    demax = df['high'].diff().clip(lower=0)
    demin = -df['low'].diff().clip(upper=0)
    
    # Smooth the values using SMA
    demax_sma = demax.rolling(window=period).mean()
    demin_sma = demin.rolling(window=period).mean()
    
    # Calculate DeMarker
    demarker = demax_sma / (demax_sma + demin_sma)
    return pd.DataFrame({'main': demarker})

Las entradas de nuestra función son un dataframe 'df' y 'period'. El dataframe, 'df', es un dataframe de pandas que contiene datos de precios de apertura, máximo, mínimo y cierre, mientras que period es el período de análisis retrospectivo para los cálculos de DeMarker. Esta función encapsula la lógica de DeMarker, a la vez que ofrece modularidad y reutilización.

El cálculo de DeMax proporciona la diferencia en precios máximos consecutivos (df['high'].diff()). En este búfer, si la diferencia es positiva, lo que implicaría máximos crecientes, permanece en el búfer; de lo contrario, el valor se establece en cero. Esta configuración a cero se realiza mediante la función de recorte (clip(lower=0)). De manera similar, para las diferencias en los precios bajos, si la diferencia es negativa (es decir, el mínimo actual es menor que el mínimo anterior), este valor se mantiene en el búfer como un valor absoluto; de lo contrario, también se establece en cero mediante recorte (clip(upper=0)).

Es importante hacer un seguimiento de estos cambios absolutos porque DeMax mide el impulso alcista de los precios registrando los aumentos en los precios altos, mientras que DeMin mide el impulso bajista de los precios registrando las disminuciones en los precios bajos. Estas dos métricas son fundamentales para DeMarker, ya que determinan la magnitud de las variaciones de precio en relación con períodos anteriores. En Python, al implementar con la función diff() es esencial asegurarse de que el dataframe de entrada tenga suficientes puntos de datos y tener en cuenta que la primera fila siempre tendrá un valor NaN.

Después de esto, suavizamos los valores en cada búfer. El suavizado reduce el ruido en cada serie y hace que los valores del indicador sean menos sensibles a las fluctuaciones de precios a corto plazo. El método rolling(window=period).mean() calcula el promedio durante el período especificado, y esto introduce un retraso que a menudo coincide con el propósito del indicador de identificar tendencias macro. Los primeros valores, hasta completar period-1, serán NaN debido a la falta de datos suficientes para la ventana deslizante, y deben ser tratados eliminándolos o rellenándolos con valores normalizados. La elección del período también afecta la sensibilidad del DeMarker, ya que un período más corto lo hace más sensible.

Luego calculamos el valor DeMarker dividiendo la media DeMax entre su valor más la media DeMin. Este paso normaliza DeMarker al rango de 0 a 1, lo que simplifica su interpretación. En efecto, esta relación mide la fuerza relativa de los movimientos alcistas de los precios con respecto al total de los movimientos alcistas y bajistas. Los valores cercanos a 1 indican un fuerte impulso alcista, mientras que los cercanos a 0 implican que el impulso bajista es más fuerte.

Al implementarlo, es fundamental asegurarse de que no haya divisiones por cero en este paso. Sin embargo, esto ocurre rara vez, ya que el precio siempre está cambiando, especialmente en los máximos y mínimos. La función devuelve los valores de DeMarker como un dataframe de pandas con una sola columna que hemos decidido etiquetar como 'main'. El formato de dataframe garantiza la compatibilidad con otras herramientas de análisis basadas en pandas y también permite una fácil integración en un sistema de negociación más amplio.


Las Envolventes

Las Envolventes de precios son un indicador de soporte/resistencia que traza dos bandas, una superior y otra inferior, alrededor de una media móvil del precio. Las bandas están desplazadas por una cantidad fija de desviación en porcentaje, lo que puede ayudar a los operadores a identificar zonas aproximadas de soporte y resistencia cuando el precio toca estas bandas. Nuestra implementación en Python es la siguiente:

def Envelopes(df, period=14, deviation=0.1):
    """
    Calculate Price Envelopes for a DataFrame with OHLC prices
    
    Args:
        df: Pandas DataFrame with columns ['open', 'high', 'low', 'close']
        period: MA period (default 20)
        deviation: Percentage deviation from MA (default 0.05 for 5%)
        
    Returns:
        DataFrame with columns ['upper', 'ma', 'lower']
    """
    # Calculate moving average (typically using close prices)
    ma = df['close'].rolling(window=period).mean()
    
    # Calculate upper and lower envelopes
    upper = ma * (1 + deviation)
    lower = ma * (1 - deviation)
    
    return pd.DataFrame({'upper': upper, 'ma': ma, 'lower': lower})

Nuestro código anterior proporciona una forma flexible de calcular envolventes, con periodos personalizables y parámetros de entrada de desviación para diferentes estrategias. El primer paso para calcular las envolventes es calcular la media móvil. Utilizamos una media móvil simple de los precios de cierre durante el período de entrada. La media móvil actúa como la línea central de las envolventes y el precio medio de la tendencia. El cálculo del buffer rolling(window=period).mean() devuelve un vector/buffer con valores medios, pero con los primeros valores del período-1 siendo un NaN como se esperaba. Estos asuntos deben ser tratados adecuadamente.

A continuación, calculamos los valores del búfer de banda superior e inferior. Los cálculos para la envolvente superior se obtienen multiplicando la MA por (1 + desviación). Por ejemplo, si nuestra desviación es del 10%, entonces la banda superior será el 110% de la media móvil. Dado que nuestro valor de desviación predeterminado es 0,1, esto equivale al 10%. Los cálculos de la envolvente inferior se obtienen multiplicando la media móvil por (1 - desviación), que en nuestro caso es del 90%.

Las bandas superior e inferior definen el rango de precios dentro del cual se espera que el precio fluctúe en condiciones "normales". Cuando los precios tocan cualquiera de las bandas, se presentan escenarios de ruptura o reversión, ya que consideramos que estas bandas son nuestros niveles de soporte y resistencia. Estas dos situaciones apuntan a la continuación de la tendencia o a oscilaciones de sobrecompra/sobreventa del mercado.

El parámetro de desviación controla el ancho de las envolventes. Un porcentaje de desviación más elevado implica rangos más amplios, que pueden ser adecuados para activos volátiles, mientras que una desviación menor crea un rango mucho más estrecho para activos estables. Esta desviación debe ser positiva para evitar bandas no válidas. Estas bandas envolventes son simétricas alrededor de la media móvil, suponiendo una volatilidad igual por encima y por debajo de la tendencia. Para bandas asimétricas, pueden usarse las Bandas de Bollinger.

El dataframe de pandas devuelto contiene 3 columnas: 'upper', 'lower' y 'ma'. Este formato de dataframe permite un fácil acceso a los tres componentes, posibilitando la visualización, la generación de señales o un análisis más profundo.


Características en Python

Lo que denominamos características son representaciones vectorizadas de las señales de los dos indicadores: DeMarker y Envelopes. Estas características sirven luego como entradas para nuestro modelo de aprendizaje automático, que es una red neuronal recurrente que utiliza un núcleo de ruido blanco; hablaremos de eso más adelante. Esta implementación de aprendizaje automático en Python se basa en las características que vimos por primera vez en el artículo anterior, que eran 10 en total. De estas 10, solo 6 superaron la prueba forward walk: 0, 1, 5, 6, 7 y 8. Esas son, por lo tanto, las características que probaremos a través de nuestra Red Neuronal Recurrente (RNN). Por lo tanto, tenemos que codificarlos manualmente en Python antes de introducirlos en la RNN.


Característica-0

Como vimos en el artículo anterior, la característica 0, también conocida como patrón 0, genera señales basadas en que el DeMarker cruce los umbrales de sobrecompra o sobreventa y el precio cruce simultáneamente las bandas superior o inferior de las Envolventes. Por lo tanto, implementamos esto en Python de la siguiente manera:

def feature_0(dem_df, env_df, price_df):
    """
    """
    # Initialize empty array with 2 dimensions and same length as input
    feature = np.zeros((len(dem_df), 2))
    
    # Dimension 1:
    feature[:, 0] = ((dem_df['main'] <= 0.3) &
                     (price_df['close'] > env_df['lower']) &
                     (price_df['close'].shift(1) <= env_df['lower'].shift(1)) &
                     (price_df['close'].shift(2) >= env_df['lower'].shift(2))).astype(int)
    feature[:, 1] = ((dem_df['main'] >= 0.7) &
                     (price_df['close'] < env_df['upper']) &
                     (price_df['close'].shift(1) >= env_df['upper'].shift(1)) &
                     (price_df['close'].shift(2) <= env_df['upper'].shift(2))).astype(int)
    
    # Set first 2 rows to 0 (no previous values to compare)
    feature[0, :] = 0
    feature[1, :] = 0
    
    return feature

Nuestra primera línea de código en la función crea una matriz NumPy rellena de ceros con forma (len(dem_df), 2) que almacena, categóricamente, señales alcistas y bajistas. Inicializa el array, asegurando que todas las filas tengan un valor predeterminado de cero. El tamaño del array debe coincidir con la longitud del data frame para evitar la desalineación de índices.

El índice de verificación alcista está en 0, y se verifica primero. Como se argumentó en el artículo anterior, según nuestra implementación anterior, se genera una señal alcista cuando DeMarker se encuentra en territorio de sobreventa (normalmente por debajo de 0,3); el precio de cierre actual está por encima del límite inferior; el precio de cierre anterior estaba en o por debajo del límite inferior; y hace dos períodos, el precio de cierre estaba en o por encima del límite inferior.

Esta condición de verificación alcista combina la condición de sobreventa de DeMarker con una ruptura del precio por encima del límite inferior, lo que implica una posible continuación o reversión de la tendencia. Las condiciones shift-1 y shift-2 aseguran que el precio haya interactuado previamente con la envolvente inferior, tal como se define en el último artículo. En la práctica, se pueden añadir algunas medidas adicionales. Estas medidas pueden incluir comprobaciones de varios períodos para reducir las falsas alarmas, exigiendo un patrón de precios específico. Además, la verificación de env_df['lower'] y price_df['close'] para NaN evitará comparaciones no válidas.

La columna bajista indica una señal generada cuando DeMarker se encuentra en territorio de sobrecompra; el precio de cierre actual está por debajo del límite superior; el precio de cierre anterior estaba en o por encima del límite superior; y hace dos períodos, el precio de cierre estaba en o por debajo del límite superior. Este patrón capta posibles reversiones o retrocesos tras una situación de sobrecompra. Al utilizarlo, conviene asegurarse de que el historial de datos sea suficiente para manejar correctamente los desplazamientos (shift).

Para ello, establecemos los valores iniciales de las dos primeras filas en cero. Esto se debe a que estamos utilizando los valores shift-1 y shift-2.

La instrucción return devuelve el array NumPy que contiene nuestros patrones de señal. Esto proporciona un formato adecuado para introducirlo en nuestra red neuronal recurrente (RNN). La interacción de la envolvente de precios de múltiples períodos (shift(1) y shift(2)) agrega una capa de confirmación temporal, un filtro adicional de hecho, lo que la hace más única en comparación con otras señales de cruce simples.


Característica-1

Este patrón genera señales cuando DeMarker se encuentra en las zonas extremas de sobrecompra o sobreventa y el precio permanece fuera de las bandas superior/inferior de los Envelopes durante períodos consecutivos. Lo implementamos en Python de la siguiente manera:

def feature_1(dem_df, env_df, price_df):
    """
    """
    # Initialize empty array with 2 dimensions and same length as input
    feature = np.zeros((len(dem_df), 2))
    
    # Dimension 1:
    feature[:, 0] = ((dem_df['main'] > 0.7) &
                     (price_df['close'] > env_df['upper']) &
                     (price_df['close'].shift(1) > env_df['upper'].shift(1))).astype(int)
    feature[:, 1] = ((dem_df['main'] < 0.3) &
                     (price_df['close'] < env_df['lower']) &
                     (price_df['close'].shift(1) < env_df['lower'].shift(1))).astype(int)
    
    # Set first row to 0 (no previous values to compare)
    feature[0, :] = 0
    
    return feature

Nuestro primer paso, al igual que con la característica 0, es dimensionar e inicializar el array con ceros. La inicialización implica que no hay señales por defecto, lo cual es mejor que tener un NaN. A continuación, procedemos a definir los valores para cada índice. Para el primer índice/columna, comprobamos si existen condiciones alcistas. Nuestro código comprueba si DeMarker está en territorio de sobrecompra (>0,7); y el precio de cierre actual está por encima del límite superior; y el precio de cierre anterior también estaba por encima del límite superior. 

Esto es importante porque identifica un fuerte impulso alcista en situaciones donde el precio se mantiene por encima del límite superior a pesar de una sobrecompra en DeMarker. Como se argumentó en el artículo anterior, esto sugiere una continuación de la tendencia en lugar de una reversión. A partir de esto, nuestro código procede a establecer el siguiente valor del índice, que comprueba si existen condiciones bajistas. El listado comprueba si DeMarker se encuentra en territorio de sobreventa (<0,3); y si el precio de cierre actual está por debajo del límite inferior; y si el precio de cierre anterior también estuvo por debajo del límite inferior.

Como reflejo esperado de la señal alcista, refleja un fuerte impulso bajista con un movimiento sostenido del precio por debajo del límite inferior. Esto supone la continuación de la tendencia a la baja, como también se argumentó en el artículo anterior. Dado que estamos utilizando comparaciones de desplazamiento, necesitamos establecer la primera fila de valores en 0. En este caso, solo estamos configurando la primera fila porque nuestro desplazamiento de comparación es solo para un índice. La instrucción return devuelve el array de señales en formato NumPy.

Esta función detecta rupturas de precios sostenidas más allá de las bandas de la envolvente, como lo confirman los valores extremos de DeMarker, una señal de una fuerte continuación de la tendencia. Principalmente, indica una oportunidad para sumarse a una tendencia existente, en lugar de anticipar cambios de tendencia. Se diferencia de la característica 0 en que se centra en períodos consecutivos fuera de las bandas sin necesidad de un patrón de cruce.


Característica-5

La característica 5 es la siguiente de las pautas que logró superar la prueba de forward walk en el artículo anterior. De forma similar a nuestras dos funciones anteriores, obtiene sus señales de los valores extremos de DeMarker, sin embargo, utiliza la dirección de las bandas de las envolventes en lugar de centrarse en la interacción del precio con las bandas. Lo implementamos en Python de la siguiente manera:

def feature_5(dem_df, env_df, price_df):
    """
    """
    # Initialize empty array with 2 dimensions and same length as input
    feature = np.zeros((len(dem_df), 2))
    
    # Dimension 1:
    feature[:, 0] = ((dem_df['main'] > 0.7) &
                     (env_df['upper'] > env_df['upper'].shift(1))).astype(int)
    feature[:, 1] = ((dem_df['main'] < 0.3) &
                     (env_df['lower'] < env_df['lower'].shift(1))).astype(int)
    
    # Set first row to 0 (no previous values to compare)
    feature[0, :] = 0
    
    return feature

Lo primero que hay que hacer, al igual que con las otras dos funciones, es inicializar el array de salida con ceros y establecer su tamaño en 2. Esto se cumple con la primera línea de código de nuestra función anterior. A continuación, procedemos a establecer el primer valor del índice que, al igual que las demás funciones, verifica la tendencia alcista. El requisito es relativamente sencillo: si DeMarker está sobrecomprado (>0,7) y el límite superior está subiendo, entonces tenemos una señal alcista. A continuación, establecemos el segundo valor del índice que comprueba si existe una tendencia bajista. Como cabría esperar, un DeMarker por debajo de 0,3 y la banda inferior de las envolventes descendentes apuntan a una señal bajista. Luego, como era de esperar, establecemos los valores de la primera fila en cero para evitar comparaciones no válidas debido al NaN, que proviene de la comparación de desplazamiento.


Característica-6

La característica 6, o patrón 6, tal como se presentó en el artículo anterior, genera señales basadas en cambios en el impulso de DeMarker y formaciones de ondas de precios en las bandas del indicador de envolventes. Implementamos esto en Python de la siguiente manera:

def feature_6(dem_df, env_df, price_df):
    """
    """
    # Initialize empty array with 2 dimensions and same length as input
    feature = np.zeros((len(dem_df), 2))
    
    # Dimension 1:
    feature[:, 0] = ((dem_df['main'] > dem_df['main'].shift(1)) &
                     (price_df['low'].shift(1) <= env_df['lower'].shift(1)) &
                     (price_df['low'].shift(2) >= env_df['lower'].shift(2)) &
                     (price_df['low'].shift(3) <= env_df['lower'].shift(3)) &
                     (price_df['low'].shift(4) >= env_df['lower'].shift(4))).astype(int)
    feature[:, 1] = ((dem_df['main'] < dem_df['main'].shift(1)) &
                     (price_df['high'].shift(1) >= env_df['upper'].shift(1)) &
                     (price_df['high'].shift(2) <= env_df['upper'].shift(2)) &
                     (price_df['high'].shift(3) >= env_df['upper'].shift(3)) &
                     (price_df['high'].shift(4) <= env_df['upper'].shift(4))).astype(int)
    
    # Set first 4 rows to 0 (no previous values to compare)
    feature[0, :] = 0
    feature[1, :] = 0
    feature[2, :] = 0
    feature[3, :] = 0
    
    return feature

El protocolo de inicialización es similar al que ya hemos visto anteriormente, con las diferencias radicando en los valores de los arreglos establecidos, como era de esperar. Para el primer índice que comprueba la tendencia alcista, asignamos 1 (equivalente a verdadero) si se cumple la condición de compra. Esta condición consiste en un aumento del DeMarker junto con un patrón de precio de cierre en relación con la envolvente inferior durante cuatro períodos alternos anteriores de por debajo y por encima. El segundo índice comprueba la tendencia bajista observando si DeMarker está disminuyendo, al igual que la confirmación alcista de un patrón de precio de cierre en forma de M en la banda superior.

A continuación, se procede a inicializar las filas a cero, lo cual se realiza para evitar comparaciones de indicadores no válidas. Se realizarían comparaciones no válidas si no se asignan ceros, ya que estos valores son NaN por defecto, y los NaN provienen del uso de comparaciones de desplazamiento. Nuestra asignación de cero abarca, por lo tanto, 4 filas, de 0 a 3, ya que utilizamos el desplazamiento para los índices del 1 al 4.


Característica-7

Esta característica, como se argumenta en el último artículo, genera señales basadas en los valores de DeMarker en diferentes desfases temporales y cruces de precios de las bandas de los Envelopes. Esto nos lleva a centrarnos en los cambios de impulso. Implementamos esto en Python de la siguiente manera:

def feature_7(dem_df, env_df, price_df):
    """
    """
    # Initialize empty array with 2 dimensions and same length as input
    feature = np.zeros((len(dem_df), 2))
    
    # Dimension 1:DEM(X()) >= 0.5 && DEM(X() + 2) <= 0.3 && Close(X()) > ENV_UP(X()) && Close(X() + 1) <= ENV_UP(X() + 1)
    feature[:, 0] = ((dem_df['main'] >= 0.5) &
                     (dem_df['main'].shift(2) <= 0.3) &
                     (price_df['close'] >= env_df['upper']) &
                     (price_df['close'].shift(1) <= env_df['upper'].shift(1))).astype(int)
    feature[:, 1] = ((dem_df['main'] <= 0.5) &
                     (dem_df['main'].shift(2) >= 0.8) &
                     (price_df['close'] <= env_df['lower']) &
                     (price_df['close'].shift(1) >= env_df['lower'].shift(1))).astype(int)
    
    # Set first row to 0 (no previous values to compare)
    feature[0, :] = 0
    
    return feature

La inicialización se establece en 2 y se rellena con ceros, y el primer índice se utiliza para comprobar si hay una señal larga. Nuestro código marca una tendencia alcista como verdadera si el DeMarker actual es neutral o alcista (>=0,5); y el DeMarker de hace dos períodos estaba sobrevendido (<=0,3); y el precio de cierre actual está en o por debajo del límite superior. Estos extensos plazos tienen como objetivo capturar el cambio de impulso desde condiciones de sobreventa a condiciones neutrales o alcistas, tal como lo confirma una ruptura por encima del límite superior. Esto suele indicar un fuerte cambio de tendencia o el inicio de una nueva tendencia. La condición shift(2) introduce un retardo, que requiere una condición de sobreventa reciente. 

La condición de verificación bajista, en el índice 1, se cumple si el DeMarker actual es neutral a bajista (<= 0,5); y el DeMarker de hace dos períodos estaba sobrecomprado (>=0,8); y el precio de cierre actual está en o por debajo del límite inferior; y finalmente el precio de cierre anterior estaba en o por encima del límite inferior. Concluimos asignando solo la primera fila a cero porque usamos un único índice de desplazamiento.


Característica-8

Nuestra última función recibe señales cuando DeMarker se encuentra en zonas extremas y el precio mínimo/máximo está significativamente más allá de las bandas de los límites, lo que significa movimientos de precios extremos. Lo implementamos en Python de la siguiente manera:

def feature_8(dem_df, env_df, price_df):
    """
    """
    # Initialize empty array with 2 dimensions and same length as input
    feature = np.zeros((len(dem_df), 2))
    
    # Dimension 1:DEM(X()) > 0.7 && Low(X()) > ENV_UP(X())
    feature[:, 0] = ((dem_df['main'] > 0.7) &
                     (price_df['low'] > env_df['upper'])).astype(int)
    feature[:, 1] = ((dem_df['main'] < 0.3) &
                     (price_df['high'] < env_df['lower'])).astype(int)
    
    # Set first row to 0 (no previous values to compare)
    # feature[0, :] = 0
    
    return feature

Tras inicializar la matriz NumPy de salida con ceros y asignarle un tamaño de 2, establecemos el valor del índice 0. Este índice, al igual que todos los demás patrones mencionados anteriormente, indica si existe una tendencia alcista. En este caso, una señal alcista se produce cuando DeMarker está sobrecomprado (>0,7) y el precio mínimo actual está por encima del límite superior. Este patrón suele indicar un fuerte movimiento alcista, ya que el precio mínimo del período supera el límite superior. Esto suele indicar un impulso alcista significativo.

La señal bajista que se asigna al índice 1 se confirma si el DeMarker está sobrevendido (<0,3) y el máximo actual está por debajo del límite inferior. Al igual que en la configuración alcista anterior, ambos escenarios son poco comunes; sin embargo, en las pruebas que realizamos en el artículo anterior, pudimos encontrar algunas oportunidades de trading. Sin embargo, en situaciones reales, dada su rareza, se puede aplicar un filtro de confirmación adicional por motivos de seguridad.


Red neuronal recurrente en Python

Nuestra red neuronal recurrente (RNN), que recibe las salidas de indicadores vectorizados anteriores de las características 0, 1, 5, 6, 7 y 8, es un módulo de red neuronal de PyTorch que combina una RNN estándar con un mecanismo de inyección de ruido para mejorar la robustez o modelar procesos estocásticos. Está diseñado específicamente para tareas de regresión y produce un único valor de salida por cada secuencia de entrada. Esta inyección de ruido se aplica a los estados ocultos de la RNN, modulada por una capa de proyección y una función de activación sigmoide para controlar su impacto.

La entrada a esta RNN es un tensor de forma (tamaño_lote, tamaño_entrada). El diseño consta de una capa RNN seguida de una proyección lineal para el ruido y, finalmente, una capa totalmente conectada para la salida. La inyección de ruido se realiza mediante un núcleo de ruido blanco aplicado a los estados ocultos de la RNN. Esto se puede hacer sobre la marcha o proporcionarlo como un tensor precalculado. La salida es un único valor de regresión para cada secuencia y su forma es (tamaño_lote, ). Esto se implementa en Python de la siguiente manera:

# Define the network as a class
class WhiteNoiseRNN(nn.Module):
    def __init__(self, input_size=5, hidden_size=64, num_layers=1):
        super().__init__()
        self.hidden_size = hidden_size
        self.rnn = nn.RNN(input_size, hidden_size, num_layers, batch_first=True)
        self.noise_proj = nn.Linear(hidden_size, hidden_size)  # Projects noise to hidden space
        self.fc = nn.Linear(hidden_size, 1)  # Single output for regression
        
    def forward(self, x, noise_std=0.05):
        """
        Args:
            x: Input tensor of shape (batch_size, seq_len, input_size)
            noise_std: Either:
                - float: Standard deviation for generated noise
                - tensor: Pre-computed noise (must match rnn_out shape: batch_size × seq_len × hidden_size)
        """
        # Ensure proper input dimensions
        if x.dim() == 2:
            x = x.unsqueeze(1)  # (batch, features) → (batch, 1, features)
            
        # RNN processing
        rnn_out, _ = self.rnn(x)  # (batch_size, seq_len, hidden_size)
        
        # Noise injection
        if isinstance(noise_std, torch.Tensor):
            noise_std = noise_std.float()
            if noise_std.dim() == 1:
                noise_std = noise_std.view(-1, 1, 1)  # (batch,) → (batch, 1, 1)
            noise = noise_std.expand_as(rnn_out)
        elif noise_std > 0:
            noise = torch.randn_like(rnn_out) * noise_std
        else:
            noise = 0
            
        if isinstance(noise, torch.Tensor):
            projected_noise = self.noise_proj(noise)
            rnn_out = rnn_out + torch.sigmoid(rnn_out) * projected_noise
            
        return self.fc(rnn_out[:, -1, :])  # (batch_size,)

Según la lista anterior, la clase WhiteNoiseRNN se define como una subclase de nn.Module y se inicializa con tres parámetros de entrada: tamaño de la capa de entrada, tamaño de la capa oculta y número de capas. Este paso establece la arquitectura de la red y los hiperparámetros.

Al heredar de nn.Module, las funciones de autograd y gestión de modelos de PyTorch, como las de entrenamiento y evaluación, se pueden utilizar fácilmente. La elección del tamaño de entrada viene determinada por la dimensionalidad de las características de los datos de entrada. En nuestro caso, todas las características tienen 2 dimensiones, por lo que este valor será 2.

El tamaño oculto determina la capacidad del modelo. Valores más grandes aumentan la expresividad del modelo, pero conllevan el riesgo de sobreajuste y de un mayor coste computacional. Para la mayoría de las tareas o problemas sencillos, basta con establecer el número de capas en 1; sin embargo, aumentar este valor puede provocar problemas de desaparición del gradiente.

Invocamos la inicialización super() para llamar a la clase padre, nn.Module, y garantizar la configuración adecuada de la funcionalidad del módulo de PyTorch, como el seguimiento de parámetros y la gestión de dispositivos. Incluir esta llamada como regla tiende a evitar muchos errores de inicialización. El parámetro hidden-size se almacena como una variable de instancia para su posible uso en otros métodos.

Inicializamos la variable de clase rnn como una capa RNN con un tamaño de entrada, un tamaño oculto y un número de capas especificados. Asignar el parámetro batch-first a true garantiza que los tensores de entrada y salida tengan formas que tomen el tamaño del lote procesado como la primera dimensión dentro de la forma. A continuación, definimos una capa de proyección de ruido para proyectar el ruido en la misma dimensionalidad que los estados ocultos de la RNN. Esto permite transformar el ruido, ya sea mediante escalado o rotación, etc., antes de agregarlo a los estados ocultos, de manera que la inyección de ruido sea aprendible. Esto mejora la capacidad del modelo para adaptarse al impacto del ruido durante el entrenamiento. La capa lineal añade parámetros que aumentan la complejidad del modelo, por lo que es necesario un entrenamiento suficiente.

A continuación, definimos una capa totalmente conectada para mapear el estado oculto final de la RNN a un único valor de salida. Esto produce el resultado de la regresión, reduciendo la dimensionalidad del estado oculto a un único escalar. Esto es fundamental para la tarea del modelo, especialmente en los casos en que se trata de pronosticar un valor continuo, como ocurre en nuestra situación.

El método forward define un paso hacia adelante de la red, procesando la entrada x y aplicando ruido con una desviación estándar noise_std. Esta función implementa el cálculo central, combinando el procesamiento de RNN, la inyección de ruido y la predicción de la salida. Siempre es crucial asegurarse de que x tenga la forma adecuada y esté preprocesada antes de introducirla en la aplicación. El valor predeterminado de noise_std de 0,05 es un hiperparámetro que debe ajustarse en función del nivel de ruido deseado. Si se utiliza ruido precalculado, se debe verificar su forma y tipo para evitar errores en tiempo de ejecución.

La primera acción en la función forward es remodelar x agregando una longitud de secuencia de dimensión 1. Esto garantiza la compatibilidad con la RNN, que espera una dimensión de secuencia incluso para entradas de un solo paso de tiempo. Además, esto hace que el modelo sea flexible tanto para entradas de un solo paso como de varios pasos.

A continuación, procesamos la RNN pasando x, la entrada, a través de la RNN para producir estados ocultos para cada paso de tiempo. Los resultados de esto son dobles. Primero obtenemos rnn_out, que es un tensor que contiene los estados ocultos; en segundo lugar obtenemos _ que es el estado oculto de la capa final, que ignoramos en esta situación porque no lo estamos utilizando. Este procesamiento es vital porque la RNN captura las dependencias temporales en la secuencia de entrada, formando la columna vertebral del procesamiento secuencial del modelo. Estos estados ocultos son los mecanismos principales para la inyección de ruido y la predicción de resultados.

Después de esto, procedemos al manejo del ruido precalculado, como lo verifica la cláusula if superior. Convertimos el tensor a tipo float para mayor coherencia, remodelamos los tensores 1D para su difusión y expandimos el ruido para que coincida con la forma de rnn_out. Alternativamente, si se genera ruido, introducimos aleatoriedad en los estados ocultos, haciéndolos así más robustos al proceso estocástico de modelado. El ruido se escala mediante noise_std, controlando así su magnitud. De lo contrario, si no hay ruido y noise_std es cero o negativo, entonces también se le asigna cero al ruido. Esto permite que el modelo funcione sin ruido, lo cual puede ser útil para pronósticos deterministas o al depurar errores.

Tras asignar el valor de ruido, proyectamos dicho ruido a través de la capa lineal noise_proj para alinearlo con el espacio de estados ocultos. Esto modula el impacto del ruido utilizando la función sigmoide de la salida de la RNN y lo agrega a los estados ocultos. Como ya se ha mencionado, esta proyección lineal hace que la inyección de ruido sea aprendible, lo que permite que el modelo adapte el efecto del ruido durante el entrenamiento. El término torch.sigmoid(rnn_out) escala el ruido entre 0 y 1, asegurando que no sature los estados ocultos. Se argumenta que este enfoque aporta robustez al introducir variables estocásticas controladas que pueden prevenir el sobreajuste.

A continuación, seleccionamos el estado oculto del último paso de tiempo y lo pasamos a través de la capa totalmente conectada para producir una única salida por secuencia. El estado oculto del último paso temporal resume la información de la secuencia, lo cual resulta adecuado para tareas de regresión. La capa fc asigna el estado oculto a la salida deseada.


Pruebas de funcionamiento

Una vez definida nuestra red, realizamos pruebas sobre los seis patrones que superaron la prueba de forward walk en el artículo anterior; estos son 0, 1, 5, 6, 7 y 8. Antes del último artículo, habíamos experimentado con entradas de red más largas porque no emparejaban las señales del indicador en torno a una condición alcista o bajista.

No hemos hecho eso en este artículo, ya que todos los patrones tienen claramente un índice de verificación alcista y un índice de verificación bajista. Esto puede explicar por qué también obtuvimos un porcentaje de éxito en forward walk más alto en el último artículo que en el anterior, donde no seguimos esta regla. Estamos realizando pruebas con GBP/USD. Las simulaciones de entrenamiento se realizan en Python, en un intervalo de 4 horas, durante el año 2023. Lo que nos proporciona el entrenamiento en Python es una orientación sobre si debemos abrir posiciones largas o cortas.

Dado que el modelo ONNX importado en MetaTrader 5 se utiliza posteriormente en un Asesor Experto generado automáticamente que emplea condiciones ponderadas de compra y venta, estos valores también deben optimizarse, y esto se realiza igualmente para el año 2023.

Tras el entrenamiento y la optimización, realizamos pruebas desde el 1 de enero de 2023 hasta el 1 de enero de 2025, y se nos presentan los siguientes resultados para los 6 patrones/características:

r0

c0

La característica 0 no supera la prueba de forward walk.

r1

c1

La característica 1 no supera la prueba de forward walk.

r5

c5

La característica 5 supera la prueba de forward walk.

r6

c6

La característica 6 no supera la prueba de forward walk.

r7

c7

La característica 7 supera la prueba de forward walk.

r8

c8

La característica 8 supera la prueba de forward walk.


Conclusión

Hemos examinado los patrones generados al combinar los indicadores del DeMarker y los Envelopes. Un oscilador de impulso y un indicador de soporte/resistencia, su combinación complementaria, se probaron por primera vez en el artículo anterior, donde se realizaron operaciones basándose en patrones brutos. En este artículo, procesamos estos patrones mediante una red neuronal recurrente que utiliza el núcleo de ruido blanco en el entrenamiento para procesar los mismos patrones indicadores. Todavía no hemos considerado una red neuronal que combine todos los patrones, algo que sí era factible con los patrones de señal en bruto. Podríamos analizar esto en próximos artículos.

Nombre Descripción
wz64.mq5 Asistente Asesor experto ensamblado cuyo encabezado muestra los archivos utilizados.
SignalWZ_64.mqh Archivo de clase de señal personalizada.
64_0.onnx Modelo ONNX exportado para la característica 0.
64_1.onnx Modelo ONNX exportado para la característica 1.
64_5.onnx Modelo ONNX exportado para la característica 5.
64_6.onnx Modelo ONNX exportado para la característica 6.
64_7.onnx Modelo ONNX exportado para la característica 7.
64_8.onnx Modelo ONNX exportado para la característica 8.

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

Archivos adjuntos |
wz64.mq5 (7.56 KB)
SignalWZ_64.mqh (18.51 KB)
64_0.onnx (35.81 KB)
64_1.onnx (35.81 KB)
64_5.onnx (35.81 KB)
64_6.onnx (35.81 KB)
64_7.onnx (35.81 KB)
64_8.onnx (35.81 KB)
Determinación de los tipos de cambio justos en PPA usando los datos del FMI Determinación de los tipos de cambio justos en PPA usando los datos del FMI
Construcción de un sistema de análisis de tipo de cambio basado en paridad de poder adquisitivo (PPA) en Python. El autor ha desarrollado un algoritmo con cinco métodos para calcular tipos de cambio justos utilizando datos del FMI. El presente artículo supone una guía práctica para el análisis fundamental de divisas, el procesamiento de datos económicos y la integración con sistemas comerciales. Encontrará el código completo en open source.
Características del Wizard MQL5 que debe conocer (Parte 63): Uso de los patrones de DeMarker y los canales de envolvente Características del Wizard MQL5 que debe conocer (Parte 63): Uso de los patrones de DeMarker y los canales de envolvente
El oscilador DeMarker y el indicador de envolvente son herramientas de impulso y de soporte/resistencia que pueden combinarse al desarrollar un asesor experto. Por lo tanto, examinamos patrón por patrón qué podría ser útil y qué podría evitarse. Como siempre, estamos utilizando un Asesor Experto creado mediante un asistente, junto con las funciones de uso de patrones integradas en la clase Expert Signal.
Del básico al intermedio: Objetos (IV) Del básico al intermedio: Objetos (IV)
Puede que este sea el artículo más divertido hasta ahora. Esto ocurre porque aquí implementaremos una modificación de un objeto presente en MetaTrader 5 para crear otro que no existe originalmente en la plataforma. Claro, lo que verás aquí puede parecer una locura, pero funciona y tiene un objetivo muy interesante.
Herramientas de trading de MQL5 (Parte 2): Mejora del asistente interactivo de trading con retroalimentación visual dinámica Herramientas de trading de MQL5 (Parte 2): Mejora del asistente interactivo de trading con retroalimentación visual dinámica
En este artículo, actualizamos nuestra herramienta de asistente de operaciones añadiendo la función de arrastrar y soltar en los paneles y efectos al pasar el cursor, con el fin de que la interfaz resulte más intuitiva y receptiva. Perfeccionamos la herramienta para validar la configuración de las órdenes en tiempo real, garantizando que las configuraciones de las operaciones se ajusten con precisión a los precios de mercado. También realizamos backtesting de estas mejoras para confirmar su fiabilidad.