English Русский 中文 Deutsch 日本語 Português
preview
Sistemas neurosimbólicos en trading algorítmico: Combinación de reglas simbólicas y redes neuronales

Sistemas neurosimbólicos en trading algorítmico: Combinación de reglas simbólicas y redes neuronales

MetaTrader 5Sistemas comerciales |
259 2
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Introducción a los sistemas neurosimbólicos: principios de combinación de reglas y redes neuronales

Imagínese lo que supone intentar explicarle a un ordenador cómo negociar en la bolsa. Por un lado, tenemos las reglas y pautas clásicas: la misma "cabeza y hombros", el "doble fondo" y cientos de otras figuras familiares para cualquier tráder. Muchos de nosotros hemos escrito asesores expertos en MQL5 tratando de codificar estos patrones. Pero el mercado es un organismo vivo que cambia constantemente, y las normas rígidas suelen fallar.

Por otra parte, están las redes neuronales: sofisticadas, potentes, pero a veces completamente opacas en sus decisiones. Si suministramos datos históricos a la red LSTM, esta hará predicciones con bastante precisión. Por qué la red toma tal o cual decisión resulta a menudo un misterio. Y en el trading, cada movimiento en falso puede costarnos dinero real.

Recuerdo luchar con este dilema en mi algoritmo comercial hace unos años. Los patrones clásicos daban falsos positivos, y la red neuronal ofrecía a veces predicciones increíbles sin ninguna lógica. Y entonces se me ocurrió: ¿y si combinamos los dos enfoques? El uso de reglas claras como estructura, un marco de sistema y una red neuronal como mecanismo adaptativo que tenga en cuenta el estado actual del mercado.

Así nació la idea de un sistema de neurosímbolos para el trading algorítmico. Imagínesela como un tráder experimentado que conoce todas las cifras y reglas clásicas, pero que también es capaz de ajustarse al mercado y considerar sutiles matices e interrelaciones. Un sistema así posee un "esqueleto" de reglas claras y un "músculo" en forma de red neuronal que le añade flexibilidad y adaptabilidad.

En este artículo, hablaremos de cómo mi equipo y yo desarrollamos un sistema de este tipo en Python y le mostraremos cómo combinar el análisis clásico de patrones con técnicas modernas de aprendizaje automático. Asimismo, recorreremos la arquitectura desde los componentes básicos hasta los complejos mecanismos de toma de decisiones y, por supuesto, compartiremos el código real y los resultados de pruebas.

¿Está listo para sumergirse en un mundo donde las reglas clásicas del trading se encuentran con las redes neuronales? ¡Entonces vamos a ello!


Reglas simbólicas en el comercio: los patrones y sus estadísticas

Empezaremos por algo sencillo: ¿qué es una pauta en el mercado? En el análisis técnico clásico, es una figura determinada del gráfico, como un doble fondo o una bandera. Pero cuando hablamos de programar sistemas comerciales, tenemos que pensar de forma más abstracta. En nuestro código, una pauta es una secuencia de movimientos de precios codificada de forma binaria: 1 para el ascenso, 0 para la caída.

Parece primitivo, ¿verdad? Pues no lo es en absoluto. Esta visión nos ofrece una poderosa herramienta de análisis. Tomemos la secuencia [1, 1, 1, 0, 1, 0]: no es solo un conjunto de números, sino una minitendencia codificada. En Python, podemos buscar esos patrones con un código sencillo pero eficiente:

pattern = tuple(np.where(data['close'].diff() > 0, 1, 0))

Sin embargo, la verdadera magia comienza cuando empezamos a analizar las estadísticas. Para cada patrón, podemos calcular tres parámetros clave:

  1. Frecuencia de aparición (frequency): cuántas veces se ha encontrado el patrón en la historia.
  2. Porcentaje de activaciones exitosas (winrate): frecuencia con la que, tras la pauta, el precio siguió la dirección prevista.
  3. Fiabilidad (reliability): es un indicador compuesto que tiene en cuenta tanto la frecuencia como el winrate.

