English Русский Español Deutsch 日本語
preview
Busca de padrões arbitrários em pares de moedas no Python com o uso do MetaTrader 5

Busca de padrões arbitrários em pares de moedas no Python com o uso do MetaTrader 5

MetaTrader 5Negociação | 3 abril 2025, 13:35
176 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

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

Arquivos anexados |
PredictPattern.py (9.23 KB)
AutoPattern.mq5 (18.98 KB)
PatternEA.mq5 (16.12 KB)
PatternEAMult.mq5 (9.59 KB)
Sistema de negociação de arbitragem de alta frequência em Python usando MetaTrader 5 Sistema de negociação de arbitragem de alta frequência em Python usando MetaTrader 5
Criamos um sistema de arbitragem legal aos olhos das corretoras, que gera milhares de preços sintéticos no mercado Forex, os analisa e negocia com sucesso e de forma lucrativa.
Construção de previsões econômicas: potencialidades do Python Construção de previsões econômicas: potencialidades do Python
Como utilizar os dados econômicos do Banco Mundial para fazer previsões? O que acontece se combinarmos modelos de IA com economia?
Do básico ao intermediário: Indicador (IV) Do básico ao intermediário: Indicador (IV)
Neste artigo, vermos como é fácil de criar e implementar uma metodologia operacional, visando colorir candles. Sendo este um conceito, que diversos operadores apreciam imensamente. Porém, é preciso se tomar cuidado ao implementar tal tipo de coisa. Isto para que as barras, ou candles, mantenham a sua aparência original. Visando assim não prejudicar a leitura que muitos operadores fazem candle a candle.
Algoritmo de Irrigação Artificial — Artificial Showering Algorithm (ASHA) Algoritmo de Irrigação Artificial — Artificial Showering Algorithm (ASHA)
Este artigo apresenta o Algoritmo de Irrigação Artificial (ASHA), um novo método metaheurístico desenvolvido para resolver problemas gerais de otimização. Baseado na simulação dos processos de fluxo e acúmulo de água, este algoritmo constrói o conceito de um campo ideal, no qual cada unidade de recurso (água) é convocada para buscar a solução ótima. Descubra como o ASHA adapta os princípios de fluxo e acúmulo para distribuir recursos de forma eficiente em um espaço de busca e conheça sua implementação e os resultados dos testes.