
Busca de padrões arbitrários em pares de moedas no Python com o uso do MetaTrader 5
Introdução à análise de padrões no Forex
O que um iniciante vê ao olhar os gráficos dos pares de moedas pela primeira vez? Uma série de oscilações intradiárias, aumento e enfraquecimento da volatilidade, trocas de tendência e muito mais. Como entender tudo isso: altas, quedas, zigue-zagues? Foi assim que iniciei meu mergulho no Forex e passei a estudar a análise de padrões de preços.
No nosso mundo, muitas coisas parecem caóticas à primeira vista. Porém, qualquer especialista experiente em uma área que parece confusa para os outros enxerga regularidades e oportunidades. O mesmo vale para os gráficos de pares de moedas. Ao sistematizarmos esse caos, podemos encontrar padrões ocultos que sugerem o próximo movimento do preço.
Mas como encontrá-los? Como diferenciar um padrão verdadeiro de um ruído aleatório? É aí que começa a parte mais interessante. Decidi criar meu próprio sistema de análise de padrões usando Python e MetaTrader 5. Uma espécie de simbiose entre matemática e programação para conquistar o Forex.
A ideia era a seguinte: estudar uma grande quantidade de dados históricos com um algoritmo capaz de encontrar padrões repetitivos e avaliar sua eficácia. Parece interessante? Na prática, porém, a implementação não foi tão simples.
Configuração do ambiente: instalação das bibliotecas necessárias e conexão ao MetaTrader 5
Então, nossa primeira tarefa é instalar o Python. Ele pode ser baixado do site oficial python.org. Baixe e instale. Não se esqueça de marcar a opção "Add Python to PATH".
O próximo passo importante são as bibliotecas. Precisamos de algumas. A principal é o MetaTrader 5. Assim como o pandas, para trabalhar com dados. Talvez também o Numpy. Abrimos o terminal e digitamos:
pip install MetaTrader5 pandas numpy matplotlib pytz
Primeiro, é preciso instalar o próprio MetaTrader 5. Baixando-o do site oficial ou do site da sua corretora e instalando-o. Não é nada complicado.
Agora é necessário encontrar o caminho para o terminal. Geralmente é algo como "C:\Program Files\MetaTrader 5\terminal64.exe". Guarde esse caminho, pois vamos precisar dele.
Agora abra o Python e digite:
import MetaTrader5 as mt5 if not mt5.initialize(path="C:/Program Files/MetaTrader 5/terminal64.exe"): print("MetaTrader 5 initialization failed.") mt5.shutdown() else: print("MetaTrader 5 initialized successfully.")
Execute e, se aparecer uma mensagem indicando que o terminal foi inicializado com sucesso, então fizemos tudo certo.
Quer verificar se tudo está funcionando? Vamos tentar obter alguns dados:
import MetaTrader5 as mt5 import pandas as pd from datetime import datetime if not mt5.initialize(): print("Oops! Something went wrong.") mt5.shutdown() eurusd_ticks = mt5.copy_ticks_from("EURUSD", datetime.now(), 10, mt5.COPY_TICKS_ALL) ticks_frame = pd.DataFrame(eurusd_ticks) print("Look at this beauty:") print(ticks_frame) mt5.shutdown()
Se você está vendo uma tabela com os dados, parabéns! Você acaba de dar o primeiro passo no mundo do trading algorítmico no Forex com a ajuda do Python. Não é tão complicado quanto parece.
Estrutura do código: funções principais e seus propósitos
Então, vamos começar a destrinchar a estrutura do código. Trata-se de um sistema completo para análise de padrões no mercado cambial.
Começamos com o principal da nossa estrutura: a função find_patterns. Ela percorre os dados históricos, identificando padrões com um comprimento definido. Depois de encontrá-los, é preciso avaliar sua eficácia. Ela também armazena o último padrão para uso posterior.
A próxima função é calculate_winrate_and_frequency. Essa função analisa as regularidades encontradas, considerando a frequência de ocorrência, o winrate e a ordenação dos padrões.
Outra função importante é a process_currency_pair. Trata-se de um processador bastante relevante. Ele carrega os dados, percorre os registros, busca padrões de diferentes tamanhos e também retorna os 300 melhores padrões para venda e compra. Quanto ao início do código, estão ali a inicialização, a configuração dos parâmetros, o intervalo do gráfico (TF) e o período de tempo (no meu caso, de 1990 até 2024).
Agora, vamos ao laço principal de execução do código. Entre as particularidades do algoritmo de busca de padrões, podemos destacar os diferentes comprimentos, pois os curtos são mais frequentes, mas pouco confiáveis, enquanto os longos são raros, porém mais eficazes. É necessário considerar todas as dimensões.
Recebendo dados do MetaTrader 5: função copy_rates_range
A primeira função que usamos é para obtenção de dados do terminal. Vamos dar uma olhada no código:
import MetaTrader5 as mt5 import pandas as pd import time from datetime import datetime, timedelta import pytz # List of major currency pairs major_pairs = ['EURUSD'] # Setting up data request parameters timeframe = mt5.TIMEFRAME_H4 start_date = pd.Timestamp('1990-01-01') end_date = pd.Timestamp('2024-05-31') def process_currency_pair(symbol): max_retries = 5 retries = 0 while retries < max_retries: try: # Loading OHLC data rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date) if rates is None: raise ValueError("No data received") ohlc_data = pd.DataFrame(rates) ohlc_data['time'] = pd.to_datetime(ohlc_data['time'], unit='s') break except Exception as e: print(f"Error loading data for {symbol}: {e}") retries += 1 time.sleep(2) # Wait before retrying if retries == max_retries: print(f"Failed to load data for {symbol} after {max_retries} attempts") return # Further data processing...
O que está acontecendo nesse trecho? Primeiro, definimos os pares de moedas. No momento, temos apenas EURUSD, mas você pode adicionar outros. Em seguida, configuramos o intervalo de tempo. H4 são 4 horas. Essa é a faixa de tempo ideal.
Depois, as datas. De 1990 a 2024. Precisamos de muitas cotações históricas. Quanto mais dados, mais preciso será nosso estudo. Agora vem a parte principal, nomeadamente a função process_currency_pair. Ela carrega os dados usando a copy_rates_range.
E o que obtemos ao final? Um DataFrame com os dados históricos. Hora, abertura, fechamento, máxima, mínima, isto é, tudo o que é necessário para o trabalho.
Caso algo dê errado, os erros são identificados, exibidos na tela, e tentamos novamente.
Processamento de séries temporais: conversão dos dados OHLC em direções de movimento de preço
Vamos voltar à nossa principal tarefa. A transformação das oscilações caóticas do mercado Forex em algo mais organizado, ou seja, a criação de tendências e reversões. Como fazemos isso? Com um esquema bastante compreensível: transformamos os preços em direções.
Aqui está nosso código:
# Fill missing values with the mean ohlc_data.fillna(ohlc_data.mean(), inplace=True) # Convert price movements to directions ohlc_data['direction'] = np.where(ohlc_data['close'].diff() > 0, 'up', 'down')
O que está acontecendo aqui? Primeiro, preenchemos os dados ausentes. Lacunas podem prejudicar bastante nosso resultado final. Nós as preenchemos com valores médios.
E agora vem a parte mais interessante. Criamos uma nova coluna chamada 'direction'. Com essa coluna, transformamos os dados de preços em dados que imitam o comportamento de tendências. Ela funciona de forma bem simples:
- Se o preço de fechamento atual for maior que o anterior, escrevemos "up".
- Se for menor, escrevemos "down".
É uma formulação bastante simples, mas eficaz. Agora, em vez de números complexos, temos uma sequência básica de "up" e "down". Essa sequência é muito mais fácil para o cérebro humano interpretar. Mas por que fazemos isso? Esses "up" e "down" são os blocos de construção dos nossos padrões. É com eles que vamos montar a imagem completa do que está acontecendo no mercado.
Algoritmo de busca de padrões: função find_patterns
Então, temos nossa sequência de "up" e "down". Agora, vamos buscar padrões repetitivos nessa sequência, os chamados padrões.
Aqui está a função find_patterns:
def find_patterns(data, pattern_length, direction): patterns = defaultdict(list) last_pattern = None last_winrate = None last_frequency = None for i in range(len(data) - pattern_length - 6): pattern = tuple(data['direction'][i:i+pattern_length]) if data['direction'][i+pattern_length+6] == direction: patterns[pattern].append(True) else: patterns[pattern].append(False) # Check last prices for pattern match last_pattern_tuple = tuple(data['direction'][-pattern_length:]) if last_pattern_tuple in patterns: last_winrate = np.mean(patterns[last_pattern_tuple]) * 100 last_frequency = len(patterns[last_pattern_tuple]) last_pattern = last_pattern_tuple return patterns, last_pattern, last_winrate, last_frequency
Como isso tudo funciona?
- Criamos um dicionário chamado patterns. Ele funciona como uma biblioteca onde armazenamos todos os padrões encontrados.
- Depois, começamos a iterar sobre os dados. Pegamos um trecho da sequência com tamanho pattern_length (pode ser 3, 4, 5 e assim por diante até 25) e observamos o que acontece 6 barras depois dele.
- Se, após 6 barras, o preço se move na direção desejada (ou seja, sobe para padrões de compra ou desce para padrões de venda), registramos True. Caso contrário, False.
- Fazemos isso para todos os exemplos possíveis de dados. Devemos obter combinações como: "up-up-down" - True, "down-up-up" - False, e assim por diante.
- Depois, verificamos se algum padrão já conhecido está se formando agora. Se estiver, calculamos seu winrate (percentual de acertos) e a frequência com que aparece.
Assim, transformamos uma sequência simples de 'up' e 'down' em uma ferramenta de previsão bastante eficaz. Mas ainda não terminamos. Em seguida, vamos filtrar esses padrões, selecionar os mais eficazes e analisá-los.
Cálculo das estatísticas dos padrões: WinRate e frequência de ocorrência
Agora que encontramos alguns padrões, precisamos selecionar os melhores.
Vamos dar uma olhada no código:
def calculate_winrate_and_frequency(patterns): results = [] for pattern, outcomes in patterns.items(): winrate = np.mean(outcomes) * 100 frequency = len(outcomes) results.append((pattern, winrate, frequency)) results.sort(key=lambda x: x[1], reverse=True) return results
Aqui, pegamos cada padrão e seus resultados (que mencionamos antes como True e False), e então calculamos a taxa de sucesso, também conhecida como winrate. Se um padrão funcionou sete vezes em dez, sua taxa de vitória é de 70%. Também calculamos a frequência, ou seja, o número de vezes que o padrão apareceu. Quanto mais frequente, mais confiável a estatística. Reunimos tudo isso na lista results. Por fim, ordenamos os dados. Os melhores padrões vão para o início da lista.
Filtragem dos resultados: seleção dos padrões mais relevantes
Agora temos uma quantidade suficiente de dados. Mas nem todos serão úteis, por isso precisamos aplicar uma filtragem.
filtered_buy_results = [result for result in all_buy_results if result[2] > 20] filtered_sell_results = [result for result in all_sell_results if result[2] > 20] filtered_buy_results.sort(key=lambda x: x[1], reverse=True) top_300_buy_patterns = filtered_buy_results[:300] filtered_sell_results.sort(key=lambda x: x[1], reverse=True) top_300_sell_patterns = filtered_sell_results[:300]
Fazemos essa filtragem da seguinte maneira. Primeiro, descartamos todos os padrões que ocorreram menos de 20 vezes. Como mostra a estatística, padrões raros são menos confiáveis.
Depois, ordenamos os padrões restantes pelo winrate. Os mais eficazes aparecem no topo da lista. No final, selecionamos os 300 melhores. É isso que deve restar do conjunto inicial, que ultrapassava mil padrões.
Trabalho com diferentes comprimentos de padrões: de 3 a 25
Agora precisamos selecionar as variações de padrões que de forma estatística e constante tragam lucro nas negociações. As variações diferem em comprimento. Podem ser compostas por 3 até 25 movimentos de preço. Vamos testar todas as possibilidades:
pattern_lengths = range(3, 25) # Pattern lengths from 3 to 25 all_buy_patterns = {} all_sell_patterns = {} for pattern_length in pattern_lengths: buy_patterns, last_buy_pattern, last_buy_winrate, last_buy_frequency = find_patterns(ohlc_data, pattern_length, 'up') sell_patterns, last_sell_pattern, last_sell_winrate, last_sell_frequency = find_patterns(ohlc_data, pattern_length, 'down') all_buy_patterns[pattern_length] = buy_patterns all_sell_patterns[pattern_length] = sell_patterns
Executamos nosso filtro de busca de padrões para cada comprimento, de 3 a 25. Por que exatamente assim? Padrões com menos de três movimentos são pouco confiáveis, como já discutimos antes. E os que têm mais de 25 movimentos ocorrem com extrema raridade. Para cada comprimento, buscamos padrões tanto de compra quanto de venda.
Mas por que precisamos de tantos comprimentos diferentes? Padrões curtos podem captar reversões rápidas do mercado, enquanto os longos podem indicar tendências de longo prazo. Ainda não sabemos de antemão o que será mais eficaz, então testamos tudo.
Análise dos padrões de compra e venda
Agora que temos uma seleção de padrões de diferentes comprimentos, é hora de descobrir quais realmente funcionam.
Aqui está nosso código em ação:
all_buy_results = [] for pattern_length, patterns in all_buy_patterns.items(): results = calculate_winrate_and_frequency(patterns) all_buy_results.extend(results) all_sell_results = [] for pattern_length, patterns in all_sell_patterns.items(): results = calculate_winrate_and_frequency(patterns) all_sell_results.extend(results)
Pegamos cada padrão — tanto de compra quanto de venda — e filtramos com nossa função de cálculo de winrate e frequência.
Mas não estamos apenas calculando estatísticas. Estamos comparando padrões de compra com os de venda. Por que isso importa? Porque o mercado pode se comportar de maneiras diferentes durante altas e quedas. Às vezes os padrões de compra funcionam melhor, outras vezes são os de venda que se destacam.
Em seguida, partimos para o próximo passo, comparamos padrões de diferentes comprimentos entre si. Pode ser que padrões curtos funcionem melhor para identificar pontos de entrada no mercado, enquanto os longos sejam mais úteis para prever tendências duradouras. No entanto, o inverso também pode acontecer. Por isso, analisamos tudo e não descartamos nada antecipadamente.
Ao final dessa análise, temos os primeiros resultados formados: quais padrões funcionam melhor para compra, quais para venda, e quais comprimentos de padrão são mais eficazes em diferentes condições de mercado. Com esses dados, já podemos fazer uma análise de preços no mercado Forex.
Mas lembrem-se, amigos, que nem mesmo o melhor dos padrões é garantia de sucesso. O mercado está cheio de surpresas. Nossa missão é aumentar as chances de acerto, e é exatamente isso que fazemos ao analisar os padrões em todos os ângulos.
Olhando para o futuro: previsão com base nos últimos padrões
Agora chegou a hora dos prognósticos concretos. Vamos dar uma olhada no nosso código-oráculo:
if last_buy_pattern: print(f"\nLast buy pattern for {symbol}: {last_buy_pattern}, Winrate: {last_buy_winrate:.2f}%, Frequency: {last_buy_frequency}") print(f"Forecast: Price will likely go up.") if last_sell_pattern: print(f"\nLast sell pattern for {symbol}: {last_sell_pattern}, Winrate: {last_sell_winrate:.2f}%, Frequency: {last_sell_frequency}") print(f"Forecast: Price will likely go down.")
Observamos o último padrão que se formou e tentamos prever o que vem pela frente. Observamos o último padrão identificado e fazemos nossa análise de trading.
Preste atenção, consideramos dois cenários: padrão de compra e padrão de venda. Por quê? Porque o mercado é um confronto constante entre touros e ursos, compradores e vendedores. Precisamos estar preparados para qualquer virada.
Para cada padrão, exibimos três parâmetros-chave: o padrão em si, seu winrate e a frequência com que aparece. O winrate é especialmente importante. Se um padrão de compra tem 70% de winrate, isso significa que em 70% dos casos, após ele surgir, o preço realmente subiu. São resultados bastante interessantes. Mas lembre-se, nem mesmo 90% é uma garantia. No mundo do Forex sempre há espaço para o inesperado.
A frequência também é essencial. Um padrão que aparece com frequência é mais confiável do que um raro.
Uma parte bem interessante é o nosso prognóstico. “Price will likely go up” ou “Price will likely go down”. Essas previsões trazem certa satisfação pelo trabalho feito. No entanto, lembrem-se, meus amigos, mesmo a previsão mais precisa ainda é apenas uma probabilidade, não uma certeza. O mercado Forex é extremamente difícil de prever. Notícias, eventos econômicos, até mesmo tuítes de pessoas influentes podem mudar a direção do preço em questão de segundos.
Por isso, nosso código não é uma cura milagrosa, mas sim um EA muito inteligente. Ele pode ser interpretado da seguinte forma: “Veja, com base nos dados históricos, temos razões para acreditar que o preço irá subir (ou cair).” Mas a decisão de entrar ou não no mercado é sua. Usar essas previsões exige reflexão. Você tem informações sobre os possíveis movimentos, mas cada passo ainda precisa ser dado com sabedoria, levando em conta o cenário geral do mercado.
Desenhando o futuro: visualização dos melhores padrões e previsões
Vamos adicionar um pouco de mágica visual ao nosso código:
import matplotlib.pyplot as plt def visualize_patterns(patterns, title, filename): patterns = patterns[:20] # Take top 20 for clarity patterns.reverse() # Reverse the list to display it correctly on the chart fig, ax = plt.subplots(figsize=(12, 8)) winrates = [p[1] for p in patterns] frequencies = [p[2] for p in patterns] labels = [' '.join(p[0]) for p in patterns] ax.barh(range(len(patterns)), winrates, align='center', color='skyblue', zorder=10) ax.set_yticks(range(len(patterns))) ax.set_yticklabels(labels) ax.invert_yaxis() # Invert the Y axis to display the best patterns on top ax.set_xlabel('Winrate (%)') ax.set_title(title) # Add occurrence frequency for i, v in enumerate(winrates): ax.text(v + 1, i, f'Freq: {frequencies[i]}', va='center') plt.tight_layout() plt.savefig(filename) plt.close() # Visualize top buy and sell patterns visualize_patterns(top_300_buy_patterns, f'Top 20 Buy Patterns for {symbol}', 'top_buy_patterns.png') visualize_patterns(top_300_sell_patterns, f'Top 20 Sell Patterns for {symbol}', 'top_sell_patterns.png') # Visualize the latest pattern and forecast def visualize_forecast(pattern, winrate, frequency, direction, symbol, filename): fig, ax = plt.subplots(figsize=(8, 6)) ax.bar(['Winrate'], [winrate], color='green' if direction == 'up' else 'red') ax.set_ylim(0, 100) ax.set_ylabel('Winrate (%)') ax.set_title(f'Forecast for {symbol}: Price will likely go {direction}') ax.text(0, winrate + 5, f'Pattern: {" ".join(pattern)}', ha='center') ax.text(0, winrate - 5, f'Frequency: {frequency}', ha='center') plt.tight_layout() plt.savefig(filename) plt.close() if last_buy_pattern: visualize_forecast(last_buy_pattern, last_buy_winrate, last_buy_frequency, 'up', symbol, 'buy_forecast.png') if last_sell_pattern: visualize_forecast(last_sell_pattern, last_sell_winrate, last_sell_frequency, 'down', symbol, 'sell_forecast.png')
Criei duas funções: visualize_patterns e visualize_forecast. A primeira desenha um gráfico de barras horizontal informativo com os 20 principais padrões, seus winrates e frequência de ocorrência. A segunda cria uma visualização clara da nossa previsão com base no último padrão.
Para os padrões, utilizamos barras horizontais, já que os padrões podem ser longos, e assim fica mais fácil de ler. A cor escolhida é agradável para o olho humano, um azul celeste.
Salvamos nossas obras-primas em arquivos PNG.
Testes e backtesting do sistema de análise de padrões
Criamos nosso sistema de análise de padrões, mas como saber se ele realmente funciona? Para isso, precisamos testá-lo com dados históricos.
Aqui está o código necessário para essa tarefa:
def simulate_trade(data, direction, entry_price, take_profit, stop_loss): for i, row in data.iterrows(): current_price = row['close'] if direction == "BUY": if current_price >= entry_price + take_profit: return {'profit': take_profit, 'duration': i} elif current_price <= entry_price - stop_loss: return {'profit': -stop_loss, 'duration': i} else: # SELL if current_price <= entry_price - take_profit: return {'profit': take_profit, 'duration': i} elif current_price >= entry_price + stop_loss: return {'profit': -stop_loss, 'duration': i} # If the loop ends without reaching TP or SL, close at the current price last_price = data['close'].iloc[-1] profit = (last_price - entry_price) if direction == "BUY" else (entry_price - last_price) return {'profit': profit, 'duration': len(data)} def backtest_pattern_system(data, buy_patterns, sell_patterns): equity_curve = [10000] # Начальный капитал $10,000 trades = [] for i in range(len(data) - max(len(p[0]) for p in buy_patterns + sell_patterns)): current_data = data.iloc[:i+1] last_pattern = tuple(current_data['direction'].iloc[-len(buy_patterns[0][0]):]) matching_buy = [p for p in buy_patterns if p[0] == last_pattern] matching_sell = [p for p in sell_patterns if p[0] == last_pattern] if matching_buy and not matching_sell: entry_price = current_data['close'].iloc[-1] take_profit = 0.001 # 10 pips stop_loss = 0.0005 # 5 pips trade_result = simulate_trade(data.iloc[i+1:], "BUY", entry_price, take_profit, stop_loss) trades.append(trade_result) equity_curve.append(equity_curve[-1] + trade_result['profit'] * 10000) # Multiply by 10000 to convert to USD elif matching_sell and not matching_buy: entry_price = current_data['close'].iloc[-1] take_profit = 0.001 # 10 pips stop_loss = 0.0005 # 5 pips trade_result = simulate_trade(data.iloc[i+1:], "SELL", entry_price, take_profit, stop_loss) trades.append(trade_result) equity_curve.append(equity_curve[-1] + trade_result['profit'] * 10000) # Multiply by 10000 to convert to USD else: equity_curve.append(equity_curve[-1]) return equity_curve, trades # Conduct a backtest equity_curve, trades = backtest_pattern_system(ohlc_data, top_300_buy_patterns, top_300_sell_patterns) # Visualizing backtest results plt.figure(figsize=(12, 6)) plt.plot(equity_curve) plt.title('Equity Curve') plt.xlabel('Trades') plt.ylabel('Equity ($)') plt.savefig('equity_curve.png') plt.close() # Calculating backtest statistics total_profit = equity_curve[-1] - equity_curve[0] win_rate = sum(1 for trade in trades if trade['profit'] > 0) / len(trades) if trades else 0 average_profit = sum(trade['profit'] for trade in trades) / len(trades) if trades else 0 print(f"\nBacktest Results:") print(f"Total Profit: ${total_profit:.2f}") print(f"Win Rate: {win_rate:.2%}") print(f"Average Profit per Trade: ${average_profit*10000:.2f}") print(f"Total Trades: {len(trades)}")
O que está acontecendo aqui? A função simulate_trade é nosso simulador de operação individual. Ela acompanha o preço e encerra a operação ao atingir o take profit ou o stop loss.
Já a backtest_pattern_system é uma função ainda mais importante. Ela percorre os dados históricos, passo a passo, dia após dia, verificando se algum dos nossos padrões se formou. Detectou um padrão de compra? Compramos. Um de venda? Vendemos.
Usamos take profit fixo de 100 pontos e stop loss de 50 pontos. Isso porque precisamos definir um limite aceitável de lucro, nem muito alto para evitar riscos excessivos, mas também não muito baixo, para permitir que o lucro se desenvolva.
Após cada operação, atualizamos nossa curva de capital. No final do processo, obtemos o resultado: quanto ganhamos no total, qual o percentual de operações lucrativas, qual o lucro médio por operação. E, claro, visualizamos os resultados.
Implementamos a busca de padrões usando os recursos da linguagem MQL5. Aqui está nosso código:
//+------------------------------------------------------------------+ //| PatternProbabilityIndicator| //| Copyright 2024 | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, Your Name Here" #property link "https://www.mql5.com" #property version "1.06" #property indicator_chart_window #property indicator_buffers 2 #property indicator_plots 2 //--- plot BuyProbability #property indicator_label1 "BuyProbability" #property indicator_type1 DRAW_LINE #property indicator_color1 clrGreen #property indicator_style1 STYLE_SOLID #property indicator_width1 2 //--- plot SellProbability #property indicator_label2 "SellProbability" #property indicator_type2 DRAW_LINE #property indicator_color2 clrRed #property indicator_style2 STYLE_SOLID #property indicator_width2 2 //--- input parameters input int InpPatternLength = 5; // Pattern Length (3-10) input int InpLookback = 1000; // Lookback Period (100-5000) input int InpForecastHorizon = 6; // Forecast Horizon (1-20) //--- indicator buffers double BuyProbabilityBuffer[]; double SellProbabilityBuffer[]; //--- global variables int g_pattern_length; int g_lookback; int g_forecast_horizon; string g_patterns[]; int g_pattern_count; int g_pattern_occurrences[]; int g_pattern_successes[]; int g_total_bars; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- validate inputs if(InpPatternLength < 3 || InpPatternLength > 10) { Print("Invalid Pattern Length. Must be between 3 and 10."); return INIT_PARAMETERS_INCORRECT; } if(InpLookback < 100 || InpLookback > 5000) { Print("Invalid Lookback Period. Must be between 100 and 5000."); return INIT_PARAMETERS_INCORRECT; } if(InpForecastHorizon < 1 || InpForecastHorizon > 20) { Print("Invalid Forecast Horizon. Must be between 1 and 20."); return INIT_PARAMETERS_INCORRECT; } //--- indicator buffers mapping SetIndexBuffer(0, BuyProbabilityBuffer, INDICATOR_DATA); SetIndexBuffer(1, SellProbabilityBuffer, INDICATOR_DATA); //--- set accuracy IndicatorSetInteger(INDICATOR_DIGITS, 2); //--- set global variables g_pattern_length = InpPatternLength; g_lookback = InpLookback; g_forecast_horizon = InpForecastHorizon; //--- generate all possible patterns if(!GeneratePatterns()) { Print("Failed to generate patterns."); return INIT_FAILED; } g_total_bars = iBars(_Symbol, PERIOD_CURRENT); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- check for rates total if(rates_total <= g_lookback + g_pattern_length + g_forecast_horizon) { Print("Not enough data for calculation."); return 0; } int start = (prev_calculated > g_lookback + g_pattern_length + g_forecast_horizon) ? prev_calculated - 1 : g_lookback + g_pattern_length + g_forecast_horizon; if(ArraySize(g_pattern_occurrences) != g_pattern_count) { ArrayResize(g_pattern_occurrences, g_pattern_count); ArrayResize(g_pattern_successes, g_pattern_count); } ArrayInitialize(g_pattern_occurrences, 0); ArrayInitialize(g_pattern_successes, 0); // Pre-calculate patterns for efficiency string patterns[]; ArrayResize(patterns, rates_total); for(int i = g_pattern_length; i < rates_total; i++) { patterns[i] = ""; for(int j = 0; j < g_pattern_length; j++) { patterns[i] += (close[i-j] > close[i-j-1]) ? "U" : "D"; } } // Main calculation loop for(int i = start; i < rates_total; i++) { string current_pattern = patterns[i]; if(StringLen(current_pattern) != g_pattern_length) continue; double buy_probability = CalculateProbability(current_pattern, true, close, patterns, i); double sell_probability = CalculateProbability(current_pattern, false, close, patterns, i); BuyProbabilityBuffer[i] = buy_probability; SellProbabilityBuffer[i] = sell_probability; } // Update Comment with pattern statistics if total bars changed if(g_total_bars != iBars(_Symbol, PERIOD_CURRENT)) { g_total_bars = iBars(_Symbol, PERIOD_CURRENT); UpdatePatternStatistics(); } return(rates_total); } //+------------------------------------------------------------------+ //| Generate all possible patterns | //+------------------------------------------------------------------+ bool GeneratePatterns() { g_pattern_count = (int)MathPow(2, g_pattern_length); if(!ArrayResize(g_patterns, g_pattern_count)) { Print("Failed to resize g_patterns array."); return false; } for(int i = 0; i < g_pattern_count; i++) { string pattern = ""; for(int j = 0; j < g_pattern_length; j++) { pattern += ((i >> j) & 1) ? "U" : "D"; } g_patterns[i] = pattern; } return true; } //+------------------------------------------------------------------+ //| Calculate probability for a given pattern | //+------------------------------------------------------------------+ double CalculateProbability(const string &pattern, bool is_buy, const double &close[], const string &patterns[], int current_index) { if(StringLen(pattern) != g_pattern_length || current_index < g_lookback) { return 50.0; // Return neutral probability on error } int pattern_index = ArraySearch(g_patterns, pattern); if(pattern_index == -1) { return 50.0; } int total_occurrences = 0; int successful_predictions = 0; for(int i = g_lookback; i > g_pattern_length + g_forecast_horizon; i--) { int historical_index = current_index - i; if(historical_index < 0 || historical_index + g_pattern_length + g_forecast_horizon >= ArraySize(close)) { continue; } if(patterns[historical_index] == pattern) { total_occurrences++; g_pattern_occurrences[pattern_index]++; if(is_buy && close[historical_index + g_pattern_length + g_forecast_horizon] > close[historical_index + g_pattern_length]) { successful_predictions++; g_pattern_successes[pattern_index]++; } else if(!is_buy && close[historical_index + g_pattern_length + g_forecast_horizon] < close[historical_index + g_pattern_length]) { successful_predictions++; g_pattern_successes[pattern_index]++; } } } return (total_occurrences > 0) ? (double)successful_predictions / total_occurrences * 100 : 50; } //+------------------------------------------------------------------+ //| Update pattern statistics and display in Comment | //+------------------------------------------------------------------+ void UpdatePatternStatistics() { string comment = "Pattern Statistics:\n"; comment += "Pattern Length: " + IntegerToString(g_pattern_length) + "\n"; comment += "Lookback Period: " + IntegerToString(g_lookback) + "\n"; comment += "Forecast Horizon: " + IntegerToString(g_forecast_horizon) + "\n\n"; comment += "Top 5 Patterns:\n"; int sorted_indices[]; ArrayResize(sorted_indices, g_pattern_count); for(int i = 0; i < g_pattern_count; i++) sorted_indices[i] = i; // Use quick sort for better performance ArraySort(sorted_indices); for(int i = 0; i < 5 && i < g_pattern_count; i++) { int idx = sorted_indices[g_pattern_count - 1 - i]; // Reverse order for descending sort double win_rate = g_pattern_occurrences[idx] > 0 ? (double)g_pattern_successes[idx] / g_pattern_occurrences[idx] * 100 : 0; comment += g_patterns[idx] + ": " + "Occurrences: " + IntegerToString(g_pattern_occurrences[idx]) + ", " + "Win Rate: " + DoubleToString(win_rate, 2) + "%\n"; } Comment(comment); } //+------------------------------------------------------------------+ //| Custom function to search for a string in an array | //+------------------------------------------------------------------+ int ArraySearch(const string &arr[], string value) { for(int i = 0; i < ArraySize(arr); i++) { if(arr[i] == value) return i; } return -1; }
Como isso se apresenta no gráfico:
Criando um EA para detecção de padrões e execução de trades
Depois, testei os desenvolvimentos no testador do MetaTrader 5, já que os testes em Python foram bem-sucedidos. Publiquei o código abaixo e também o incluí no artigo. O código é uma implementação prática do conceito de análise de padrões no mercado cambial. Ele materializa a ideia de que padrões históricos de preços podem fornecer informações estatisticamente relevantes sobre movimentos futuros do mercado.
Componentes principais do EA:
- Geração de padrões: O EA utiliza uma representação binária dos movimentos de preços (alta ou baixa), criando todas as combinações possíveis para um comprimento de padrão definido.
- Análise estatística: O EA realiza uma análise retrospectiva, avaliando a frequência de cada padrão e sua eficácia preditiva.
- Adaptação dinâmica: O EA atualiza constantemente as estatísticas dos padrões, adaptando-se às mudanças nas condições do mercado.
- Tomada de decisões de trade: Com base nos padrões mais eficazes identificados para compra e venda, o EA abre, fecha ou mantém posições.
- Parametrização: O EA permite configurar parâmetros-chave como comprimento do padrão, período de análise, horizonte de previsão e número mínimo de ocorrências para considerar o padrão.
No total, criei 4 versões do EA: o primeiro segue exatamente a proposta do artigo, abrindo operações com base nos padrões e fechando quando um novo padrão melhor na direção oposta é detectado. A segunda é igual, mas trabalha com os 10 pares de Forex mais líquidos, conforme estatísticas do Banco Mundial. A terceira também é semelhante, mas encerra as operações após a quantidade de barras ultrapassar o horizonte de previsão. Por fim, a última fecha as ordens por take profit e stop loss.
Aqui está o código do primeiro EA, os demais estão nos arquivos anexados:
//+------------------------------------------------------------------+ //| PatternProbabilityExpertAdvisor | //| Copyright 2024, Evgeniy Koshtenko | //| https://www.mql5.com/en/users/koshtenko | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, Evgeniy Koshtenko" #property link "https://www.mql5.com/en/users/koshtenko" #property version "1.00" #include <Trade\Trade.mqh> // Include the CTrade trading class //--- input parameters input int InpPatternLength = 5; // Pattern Length (3-10) input int InpLookback = 1000; // Lookback Period (100-5000) input int InpForecastHorizon = 6; // Forecast Horizon (1-20) input double InpLotSize = 0.1; // Lot Size input int InpMinOccurrences = 30; // Minimum Pattern Occurrences //--- global variables int g_pattern_length; int g_lookback; int g_forecast_horizon; string g_patterns[]; int g_pattern_count; int g_pattern_occurrences[]; int g_pattern_successes[]; int g_total_bars; string g_best_buy_pattern = ""; string g_best_sell_pattern = ""; CTrade trade; // Use the CTrade trading class //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- validate inputs if(InpPatternLength < 3 || InpPatternLength > 10) { Print("Invalid Pattern Length. Must be between 3 and 10."); return INIT_PARAMETERS_INCORRECT; } if(InpLookback < 100 || InpLookback > 5000) { Print("Invalid Lookback Period. Must be between 100 and 5000."); return INIT_PARAMETERS_INCORRECT; } if(InpForecastHorizon < 1 || InpForecastHorizon > 20) { Print("Invalid Forecast Horizon. Must be between 1 and 20."); return INIT_PARAMETERS_INCORRECT; } //--- set global variables g_pattern_length = InpPatternLength; g_lookback = InpLookback; g_forecast_horizon = InpForecastHorizon; //--- generate all possible patterns if(!GeneratePatterns()) { Print("Failed to generate patterns."); return INIT_FAILED; } g_total_bars = iBars(_Symbol, PERIOD_CURRENT); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { if(!IsNewBar()) return; UpdatePatternStatistics(); string current_pattern = GetCurrentPattern(); if(current_pattern == g_best_buy_pattern) { if(PositionSelect(_Symbol) && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { trade.PositionClose(_Symbol); } if(!PositionSelect(_Symbol)) { trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "Buy Pattern: " + current_pattern); } } else if(current_pattern == g_best_sell_pattern) { if(PositionSelect(_Symbol) && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { trade.PositionClose(_Symbol); } if(!PositionSelect(_Symbol)) { trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "Sell Pattern: " + current_pattern); } } } //+------------------------------------------------------------------+ //| Generate all possible patterns | //+------------------------------------------------------------------+ bool GeneratePatterns() { g_pattern_count = (int)MathPow(2, g_pattern_length); if(!ArrayResize(g_patterns, g_pattern_count)) { Print("Failed to resize g_patterns array."); return false; } for(int i = 0; i < g_pattern_count; i++) { string pattern = ""; for(int j = 0; j < g_pattern_length; j++) { pattern += ((i >> j) & 1) ? "U" : "D"; } g_patterns[i] = pattern; } return true; } //+------------------------------------------------------------------+ //| Update pattern statistics and find best patterns | //+------------------------------------------------------------------+ void UpdatePatternStatistics() { if(ArraySize(g_pattern_occurrences) != g_pattern_count) { ArrayResize(g_pattern_occurrences, g_pattern_count); ArrayResize(g_pattern_successes, g_pattern_count); } ArrayInitialize(g_pattern_occurrences, 0); ArrayInitialize(g_pattern_successes, 0); int total_bars = iBars(_Symbol, PERIOD_CURRENT); int start = total_bars - g_lookback; if(start < g_pattern_length + g_forecast_horizon) start = g_pattern_length + g_forecast_horizon; double close[]; ArraySetAsSeries(close, true); CopyClose(_Symbol, PERIOD_CURRENT, 0, total_bars, close); string patterns[]; ArrayResize(patterns, total_bars); ArraySetAsSeries(patterns, true); for(int i = 0; i < total_bars - g_pattern_length; i++) { patterns[i] = ""; for(int j = 0; j < g_pattern_length; j++) { patterns[i] += (close[i+j] > close[i+j+1]) ? "U" : "D"; } } for(int i = start; i >= g_pattern_length + g_forecast_horizon; i--) { string current_pattern = patterns[i]; int pattern_index = ArraySearch(g_patterns, current_pattern); if(pattern_index != -1) { g_pattern_occurrences[pattern_index]++; if(close[i-g_forecast_horizon] > close[i]) { g_pattern_successes[pattern_index]++; } } } double best_buy_win_rate = 0; double best_sell_win_rate = 0; for(int i = 0; i < g_pattern_count; i++) { if(g_pattern_occurrences[i] >= InpMinOccurrences) { double win_rate = (double)g_pattern_successes[i] / g_pattern_occurrences[i]; if(win_rate > best_buy_win_rate) { best_buy_win_rate = win_rate; g_best_buy_pattern = g_patterns[i]; } if((1 - win_rate) > best_sell_win_rate) { best_sell_win_rate = 1 - win_rate; g_best_sell_pattern = g_patterns[i]; } } } Print("Best Buy Pattern: ", g_best_buy_pattern, " (Win Rate: ", DoubleToString(best_buy_win_rate * 100, 2), "%)"); Print("Best Sell Pattern: ", g_best_sell_pattern, " (Win Rate: ", DoubleToString(best_sell_win_rate * 100, 2), "%)"); } //+------------------------------------------------------------------+ //| Get current price pattern | //+------------------------------------------------------------------+ string GetCurrentPattern() { double close[]; ArraySetAsSeries(close, true); CopyClose(_Symbol, PERIOD_CURRENT, 0, g_pattern_length + 1, close); string pattern = ""; for(int i = 0; i < g_pattern_length; i++) { pattern += (close[i] > close[i+1]) ? "U" : "D"; } return pattern; } //+------------------------------------------------------------------+ //| Custom function to search for a string in an array | //+------------------------------------------------------------------+ int ArraySearch(const string &arr[], string value) { for(int i = 0; i < ArraySize(arr); i++) { if(arr[i] == value) return i; } return -1; } //+------------------------------------------------------------------+ //| Check if it's a new bar | //+------------------------------------------------------------------+ bool IsNewBar() { static datetime last_time = 0; datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0); if(current_time != last_time) { last_time = current_time; return true; } return false; }
Quanto aos resultados dos testes, no euro-dólar foram os seguintes:
E detalhadamente:
Nada mal, e o gráfico ficou bonito. As outras versões dos EAs ou ficam oscilando no zero, ou entram em longas e prolongadas fases de rebaixamento. Mesmo a melhor versão ainda não atende aos meus critérios, prefiro EAs com profit factor acima de 2 e coeficiente de Sharpe maior que 1. Uma lâmpada se acendeu na minha cabeça, que no testador Python, eu deveria ter considerado tanto a comissão por operação quanto o spread e o swap, e, no geral, ter feito um testador de verdade...
Potenciais melhorias: expansão do período e adição de indicadores
Seguimos com nossas reflexões. A estratégia dá resultados positivos, mas como melhorá-los, e será que isso é realmente viável?
Atualmente, estamos olhando para o time frame de 4 horas. Vamos tentar ir além. Adicionar gráficos diários, semanais, talvez até mensais. Com essa abordagem, poderemos ver tendências mais amplas, padrões em uma escala maior. Vamos adaptar o código para abranger todos esses intervalos de tempo:
timeframes = [mt5.TIMEFRAME_H4, mt5.TIMEFRAME_D1, mt5.TIMEFRAME_W1, mt5.TIMEFRAME_MN1]
for tf in timeframes:
ohlc_data = get_ohlc_data(symbol, tf, start_date, end_date)
patterns = find_patterns(ohlc_data)
Mais dados significam mais ruído. Precisamos aprender a filtrar esse ruído para obter informações mais nítidas.
Vamos ampliar o conjunto de características analisadas. No universo do trading, isso significa adicionar indicadores técnicos. RSI, MACD e Bandas de Bollinger são as ferramentas mais utilizadas.
def add_indicators(data): data['RSI'] = ta.RSI(data['close']) data['MACD'] = ta.MACD(data['close']).macd() data['BB_upper'], data['BB_middle'], data['BB_lower'] = ta.BBANDS(data['close']) return data ohlc_data = add_indicators(ohlc_data)
Os indicadores podem nos ajudar a confirmar os sinais dos nossos padrões. Ou ainda, podemos procurar padrões diretamente sobre os indicadores.
Conclusão
Assim, encerramos nosso trabalho sobre busca e análise de padrões. Criamos um sistema que encontra regularidades no caos do mercado. Aprendemos a visualizar os resultados, realizar backtests e refletir sobre melhorias futuras. Mas o mais importante é que aprendemos a pensar como traders analistas. Não seguimos simplesmente a multidão, mas buscamos nosso próprio caminho, nossos próprios padrões e nossas próprias oportunidades.
Lembre-se: o mercado é resultado das ações de pessoas reais. Ele cresce e muda. E nossa tarefa é mudar junto com ele. Os padrões de hoje podem não funcionar amanhã, mas isso não é motivo para desânimo. É um motivo para aprender, se adaptar e evoluir. Use este sistema como ponto de partida. Experimente, melhore, crie o seu próprio. Quem sabe seja você quem descubra aquele padrão que abrirá as portas para uma negociação bem-sucedida!
Boa sorte nessa jornada empolgante! Que seus padrões sejam sempre lucrativos e os prejuízos se transformem em apenas mais uma lição no caminho do sucesso. Até a próxima no universo do Forex!
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/15965





- 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