Aquí tenemos un ejemplo real de mi práctica: el patrón [1, 1, 1, 1, 1, 0, 0] en el gráfico EURUSD de 4 horas ha mostrado una tasa de ganancias del 68% con una frecuencia de aparición de más de 200 veces en un año. Suena divertido, ¿verdad? Pero es importante no caer en la trampa de la sobreoptimización.

Para ello, hemos añadido un filtro de fiabilidad dinámico:

reliability = frequency * winrate * (1 - abs(0.5 - winrate))

Esta fórmula resulta asombrosa por su sencillez. No solo tiene en cuenta la frecuencia y el winrate, sino que también penaliza los patrones con una eficacia sospechosamente alta, que con frecuencia resultan ser una anomalía estadística.

Una historia aparte es la longitud de los patrones. Los patrones cortos (3-4 barras) son habituales, pero producen mucho ruido. Los más largos (20-25 barras) son más fiables, pero también más raros. La media dorada suele situarse entre 5 y 8 barras. Aunque, debo reconocerlo, para algunos instrumentos también he visto grandes resultados en patrones de 12 barras.

Lo importante es el horizonte de previsión. En nuestro sistema usamos el parámetro forecast_horizon, que determina cuántas barras por delante estamos intentando predecir el movimiento. Empíricamente, llegamos a un valor de 6, que ofrece un equilibrio óptimo entre la precisión de la previsión y las oportunidades comerciales.

Pero lo más interesante sucede cuando empezamos a analizar patrones en diferentes condiciones de mercado. El mismo patrón puede comportarse de manera muy diferente con distinta volatilidad o a distintas horas del día. Por eso las estadísticas a secas suponen solo el primer paso. A continuación entran en juego las redes neuronales, de las que hablaremos en el próximo apartado.


Arquitectura de redes neuronales para analizar datos de mercado

Analicemos ahora el "cerebro" de nuestro sistema: la red neuronal. Tras muchos experimentos, nos hemos decidido por una arquitectura híbrida que combina capas LSTM para procesar las series temporales y capas completamente conectadas para procesar las firmas estadísticas de patrones.

¿Por qué LSTM? El asunto es que los datos del mercado no suponen solo un conjunto de números, sino una secuencia en la que cada valor está relacionado con los anteriores. Y las redes LSTM resultan excelentes para captar estas dependencias a largo plazo. Así es la estructura básica de nuestra red:

model = tf.keras.Sequential([
    tf.keras.layers.LSTM(256, input_shape=input_shape, return_sequences=True),
    tf.keras.layers.Dropout(0.4),
    tf.keras.layers.LSTM(128),
    tf.keras.layers.Dropout(0.3),
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dense(1, activation='sigmoid')
])

Fíjese en las capas Dropout: es nuestra defensa contra el sobreentrenamiento. En las primeras versiones del sistema no las utilizábamos, y la red funcionaba bien con datos históricos, pero se "desorientaba" en el mercado real. El Dropout desconecta aleatoriamente algunas neuronas durante el entrenamiento, lo que obliga a la red a buscar patrones más robustos.

Un punto importante es la dimensionalidad de los datos de entrada. El parámetro input_shape viene definido por tres factores clave:

  1. El tamaño de la ventana de análisis (para nosotros es de 10 pasos temporales)
  2. El número de señales básicas (precio, volumen, indicadores técnicos)
  3. El número de características extraídas de los patrones

El resultado es un tensor de dimensionalidad (batch_size, 10, features), donde features es el número total de todas las características. Este es el formato de datos que espera la primera capa LSTM.

Observe el parámetro return_sequences=True en la primera capa LSTM. Esto significa que la capa retorna una secuencia de salidas para cada paso temporal, no solo para el último. Esto permite a la segunda capa LSTM obtener información más detallada sobre la dinámica temporal, mientras que la segunda LSTM ya solo da el estado final: su salida va a capas totalmente conectadas.

Las capas totalmente conectadas (Dense) cumplen el papel de "intérprete": traducen los patrones complejos encontrados por la LSTM en una solución concreta. La primera capa densa con activación ReLU se encarga de las dependencias no lineales, mientras que la capa final con activación sigmoide da la probabilidad de movimiento alcista de los precios.

El proceso de compilación de modelos merece especial atención:

model.compile(
    optimizer='adam',
    loss='binary_crossentropy',
    metrics=['accuracy', tf.keras.metrics.Precision(), tf.keras.metrics.Recall()]
)

