Sistemas neurossimbólicos no algotrading: Unindo regras simbólicas e redes neurais
Introdução aos sistemas neurossimbólicos: princípios da união entre regras e redes neurais
Imagine que você está tentando ensinar um computador a operar na bolsa. De um lado, temos regras clássicas e padrões, como os famosos "ombro-cabeça-ombro", "fundo duplo" e centenas de outras figuras conhecidas de qualquer trader. Muitos de nós já programamos EAs no MQL5 tentando codificar essas estruturas. Mas o mercado é um organismo vivo, está sempre mudando, e regras rígidas muitas vezes falham.
Do outro lado, temos as redes neurais, redes essas que são modernas, potentes, mas por vezes completamente opacas nas suas decisões. Alimente uma rede LSTM com dados históricos e ela vai gerar previsões com boa precisão. Mas por que ela tomou determinada decisão, geralmente permanece um mistério. E no trading, cada erro pode custar dinheiro de verdade.
Lembro de alguns anos atrás, quando eu travava essa luta no meu algoritmo de trading. Os padrões clássicos geravam muitos falsos positivos, enquanto a rede neural às vezes soltava previsões incríveis, mas sem qualquer lógica. E então tive um estalo: e se a gente unisse as duas abordagens? Usar regras claras como estrutura, o esqueleto do sistema, e a rede neural como o mecanismo adaptativo, que reage ao estado atual do mercado.
Assim nasceu a ideia do sistema neurossimbólico para algotrading. Pense nele como um trader experiente que conhece todas as figuras clássicas e regras, mas que também sabe se adaptar ao mercado, levando em conta nuances e correlações sutis. Um sistema desses tem um "esqueleto" de regras bem definidas e "músculos" na forma de uma rede neural, trazendo flexibilidade e adaptabilidade.
Neste artigo, vou mostrar como nossa equipe desenvolveu esse sistema em Python e explicar como unir a análise clássica de padrões com métodos modernos de aprendizado de máquina. Vamos explorar a arquitetura desde os componentes básicos até os mecanismos complexos de decisão e, claro, vou compartilhar código real e resultados de testes.
Pronto para mergulhar num mundo onde as regras clássicas do trading encontram as redes neurais? Então vamos nessa!
Regras simbólicas no trading: padrões e suas estatísticas
Vamos começar pelo básico: o que é um padrão no mercado? Na análise técnica clássica, é uma figura específica no gráfico, como um "fundo duplo" ou uma "bandeira". Mas quando falamos em programar sistemas de trading, precisamos pensar de forma mais abstrata. No nosso código, um padrão é uma sequência de movimentos de preço codificada em binário: 1 para alta, 0 para queda.
Parece primitivo? Nada disso. Essa representação nos dá uma ferramenta poderosa de análise. Pegue a sequência [1, 1, 0, 1, 0]. Isso não é só um monte de números, senão que é um mini-tendência codificada. No Python, podemos procurar por esses padrões com um código simples, porém eficiente:
pattern = tuple(np.where(data['close'].diff() > 0, 1, 0))
Mas a verdadeira mágica começa quando analisamos as estatísticas. Para cada padrão, podemos calcular três parâmetros chave:
- Frequência de ocorrência (frequency) — quantas vezes o padrão apareceu no histórico
- Taxa de acerto (winrate) — com que frequência, após o padrão, o preço seguiu na direção prevista
- Confiabilidade (reliability) — um indicador composto que leva em conta tanto a frequência quanto o winrate
Aqui vai um exemplo real da minha prática: o padrão [1, 1, 1, 0, 0] no gráfico de 4 horas do EURUSD mostrou um winrate de 68% com frequência de mais de 200 ocorrências ao longo de um ano. Soa promissor, certo? Mas é aqui que mora o perigo da reotimização.
Por isso, adicionamos um filtro dinâmico de confiabilidade:
reliability = frequency * winrate * (1 - abs(0.5 - winrate))
Essa fórmula é surpreendentemente simples. Ela não só leva em conta frequência e winrate, mas também penaliza padrões com desempenho suspeitamente alto — que geralmente não passam de uma anomalia estatística.
Outro ponto importante é o comprimento dos padrões. Padrões curtos (3–4 barras) são frequentes, mas geram muito ruído. Os longos (20–25 barras) são mais confiáveis, mas raros. O ponto ideal geralmente está entre 5 e 8 barras. Embora, admito, para alguns ativos eu vi excelentes resultados com padrões de 12 barras.
Um aspecto crucial é o horizonte de previsão. No nosso sistema usamos o parâmetro forecast_horizon, que define quantas barras à frente tentamos prever o movimento. Empiricamente, chegamos ao valor 6, pois ele oferece o melhor equilíbrio entre precisão na previsão e viabilidade de execução nas operações.
Mas o mais interessante acontece quando começamos a analisar padrões em diferentes condições de mercado. Um mesmo padrão pode se comportar de forma totalmente diferente dependendo da volatilidade ou do horário do dia. Por isso, a estatística simples é só o primeiro passo. É aí que entram as redes neurais, mas isso veremos na próxima parte.
Arquitetura da rede neural para análise de dados de mercado
Agora vamos dar uma olhada no "cérebro" do nosso sistema — a rede neural. Depois de muitos experimentos, optamos por uma arquitetura híbrida que combina camadas LSTM para lidar com séries temporais e camadas totalmente conectadas para processar os atributos estatísticos dos padrões.
Por que LSTM? Porque os dados de mercado não são apenas uma coleção de números, mas uma sequência, onde cada valor depende dos anteriores. Redes LSTM são excelentes para captar essas dependências de longo prazo. Veja como é a estrutura básica da nossa rede:
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') ])
Repare nas camadas Dropout, camadas essas que são a nossa defesa contra o sobreajuste. Nas versões iniciais do sistema, não usávamos isso, e a rede funcionava muito bem com dados históricos, mas "derretia" no mercado real. O Dropout desativa aleatoriamente parte dos neurônios durante o aprendizado, forçando a rede a encontrar padrões mais robustos.
Um ponto importante é a dimensionalidade dos dados de entrada. O parâmetro input_shape é definido por três fatores principais:
- Tamanho da janela de análise (no nosso caso, 10 passos temporais)
- Quantidade de atributos básicos (preço, volume, indicadores técnicos)
- Quantidade de atributos extraídos dos padrões
O resultado é um tensor com dimensão (batch_size, 10, features), onde features é o número total de atributos. Esse é exatamente o formato de dados esperado pela primeira camada LSTM.
Preste atenção ao parâmetro return_sequences=True na primeira camada LSTM. Isso significa que ela retorna uma sequência de saídas para cada passo de tempo, e não apenas a saída final. Isso permite que a segunda camada LSTM tenha acesso a uma visão mais detalhada da dinâmica temporal. Já essa segunda LSTM retorna apenas o estado final, e sua saída segue para as camadas totalmente conectadas.
As camadas Dense atuam como um "intérprete", transformando os padrões complexos encontrados pela LSTM em uma decisão concreta. A primeira camada Dense com ativação ReLU lida com as dependências não lineares, e a camada final, com ativação sigmoide, gera a probabilidade de movimento de preço para cima.
Vale destacar o processo de compilação do modelo:
model.compile( optimizer='adam', loss='binary_crossentropy', metrics=['accuracy', tf.keras.metrics.Precision(), tf.keras.metrics.Recall()] )
Usamos o otimizador Adam, já que ele tem bom desempenho com dados não estacionários, como é o caso dos preços de mercado. A função de perda binary crossentropy é perfeita para nossa tarefa de classificação binária (prever a direção do movimento de preço). E o conjunto de métricas nos ajuda a acompanhar não só a acurácia, mas também a qualidade das previsões em termos de falsos positivos e falsos negativos.
Durante o desenvolvimento, experimentamos diferentes configurações de rede. Testamos a adição de camadas convolucionais (CNN) para identificar padrões locais, experimentamos com o mecanismo de atenção (Attention), mas no fim das contas percebemos que a simplicidade e a transparência da arquitetura são mais importantes. Quanto mais complexa a rede, mais difícil se torna interpretar suas decisões; e no trading, entender a lógica por trás do sistema é absolutamente crucial.
Integração de padrões na rede neural: enriquecendo os dados de entrada
Agora vem a parte mais interessante: como "fundimos" os padrões clássicos com a rede neural. Não se trata apenas de concatenar atributos, e sim de um sistema completo de pré-processamento e análise dos dados.
Começamos com um conjunto básico de dados de entrada. Para cada ponto no tempo, formamos um vetor multidimensional de atributos que inclui:
base_features = [ 'close', # Close price 'volume', # Volume 'rsi', # Relative Strength Index 'macd', # MACD 'bb_upper', 'bb_lower' # Bollinger Bands borders ]
Mas isso é só o começo. A verdadeira inovação é a inclusão de estatísticas dos padrões. Para cada padrão, calculamos três indicadores principais:
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
}
Damos atenção especial à última métrica, a reliability. Essa é uma criação nossa, que leva em conta não só a frequência e o winrate, mas também a "suspeição" da estatística. Se o winrate estiver perto demais de 100% ou for muito volátil, o índice de confiabilidade é reduzido.
O processo de integrar esses dados à rede neural exige cuidado especial.
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))
A solução para o problema da variação no tamanho dos padrões:
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 padrão, independentemente do seu comprimento, é transformado em um vetor de dimensão fixa. Isso resolve o problema do número variável de padrões ativos e permite que a rede trabalhe com uma entrada de tamanho constante.
Um capítulo à parte é a consideração do contexto de mercado. Adicionamos atributos especiais que caracterizam o estado atual do 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
}
Isso ajuda o sistema a se adaptar a diferentes condições de mercado. Por exemplo, em períodos de alta volatilidade, aumentamos automaticamente os requisitos de confiabilidade dos padrões.
Um ponto importante é o tratamento de dados ausentes. No trading real, isso é um problema comum, especialmente ao lidar com múltiplos timeframes. Resolvemos isso com uma combinação 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
O resultado é que a rede neural recebe um conjunto de dados completo e coerente, onde os padrões técnicos clássicos se integram de forma natural com os indicadores de mercado básicos. Isso dá ao sistema uma vantagem única: ele pode se apoiar tanto em estruturas consagradas pelo tempo quanto em relações complexas descobertas durante o aprendizado.
Sistema de tomada de decisão: da análise aos sinais
Vamos falar sobre como o sistema realmente toma decisões. Esqueça por um momento as redes neurais e os padrões. No final do dia, precisamos de uma decisão clara: entrar no mercado ou não. E se entrar, com qual volume.
A lógica básica que seguimos é simples: usamos dois fluxos de dados, a previsão da rede neural e a estatística dos padrões. A rede neural nos dá a probabilidade de movimento para cima/baixo, e os padrões confirmam ou refutam essa previsão. Mas, como sempre, o diabo mora nos detalhes.
Veja o que acontece por baixo do capô:
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 )
A primeira coisa que verificamos são as condições básicas do mercado. Nada de rocket science aqui, e sim de bom senso:
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
Depois, vem a verificação da consistência dos sinais. Aqui há um ponto importante: não exigimos que todos os sinais estejam perfeitamente alinhados. Basta que os principais indicadores não entrem em contradição entre si:
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
A parte mais difícil é calcular a confiança no sinal. Após muitos testes e análise de diferentes abordagens, adotamos uma métrica combinada que leva em conta tanto a confiabilidade estatística da previsão da rede neural quanto a eficácia histórica dos padrões identificados:
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)
Essa arquitetura de tomada de decisão demonstra a eficácia do nosso modelo híbrido, onde métodos clássicos da análise técnica se integram de forma fluida com o aprendizado de máquina. Cada componente da estrutura contribui para a decisão final, e a presença de um sistema de verificações em múltiplos níveis garante o grau necessário de confiabilidade e resistência às diferentes condições do mercado.
Considerações finais
Unir padrões clássicos com análise baseada em redes neurais gera um resultado de outra ordem: a rede neural capta conexões de mercado muito sutis, enquanto os padrões testados ao longo do tempo fornecem uma estrutura confiável para a tomada de decisões de trading. Nos nossos testes, essa abordagem demonstrou resultados consistentemente superiores — tanto em comparação com a análise técnica pura quanto com o uso isolado de aprendizado de máquina.
Uma das descobertas mais importantes foi entender que simplicidade e interpretabilidade são cruciais. Fizemos questão de abandonar arquiteturas mais complexas em favor de um sistema claro e compreensível. Isso não só facilita o controle sobre as decisões de trading, como também permite ajustes rápidos conforme o mercado muda. Em um cenário onde muitos correm atrás da complexidade, foi a simplicidade que nos deu vantagem competitiva.
Espero que nossa experiência seja útil para quem também está explorando os limites do possível na fronteira entre o trading clássico e a inteligência artificial. Porque é justamente nessas áreas interdisciplinares que surgem as soluções mais interessantes e aplicáveis. Continuem experimentando, mas lembrem-se: no trading, não existe bala de prata. Só existe o caminho do desenvolvimento contínuo e da evolução constante das ferramentas.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/16894
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Dados de mercado sem intermediários: conectando MetaTrader 5 à MOEX via ISS API
Redes neurais em trading: Sistema multiagente com confirmação conceitual (Conclusão)
Algoritmo de Partenogênese Cíclica — Cyclic Parthenogenesis Algorithm (CPA)
Simulação de mercado: Iniciando o SQL no MQL5 (III)
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
Artigo publicado Neurosymbolic Systems in Algorithm Trading: Combining Symbolic Rules and Neural Networks:
Autor: Yevgeniy Koshtenko