Características del Wizard MQL5 que debe conocer (Parte 76): Uso de los patrones del Awesome Oscillator y los canales Envelopes con aprendizaje supervisado
Introducción
En nuestro artículo anterior, presentamos la combinación de los indicadores Awesome Oscillator y Envelope Channels y, al someterla a pruebas, entre 7 y 8 de los 10 patrones superaron la prueba walk-forward en una ventana de prueba de 2 años. Normalmente, tras la presentación de un par de indicadores, analizamos qué impacto, si lo hay, puede tener el aprendizaje automático en el rendimiento de las señales de estos indicadores. Este artículo no es una excepción, por lo que vamos a examinar cómo los patrones 4, 8 y 9 pueden verse afectados si complementamos sus señales con una red de aprendizaje supervisado como filtro. Para nuestra red, estamos utilizando una CNN cuyos núcleos/canales tienen un tamaño determinado por el núcleo de producto escalar con atención entre pasos temporales.
Núcleo de producto escalar con atención entre pasos temporales
Este núcleo es una forma de mecanismo de atención en el que, dadas dos secuencias, que suelen ser características de diferentes pasos de tiempo o capas, se calcula una puntuación de atención entre cada par utilizando su producto escalar. Este enfoque pondría de relieve las relaciones a lo largo del tiempo y cómo una característica pasada o futura es pertinente para una actual. Así es como funciona; si tienes las siguientes dos secuencias:
X=[x1,x2,...,xT]
Y=[y1,y2,...,yS]
Para cada par (xi,yj) se calcularía la puntuación del producto escalar de la siguiente manera:
Scorei,j = xi⋅yj
Alternativamente, se puede aplicar una función softmax sobre 'j' o 'i' para obtener pesos de atención, que posteriormente se utilizan para combinar o reponderar las características a lo largo del tiempo. Esto resulta útil porque es eficiente en cuanto a parámetros, ya que no requiere parámetros aprendidos adicionales más allá de las proyecciones. Es flexible, ya que puede manejar secuencias de longitud variable, muestreo irregular e interacciones entre diferentes modos y momentos temporales. También es eficaz porque se centra en momentos relevantes, no solo en los vecindarios/campos receptivos locales de la convolución. Sus aplicaciones se encuentran en la previsión de series temporales, especialmente donde el retardo es importante, el análisis de vídeo donde los fotogramas están vinculados a lo largo del tiempo, el habla y cualquier dominio donde las dependencias temporales no sean triviales.
Entonces, ¿por qué seleccionamos este kernel para guiar el tamaño de los canales y kernels de nuestra CNN? Bueno, en cuanto a los núcleos, tienen características espaciales y temporales. En primer lugar, los núcleos de atención te indican qué pasos de tiempo son más importantes. Esto quizás no sea tan significativo en instancias de núcleos pequeños que están muy focalizados; sin embargo, a medida que los núcleos se dispersan, se captura más contexto. Las CNN también son inherentemente locales, por lo que la atención puede ampliar/enfocar de forma adaptativa el campo receptivo efectivo influyendo en el tamaño del núcleo en función de la dinámica temporal de la secuencia de datos.
En cuanto a los canales, el núcleo de atención puede identificar qué canales de características o dimensiones reciben "mayor atención". Es una guía para podar los canales. Si la atención ignora sistemáticamente ciertos canales, estos se convierten en peso muerto y reducen de hecho el tamaño útil total de canales. También es un indicador de expansión. Si la atención está dispersa y sin un punto de enfoque claro, entonces podría necesitar una representación más rica con más canales. Nuestro enfoque, al igual que en artículos anteriores donde hemos aprovechado las mejoras de las CNN, consiste en tener un diseño dinámico para la CNN en lugar de tamaños de kernel/canal estáticos que sirvan para todos los casos. Utilizamos patrones de atención en el entrenamiento y la inferencia para reconfigurar las CNN, con el objetivo de obtener modelos más eficientes y efectivos para datos no estacionarios del mercado financiero.
A pesar de nuestro enfoque dinámico, orientado a mejorar la adaptabilidad y el rendimiento del modelo, hay algunos inconvenientes que merece la pena mencionar. En primer lugar, tenemos una sobrecarga computacional de O(N²) en función de la longitud de la secuencia de datos de entrada, mientras que las CNN estáticas suelen ser de O(N). En el caso de secuencias largas, esto resulta prohibitivo. En segundo lugar, nos encontramos ante una paradoja de la interpretabilidad. Las puntuaciones de atención son «buenas», pero combinarlas con la selección dinámica de kernel/canal hace que el modelo sea más complejo de interpretar y que resulte más difícil extraer información significativa del mismo para aplicarla de forma fiable en contextos ajenos al conjunto de entrenamiento.
Además, no siempre es mejor, ya que algunas pruebas han indicado que, en tareas puramente locales, como la detección de bordes en imágenes, las CNN clásicas pueden superar a los modelos que hacen un uso intensivo de la atención. Además, los requisitos de datos para los núcleos de atención son considerablemente mayores que los de las CNN clásicas durante el entrenamiento y la optimización, con el fin de aprender dependencias significativas a lo largo del tiempo. Por último, existe el riesgo de sobreajuste. Si los cambios arquitectónicos se basan en la atención, se corre el riesgo de acabar persiguiendo el ruido o la aleatoriedad en los datos probados al ser demasiado adaptativo.
Entre las posibles alternativas a nuestro enfoque se incluyen: las convoluciones dilatadas, en las que el campo receptivo se amplía sin aumentar el tamaño del núcleo para abarcar diferencias temporales amplias pero dispersas; o las convoluciones dinámicas o adaptativas, en las que los pesos del núcleo se establecen en función de la naturaleza de los datos de entrada o de las señales de atención; o podríamos utilizar convoluciones separables en profundidad, en las que nuestra variable es únicamente el tamaño del canal y nuestro objetivo es la eficiencia computacional; o podríamos utilizar bloques squeeze-and-excitation (SE); o redes convolucionales temporales. Cada uno de ellos supone un enfoque novedoso con sus respectivas ventajas y retos, pero se presentan aquí con el fin de ofrecer una visión completa de las posibilidades que ofrece el ajuste de las redes neuronales convolucionales (CNN).
La red
Tras esta breve introducción al kernel de atención temporal cruzada de producto escalar, pasemos a un análisis minucioso del código. El código fuente de esta red, como una clase, se presenta a continuación:
import torch import torch.nn as nn import torch.nn.functional as F class DotProductAttentionConv1D(nn.Module): def __init__(self, input_length=100): super(DotProductAttentionConv1D, self).__init__() self.input_length = input_length # Deeper and wider design with attention self.kernel_sizes, self.channels = self._design_architecture() self.conv_layers = nn.ModuleList() self.attention_layers = nn.ModuleList() # For cross-time attention in_channels = 1 for i, (out_channels, kernel_size) in enumerate(zip(self.channels, self.kernel_sizes)): # Convolutional block conv_layer = nn.Sequential( nn.Conv1d(in_channels, out_channels, kernel_size=kernel_size, padding=kernel_size // 2), nn.BatchNorm1d(out_channels), nn.ReLU(), nn.Dropout(0.2)) self.conv_layers.append(conv_layer) # Attention block (cross-time attention) if i % 2 == 0: # Apply attention to every other layer attn_layer = nn.MultiheadAttention(embed_dim=out_channels, num_heads=4) self.attention_layers.append(attn_layer) else: self.attention_layers.append(None) in_channels = out_channels # Fully connected head (same as original) self.head = nn.Sequential( nn.AdaptiveAvgPool1d(1), nn.Flatten(), nn.Linear(in_channels, 128), nn.ReLU(), nn.Dropout(0.2), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, 1), nn.Sigmoid() ) def _dot_product_kernel(self, x): """Dot product similarity kernel with positional encoding""" # x shape: (B, C, L) x = x.permute(0, 2, 1) # (B, L, C) # Compute dot product attention attention_scores = torch.bmm(x, x.transpose(1, 2)) # (B, L, L) attention_weights = F.softmax(attention_scores / (x.size(-1) ** 0.5), dim=-1) return torch.bmm(attention_weights, x).permute(0, 2, 1) # (B, C, L) def _design_architecture(self): # Simulate attention response pattern num_layers = 10 kernel_sizes = [3 + (i % 4) * 2 for i in range(num_layers)] # cyclic 3, 5, 7, 9 channels = [32 * (i + 1) for i in range(num_layers)] # 32, 64, ..., 320 return kernel_sizes, channels def forward(self, x): x = x.unsqueeze(1) # (B, 1, L) for conv_layer, attn_layer in zip(self.conv_layers, self.attention_layers): x = conv_layer(x) if attn_layer is not None: # Reshape for attention (MultiheadAttention expects seq_len first) attn_input = x.permute(2, 0, 1) # (L, B, C) attn_output, _ = attn_layer(attn_input, attn_input, attn_input) x = attn_output.permute(1, 2, 0) # (B, C, L) # Also apply dot product kernel x = self._dot_product_kernel(x) return self.head(x)
En primer lugar, nuestra clase hereda de nn.Module, un estándar para redes que utilizan PyTorch. El parámetro input-length establece la longitud esperada de la secuencia; sin embargo, en los ajustes, la clase aún puede aceptar secuencias de entrada de longitud variable. La directiva super garantiza que la clase base esté inicializada, un paso muy importante para que "toda la magia" funcione en PyTorch, por lo que sin duda no merece la pena omitirlo. Con esto, pasamos a la arquitectura o al núcleo y al dimensionamiento de los canales.
Como se mencionó anteriormente, estamos utilizando un enfoque dinámico. En lugar de utilizar una codificación fija, nuestro modelo selecciona/establece los tamaños del kernel y de los canales algorítmicamente, lo que permite la adaptación a los patrones de atención en los datos de entrada que se procesan en cada propagación hacia adelante (forward pass). Como recomendación, podría ser prudente vincular las estadísticas de atención de ejecuciones anteriores o de metaaprendizaje con las opciones reales de kernel/canal para cada capa.
Una vez ejecutada la función de diseño y arquitectura, pasamos a la construcción de capas. Aquí, la función Module-List permite construir una red de profundidad variable. Este paso es muy importante al apilar capas de forma dinámica. Luego llegamos al bucle for de apilamiento de capas. En esta etapa, el tamaño del núcleo se asigna por separado para cada capa convolucional. Esto permite utilizar filtros anchos o estrechos, según sea el caso. El núcleo de atención puede informar posteriormente sobre cuál es el valor óptimo. Nuestro relleno, que es el tamaño del kernel dividido por 2, proporciona el mismo relleno con una longitud de salida igual a la longitud de entrada para la mayoría de los kernels. Esto es vital en las series temporales, donde es necesario alinear los intervalos de tiempo. Finalmente, la normalización por lotes, la activación ReLU y el Drop-out, la receta clásica del aprendizaje profundo, ayudan a estabilizar el entrenamiento, acelerar la convergencia y regularizar el modelo.
Dentro del bucle for, además del bloque de código de convolución que mencionamos anteriormente, también tenemos un "bloque de atención" de código. Aquí añadimos alternativamente capas de atención multi-cabeza, el núcleo del transformador que captura todas las dependencias. Realizamos esta adición de forma alternada y no en cada posición del apilamiento de capas, principalmente para ahorrar recursos computacionales. Esto permite que el modelo alterne entre una CNN local y un modelado global de dependencias. Por último, a nuestro parámetro de canales de entrada se le asigna el valor de los canales de salida. Esto se debe a que cada canal del mapa de características se convierte en una dimensión de incrustación, unificando así las representaciones de la CNN y de la atención.
Llegamos entonces a la parte final de nuestra función de inicialización de clase, donde definimos la cabeza completamente conectada. Aquí utilizamos agrupamiento adaptativo para reducir la dimensión temporal a 1, independientemente de la longitud de entrada, ya que el tamaño de salida es fijo; luego aplanamos para prepararnos para las capas totalmente conectadas. Se trata de capas densas que consisten en una pila de transformaciones no lineales que terminan en una única salida sigmoide. Esto puede interpretarse como una predicción binaria o una puntuación normalizada, pero en última instancia es nuestra previsión de la tendencia de precios, donde los valores inferiores a 0,5 son bajistas y los superiores a 0,5 son alcistas. El método dropout evita el sobreajuste en la etapa de la capa densa.
Tras haber examinado la inicialización de la clase de esta red, lo que sigue es la función kernel del producto escalar. Al calcular el núcleo, lo primero que hacemos es cambiar la entrada a BLC para facilitar los productos escalares a lo largo de la secuencia de vectores de datos de entrada. A continuación, realizamos una multiplicación de matrices por lotes o BMM, donde calculamos de forma eficiente todos los productos escalares por pares para cada lote. Las GPU pueden acelerar aún más este proceso. A continuación, realizamos un escalado softmax, un estándar para el núcleo de atención donde la temperatura es la raíz cuadrada del tamaño del vector de entrada o la dimensión incrustada.
A continuación, realizamos la multiplicación final de la matriz por lotes, donde combinamos características de diferentes momentos en el tiempo ponderadas por la atención. No solo eso, sino que luego volvemos a permutar el resultado de la multiplicación para restaurarlo a la forma del vector de entrada y también prepararlo para su uso posterior. Esta función es la que proporciona a la red una comprensión del tiempo cruzado que va más allá de lo que puede lograr la convolución regular por sí sola, y la invocamos dentro de la función de propagación hacia adelante (forward) de la red.
Nuestra siguiente función especial dentro de la clase de red es la herramienta de ayuda para el diseño de la arquitectura. Ya hemos utilizado funciones similares al personalizar la CNN en artículos anteriores, y lo que estamos haciendo en esta situación es alternar entre diferentes tamaños de núcleo para simular una variación "informada por la atención". También aumentamos progresivamente los canales, un enfoque clásico del aprendizaje profundo en el que buscamos aprender representaciones más abstractas y de mayor dimensión en capas posteriores.
Nuestra función final para la clase de red es, como suele ser habitual, la función de paso hacia adelante. Lo primero que hacemos aquí es preprocesar el vector de entrada añadiendo la dimensión intermedia adicional, o canal, que es un requisito para las convoluciones 1D. Una vez hecho esto, para cada capa comenzamos realizando una convolución donde extraemos características locales de la entrada. A continuación, procesamos la atención condicional, que consiste en una comprobación de SI existe una capa de atención. Recordemos que en la función de inicialización de esta clase asignamos estas capas de atención de forma alternada y no en cada ranura.
Si contamos con una capa de atención, reformateamos los datos, ya que el mecanismo de atención multi-cabeza de PyTorch espera el formato (longitud de secuencia, lote, embedding/canales), mientras que aquí partimos de (lote, canales, longitud). Una vez que tenemos la salida de atención, realizamos el kernel del producto escalar llamando a la función que definimos justo arriba. Como nota al margen, estamos utilizando tanto el transformador como el producto escalar en una pasada hacia adelante, aunque solo uno de ellos podría utilizarse como enfoque alternativo. Este cambio se puede realizar con el objetivo de determinar qué algoritmo es el que aporta la precisión al modelo. La salida final se pasa entonces al cabezal para realizar la clasificación o la regresión.
Resumen del código por sección
| Sección | Qué hace | Por qué es importante |
|---|---|---|
| __init__ + super | Configuración de la clase base | Imprescindible para el funcionamiento de PyTorch |
| self.kernel_sizes, channels | Diseño dinámico de redes neuronales convolucionales (CNN) | Extracción de características adaptativa y a medida |
| Capas de ModuleList | Arquitectura modular y ampliable | Escalabilidad, experimentación |
| Bloques de convolución | Extracción de características locales | Las ventajas tradicionales de las redes neuronales convolucionales (CNN), la normalización por lotes, etc. |
| Bloques MultiheadAttention | Dependencias globales entre tiempos | Detecta dependencias de largo alcance |
| _dot_product_kernel | Cálculo de la atención explícita | Señal redundante/interconectada para enlaces entre tiempos |
| Cabezal de capas totalmente conectadas | Agregación, salida | Flexible para la tarea (clasificación, regresión, etc.) |
| Lógica del método forward | Flujo de datos a través de la red | Garantiza la correcta aplicación de cada operación |
| _design_architecture | Cómo se eligen los núcleos/canales | Puede basarse en datos o en la atención |
Función del oscilador Awesome
Las funciones de los indicadores que se utilizan en MQL5 no se importan cuando utilizamos el módulo de MetaTrader 5 en Python. Por lo tanto, siempre tenemos que utilizar bibliotecas ya existentes o programar las nuestras propias. La segunda opción es la que hemos venido utilizando, así que nos ceñiremos a ella e implementaremos nuestra función del Awesome Oscillator en Python de la siguiente manera:
def Awesome_Oscillator(df: pd.DataFrame, short_period: int = 5, long_period: int = 34) -> pd.DataFrame: """ Calculate the Bill Williams Awesome Oscillator (AO) and append it to the input DataFrame. AO = SMA(Median Price, short_period) - SMA(Median Price, long_period) Args: df (pd.DataFrame): DataFrame with 'high' and 'low' columns. short_period (int): Short period for SMA (default 5). long_period (int): Long period for SMA (default 34). Returns: pd.DataFrame: Input DataFrame with 'AO' column added. """ required_cols = {'high', 'low'} if not required_cols.issubset(df.columns): raise ValueError("DataFrame must contain 'high' and 'low' columns") if not all(p > 0 for p in [short_period, long_period]): raise ValueError("Period values must be positive integers") result_df = df.copy() median_price = (result_df['high'] + result_df['low']) / 2 short_sma = median_price.rolling(window=short_period).mean() long_sma = median_price.rolling(window=long_period).mean() result_df['AO'] = short_sma - long_sma return result_df
Entre los módulos que importamos para esta función se encuentra pandas, que utilizamos para manipular datos tabulares y series temporales, ya que resulta especialmente adecuado para trabajar con DataFrames y realizar operaciones con ventanas móviles. También importamos NumPy, la base de las operaciones matemáticas rápidas que se utiliza mucho internamente, aunque no la llamemos explícitamente aquí. La firma de nuestra función, la línea de texto que aparece después de 'def', es clara y proporciona pistas sobre los tipos de datos de entrada necesarios para llamar a la función. Esta claridad se combina con la función de autocompletado del IDE, ya que los valores predeterminados para los períodos de promedio están predefinidos. Estas configuraciones están en consonancia con las clásicas de Bill Williams.
Una vez verificada la firma, comenzamos validando los datos de entrada de la función, empezando por lo que es obligatorio. El marco de datos. Las columnas de 'high' y 'low' deben estar presentes. Se trata de una comprobación básica que evita errores crípticos posteriores si faltan columnas. Además, comprobamos que los periodos introducidos sean números enteros sin signo válidos. Al emplear mensajes de error fáciles de entender, provocamos fallos tempranos que pueden ayudar a realizar diagnósticos eficaces. Esta postura defensiva al verificar los datos de entrada es muy importante para evitar valores NaN. Nunca confíes en datos externos.
A continuación, creamos una copia del marco de datos de entrada para garantizar que los datos originales permanezcan en su estado original. Este método es "no destructivo" y resulta esencial para los sistemas en los que se superponen funciones indicadoras. A continuación, calculamos el precio medio a partir del marco de datos copiado. Este es un dato fundamental para el AO, y refleja un precio medio de la barra calculado a partir del máximo y el mínimo, no solo el precio de cierre, y tiene como objetivo evitar la fluctuación brusca entre el precio de apertura y el precio de cierre. A continuación, calculamos las medias móviles suavizadas a corto y largo plazo. El uso de rolling().mean() crea una media móvil que es una señal de tendencia suave. La comparación entre periodos cortos y largos es la base de la lógica de los osciladores. Mide el impulso como la distancia entre las tendencias rápidas y lentas.
Finalmente, añadimos una nueva columna, 'AO', a nuestro marco de datos copiado. Este es el valor AO real. Cuando el impulso positivo a corto plazo es mayor que el de largo plazo, se trata de una señal alcista. Cuando el impulso negativo a corto plazo es menor, lo cual es una señal bajista de que el repunte se está debilitando. Como se destacó en el artículo anterior, el oscilador Awesome se basa en la línea cero, por lo que los cruces son importantes.
Función del canal de envolventes
Implementamos los canales Envelopes en Python de la siguiente manera:
def Envelope_Channels(df: pd.DataFrame, period: int = 20, deviation: float = 0.025) -> pd.DataFrame: """ Calculate Envelope Channels (Upper & Lower Bands) and append to the input DataFrame. Envelope Channels = SMA(close, period) * (1 ± deviation) Args: df (pd.DataFrame): DataFrame with 'close' column. period (int): Period for SMA calculation (default 20). deviation (float): Deviation as a decimal (e.g., 0.025 for 2.5%, default 0.025). Returns: pd.DataFrame: Input DataFrame with 'Envelope_Upper' and 'Envelope_Lower' columns added. """ required_cols = {'close'} if not required_cols.issubset(df.columns): raise ValueError("DataFrame must contain 'close' column") if period <= 0 or deviation < 0: raise ValueError("Period must be positive and deviation non-negative") result_df = df.copy() sma = result_df['close'].rolling(window=period).mean() result_df['Envelope_Upper'] = sma * (1 + deviation) result_df['Envelope_Lower'] = sma * (1 - deviation) result_df['Envelope_Mid'] = 0.5 * (result_df['Envelope_Upper'] + result_df['Envelope_Lower']) return result_df
Al igual que con la AO, la firma tiene sugerencias de tipo y se utilizan valores predeterminados "sensatos" para los parámetros de período y desviación. Nuestra validación del marco de datos de entrada se centra en comprobar únicamente la presencia de la columna 'close'. Este es nuestro único requisito, para el marco de datos, para evitar que entren datos basura y salgan datos basura. También comprobamos que el período de entrada no sea cero y que la desviación no sea negativa. Si no se cumplen estos requisitos, se produce un error de valor que finaliza la ejecución del script.
También hacemos una copia del marco de datos de entrada, al que llamamos marco de datos de resultado, una práctica segura, como ya se ha argumentado anteriormente y en artículos previos. Nuestro primer cálculo se basa en una media móvil suavizada del precio de cierre. Las envolventes se construyen a partir de esta línea de base.. Una SMA más suave significa menos oscilaciones bruscas, pero más retardo. Sirve como centro de gravedad de las bandas. Partiendo de esta línea base, procedemos a calcular los búferes superior e inferior del Envelopes. Estos son los canales.
Un precio por encima del límite superior podría indicar una situación de sobrecompra, mientras que si está por debajo, podría tratarse de una situación de sobreventa. La desviación se utiliza como un valor decimal, con un valor predeterminado de 2,5 % o 0,025. La elección del valor de desviación debe ser óptima para el activo que se negocia, ya que tiende a ser un valor muy sensible. Los activos más volátiles pueden utilizar desviaciones de hasta el 5 por ciento. El margen adicional que añadimos al marco de datos es la línea media de la envolvente, y simplemente calculamos la media de las bandas superior e inferior para obtener su valor.
Las características
Estamos probando 3 de los 10 patrones de señal que presentamos en el artículo anterior. En este artículo nos referimos a estos patrones de señal como características porque sirven como entradas a una red, pero para nuestros propósitos, ambos nombres pueden usarse indistintamente. Como ya se mencionó en el artículo anterior, y como ha sucedido en esta serie de artículos, cada característica es un vector que combina señales de dos indicadores. Cada característica es un vector binario de 0 y 1. En artículos anteriores hemos explorado la posibilidad de ampliar el tamaño de nuestras funciones, pasando del tamaño 2 al tamaño actual, y hacer que se correspondan con cada lectura de verificación del indicador para obtener una señal alcista o bajista. Los resultados de las pruebas realizadas con este método fueron pésimos en comparación con lo que estábamos obteniendo, y seguimos obteniendo, cuando nos centramos en la señal general de tendencias alcistas y bajistas.
Estamos revisando las características 4, 8 y 9. Por lo tanto, la estructura general de estas funciones de características, tal como las adoptamos aquí, no se desvía mucho de lo que hemos estado utilizando. Cada función genera una matriz NumPy bidimensional cuya forma es igual al número de filas del marco de datos y cuyas columnas son dos. El índice [0] de cada fila es un patrón alcista o de compra único; mientras que el [1] de cada fila presenta un marcador bajista o de venta único. En nuestro formato, 1 significa que existe un patrón de compra/venta, mientras que 0 implica que no lo hay. Cada patrón es una combinación de señales de dos indicadores: el AO y el canal de envolventes. Utilizamos shift[n] en cada marco de datos cuando realizamos comparaciones de múltiples barras. Por lo tanto, cada marco de datos cargado necesita contener datos suficientes.
Característica-4
En resumen, la lógica principal de este patrón es que marcamos una señal alcista cuando el AO forma un mínimo por encima de cero y el precio se sitúa dentro de la mitad inferior del canal de envolventes. Por el contrario, la señal bajista se produce cuando el AO forma un pico por debajo de cero y el precio se encuentra en la mitad superior de la envolvente. Implementamos esto en Python de la siguiente manera:
def feature_4(df): """ //+------------------------------------------------------------------+ //| Check for Pattern 4. | //+------------------------------------------------------------------+ """ feature = np.zeros((len(df), 2)) feature[:, 0] = ((df['AO'].shift(2) > df['AO'].shift(1)) & (df['AO'].shift(1) < df['AO']) & (df['AO'].shift(1) > 0.0) & (df['close'].shift(2) >= df['Envelope_Mid'].shift(2)) & (df['close'].shift(2) <= df['Envelope_Lower'].shift(2)) & (df['close'].shift(1) >= df['Envelope_Mid'].shift(1)) & (df['close'].shift(1) <= df['Envelope_Lower'].shift(1)) & (df['close'] >= df['Envelope_Mid']) & (df['close'] <= df['Envelope_Lower'])).astype(int) feature[:, 1] = ((df['AO'].shift(2) < df['AO'].shift(1)) & (df['AO'].shift(1) > df['AO']) & (df['AO'].shift(1) < 0.0) & (df['close'].shift(2) <= df['Envelope_Mid'].shift(2)) & (df['close'].shift(2) >= df['Envelope_Upper'].shift(2)) & (df['close'].shift(1) <= df['Envelope_Mid'].shift(1)) & (df['close'].shift(1) >= df['Envelope_Upper'].shift(1)) & (df['close'] <= df['Envelope_Mid']) & (df['close'] >= df['Envelope_Upper'])).astype(int) feature[0, :] = 0 feature[1, :] = 0 return feature
En el código anterior, comenzamos preparando la matriz de salida para que no refleje ni suponga ninguna señal, rellenándola con ceros y asegurándonos de que tenga 2 columnas. A continuación, asignamos un índice a la primera columna comprobando si se cumplen todos los requisitos del indicador alcista. Para asignar un 1 a una columna, deben cumplirse simultáneamente las condiciones del AO y de las bandas envolventes. Las condiciones del AO son que forme una figura en forma de 'V', una señal de un descenso seguido de un repunte o una señal de reversión alcista. Esta forma de V también debe estar completamente por encima de cero o en territorio alcista. Las condiciones del rango son que el precio se mantenga cerca o en la mitad inferior del canal de envolventes, pero sin estar sobrevendido. Esto puede equipararse a un retroceso, pero no a una ruptura, lo cual es bueno para operaciones de retroceso o para la reanudación de la tendencia.
Asignar el valor 1 a cualquier columna implica casi con toda seguridad que la otra columna será cero, ya que estas señales son un reflejo la una de la otra. Nuestra lógica bajista es, por lo tanto, una inversión de lo que hemos expuesto anteriormente. Requiere un 'pico' de AO por debajo de cero y que el precio se quede atascado en la mitad superior del rango. Lo último que hacemos en la función feature-4 es asignar ceros a las dos primeras filas porque realizamos comparaciones que abarcan hasta 2 índices. Como se ha argumentado anteriormente, esto evita señales accidentales de NaN inducidos por desplazamiento al comienzo de una serie.
Realizamos optimizaciones y pruebas de ejecución directa para la característica 4 cuando la complementamos con nuestra CNN descrita anteriormente como filtro. Retomando el tema del artículo anterior, nuestro símbolo es USD JPY, el marco temporal es de 30 minutos y, como siempre, al menos para este año, el período de prueba es 2023 y 2024. La característica 4 presenta un mejor rendimiento en la prueba walk-forward, aunque sigue arrojando pérdidas, que la analizada en el artículo anterior. Este informe se presenta a continuación:


Característica-8
Como se explicó en el artículo anterior, este patrón de señal presenta una señal alcista de AO y un aumento constante del precio, con AO por encima de cero y el precio alejándose de la banda inferior del rango. La lógica bajista indica que AO y el precio están en una caída libre, con AO por debajo de cero y el precio cayendo por debajo de la banda inferior. Implementamos esta función en Python de la siguiente manera:
def feature_8(df): """ //+------------------------------------------------------------------+ //| Check for Pattern 8. | //+------------------------------------------------------------------+ """ feature = np.zeros((len(df), 2)) feature[:, 0] = ((df['AO'].shift(2) > 0.0) & (df['AO'].shift(1) > df['AO'].shift(2)) & (df['AO'] > df['AO'].shift(1)) & (df['close'].shift(2) > df['Envelope_Lower'].shift(2)) & (df['close'].shift(1) > df['close'].shift(2)) & (df['close'] > df['close'].shift(1))).astype(int) feature[:, 1] = ((df['AO'].shift(2) < 0.0) & (df['AO'].shift(1) < df['AO'].shift(2)) & (df['AO'] > df['AO'].shift(1)) & (df['close'].shift(2) < df['Envelope_Lower'].shift(2)) & (df['close'].shift(1) < df['close'].shift(2)) & (df['close'] < df['close'].shift(1))).astype(int) feature[0, :] = 0 feature[1, :] = 0 return feature
Por lo tanto, a partir de nuestro código anterior, le damos al primer índice un 1, lo que confirma que hay una señal alcista presente si el AO continúa moviéndose hacia arriba, así como el precio con una clara aceleración desde una zona positiva. También debemos confirmar que el precio se mantiene por encima de la banda inferior del Envelopes y que no se trata de un «rebote del gato muerto» (dead cat bounce). El segundo índice obtiene un 1 si AO comienza en negativo tras haber estado cayendo, pero luego sube ligeramente, potencialmente en un retroceso, mientras que el precio continúa cayendo en una fuerte tendencia bajista por debajo del límite inferior. Luego, ponemos a cero los dos primeros índices, tal como hicimos con la característica 4. Al probar la función 8, seguimos sin lograr un avance rentable. Este informe se presenta a continuación:


Característica-9
La lógica fundamental de nuestro patrón de señal final, como se mencionó en el artículo anterior, consiste en que el AO empieza a girarse a la baja pero permanece por encima de cero mientras el precio cae hasta la línea media y rebota para generar la señal alcista. En cuanto a la señal bajista, el AO está subiendo pero se mantiene por debajo de cero y el precio sube hasta la línea media, pero luego no logra superarla. Lo implementamos en Python de la siguiente manera:
def feature_9(df): """ //+------------------------------------------------------------------+ //| Check for Pattern 9. | //+------------------------------------------------------------------+ """ feature = np.zeros((len(df), 2)) feature[:, 0] = ((df['AO'].shift(2) > df['AO'].shift(1)) & (df['AO'].shift(1) > df['AO']) & (df['AO'] > 0.0) & (df['close'].shift(2) > df['Envelope_Mid'].shift(2)) & (df['close'].shift(1) <= df['Envelope_Mid'].shift(1)) & (df['close'] > df['Envelope_Mid'])).astype(int) feature[:, 1] = ((df['AO'].shift(2) < df['AO'].shift(1)) & (df['AO'].shift(1) < df['AO']) & (df['AO'] < 0.0) & (df['close'].shift(2) < df['Envelope_Mid'].shift(2)) & (df['close'].shift(1) >= df['Envelope_Mid'].shift(1)) & (df['close'] < df['Envelope_Mid'])).astype(int) feature[0, :] = 0 feature[1, :] = 0 return feature
Nuestra filosofía de implementación está en consonancia con las dos funciones que hemos tratado anteriormente, por lo que no repetiré los puntos clave ya abordados. La prueba de nuestro patrón final ofrece un resultado walk-forward casi favorable. Este informe se muestra a continuación;