Nosotros usamos el optimizador Adam, que ha demostrado su eficacia con datos no estacionarios, como los precios de mercado. La entropía cruzada binaria como función de pérdida resulta ideal para nuestra tarea de clasificación binaria (predecir la dirección de los precios). Y el conjunto de métricas, a su vez, ayuda a monitorear no solo la precisión, sino también la calidad de las predicciones en términos de falsos positivos y falsos negativos.

Durante el proceso de desarrollo, hemos experimentado con distintas configuraciones de red. Asimismo, hemos probado a añadir capas convolucionales (CNN) para detectar patrones locales, experimentando con el mecanismo de atención, pero al final hemos llegado a la conclusión de que la sencillez y la transparencia de la arquitectura resultan más importantes. Cuanto más compleja es la red, más difícil resulta interpretar sus decisiones, y en el comercio, comprender la lógica del sistema es algo fundamental.


Integración de patrones en una red neuronal: enriquecimiento de los datos de entrada

Ahora lo más interesante es cómo "cruzar" los patrones clásicos con una red neuronal. No se trata de una simple concatenación de características, sino de todo un sistema de preprocesamiento y análisis de datos.

Vamos a empezar con un conjunto básico de datos de entrada. Para cada punto temporal, generamos un vector de características multidimensional que incluirá:

base_features = [
    'close',  # Close price 
    'volume',  # Volume
    'rsi',    # Relative Strength Index
    'macd',   # MACD
    'bb_upper', 'bb_lower'  # Bollinger Bands borders
]

Pero eso sería solo el principio. La principal innovación consistiría en la incorporación de estadísticas de patrones. Para cada patrón, calculamos tres indicadores clave:

pattern_stats = {
    'winrate': np.mean(outcomes),  # Percentage of successful triggers
    'frequency': len(outcomes),     # Occurrence frequency
    'reliability': len(outcomes) * np.mean(outcomes) * (1 - abs(0.5 - np.mean(outcomes)))  # Reliability
}

Preste especial atención a la última métrica, la fiabilidad. Se trata de un desarrollo propio que tiene en cuenta no solo la frecuencia y el winrate, sino también lo "sospechoso" de las estadísticas. Si el porcentaje de victorias se acerca demasiado al 100% o resulta demasiado volátil, la puntuación de fiabilidad se reducirá.

El proceso de integración de estos datos en la red neuronal requiere de especial cuidado. 

def prepare_data(df):
    # We normalize the basic features using MinMaxScaler
    X_base = self.scaler.fit_transform(df[base_features].values)
    
    # For pattern statistics we use special normalization
    pattern_features = self.pattern_analyzer.extract_pattern_features(
        df, lookback=len(df)
    )
    
    return np.column_stack((X_base, pattern_features))

Resolución del problema de la diferente dimensionalidad de los patrones:

def extract_pattern_features(self, data, lookback=100):
    features_per_length = 5  # fixed number of features per pattern
    total_features = len(self.pattern_lengths) * features_per_length
    
    features = np.zeros((len(data) - lookback, total_features))
    # ... filling the feature array

Cada patrón, independientemente de su longitud, se convierte en un vector de dimensionalidad fija. Esto resuelve el problema del número variable de patrones activos y permite a la red neuronal trabajar con entradas de dimensionalidad constante.

Otra cosa que debemos considerar es el contexto del mercado. Nosotros añadimos características especiales que detallan el estado actual del mercado:

market_features = {
    'volatility': calculate_atr(data),  # Volatility via ATR
    'trend_strength': calculate_adx(data),  # Trend strength via ADX
    'market_phase': identify_market_phase(data)  # Market phase
}

Esto ayuda al sistema a adaptarse a las distintas condiciones. Por ejemplo, durante los periodos de volatilidad alta, aumentamos automáticamente nuestros requisitos de fiabilidad de los patrones.

El punto importante es el procesamiento de los datos que faltan. En el comercio real, este es un problema común, especialmente al trabajar con múltiples marcos temporales. Resolvemos esto usando una combinación de métodos:

