Русский
preview
Sistemas neurossimbólicos no algotrading: Unindo regras simbólicas e redes neurais

Sistemas neurossimbólicos no algotrading: Unindo regras simbólicas e redes neurais

MetaTrader 5Sistemas de negociação |
102 2
Yevgeniy Koshtenko
Yevgeniy Koshtenko

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:

  1. Frequência de ocorrência (frequency) — quantas vezes o padrão apareceu no histórico
  2. Taxa de acerto (winrate) — com que frequência, após o padrão, o preço seguiu na direção prevista
  3. 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:

  1. Tamanho da janela de análise (no nosso caso, 10 passos temporais)
  2. Quantidade de atributos básicos (preço, volume, indicadores técnicos)
  3. 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',  # Цена закрытия
    'volume',  # Объём
    'rsi',    # Relative Strength Index
    'macd',   # MACD
    'bb_upper', 'bb_lower'  # Границы Bollinger Bands
]

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),  # Процент успешных сработок
    'frequency': len(outcomes),     # Частота появления
    'reliability': len(outcomes) * np.mean(outcomes) * (1 - abs(0.5 - np.mean(outcomes)))  # Надёжность
}

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):
    # Базовые признаки нормализуем через MinMaxScaler
    X_base = self.scaler.fit_transform(df[base_features].values)
    
    # Для статистик паттернов используем специальную нормализацию
    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  # фиксированное количество признаков на паттерн
    total_features = len(self.pattern_lengths) * features_per_length
    
    features = np.zeros((len(data) - lookback, total_features))
    # ... заполнение массива признаков

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),  # Волатильность через ATR
    'trend_strength': calculate_adx(data),  # Сила тренда через ADX
    'market_phase': identify_market_phase(data)  # Фаза рынка
}

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:

# Заполнение пропусков с учётом специфики каждого признака
df['close'] = df['close'].fillna(method='ffill')  # для цен
df['volume'] = df['volume'].fillna(df['volume'].rolling(24).mean())  # для объёмов
pattern_features = np.nan_to_num(pattern_features, nan=-1)  # для признаков паттернов

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):
    # Получаем прогноз от нейросети
    prediction = self.model.predict(market_data)
    
    # Вытаскиваем активные паттерны
    patterns = self.pattern_analyzer.get_active_patterns(market_data)
    
    # Базовая проверка условий рынка
    if not self._market_conditions_ok():
        return None  # Не торгуем если что-то не так
        
    # Проверяем согласованность сигналов
    if not self._signals_aligned(prediction, patterns):
        return None  # Нет консенсуса - нет сделки
        
    # Рассчитываем уверенность в сигнале
    confidence = self._calculate_confidence(prediction, patterns)
    
    # Определяем размер позиции
    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):
    # Проверяем время
    if not self.is_trading_session():
        return False
        
    # Смотрим спред
    if self.current_spread > self.MAX_ALLOWED_SPREAD:
        return False
        
    # Проверяем волатильность
    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):
    # Определяем базовое направление
    ml_direction = ml_prediction > 0.5
    
    # Считаем, сколько паттернов его подтверждают
    confirming_patterns = sum(1 for p in pattern_signals 
                            if p.predicted_direction == ml_direction)
    
    # Нужно подтверждение хотя бы 60% паттернов
    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):
    # Базовая уверенность от ML-модели
    base_confidence = abs(prediction - 0.5) * 2
    
    # Учитываем подтверждающие паттерны
    pattern_confidence = self._get_pattern_confidence(patterns)
    
    # Взвешенное среднее с эмпирически подобранными коэффициентами
    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

Arquivos anexados |
Últimos Comentários | Ir para discussão (2)
Evgeniy Chernish
Evgeniy Chernish | 22 jan. 2025 em 09:25
O principal problema é a estabilidade da frequência calculada de aparecimento de uma vela branca ou preta após o aparecimento de um padrão. Em amostras pequenas, ela não é confiável e, em amostras grandes, é 50/50.

E não entendo a lógica de primeiro alimentar o neuronka com a frequência do padrão como um dos recursos e, em seguida, usar a mesma frequência para filtrar os sinais do neuronka criados com base nela.


Stanislav Korotky
Stanislav Korotky | 22 jan. 2025 em 11:53
Sem tocar na abordagem em si, a redução dos intervalos reais de movimentos a duas classes anula as informações úteis que poderiam ser extraídas pela rede neural (para o bem da qual nós a parafusamos) - semelhante a se começássemos a alimentar o sistema de reconhecimento de imagens coloridas com imagens em preto e branco. Na minha opinião, é necessário não ajustar a rede aos métodos antigos de padrões binários, mas destacar os padrões reais e difusos em dados completos.
Dados de mercado sem intermediários: conectando MetaTrader 5 à MOEX via ISS API Dados de mercado sem intermediários: conectando MetaTrader 5 à MOEX via ISS API
Este artigo propõe uma solução para integrar o MetaTrader 5 com o serviço web ISS da MOEX. São fornecidas utilidades para geração automática de códigos-fonte com base no diretório da API e no índice dos principais elementos do serviço.
Redes neurais em trading: Sistema multiagente com confirmação conceitual (Conclusão) Redes neurais em trading: Sistema multiagente com confirmação conceitual (Conclusão)
Continuamos a implementação das abordagens propostas pelos autores do framework FinCon. O FinCon é um sistema multiagente baseado em grandes modelos de linguagem (LLM). Hoje vamos implementar os módulos necessários e realizar testes abrangentes do modelo com dados históricos reais.
Algoritmo de Partenogênese Cíclica — Cyclic Parthenogenesis Algorithm (CPA) Algoritmo de Partenogênese Cíclica — Cyclic Parthenogenesis Algorithm (CPA)
Neste artigo, vamos analisar um novo algoritmo populacional de otimização, o CPA (Cyclic Parthenogenesis Algorithm), inspirado na estratégia reprodutiva única dos pulgões. O algoritmo combina dois mecanismos de reprodução — partenogênese e sexual — e utiliza uma estrutura de colônia populacional com possibilidade de migração entre colônias. As principais características do algoritmo são a alternância adaptativa entre diferentes estratégias reprodutivas e o sistema de troca de informação entre colônias por meio do mecanismo de voo.
Simulação de mercado: Iniciando o SQL no MQL5 (III) Simulação de mercado: Iniciando o SQL no MQL5 (III)
No artigo anterior vimos como poderíamos desenvolver uma classe em MQL5, que seria capaz de nos dar algum suporte. Cuja finalidade, se dá justamente para que possamos colocar o código SQL dentro de um arquivo de script. Isto de forma que não precisaríamos, ter que digitar o mesmo código em uma string, no código MQL5. Mas apesar de daquela solução, ser funcional. Ela contem alguns detalhes, que podemos melhorar e devemos melhorar.