Conclusión
En resumen, hemos explorado la integración del aprendizaje automático, específicamente mediante el uso de una CNN guiada por un núcleo de producto escalar con atención temporal cruzada, para mejorar las señales de los canales AO y de envolvente. Nuestras pruebas se centraron en los tres patrones previamente probados (patrón 4, patrón 8 y patrón 9) para determinar si la adición de filtrado neuronal avanzado podría mejorar su rendimiento de predicción.
Nuestro método de CNN dinámica demostró ser eficaz, especialmente para las características 4 y 9, donde conseguimos transformar unas pruebas walk-forward que anteriormente habían arrojado resultados muy deficientes en ejecuciones con pérdidas mucho más contenidas. El mecanismo de atención de nuestro núcleo permitió que la red adaptara dinámicamente tanto el tamaño del núcleo como las dimensiones del canal en función de la relevancia temporal, lo que acabó por capturar patrones más profundos en el comportamiento del mercado. Esta adaptabilidad nos ha resultado muy útil, ya que trabajamos con mercados financieros no estacionarios. En este caso, los modelos rígidos suelen tener un rendimiento inferior debido a cambios en la volatilidad, la dinámica de las tendencias o el régimen del mercado.
Sin embargo, no todos los resultados fueron positivos. La característica 8, que depende en gran medida del impulso direccional sostenido, no logró ser rentable ni siquiera con la adaptabilidad mejorada de la CNN. Esto sugiere que ciertas señales son limitadas, independientemente de las mejoras del modelo, lo que subraya la importancia de la selección de indicadores y señales en la estrategia de negociación. Además del aprendizaje supervisado que hemos explorado en este artículo, podemos retomar el aprendizaje por refuerzo como un medio para mejorar nuestras señales. En los próximos artículos o en futuros artículos se podría explorar este tema en relación con las características aquí comentadas.
| Nombre | Descripción |
|---|---|
| WZ-76.mq5 | Asesor experto creado con el asistente, cuyo encabezado destaca los archivos incluidos. |
| SignalWZ-76.mqh | Clase de señal personalizada utilizada por el Asistente MQL5 durante el ensamblaje. |
| 76-4.onnx | Red exportada para el patrón de señal 4. |
| 76-8.onnx | Red exportada para el patrón de señal 8. |
| 76-9.onnx | Red exportada para el patrón de señal 9. |
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/18878
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Automatización de estrategias de trading en MQL5 (Parte 24): Sistema de ruptura de la sesión de Londres con gestión del riesgo y stops dinámicos
Aprendizaje automático y Data Science (Parte 46): Pronóstico de los mercados bursátiles con N-BEATS en Python
Redes neuronales en el trading: Extracción eficaz de características para una clasificación precisa (Implementación de objetos)
Reimaginando las estrategias clásicas en MQL5 (Parte 14): Análisis de múltiples estrategias
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso