Características del Wizard MQL5 que debe conocer (Parte 72): Uso de los patrones del MACD y del OBV con aprendizaje supervisado
Introducción
En nuestro último artículo, en el que presentamos el par de indicadores formado por el MACD y el oscilador On-Balance-Volume (OBV), analizamos diez posibles señales que podrían generarse a partir de la combinación complementaria de estos indicadores. Siempre probamos 10 patrones de señal derivados de un emparejamiento de 2 indicadores mediante una prueba walk-forward durante 1 año, después de haber realizado el entrenamiento o la optimización durante el año anterior. Con el último artículo, solo un patrón de señal, el patrón 7, pudo superar una prueba walk-forward con rentabilidad. Repasamos algunas de las razones por las que este par de indicadores tuvo un rendimiento pésimo en comparación con otros indicadores que hemos analizado; sin embargo, esto también nos brindó una "oportunidad" para explorar el aprendizaje automático en la mejora de algunos de estos patrones de señales.
Por lo tanto, complementamos ese artículo examinando cómo, si es que lo hace, una red CNN que utiliza el núcleo cuadrático racional podría ayudar y tal vez mejorar la interpretación y aplicación de las señales de los indicadores. El núcleo cuadrático racional (RQ) se define mediante la siguiente fórmula:

Donde:
- k(x,x′) es el valor de similitud del núcleo entre dos entradas x y x′. Su valor oscila entre 0 y 1, y los valores más cercanos a 1 indican una mayor similitud.
- x y x′ son dos vectores de entrada (por ejemplo, segmentos de una señal 1D, vectores de características o posiciones en el tiempo). Pueden ser valores escalares (de dimensión = 1) o vectores con múltiples dimensiones.
- ∥x−x′∥2 es la distancia euclidiana al cuadrado entre los dos vectores de entrada. Mide la distancia que existe entre x y x′ en un espacio de características. Esta distancia se puede obtener mediante esta fórmula:
-