# Fill in the blanks, taking into account the specifics of each feature
df['close'] = df['close'].fillna(method='ffill')  # for prices
df['volume'] = df['volume'].fillna(df['volume'].rolling(24).mean())  # for volumes
pattern_features = np.nan_to_num(pattern_features, nan=-1)  # for pattern features

Como resultado, la red neuronal recibe un conjunto de datos completo y coherente en el que los patrones técnicos clásicos complementan a la perfección los indicadores básicos de mercado. Esto confiere al sistema una ventaja única: puede basarse tanto en patrones probados a lo largo del tiempo como en relaciones complejas halladas durante el proceso de aprendizaje.


Sistema de toma de decisiones: del análisis a las señales

Ahora vamos a hablar de cómo el sistema toma realmente las decisiones. Olvídese por un momento de las neuronas y los patrones: al fin y al cabo, debemos tomar una decisión clara: entrar en el mercado o no. Y si entramos, con cuánto volumen.

Nuestra lógica básica es sencilla, así tomamos dos flujos de datos: la predicción de la red neuronal y las estadísticas del patrón. La red neuronal nos ofrece la probabilidad de un movimiento alcista o bajista, mientras que los patrones confirman o desmienten esta predicción. Pero el diablo, como siempre, está en los detalles.

Esto es lo que sucede:

def get_trading_decision(self, market_data):
    # Get a forecast from the neural network
    prediction = self.model.predict(market_data)
    
    # Extract active patterns
    patterns = self.pattern_analyzer.get_active_patterns(market_data)
    
    # Basic check of market conditions
    if not self._market_conditions_ok():
        return None  # Do not trade if something is wrong
        
    # Check the consistency of signals
    if not self._signals_aligned(prediction, patterns):
        return None  # No consensus - no deal
        
    # Calculate the signal confidence
    confidence = self._calculate_confidence(prediction, patterns)
    
    # Determine the position size
    size = self._get_position_size(confidence)
    
    return TradingSignal(
        direction='BUY' if prediction > 0.5 else 'SELL',
        size=size,
        confidence=confidence,
        patterns=patterns
    )

Lo primero que comprobamos son las condiciones básicas del mercado. Nada de ciencia espacial, solo sentido común:

def _market_conditions_ok(self):
    # Check the time
    if not self.is_trading_session():
        return False
        
    # Look at the spread
    if self.current_spread > self.MAX_ALLOWED_SPREAD:
        return False
        
    # Check volatility
    if self.current_atr > self.volatility_threshold:
        return False
    
    return True

El siguiente paso consistirá en comprobar la coherencia de las señales. Lo importante aquí es que no exigimos que todas las señales coincidan a la perfección. Bastará con que los principales indicadores no se contradigan entre sí:

def _signals_aligned(self, ml_prediction, pattern_signals):
    # Define the basic direction
    ml_direction = ml_prediction > 0.5
    
    # Count how many patterns confirm it
    confirming_patterns = sum(1 for p in pattern_signals 
                            if p.predicted_direction == ml_direction)
    
    # At least 60% of patterns need to be confirmed
    return confirming_patterns / len(pattern_signals) >= 0.6

Lo más difícil es calcular la confianza en la señal. Tras muchos experimentos y análisis de distintos enfoques, comenzamos a usar una métrica combinada que considera tanto la validez estadística de la predicción de la red neuronal como el rendimiento histórico de los patrones detectados:

def _calculate_confidence(self, prediction, patterns):
    # Baseline confidence from ML model
    base_confidence = abs(prediction - 0.5) * 2
    
    # Consider confirming patterns
    pattern_confidence = self._get_pattern_confidence(patterns)
    
    # Weighted average with empirically selected ratios
    return (base_confidence * 0.7 + pattern_confidence * 0.3)

Esta arquitectura de toma de decisiones demuestra la eficacia de un enfoque híbrido en el que los métodos clásicos de análisis técnico complementan a la perfección las capacidades de aprendizaje automático. Cada componente del sistema contribuye a la solución final, con un sistema escalonado de verificaciones para garantizar el grado necesario de fiabilidad y resistencia a las distintas condiciones del mercado.


Conclusión

Combinando los patrones clásicos con el análisis de redes neuronales hemos obtenido un resultado cualitativamente nuevo: la red neuronal capta las sutiles interrelaciones del mercado, mientras que los patrones probados a lo largo del tiempo proporcionan la estructura básica de las decisiones comerciales. En nuestras pruebas, este enfoque ha funcionado sistemáticamente mejor que los análisis puramente técnicos y las aplicaciones aisladas del aprendizaje automático.

Un descubrimiento importante ha sido darse cuenta de que la simplicidad y la interpretabilidad son cruciales. Hemos evitado deliberadamente arquitecturas más complejas en favor de un sistema transparente y comprensible. Esto permite no solo un mejor control de las decisiones comerciales, sino también la posibilidad de introducir ajustes rápidos cuando cambian las condiciones del mercado. En un mundo en el que muchos persiguen la complejidad, la sencillez ha demostrado ser nuestra ventaja competitiva.

Espero que nuestra experiencia resulte útil para quienes también exploran los límites de lo posible en la intersección del comercio clásico y la inteligencia artificial. Al fin y al cabo, es en estos campos interdisciplinarios donde suelen surgir las soluciones más interesantes y prácticas. Siga experimentando, pero recuerde que en el trading no existe una fórmula mágica. Solo existe el camino del desarrollo y la mejora constantes de las propias herramientas.

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/16894

Archivos adjuntos |
Evgeniy Chernish
Evgeniy Chernish | 22 ene 2025 en 09:25
El principal problema es la estabilidad de la frecuencia calculada de aparición de una vela blanca o negra tras la aparición de un patrón. En muestras pequeñas es poco fiable, y en muestras grandes es 50/50.

Y no entiendo la lógica de alimentar primero la frecuencia del patrón a neuronka como una de las características, y luego usar la misma frecuencia para filtrar las señales de neuronka construidas sobre ella.


Stanislav Korotky
Stanislav Korotky | 22 ene 2025 en 11:53
Sin tocar el planteamiento en sí, reducir las gamas reales de movimientos a dos clases anula la información útil que podría extraer la red neuronal (para lo que la atornillamos), algo parecido a si empezáramos a alimentar el sistema de reconocimiento de imágenes en color con imágenes en blanco y negro. En mi opinión, es necesario no ajustar la red a los antiguos métodos de patrones binarios, sino resaltar los reales y difusos sobre datos completos.
Redes neuronales en el trading: Aprendizaje multitarea basado en el modelo ResNeXt Redes neuronales en el trading: Aprendizaje multitarea basado en el modelo ResNeXt
El marco de aprendizaje multitarea basado en ResNeXt optimiza el análisis de datos financieros considerando su alta dimensionalidad, la no linealidad y las dependencias temporales. El uso de la convolución grupal y cabezas especializadas permite al modelo extraer eficazmente características clave de los datos de origen.
La estrategia comercial de captura de liquidez La estrategia comercial de captura de liquidez
La estrategia de negociación basada en la captura de liquidez es un componente clave de Smart Money Concepts (SMC), que busca identificar y aprovechar las acciones de los actores institucionales en el mercado. Implica apuntar a áreas de alta liquidez, como zonas de soporte o resistencia, donde las órdenes grandes pueden desencadenar movimientos de precios antes de que el mercado reanude su tendencia. Este artículo explica en detalle el concepto de «liquidity grab» (captura de liquidez) y describe el proceso de desarrollo de la estrategia de negociación basada en la captura de liquidez en MQL5.
Implementación del algoritmo criptográfico SHA-256 desde cero en MQL5 Implementación del algoritmo criptográfico SHA-256 desde cero en MQL5
La creación de integraciones de intercambio de criptomonedas sin DLL ha sido durante mucho tiempo un reto, pero esta solución proporciona un marco completo para la conectividad directa con el mercado.
Implementación de los cierres parciales en MQL5 Implementación de los cierres parciales en MQL5
En este artículo se desarrolla una clase para gestionar cierres parciales en MQL5 y se integra dentro de un EA de order blocks. Además, se presentan pruebas de backtest comparando la estrategia con y sin parciales, analizando en qué condiciones su uso puede maximizar y asegurar beneficios. Concluimos que especialmente en estilos de trading orientados a movimientos más amplios, el uso de parciales podría ser beneficioso.