Sistemas neurosimbólicos en trading algorítmico: Combinación de reglas simbólicas y redes neuronales
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:
- Frecuencia de aparición (frequency): cuántas veces se ha encontrado el patrón en la historia.
- Porcentaje de activaciones exitosas (winrate): frecuencia con la que, tras la pauta, el precio siguió la dirección prevista.
- 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:
- El tamaño de la ventana de análisis (para nosotros es de 10 pasos temporales)
- El número de señales básicas (precio, volumen, indicadores técnicos)
- 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
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.
Redes neuronales en el trading: Aprendizaje multitarea basado en el modelo ResNeXt
La estrategia comercial de captura de liquidez
Implementación del algoritmo criptográfico SHA-256 desde cero en MQL5
Implementación de los cierres parciales en MQL5
- 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
Artículo publicado Neurosymbolic Systems in Algorithm Trading: Combining Symbolic Rules and Neural Networks:
Autor: Yevgeniy Koshtenko