- l es la escala de longitud (o escala característica) del núcleo. Determina con qué rapidez disminuye la similitud con la distancia:
- l pequeña: decaimiento pronunciado (sensibilidad local).
- l grande: decaimiento lento (amplia similitud).
- α o alfa es el parámetro de mezcla de escala (a veces llamado parámetro de forma). Controla la ponderación de las escalas de variación grandes frente a las pequeñas:
- Cuando α → infinito, el núcleo se vuelve equivalente al núcleo exponencial cuadrado (RBF).
- Un valor pequeño de α significa que el núcleo tiene colas más pesadas, lo que permite interacciones de mayor alcance.
En nuestra implementación del núcleo cuadrático racional (RQK) en una CNN, lo utilizamos para establecer los tamaños de los núcleos y los canales de la CNN. Sin embargo, existen usos y aplicaciones alternativas de este núcleo dentro de una CNN que el lector puede considerar.
Aplicaciones y ventajas alternativas del kernel RQ
En primer lugar, entre estas se encuentra el enmascaramiento de atención adaptativa. En este caso de uso, el núcleo RQ construye máscaras de atención espacial o temporal dentro de la CNN. Esto ayuda con la segmentación o la previsión de series temporales. Funciona haciendo hincapié en las regiones que presentan una alta similitud con una referencia, como por ejemplo una ventana central. Otro posible uso del núcleo RQ podría ser el «pooling» sensible a la distancia. En este escenario, solo se agrupan las activaciones de la red que se encuentran dentro de un umbral de similitud establecido bajo el núcleo RQ, en lugar de promediar todo. Esto permite una agrupación adaptativa que tiene en cuenta la distancia y respeta la proximidad de las características.
Otra aplicación puede ser en la escala de importancia de las características. En este caso, la ponderación se aplicaría a los canales de la CNN de forma gradual y de forma suave y aprendida. Finalmente, el núcleo RQ podría utilizarse para reducir o "podar" los pesos de la CNN cuyo efecto, medido por el núcleo, es mínimo dentro del espacio de entrada de la red. Las razones por las que este núcleo puede y debe aplicarse dentro de las CNN también son multifacéticas. A continuación, presentamos una tabla que intenta resumir esto.
| Resistente a valores atípicos | Las largas colas de los núcleos RQ hacen que la arquitectura sea menos susceptible a anomalías espaciales o temporales. |
| Transiciones suaves | La disminución gradual del núcleo proporciona cambios de representación menos abruptos entre capas. Esto favorece la propagación del gradiente. |
| Arquitectura dinámica | El núcleo utiliza una selección de tamaños de núcleos CNN o expansión de canales que tiene en cuenta los datos, mediante el uso de medidas de similitud. |
Este diseño de CNN (conv-1d) que utiliza el kernel RQ es ideal para entradas de tamaño unidimensional donde la similitud localizada es importante, como la serie temporal financiera que estamos analizando en este artículo. Otros tipos de datos adecuados que podrían ser útiles incluyen señales de audio y detección de patrones binarios. Al utilizar esta CNN, es importante que los vectores de entrada estén normalizados o estandarizados al rango de 0 a 1, como hemos estado haciendo con nuestras redes en artículos anteriores. La forma de la entrada suele ser (batch_size, input_length), y los vectores de entrada están rellenos con ceros o unos.
El núcleo RQ tiene una serie de hiperparámetros importantes, como se indica en su fórmula anterior. El primero de ellos es «alfa», que, tal y como se menciona en las notas de la fórmula, regula la atenuación de la cola. Con valores altos, el núcleo se aproxima más a un kernel RBF único. Sin embargo, a medida que este valor se reduce, el núcleo RQ actúa como una combinación de múltiples RBF a diferentes escalas, según lo gestiona alfa. Otro hiperparámetro es la escala de longitud, que determina la sensibilidad de la similitud a la distancia.
Si nuestra red es un clasificador con una distribución de probabilidad como salida, entonces se puede utilizar la función de pérdida BCE en el entrenamiento. Una inicialización adecuada y una correcta programación de la tasa de aprendizaje también pueden ser importantes en este caso, dada nuestra arquitectura poco convencional. También se pueden realizar extensiones adicionales añadiendo conexiones residuales entre capas para una mejor propagación del gradiente. Además, la integración del núcleo RQ puede realizarse como un mecanismo de compuerta (gating). También se pueden utilizar técnicas de dropout basadas en la similitud entre las pilas de capas.
Las investigaciones apuntan a que los núcleos RQ definen los pesos de Conv1D y, por lo tanto, permiten capturar determinados patrones en los datos. Esta aplicación del núcleo reduce claramente el número total de parámetros necesarios y también mejora la interpretabilidad; sin embargo, podría limitar la adaptabilidad en comparación con los núcleos aprendidos. Los datos que apuntan a obtener mejores resultados también se inclinan por el uso de este núcleo en capas de procesos gaussianos y en la optimización de hiperparámetros en el diseño más amplio de las redes neuronales convolucionales (CNN), tal y como se explica en este artículo sobre procesos gaussianos profundos con núcleos convolucionales.
El uso de núcleos de tamaño fijo reduce el número de parámetros aprendibles y mitiga el sobreajuste, sobre todo en los casos en los que los datos son limitados. Esto quedó ilustrado en una clasificación de enfermedades de la yuca, tal y como se explica aquí. Sin embargo, aunque la interpretabilidad que proporciona el uso de núcleos preestablecidos ayuda a comprender la extracción de características, en marcado contraste con los núcleos aprendidos de caja negra, existen algunas preocupaciones con respecto a la adaptabilidad. Los núcleos fijos tienden a no adaptarse tan bien a los datos como los núcleos aprendidos, y esto puede provocar un rendimiento inferior en tareas complejas. Esto se sugiere en el estudio sobre la yuca mencionado anteriormente, donde los kernels híbridos superaron a los kernels RQ individuales. A continuación se presentan algunos de los resultados del estudio:
| Tipo de núcleo | Precisión |
|---|---|
| Núcleo RQ | 88.5% |
| Núcleo exponencial al cuadrado | 88.0% |
| Núcleo híbrido | 90.1% |
Nuestra tabla anterior, procedente del estudio sobre la enfermedad de la yuca, muestra las ligeras diferencias de rendimiento entre los núcleos aprendidos y los fijos. Se recomienda un enfoque híbrido. En épocas anteriores, el núcleo RQ desempeñaba un papel ambiguo. Sin embargo, tal y como se ha mencionado en algunos de los enlaces anteriores, este estudio, este otro y este han contribuido a poner de manifiesto su aplicación en los procesos gaussianos profundos (Deep Gaussian Processes), especialmente para datos de imágenes y su adaptación a Conv1D. El estudio sobre las enfermedades de la yuca sirvió de base para la decisión de basarse en pesos predefinidos como forma de encontrar un equilibrio entre los enfoques fijos y los aprendidos.
Al diseñar las redes utilizadas, el tamaño del núcleo se tomó en relación con la escala de longitud del hiperparámetro l. Al utilizar múltiples canales que capturan diferentes escalas, lo cual se ajusta a las prácticas de las CNN de campos receptivos de distinto tamaño, la implementación final utilizó la agrupación de promedios globales para mayor flexibilidad. Esto hizo que la salida fuera independiente de la longitud de la secuencia y también resultó muy adecuado para la salida escalar, de rango 0-1.
La Red
Nuestra implementación de red está realizada en Python porque, como hemos argumentado en artículos anteriores, programarla y entrenarla en Python es más eficiente que realizar las mismas tareas en MQL5. Implementamos una CNN en Python que escala sus núcleos y canales con el RQ-Kernel de la siguiente manera:
import torch import torch.nn as nn import torch.nn.functional as F import pandas as pd import numpy as np class RationalQuadraticConv1D(nn.Module): def __init__(self, alpha=1.0, length_scale=1.0, input_length=100): super(RationalQuadraticConv1D, self).__init__() self.alpha = alpha self.length_scale = length_scale self.input_length = input_length # Deeper and wider design self.kernel_sizes, self.channels = self._design_architecture() self.conv_layers = nn.ModuleList() in_channels = 1 for i, (out_channels, kernel_size) in enumerate(zip(self.channels, self.kernel_sizes)): 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(layer) in_channels = out_channels # Fully connected head to increase parameter count 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 _rq_kernel(self, x, x_prime): dist_sq = (x - x_prime) ** 2 return (1 + dist_sq / (2 * self.alpha * self.length_scale ** 2)) ** -self.alpha def _design_architecture(self): x = torch.linspace(0, 1, self.input_length) center = 0.5 responses = self._rq_kernel(x, center) thresholded = responses[responses > 0.01] # allow more spread 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 layer in self.conv_layers: x = layer(x) return self.head(x)
Comenzamos definiendo un módulo PyTorch personalizado que tiene como entradas algunos de los parámetros clave que hemos resaltado anteriormente en la fórmula del núcleo RQ. Nuestra clase, a la que denominamos 'RationalQuadraticConv1D', hereda de 'nn.Module', que, como hemos visto en artículos anteriores, es la clase base para todos los módulos de redes neuronales de PyTorch. El constructor inicializa tres parámetros importantes del núcleo RQ: alfa, escala de longitud y longitud de entrada. Como se mencionó anteriormente, Alpha controla la forma del núcleo, la escala de longitud determina la distancia sobre la cual el núcleo decae, y la longitud de entrada está destinada a capturar el tamaño/longitud esperado del vector de entrada.
Estos primeros pasos son importantes, ya que establecen los cimientos del modelo y permiten personalizar el comportamiento del núcleo. Esto también garantiza que la arquitectura coincida con las dimensiones del vector de entrada. Se debe ajustar el valor alfa y la escala de longitud para adaptar la sensibilidad del núcleo a las diferencias en las entradas. Por ejemplo, una escala de longitud menor se centraría en patrones locales, mientras que un valor mayor capturaría similitudes más amplias. Como siempre, se debe comprobar que la longitud de entrada coincida con el tamaño de los datos de entrada.
Tras la inicialización, asignamos dinámicamente los tamaños de los núcleos y el número de canales para las capas convolucionales. Esta línea de código llama al método '_design_architecture' (que se explicará más adelante) para calcular los tamaños y canales del kernel basándose en el perfil de similitud del RQ-Kernel en todo el espacio de entrada. Esta es la esencia de nuestra red, ya que, a diferencia de las CNN tradicionales con arquitecturas fijas, este enfoque adapta la estructura del modelo a las características de los datos. Esto puede mejorar potencialmente la eficiencia y la eficacia de la extracción de características. Nuestro enfoque de adaptabilidad hace que este modelo sea adecuado para conjuntos de datos con estructuras de similitud variables. Es importante supervisar el resultado del método '_design_architecture' para garantizar que la arquitectura se ajuste a la complejidad de la tarea.
Posteriormente, en las siguientes líneas de código, construimos una pila flexible de capas convolucionales con propiedades dinámicas. La función 'nn.ModuleList()' crea un contenedor para un número variable de capas convolucionales. El bucle itera sobre pares de 'out-channels', es decir, el número de canales de salida, con el 'kernel-size', que en este caso se refiere al tamaño del núcleo convolucional; leído de 'self.channels' y 'self.kernel-size' respectivamente. Cada capa 'nn.Conv1D' toma 'canales de entrada' comenzando en 1 para la primera capa. Envía la salida a 'out-channels' y utiliza 'kernel-size' con relleno para establecer el siguiente tamaño del kernel con el fin de preservar la longitud de la entrada. Los "canales de entrada" se actualizan a "canales de salida" para la siguiente capa, formando así una cadena.
Esto es importante, ya que la construcción dinámica permite que el modelo tenga un número personalizado de capas y parámetros. Paralelamente, el relleno garantizará que la longitud de salida coincida con la longitud de entrada, un aspecto fundamental al trabajar con datos secuenciales. Los valores mayores de "tamaño del kernel" tienden a capturar patrones más amplios de los datos de entrada, mientras que un mayor número de "canales de salida" aumenta la diversidad de características capturadas a partir de los datos. Se pueden realizar ajustes al relleno si no es importante preservar la longitud completa, como por ejemplo 0 al reducir el muestreo.
A continuación, procedemos a definir la red "final", que procesa las salidas convolucionales para convertirlas en un único valor similar a una probabilidad. Define una secuencia de operaciones que abarcan 'nn.AdaptiveAvgPool1D'; 'nn.Flatten'; 'nn.Linear'; y 'nn.Sigmoid'. Estas se encargan de: reducir la dimensión espacial a 1 mediante el cálculo de la media a lo largo de la secuencia; convertir el tensor en un vector unidimensional; asignar las características aplanadas a una única salida; y generar un valor comprendido entre 0 y 1, adecuado para la clasificación binaria o la regresión, respectivamente.
Este último paso combina las funcionalidades y produce un resultado específico para la tarea. Esto hace que el modelo sea utilizable para fines como la clasificación binaria o la estimación de probabilidades. En el caso de tareas multiclase, el reemplazo de las capas lineales y sigmoideas se puede realizar con `nn.Linear(in_channels, num_classes)` y `nn.Softmax()` respectivamente. La regresión requeriría la eliminación de la función sigmoide, ya que esta se ajusta mejor a la clasificación.
Dentro de nuestra clase, tenemos una función que calcula la similitud entre puntos de datos o vectores. Esta es la función '_rq_kernel'. Esta similitud se calcula, como cabría esperar, con el núcleo RQ. Los cálculos capturan la distancia euclidiana entre x y x prima. Se utiliza la fórmula RQ-Kernel que hemos compartido anteriormente, y disminuye a medida que aumenta la distancia bajo la moderación de los hiperparámetros alfa y de escala de longitud. El núcleo RQ cuantifica la similitud entre los datos de entrada, lo que sirve de base para el diseño de la arquitectura de la CNN.
Su flexibilidad radica en que puede capturar patrones tanto locales como globales. Un valor alfa mayor se aproxima a un núcleo gaussiano, mientras que un valor alfa menor ofrece mayor flexibilidad. También es importante ajustar la escala de longitud para controlar la desintegración, ya que los valores más pequeños enfatizan las similitudes locales.
Esto nos lleva a la función '_design_architecture' que presentamos en las notas anteriores. Diseña o establece la arquitectura de la CNN de forma dinámica utilizando el perfil de decaimiento del núcleo RQ. En primer lugar, esta función crea un rango normalizado de posiciones de entrada. Esta es la 'x'. A continuación, establecemos el punto de referencia para los cálculos de similitud, al que definimos como "centro". A continuación, calculamos las "respuestas", la respuesta del núcleo RQ para cada posición relativa al centro. A continuación, filtramos las posiciones para las respuestas del kernel que superan 0,2, centrándonos al mismo tiempo en las regiones significativas.
Una vez hecho esto, escalamos los tamaños de los núcleos en función de la posición y comenzando en 3. Esto aumenta con la distancia al "centro". En penúltimo lugar, asignamos más canales a las regiones con menor similitud, comenzando en 4. Finalmente, devolvemos la configuración de arquitectura que limita el diseño a tres capas.
Esta función adapta la CNN a la estructura de similitud de los datos. El uso de núcleos más grandes es beneficioso para patrones más amplios, y un mayor número de canales permite una extracción de características más compleja. El umbral de 0,2 y los factores de escala de 4 para los núcleos y de 60 para los canales son ajustables. Se puede aumentar el número de capas para tareas que requieren una extracción de características más profunda, pero esto debe equilibrarse con el coste computacional.
Finalmente, el método de paso hacia adelante concluye nuestra clase 'RationalQuadraticConv1D'. Realiza el cálculo del modelo desde la entrada hasta la salida. Comienza agregando una dimensión de canal a la entrada para hacerla compatible con 'nn.Conv1D'. A continuación, aplicamos cada capa convolucional seguida de una función de activación ReLU para introducir no linealidad. Luego, pasamos el resultado a través de las capas finales para obtener la salida final. Nuestra función de paso hacia adelante define el flujo de datos del modelo aprovechando una arquitectura diseñada dinámicamente para obtener las características utilizadas en la previsión.
Es fundamental asegurarse de que la forma de entrada esté en el formato (tamaño_lote, longitud_entrada) y que el tipo de activación utilizado sea adecuado para la tarea en cuestión. Las discrepancias en la forma de los datos pueden ser comunes en esta red, por eso es fundamental depurar imprimiendo la forma de los datos de entrada en cada paso. Ahora podemos analizar cómo configuramos las entradas de la red considerando las implementaciones de indicadores en Python para el MACD y el OBV.
El MACD
Nuestros indicadores están programados desde cero en Python, aunque existen bibliotecas y módulos que pueden abordar algunos de ellos. Esto permite una compilación y ejecución más fluidas y con menor consumo de memoria. El MACD calcula la línea MACD, la línea de señal y el histograma; todo lo cual ayuda a analizar el precio del activo. Lo implementamos en Python de la siguiente manera:
def MACD(df, fast_period: int = 12, slow_period: int = 26, signal_period: int = 9) -> pd.DataFrame: """ Calculate Moving Average Convergence Divergence (MACD) and append it to the input DataFrame. The MACD line is the difference between a fast and slow EMA. The Signal line is an EMA of the MACD line. The Histogram is the difference between the MACD and Signal lines. Args: df (pd.DataFrame): DataFrame with a 'close' column. fast_period (int): Lookback period for the fast EMA. Default is 12. slow_period (int): Lookback period for the slow EMA. Default is 26. signal_period (int): Lookback period for the Signal line EMA. Default is 9. Returns: pd.DataFrame: Input DataFrame with new 'MACD', 'Signal_Line', and 'MACD_Histogram' columns. """ # Input validation if 'close' not in df.columns: raise ValueError("DataFrame must contain a 'close' column") if not all(p > 0 for p in [fast_period, slow_period, signal_period]): raise ValueError("All period values must be positive integers") if fast_period >= slow_period: raise ValueError("fast_period must be less than slow_period") # Create a copy to avoid modifying the original DataFrame result_df = df.copy() # Calculate Fast and Slow Exponential Moving Averages (EMA) ema_fast = result_df['close'].ewm(span=fast_period, adjust=False).mean() ema_slow = result_df['close'].ewm(span=slow_period, adjust=False).mean() # Calculate MACD line result_df['MACD'] = ema_fast - ema_slow # Calculate Signal line (EMA of MACD) result_df['Signal_Line'] = result_df['MACD'].ewm(span=signal_period, adjust=False).mean() # Calculate MACD Histogram result_df['MACD_Histogram'] = result_df['MACD'] - result_df['Signal_Line'] return result_df
El formato de nuestra función sigue el que hemos estado utilizando, que consiste en tomar un marco de datos de pandas con una columna de precio de cierre y tres parámetros enteros opcionales: período rápido, al que se le asigna por defecto 12; período lento, al que se le asigna por defecto 26; y el período de la señal, al que se le asigna 9. El uso de un marco de datos en las entradas, como hemos estado haciendo, se realiza después de importar los datos de precios con el módulo MT5 de Python. Nuestra función devuelve una copia del marco de datos de entrada con columnas añadidas para los valores del MACD, la línea del MACD y la línea de señal.
Las primeras líneas de código comprueban que nuestro marco de datos de entrada tenga una columna de precio de cierre, que proporciona los precios de cierre necesarios para los cálculos de la EMA. Además, verificamos que todos los parámetros de período sean números enteros positivos, ya que apuntan a períodos de retrospectiva. Además, la validación comprueba que el período rápido sea menor que el período lento, asegurando que el MACD refleje las diferencias entre las tendencias a corto y largo plazo. Este paso de validación evita errores en tiempo de ejecución y garantiza que mantengamos la coherencia lógica. Siempre es importante evitar interpretaciones erróneas que puedan dar lugar a decisiones de trading desacertadas.
A continuación, creamos una copia del marco de datos de entrada que sirve para almacenar nuestros resultados, conservando al mismo tiempo los datos originales. Esta práctica preserva la integridad del marco de entrada, que puede utilizarse en múltiples funciones, cuyos análisis no necesitan verse alterados. En Python, garantiza la modularidad y la reutilización. Una vez creada una copia del marco de entrada, procedemos a utilizar la función de media móvil ponderada exponencial de pandas para calcular la EMA de los precios de cierre durante los períodos rápidos y lentos. Al hacerlo, asignamos el parámetro adjust a False, lo que garantiza que se utilice la fórmula EMA estándar y que 'span' establezca el tamaño de la ventana. La elección del "intervalo" influye directamente en la sensibilidad: los valores más pequeños hacen que el EMA sea más sensible, mientras que los valores más grandes suavizan el ruido.
Una vez que terminamos de calcular las EMA rápidas y lentas, asignamos nuestra primera serie nueva de pandas al marco de datos de salida, al que llamamos apropiadamente 'MACD'. Esto, en esencia, calcula la línea MACD como la diferencia entre las EMA rápidas y lentas. La línea MACD es el indicador principal que muestra la convergencia o divergencia de los movimientos de precios a largo y corto plazo. En resumen, un valor positivo indica una tendencia alcista, mientras que un valor negativo indica una tendencia bajista. El seguimiento de la dirección y la magnitud de la línea MACD puede servir para comprobar la fuerza de la tendencia. Los cruces pueden indicar cambios de tendencia.
A continuación, calculamos la serie pandas de la línea de señal para nuestro marco de datos de salida. Se trata de un cálculo de la línea de señal como una media móvil exponencial (EMA) de la línea MACD durante el "período de señal". Suaviza las líneas MACD y de señal para proporcionar una referencia para las señales de trading. Los puntos de cruce entre las líneas MACD y de señal también son puntos de decisión cruciales. Con un período predeterminado de 9, este margen equilibra la capacidad de respuesta y la suavización, pero aún podría ajustarse en función de las condiciones del activo negociado.
La última serie de pandas que añadimos al marco de datos de salida es el histograma MACD. Esto calcula la diferencia entre el búfer MACD y el búfer de la línea de señal. Este cálculo ayuda a visualizar la fuerza del impulso, donde los valores positivos indican una tendencia alcista y los negativos, una tendencia bajista. Además, los cambios de signo en el histograma pueden indicar inversiones. Una vez añadidas estas tres series de pandas al marco de datos de salida, el trabajo de nuestra función está completo y simplemente devuelve este marco de datos.
El OBV
Esta función calcula el On-Balance-Volume, un indicador de volumen que establece una analogía entre las variaciones de precio y el volumen. Lo implementamos en Python de la siguiente manera:
def OBV(df) -> pd.DataFrame: """ Calculate On-Balance Volume (OBV) and append it as an 'OBV' column to the input DataFrame. OBV is a momentum indicator that uses volume flow to predict changes in stock price. Args: df (pd.DataFrame): DataFrame with 'close' and 'volume' columns. Returns: pd.DataFrame: Input DataFrame with a new 'OBV' column. """ # Input validation if not all(col in df.columns for col in ['close', 'tick_volume']): raise ValueError("DataFrame must contain 'close' and 'volume' columns") # Create a copy to avoid modifying the original DataFrame result_df = df.copy() # print(result_df.columns) # Calculate the direction of price change # np.sign returns 1 for positive, -1 for negative, and 0 for zero change direction = np.sign(result_df['close'].diff()) # Calculate the OBV # Multiply the volume by the direction of price movement # Then calculate the cumulative sum obv = (result_df['tick_volume'] * direction).cumsum() # The first value of diff() is NaN, so the first OBV will be NaN. # We can fill it with the first day's volume or 0. A common practice is to start at 0. result_df['OBV'] = obv.fillna(0) return result_df
Nuestro código fuente anterior comienza validando el marco de datos para asegurarse de que tiene las columnas requeridas del precio de cierre y el volumen de "tick-volume". Nos basamos en el volumen de ticks, ya que este suele estar más disponible en la mayoría de las clases de activos, a diferencia del volumen real, que es difícil de obtener para los pares de divisas debido a su naturaleza descentralizada. Una vez completada esta validación, creamos una copia del marco de datos de entrada, ya que vamos a añadir una nueva columna al DataFrame de entrada y puede ser prudente preservar la integridad de la entrada, como ya se ha argumentado anteriormente con la función MACD.
Con esto, procedemos a calcular la "dirección" de los cambios en el precio de cierre. Estos cálculos se realizan durante períodos consecutivos. La función np.sign se utiliza para marcar -1, +1 o 0; para disminuciones de precio, aumentos de precio o ausencia de cambios de precio, respectivamente. Este paso es importante para establecer la dirección del movimiento de precios, lo que a su vez determina cómo se acumula el volumen en el OBV. Este paso también simplifica el análisis de tendencias de precios al convertirlo a un formato binario que permite una integración más sencilla con los datos de volumen.
Una vez definido el vector de dirección, procedemos a calcular el OBV, es decir, un producto entre el volumen y esta dirección con una suma acumulativa para realizar un seguimiento del OBV a lo largo del tiempo. OBV acumula los movimientos de precios basados en el volumen sumando el volumen en épocas alcistas y restándolo en épocas bajistas. Por lo tanto, el valor OBV puede reflejar la presión de compra o de venta. La tendencia del OBV es más importante que su valor absoluto. La comparación de su signo y la dirección de su precio puede ayudar a confirmar la tendencia.
La siguiente línea de código rellena el valor NaN de la primera entrada del OBV resultante de los cálculos de diff(), ya que el primer diff() produce un valor indefinido al inicio de la serie. Esta línea también agrega este valor OBV a una nueva serie OBV de pandas en nuestro marco de datos pandas de salida. La elección del OBV en el caso particular de este indicador es práctica porque 0 es un valor neutral que representa la ausencia de fluctuaciones de volumen en cualquier dirección. No equivale a asignar 0 al búfer de una media móvil.
Notas anteriores
Muchos de los patrones de señales del último artículo que introdujo esta combinación de indicadores MACD y OBV no resultaron rentables en el futuro. Solo el patrón 7 obtuvo resultados positivos en la validación walk-forward, lo que supone un rendimiento muy por debajo de lo esperado, teniendo en cuenta que, por lo general, al menos 6 de los 10 patrones analizados suelen obtener resultados positivos en las pruebas prospectivas. Por lo tanto, seleccionamos algunos de los casos con peor desempeño mencionados en el último artículo y vemos si, al aplicar el aprendizaje supervisado, podemos obtener mejores resultados. Para este artículo, hemos seleccionado 3 patrones de los 9 rezagados anteriores. Estos son el patrón 2, el patrón 3 y el patrón 5.
Patrón-2
Implementamos el patrón 2 en Python de la siguiente manera:
def feature_2(macd_df, obv_df, price_df): """ """ feature = np.zeros((len(macd_df), 2)) feature[:, 0] = ((price_df['low'] < price_df['low'].shift(1)) & (macd_df['MACD_Histogram'] > macd_df['MACD_Histogram'].shift(1)) & (obv_df['OBV'] > obv_df['OBV'].shift(1))).astype(int) feature[:, 1] = ((price_df['high'] > price_df['high'].shift(1)) & (macd_df['MACD_Histogram'] < macd_df['MACD_Histogram'].shift(1)) & (obv_df['OBV'] < obv_df['OBV'].shift(1))).astype(int) feature[0, :] = 0 feature[1, :] = 0 return feature
En resumen, se basa en la divergencia de la tendencia de precios, donde un precio mínimo más bajo y un MACD ascendente implican una divergencia alcista oculta, mientras que un precio máximo más alto junto con un MACD descendente también es indicativo de una divergencia bajista oculta. Esto se combina con la confirmación del volumen, donde un OBV en aumento confirma la acumulación, que es una señal alcista, mientras que un OBV en descenso marca la distribución, que es un indicador bajista. En resumen, este patrón busca validar la divergencia, que se detecta a partir del debilitamiento del impulso a pesar de los extremos de precios, con señales que muestran flujo de volumen.
Este patrón sirve, por lo tanto, para identificar patrones de continuación en las tendencias. Es más fiable en tendencias establecidas, como las tendencias alcistas y las tendencias bajistas. Además, suele requerir dos barras de confirmación antes de considerarse plenamente válida. Realizamos pruebas en una red CNN que utiliza el núcleo RQ para definir su arquitectura, tomando como entradas las señales generadas por este patrón desde el 1 de enero de 2023 hasta el 1 de enero de 2025, habiendo realizado entrenamiento y optimizaciones desde el 1 de enero de 2023 hasta el 1 de enero de 2024 en el par GBP/JPY en el marco temporal de 4 horas. Nuestro informe de pruebas es el siguiente:


Es evidente que se ha producido una mejora con respecto a los resultados obtenidos en el artículo anterior, por lo que podemos afirmar que nuestro modelo de aprendizaje supervisado ha supuesto una mejora apreciable. Como siempre, se espera que el lector realice una investigación exhaustiva y pruebas independientes antes de aplicar cualquier idea aquí presentada a cuentas reales. Aunque estos artículos están dirigidos al trading automatizado, puede resultar útil para hacerse una idea de cómo se ven realmente estos patrones en un gráfico. Con ese fin, a continuación se muestra la señal alcista para este patrón, el patrón 2.

Patrón-3
Implementamos este patrón en Python de la siguiente manera:
def feature_3(macd_df, obv_df, price_df): """ """ feature = np.zeros((len(macd_df), 2)) feature[:, 0] = ((macd_df['MACD_Histogram'] > macd_df['MACD_Histogram'].shift(1)) & (macd_df['MACD_Histogram'].shift(1) < macd_df['MACD_Histogram'].shift(2)) & (obv_df['OBV'] > obv_df['OBV'].shift(1))).astype(int) feature[:, 1] = ((macd_df['MACD_Histogram'] < macd_df['MACD_Histogram'].shift(1)) & (macd_df['MACD_Histogram'].shift(1) > macd_df['MACD_Histogram'].shift(2)) & (obv_df['OBV'] < obv_df['OBV'].shift(1))).astype(int) feature[0, :] = 0 feature[1, :] = 0 return feature
Este patrón busca cambios de tendencia en el histograma MACD, confirmándose una señal alcista con un mínimo, marcado por un aumento en el histograma tras un descenso. Por otro lado, la señal bajista es una confirmación de pico con un histograma descendente que había estado subiendo en las últimas barras. El histograma está diseñado para captar los puntos de inflexión del impulso. También requiere 3 barras para definir correctamente el patrón de valles y picos y reducir las señales falsas.
Paralelamente, el OBV proporcionaría una alineación de tendencias, donde la dirección del volumen debería coincidir con la tendencia del precio, con valores positivos que confirmen las señales alcistas y valores negativos que confirmen las bajistas. Esto confirma, sobre el papel, la participación institucional. Las pruebas realizadas en condiciones similares al patrón 2 anterior nos presentan el siguiente informe, que parece indicar una validación walk-forward:


Este patrón de señal es adecuado para cambios de tendencia y puede ser eficaz en puntos de inflexión de sobrecompra/sobreventa. Se requieren al menos 3 barras de historial de precios, por lo que las dos primeras barras de la serie se ponen a cero, ya que no tienen datos de comparación. Su representación en un gráfico puede ser la siguiente para una señal alcista.

Patrón-5
Codificamos este patrón en Python de la siguiente manera:
def feature_5(macd_df, obv_df, price_df): """ """ feature = np.zeros((len(macd_df), 2)) feature[:, 0] = ((macd_df['MACD_Histogram'] > macd_df['MACD_Histogram'].shift(1)) & (macd_df['MACD_Histogram'] < 0.0) & (obv_df['OBV'] > obv_df['OBV'].shift(1))).astype(int) feature[:, 1] = ((macd_df['MACD_Histogram'] < macd_df['MACD_Histogram'].shift(1)) & (macd_df['MACD_Histogram'] > 0.0) & (obv_df['OBV'] < obv_df['OBV'].shift(1))).astype(int) feature[0, :] = 0 feature[1, :] = 0 return feature
El patrón 5 se basa en el contexto de la línea cero del MACD. Su estrategia se centra en las entradas tempranas, considerándose una señal alcista cuando el MACD está por debajo de cero en territorio bajista, y una señal bajista cuando el MACD está por encima de cero. El uso de un indicador de terceros para señalar un cambio de impulso, tal como se indica en este patrón, puede ayudar a evitar señales falsas. El filtro de dirección del volumen también debería alinearse con el cambio de impulso del volumen, como confirmación del interés de la institución.
En este patrón, las señales alcistas tienden a desvanecerse en condiciones de sobreventa con un impulso creciente, mientras que las señales bajistas se desvanecen en situaciones de sobrecompra, también con un impulso debilitado. Este patrón es más efectivo en mercados laterales y requiere que el MACD esté en fase de pivote o en recuperación. Al probar este patrón después del entrenamiento y la optimización, como hemos hecho con los otros dos patrones anteriores, obtenemos el siguiente informe de prueba:


Parece ser que, de los 3 que se han probado aquí, este es el que todavía tiene dificultades para obtener resultados rentables en walk-forward. Esto podría deberse a lecturas erróneas del MACD, dado el momento temprano en que se realizó sin que se confirmaran los cambios de impulso independientes. Su patrón en el gráfico también se indica de la siguiente manera:

Conclusión
En resumen, hemos revisado la combinación de indicadores MACD y OBV que presentamos en el artículo anterior con la intención de ver si podíamos remediar el pésimo rendimiento de algunos de sus patrones de señal. Con una CNN de aprendizaje supervisado que utiliza el núcleo cuadrático racional para establecer el tamaño de su núcleo y de sus canales, ese parece ser el caso para 2 de los 3 patrones reexaminados. Atribuimos el mal desempeño del tercer patrón, el patrón 5, a puntos de entrada débiles que requieren una confirmación adicional.
| Nombre | Descripción |
|---|---|
| WZ-72.mq5 | Asesor Experto ensamblado con el asistente cuyo encabezado describe los archivos incluidos. |
| SignalWZ_72.mqh | Archivo de clase de señal personalizada para el ensamblado del asistente. |
| 72_2.onnx | Red exportada en Python para el patrón de señal 2. |
| 72_3.onnx | Red exportada en Python para el patrón de señal 3. |
| 72_5.onnx | Red exportada de Python para el patrón de señal 5. |
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/18697
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.
Utilizando redes neuronales en MetaTrader
Automatización de estrategias de trading en MQL5 (Parte 21): Mejora del trading basado en redes neuronales mediante tasas de aprendizaje adaptativas
Particularidades del trabajo con números del tipo double en MQL4
Aprendizaje automático y Data Science (Parte 45): Predicción de series temporales en Forex con el modelo Prophet de Facebook
- 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