English Русский 中文 Español Deutsch 日本語
preview
Engenharia de Atributos com Python e MQL5 (Parte IV): Reconhecimento de Padrões de Candlestick com Regressão UMAP

Engenharia de Atributos com Python e MQL5 (Parte IV): Reconhecimento de Padrões de Candlestick com Regressão UMAP

MetaTrader 5Exemplos |
17 9
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Os padrões de candlestick são amplamente utilizados em diversas estratégias e estilos de trading pela maioria dos traders algorítmicos de nossa comunidade. No entanto, nossa compreensão desses padrões está limitada aos candlesticks que já identificamos, quando, na verdade, podem existir muitos outros padrões de candlestick lucrativos dos quais simplesmente ainda não temos conhecimento. Devido à grande quantidade de informações disponíveis na maioria dos mercados modernos, é consideravelmente desafiador para os traders terem certeza de que estão sempre utilizando os padrões de candlestick mais confiáveis disponíveis no mercado escolhido.

Para amenizar esse problema, proporemos uma solução que potencialmente permitirá que nosso computador identifique novos padrões de candlestick dos quais não tínhamos conhecimento. Nossa estrutura proposta é parcialmente comparável a uma brincadeira infantil com a qual a maioria de nós provavelmente está familiarizada. A brincadeira é conhecida por diferentes nomes. No entanto, a premissa básica é a mesma. A brincadeira desafia os participantes a descrever um substantivo utilizando adjetivos sem mencionar o próprio substantivo. Assim, por exemplo, se o substantivo fornecido fosse uma banana, o jogador que conduz a brincadeira daria aos seus amigos pistas que melhor descrevessem a banana, como “amarela e curva”; esperamos que o exemplo seja intuitivo para o leitor.  

Essa brincadeira infantil é logicamente idêntica às tarefas que pediremos ao nosso computador para executar, de modo que possamos descobrir novos padrões de candlestick que, de outra forma, permaneceriam ocultos devido ao elevado número de dimensões que nossos conjuntos de dados tendem a apresentar atualmente. De forma análoga à brincadeira que acabamos de descrever, na qual o jogador deve descrever uma banana em 3 palavras ou menos, forneceremos ao nosso computador dados de mercado contendo 10 colunas que descrevem o candlestick atual e, em seguida, pediremos ao computador que descreva os dados de mercado originais em 8 colunas (embeddings) ou menos. Isso é chamado de redução de dimensionalidade.

Existem muitas técnicas conhecidas de redução de dimensionalidade com as quais o leitor talvez já esteja familiarizado, como Principal Components Analysis (PCA). Essas técnicas são úteis porque orientam nosso computador a se concentrar nos aspectos mais significativos dos dados transformados. Hoje, utilizaremos uma técnica conhecida como Uniform Manifold Approximation And Projection (UMAP). Esse é um algoritmo recente e, como o leitor verá em breve, ele pode nos ajudar a revelar relações não lineares em nossos dados de mercado de uma maneira inovadora. 

Nosso objetivo é projetar e construir cuidadosamente colunas em nosso dataset original que descrevam detalhadamente o candlestick atual. Ao fazer isso, permitimos que o algoritmo UMAP transforme nossos dados, agrupe candlesticks semelhantes e os descreva em menos “palavras” (embeddings). Isso, por sua vez, pode ajudar nosso computador a reconhecer padrões de candlestick que estavam ocultos para nós devido ao elevado número de dimensões necessárias para descrever cada candlestick com precisão.

Para testar a eficácia do algoritmo UMAP, treinamos dois modelos estatísticos idênticos para prever o retorno da taxa de câmbio diária EURGBP. O primeiro modelo foi treinado com os dados de mercado originais em sua forma bruta. Neste exemplo específico, os dados de mercado originais tinham 10 dimensões criadas diretamente a partir do mercado em nosso terminal MetaTrader 5. Utilizando o algoritmo UMAP, conseguimos transformar os dados de mercado originais em apenas 3 dimensões, que foram suficientes para superar o erro produzido pelos dados de mercado originais com os quais começamos.

Por fim, não consideraremos como implementar o algoritmo UMAP do zero nativamente em MQL5. O motivo é que o algoritmo é bastante sofisticado, e tentar implementá-lo com estabilidade numérica e eficiência computacional não é uma tarefa trivial. Se o leitor tiver confiança de que possui as habilidades necessárias em geometria analítica e topologia algébrica, poderá seguir seus interesses e implementar o algoritmo nativamente em MQL5. Forneci um link para o artigo de pesquisa original, explicando as especificações matemáticas exatas do algoritmo, aqui

Caso contrário, para leitores que talvez não possuam todas as habilidades numéricas necessárias, inclusive eu, demonstraremos como substituir a necessidade de implementar o algoritmo do zero utilizando, em vez disso, nossas habilidades em aproximação de funções.


Por que UMAP?

Considerando que existem tantas técnicas úteis e mais conhecidas de redução de dimensionalidade, alguns leitores podem naturalmente se perguntar: “Por que deveríamos nos interessar em aprender UMAP? Eu realmente preciso aprender mais uma biblioteca?”. Uma das principais vantagens do UMAP é que, à medida que o tamanho do nosso dataset aumenta, o tempo necessário para a biblioteca transformar nossos dados permanece quase constante. Além disso, o algoritmo UMAP é especializado em revelar efeitos não lineares nos dados, ao mesmo tempo em que tenta preservar a estrutura global original dos dados. Em outras palavras, isso significa que o algoritmo tenta explicitamente evitar ao máximo distorcer os dados e criar artefatos enganosos que possam introduzir ruído adicional. Essa não é uma prática comum na maioria dos algoritmos de redução de dimensionalidade.

O algoritmo UMAP é relativamente novo, e a implementação que consideraremos hoje foi desenvolvida utilizando Python e Numba. Numba é um compilador que converte código Python em código de máquina. Essa combinação de Python e código de máquina nos proporciona alta velocidade e cálculos numericamente estáveis, mesmo em grandes datasets. Essa implementação específica do algoritmo UMAP foi desenvolvida por Leland McInnes et al. A biblioteca foi publicada inicialmente em 2018.

Leland 

Fig 1: Leland McInnes é um dos principais autores do artigo de pesquisa sobre UMAP e ajuda a manter a biblioteca Python

No entanto, o leitor deve estar ciente de que essas características não podem ser garantidas caso utilize uma implementação do algoritmo UMAP proveniente de uma biblioteca diferente daquela que consideraremos em nossa discussão de hoje. É esperado que implementações distintas do mesmo algoritmo apresentem diferenças em suas propriedades numéricas. 


Primeiros Passos em MQL5

Para dar início ao processo, obteremos dados quantitativos que descrevem o candlestick atual. Queremos conhecer a variação nos níveis de preço Open, High, Low e Close que ocorreu durante um determinado período, denominado horizonte neste exemplo. Além disso, gostaríamos de acompanhar o crescimento de Open para High, Open para Low e Open para Close. Repetimos esse cálculo de crescimento para cada um dos 4 fluxos de preços disponíveis em nosso terminal MetaTrader 5. Isso nos fornece um total de 10 colunas, excluindo as duas primeiras colunas, Time e True Close. Essas 10 colunas podem descrever efetivamente qualquer padrões de candlestick, como dojis e martelos. No entanto, nossa técnica atual não será útil para identificar padrões de candlestick formados por mais de 1 candle.

//+------------------------------------------------------------------+
//|                                                      ProjectName |
//|                                      Copyright 2020, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define HORIZON 24

//+------------------------------------------------------------------+
//| File name                                                        |
//+------------------------------------------------------------------+
string file_name = Symbol() + " UMAP Candlestick Recognition.csv";

//+------------------------------------------------------------------+
//| User inputs                                                      |
//+------------------------------------------------------------------+
input int size = 3000;

//+------------------------------------------------------------------+
//| Our script execution                                             |
//+------------------------------------------------------------------+
void OnStart()
  {

//---Write to file
   int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");

   for(int i=size;i>=1;i--)
     {
      if(i == size)
        {
         FileWrite(file_handle,"Time","True Close","Open","High","Low","Close","O - H","O - L","O - C","H - L","H - C","L - C");
        }

      else
        {
         FileWrite(file_handle,
                   iTime(_Symbol,PERIOD_CURRENT,i),
                   iClose(_Symbol,PERIOD_CURRENT,i),
                   iOpen(_Symbol,PERIOD_CURRENT,i)  - iOpen(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iHigh(_Symbol,PERIOD_CURRENT,i)  - iHigh(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iLow(_Symbol,PERIOD_CURRENT,i)   - iLow(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iClose(_Symbol,PERIOD_CURRENT,i) - iClose(_Symbol,PERIOD_CURRENT,i + HORIZON),
                   iOpen(_Symbol,PERIOD_CURRENT,i)  - iHigh(_Symbol,PERIOD_CURRENT,i),
                   iOpen(_Symbol,PERIOD_CURRENT,i)  - iLow(_Symbol,PERIOD_CURRENT,i),
                   iOpen(_Symbol,PERIOD_CURRENT,i)  - iClose(_Symbol,PERIOD_CURRENT,i),
                   iHigh(_Symbol,PERIOD_CURRENT,i)  - iLow(_Symbol,PERIOD_CURRENT,i),
                   iHigh(_Symbol,PERIOD_CURRENT,i)  - iClose(_Symbol,PERIOD_CURRENT,i),
                   iLow(_Symbol,PERIOD_CURRENT,i)   - iClose(_Symbol,PERIOD_CURRENT,i)
                  );
        }
     }
//--- Close the file
   FileClose(file_handle);
  }

//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef HORIZON


Analisando os Dados em Python

Nosso objetivo é duplo:

  • Demonstrar as vantagens de utilizar transformações UMAP em vez de utilizar os dados de preço em sua forma original.
  • Obter uma cópia do algoritmo UMAP utilizando nossas técnicas de aproximação de funções, para que possamos realizar o backtest e avaliar a eficácia do algoritmo.

Demonstraremos as vantagens de utilizar UMAP em relação aos dados de preço em sua forma original para garantir que nossa motivação esteja clara e que os benefícios sejam evidentes para o leitor. Após demonstrarmos as vantagens de utilizar UMAP, utilizaremos as transformações fornecidas pela biblioteca UMAP para treinar nossa primeira rede neural a fim de estimar os embeddings UMAP de determinados dados de mercado. 

Fig 2: Visualização da nossa estrutura para estimar embeddings UMAP a partir de determinados dados de mercado.

Posteriormente, treinaremos um segundo modelo que aprenderá a prever movimentos futuros dos preços no mercado, considerando uma estimativa de seus embeddings UMAP proveniente do nosso primeiro modelo. Nosso objetivo é fazer com que esses dois modelos estatísticos trabalhem em cadeia. O primeiro modelo estima os embeddings UMAP a partir de determinados dados de mercado, e o segundo modelo utiliza a saída do primeiro modelo para prever os retornos futuros do mercado em que estamos operando. Essa estrutura se mostrará consideravelmente mais rápida e possivelmente tão eficaz quanto implementar o algoritmo UMAP do zero. 

Fig 3: Visualização da nossa estrutura para gerar uma previsão de mercado a partir dos embeddings UMAP estimados

Agora que abordamos nossa motivação e a metodologia que seguiremos, vamos começar em Python. Começamos importando as bibliotecas necessárias. Os leitores talvez precisem primeiro instalar a biblioteca UMAP em suas máquinas inserindo o comando "pip install umap-learn", caso desejem acompanhar o processo.

import pandas as pd 
import numpy as np
import matplotlib.pyplot as plt
import umap
import seaborn as sns

Em seguida, se o leitor estiver preparado para acompanhar o processo, o próximo passo será ler os dados de mercado que geramos utilizando nosso script MQL5.

HORIZON = 24
data = pd.read_csv("..\EURGBP UMAP Candlestick Recognition.csv")
data['Target'] = data['True Close'].shift(-HORIZON) - data['True Close']
data['Class'] = 0
data.loc[data['Target'] > 0,'Class'] = 1
data.dropna(inplace=True)
data

Nossos dados de mercado foram lidos com sucesso, mas, se observarmos a coluna Time, veremos que há dados recentes de mercado incluídos em nosso arquivo CSV. Queremos remover os últimos 5 anos de dados de mercado do nosso arquivo CSV, para que o backtest da estratégia não seja contaminado por informações vazadas às quais nosso modelo não teria acesso no passado. 

Fig 4: Nossos dados históricos de mercado obtidos utilizando nosso script MQL5

Vamos remover os últimos 5 anos de dados de mercado do nosso arquivo CSV. Observe que a última data em nosso arquivo CSV agora é 16 de outubro de 2019. Nosso backtest será executado a partir de 1º de janeiro de 2020. Esse intervalo entre o final do nosso período de treinamento e o início do período de teste é necessário para garantir que nossos testes sejam robustos.

#Delete all the data that overlaps with our back test
data = data.iloc[:(-(365 * 5) + (31 * 5)),:]
data

Fig 5: Certifique-se de remover todos os dados que se sobreponham ao período no qual você deseja realizar o backtest

Vamos visualizar os efeitos das colunas que criamos para nosso exercício. A coluna "H - L" representa a diferença entre o maior e o menor preço do dia. Esse é o intervalo efetivo de trading de qualquer dia. Por outro lado, a coluna "O - C" representa a variação líquida do preço no dia. Ao criar um scatter plot dessas 2 colunas, queremos descobrir se existe alguma relação entre o intervalo do dia e a variação líquida do preço nesse mesmo dia. Infelizmente, a relação parece ser complexa e não linear. Esse pode ser o tipo de dado que o UMAP pode nos ajudar a analisar.

sns.scatterplot(
    data=data,
    y='O - C',
    x='H - L',
    hue='Class'
)

plt.grid()
plt.title("Visualizing Our Custom Columns on EURUSD Market Data")
plt.axhline(0,color='black',linestyle='--')

Fig 6: Visualização da relação entre o intervalo de trading e a variação líquida do preço no mesmo dia

Aplicar as transformações UMAP é relativamente simples. Primeiro, precisamos criar um objeto UMAP. Depois, ajustamos o objeto UMAP aos nossos dados e, em seguida, obtemos os dados transformados. Por padrão, nosso objeto UMAP reduzirá os dados para 2 colunas. Demonstraremos como especificar o número desejado de colunas à medida que avançarmos. As 10 colunas que originalmente obtivemos utilizando nosso script MQL5 foram reduzidas às 2 dimensões observadas na Fig 7. 

No exemplo de código abaixo, apresentamos ao leitor determinados parâmetros de ajuste da biblioteca UMAP:

  • n_neighbors: Este parâmetro de ajuste instrui o algoritmo sobre quantos pontos de dados ele deve tentar garantir que permaneçam na mesma vizinhança.
  • metric: Existem diferentes métricas disponíveis para medir o quão "próximos" dois pontos estão um do outro e se eles se qualificam como pertencentes à mesma vizinhança. Alterar a métrica de distância modificará consideravelmente a estrutura dos dados projetados.

reducer = umap.UMAP(n_neighbors=100,metric="euclidean")

embedding = reducer.fit_transform(data.iloc[:,2:-2])
embedding = pd.DataFrame(embedding,columns=['X1','X2'])
embedding['Class'] = data['Class']

sns.scatterplot(
    data=embedding,
    x='X1',
    y='X2',
    hue='Class'
)

plt.grid()
plt.title("Visualizing the effects of UMAP on our EURUSD Market Data")

Fig 7: Visualização dos nossos dados transformados após a aplicação do algoritmo UMAP

Nossa nova representação dos dados não é perfeita. No entanto, ela possui regiões dominadas por pontos laranja e outras regiões dominadas por pontos azuis. Isso pode facilitar para nossos modelos estatísticos aprenderem a diferença entre as 2 classes que estamos tentando distinguir. No entanto, selecionamos apenas 2 colunas arbitrariamente, para dar ao leitor uma ideia de como é fácil começar. Na realidade, não sabemos quantas dimensões são necessárias para transformar os dados de forma eficaz. Portanto, realizaremos uma busca linear entre 1 e 9. A função a seguir recebe um parâmetro que especifica o número desejado de dimensões e nos retorna os dados transformados correspondentes.

def return_transformed_data(n_components):
    HORIZON = 24
    data = pd.read_csv("..\EURGBP UMAP Candlestick Recognition.csv")
    data['Target'] = data['True Close'].shift(-HORIZON) - data['True Close']
    data.dropna(inplace=True)
    data = data.iloc[:(-(365 * 5) + (31 * 5)),:]
    reducer = umap.UMAP(n_neighbors=100,metric="euclidean",n_components=n_components,n_jobs=-1)
    embedding = reducer.fit_transform(data.iloc[:,2:-1])
    cols = []
    for i in np.arange(n_components):
        s = 'X' + ' ' + str(i)
        cols.append(s)
    
    embedding = pd.DataFrame(embedding,columns=cols)
    return embedding.copy()

Agora vamos preparar nossos modelos.

from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import TimeSeriesSplit,cross_val_score

Defina o objeto time series split para uma validação cruzada adequada de séries temporais.

tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON)

Agora vamos realizar a busca linear para determinar o número ideal de dimensões necessárias para representar os dados originais.

LEVELS = 8
res = pd.DataFrame(columns=['X'],index=np.arange(LEVELS))

for i in range(LEVELS):
    new_data = return_transformed_data(i+1)
    res.iloc[i,0] = np.mean(np.abs(cross_val_score(GradientBoostingRegressor(),new_data.iloc[:,0:],data['Target'],cv=tscv)))

Obtenha o índice mínimo e o valor mínimo.

res['X'] = pd.to_numeric(res['X'], errors='coerce')
min_value = min(res.iloc[:,0])
min_index = res['X'].idxmin()

Nossos melhores resultados foram obtidos quando utilizamos 3 colunas para representar as 10 originais. O leitor deve entender que elas não devem ser consideradas as 3 melhores colunas dentre as 10 com as quais começamos. Em vez disso, as 10 colunas foram transformadas e reduzidas para 3, 

plt.plot(res,color='black')
plt.grid()
plt.title('Finding The Optimal Number of U-MAP Components')
plt.ylabel('RMSE Validation Error')
plt.xlabel('Training Iteration')
plt.scatter(min_index,min_value,color='red')

Fig 8: Nosso número ideal de colunas foi 3, a partir das 10 com as quais começamos

Agora vamos registrar nossos níveis de erro ao utilizar os dados de mercado em sua forma original.

classic_error = np.mean(np.abs(cross_val_score(GradientBoostingRegressor(),data.iloc[:,2:-2],data['Target'],cv=tscv)))

Agora compararemos o erro produzido utilizando os dados UMAP transformados com o erro produzido ao utilizar os dados de mercado sem nenhuma transformação. Como podemos observar, a transformação UMAP reduziu nossos níveis de erro para regiões ideais que não conseguiríamos alcançar utilizando os dados de preço em sua forma original. 

results = [min(res.iloc[:,0]),classic_error]

sns.barplot(results,color='black')
plt.axhline(results[0],color='red',linestyle='--')
plt.ylabel('RMSE Validation Error')
plt.xlabel('0: UMAP Transformed Data | 1: Original OHLC Data')
plt.title("UMAP Transformations Are Helping Us Reduce Our Error Rates")

Fig 9: A transformação UMAP reduziu nossos níveis de erro e está apresentando um desempenho superior ao dos dados em sua forma original

Agora que estabelecemos a motivação para utilizar transformações UMAP, podemos começar a construir a arquitetura descrita nas Fig 2 e 3. Começaremos avaliando quantas iterações de treinamento nossa rede neural requer para aprender a transformar de forma eficaz nossos dados de mercado originais em seus embeddings UMAP.

from sklearn.neural_network import MLPRegressor

Obtenha os dados necessários.

new_data = return_transformed_data(3)

Realize uma line search para observar a relação entre o erro do modelo e o número de épocas de treinamento permitidas.

LEVELS = 18
NN_ERROR = pd.DataFrame(columns=['Error'],index=np.arange(LEVELS))

for i in range(LEVELS):
    model = MLPRegressor(hidden_layer_sizes=(data.iloc[:,2:-2].shape[1],10,5),max_iter=(2 ** i),solver='adam')
    NN_ERROR.iloc[i,0] =  np.mean(np.abs(cross_val_score(model,new_data,data['Target'],cv=tscv)))

Vamos plotar os resultados. Nossos melhores resultados foram obtidos quando permitimos que o modelo realizasse 65 536 iterações de treinamento, ou simplesmente 2 elevado à potência 16.

NN_ERROR['Error'] = pd.to_numeric(NN_ERROR['Error'], errors='coerce')
min_idx   = NN_ERROR.idxmin()
min_value = NN_ERROR.min()

plt.plot(NN_ERROR,color='black')
plt.grid()
plt.ylabel('5 Fold CV RMSE')
plt.xlabel('Max Iterations As Powers of 2')
plt.scatter(min_idx,min_value,color='red')
plt.title('Minimizing The Error of Our Neural Network')

Fig 10: Visualização do número ideal de iterações de treinamento necessárias para que nosso modelo aprenda os embeddings UMAP

Agora podemos ajustar ambos os modelos.

#The first model will transform the given market data into its UMAP embeddings
umap_transform_model = MLPRegressor(hidden_layer_sizes=(data.iloc[:,2:-2].shape[1],10,5),max_iter=int(2 ** min_idx),solver='adam')
umap_transform_model.fit(data.iloc[:,2:-2],new_data)

#The second model will forecast the future EURGBP returns, given UMAP embeddings
forecast_model = MLPRegressor(hidden_layer_sizes=(new_data.shape[1],10,5),max_iter=int(2 ** min_idx),solver='adam')
forecast_model.fit(new_data,data['Target'])

Vamos nos preparar para exportar nossos modelos para o formato ONNX.

import onnx
import netron
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

O modelo responsável por estimar nossos embeddings UMAP possui um formato exclusivo de entrada e saída. Ele recebe 10 parâmetros como inputs e retorna 3 parâmetros como output. Especificaremos isso utilizando initial_types e final_types da ONNX API.

umap_transform_shape = [("float_input",FloatTensorType([1,data.iloc[:,2:-2].shape[1]]))]
umap_transform_output_shape = [("float_output",FloatTensorType([new_data.shape[1],1]))]

Por outro lado, o modelo responsável por prever as variações de preço a partir dos embeddings UMAP fornecidos possui um formato simples de entrada e saída. Ele recebe como inputs os 3 outputs do primeiro modelo e nos fornece apenas 1 output.

forecast_shape = [("float_input",FloatTensorType([1,new_data.shape[1]]))]

Defina os formatos de I/O dos modelos. Observe que devemos realizar uma etapa adicional para especificar que o primeiro modelo é multi-output e, em seguida, fornecemos o formato do nosso modelo multi-output.

umap_model_proto = convert_sklearn(umap_transform_model,initial_types=umap_transform_shape,final_types=umap_transform_output_shape,target_opset=12)
forecast_model_proto = convert_sklearn(forecast_model,initial_types=forecast_shape,target_opset=12)

Salve os modelos.

onnx.save(umap_model_proto,"EURGBP UMAP.onnx")
onnx.save(forecast_model_proto,"EURGBP UMAP Forecast.onnx")


Primeiros Passos em MQL5

Agora podemos começar a escrever nosso código MQL5 para testar a rentabilidade da regressão UMAP. Lembre-se de que, na Fig 5, excluímos todos os dados de 2020 até o presente. Portanto, o backtest que obteremos hoje nos fornece uma representação justa de como nossa estratégia se comportará em condições reais que ela ainda não viu. Vamos carregar nossos modelos ONNX.

//+------------------------------------------------------------------+
//|                                              UMAP Regression.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System resources                                                 |
//+------------------------------------------------------------------+
#resource "\\Files\\EURGBP UMAP.onnx" as uchar umap_onnx_buffer[];
#resource "\\Files\\EURGBP UMAP Forecast.onnx" as uchar umap_forecast_onnx_buffer[];

Além disso, precisamos de algumas variáveis globais. Como nossa estratégia é conduzida algoritmicamente, não precisaremos de muitas. 

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
long umap_onnx_model,umap_forecast_onnx_model;
vectorf umap_onnx_output(3),umap_forecast_onnx_output(1);
double trade_sl;

Defina handlers e buffers para nossos indicadores técnicos.

//+------------------------------------------------------------------+
//| Technical indicators                                             |
//+------------------------------------------------------------------+
int ma_o_handler,ma_c_handler;
double ma_o[],ma_c[];

Carregue a biblioteca de trading.

//+------------------------------------------------------------------+
//| Technical indicators                                             |
//+------------------------------------------------------------------+
int ma_o_handler,ma_c_handler;
double ma_o[],ma_c[];

Para manter o design do nosso código legível, optamos por dedicar funções a cada event handler. Isso torna o corpo do nosso programa fácil de ler do início ao fim. Se o usuário desejar adicionar mais funcionalidades, recomendo seguir o mesmo princípio de design, encapsulando a funcionalidade desejada em um método e chamando-o a partir do corpo do programa. Isso mantém a base de código em um estado muito mais fácil de manter quando comparado à alternativa de possivelmente ter que analisar centenas de linhas de código, todas agrupadas em um único event handler.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(!setup())
      return(INIT_FAILED);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   release();
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   update();
  }
//+------------------------------------------------------------------+

A função release realiza a limpeza do nosso Expert Advisor antes que ele seja completamente desativado.

//+------------------------------------------------------------------+
//| Custom functions                                                 |
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Free up system memory                                            |
//+------------------------------------------------------------------+
void release(void)
  {
   IndicatorRelease(ma_c_handler);
   IndicatorRelease(ma_o_handler);
   OnnxRelease(umap_onnx_model);
   OnnxRelease(umap_forecast_onnx_model);
  }

A função setup é responsável por inicializar nosso modelo ONNX e outras variáveis importantes do sistema. Ela retorna um tipo Boolean que será false se algo der errado durante a inicialização. Caso contrário, a função deverá nos retornar true. Os shapes dos modelos ONNX são pareados como inputs e outputs, e suas chamadas também são pareadas.

//+------------------------------------------------------------------+
//| Setup system variables                                           |
//+------------------------------------------------------------------+
bool setup(void)
  {
   umap_onnx_model = OnnxCreateFromBuffer(umap_onnx_buffer,ONNX_DATA_TYPE_FLOAT);
   umap_forecast_onnx_model = OnnxCreateFromBuffer(umap_forecast_onnx_buffer,ONNX_DATA_TYPE_FLOAT);

   ma_c_handler = iMA(_Symbol,PERIOD_CURRENT,2,0,MODE_EMA,PRICE_CLOSE);
   ma_o_handler = iMA(_Symbol,PERIOD_CURRENT,2,0,MODE_EMA,PRICE_OPEN);

   if(umap_onnx_model == INVALID_HANDLE)
     {
      Comment("Failed to create EURGBP UMAP Transformer ONNX model");
      return(false);
     }

   if(umap_forecast_onnx_model == INVALID_HANDLE)
     {
      Comment("Failed to create EURGBP UMAP Forecast ONNX model");
      return(false);
     }

   ulong umap_input_shape[]  = { 1 , 10 };
   ulong umap_forecast_input_shape[]  = { 1 , 3 };

   ulong umap_output_shape[] = { 3 , 1  };
   ulong umap_forecast_output_shape[] = { 1 , 1  };

   if(!OnnxSetInputShape(umap_onnx_model,0,umap_input_shape))
     {
      Comment("Failed to specify ONNX model input shape");
      Print("Actual shape: ",OnnxGetInputCount(umap_onnx_model));
      return(false);
     }

   if(!OnnxSetInputShape(umap_forecast_onnx_model,0,umap_forecast_input_shape))
     {
      Comment("Failed to specify EURGBP Forecast ONNX model input shape");
      Print("Actual shape: ",OnnxGetInputCount(umap_onnx_model));
      return(false);
     }

   if(!OnnxSetOutputShape(umap_onnx_model,0,umap_output_shape))
     {
      Comment("Failed to specify ONNX model output shape");
      Print("Actual shape: ",OnnxGetOutputCount(umap_onnx_model));
      return(false);
     }

   if(!OnnxSetOutputShape(umap_forecast_onnx_model,0,umap_forecast_output_shape))
     {
      Comment("Failed to specify EURGBP Forecast ONNX model output shape");
      Print("Actual shape: ",OnnxGetOutputCount(umap_onnx_model));
      return(false);
     }

   trade_sl = 2e-2;

   return(true);
  }

Nossa função update nos ajudará a copiar as leituras dos indicadores para seus buffers e executar nossas rotinas de trading periodicamente, uma vez por dia.

//+------------------------------------------------------------------+
//| Update our system variables                                      |
//+------------------------------------------------------------------+
void update(void)
  {
   static datetime time_stamp;
   datetime current_time = iTime(_Symbol,PERIOD_CURRENT,0);

   if(current_time != time_stamp)
     {
      time_stamp = current_time;

      CopyBuffer(ma_c_handler,0,0,1,ma_c);
      CopyBuffer(ma_o_handler,0,0,1,ma_o);

      if(PositionsTotal() == 0)
        {
         GetModelForecast();
         FindSetup();
        }
     }
  }

A função forecast é necessária para obter nossa cadeia de previsões. A primeira previsão será uma aproximação dos embeddings UMAP dos dados de mercado originais. A segunda previsão é nosso sinal de trading, o retorno previsto do mercado EURGBP, considerando uma aproximação de seu embedding UMAP.

//+------------------------------------------------------------------+
//| Get a forecast from our models                                   |
//+------------------------------------------------------------------+
void GetModelForecast(void)
  {
   vectorf model_inputs = GetUmapModelInputs();
   OnnxRun(umap_onnx_model,ONNX_DATA_TYPE_FLOAT,model_inputs,umap_onnx_output);
   OnnxRun(umap_forecast_onnx_model,ONNX_DATA_TYPE_FLOAT,umap_onnx_output,umap_forecast_onnx_output);
   Print("Model Inputs: \n",model_inputs);
   Print("Umap Transformer Forecast: \n",umap_onnx_output);
   Print("EURUSD Return UMAP Forecast: \n",umap_forecast_onnx_output);
  }

Antes de obtermos uma previsão do nosso modelo, precisamos preparar seus inputs. Lembre-se de que devemos preparar os inputs do primeiro modelo, e seu output alimentará o segundo modelo.

//+------------------------------------------------------------------+
//| Get our model's input data                                       |
//+------------------------------------------------------------------+
vectorf GetUmapModelInputs(void)
  {
   vectorf umap_model_inputs(10);

   umap_model_inputs[0] = (float)(iOpen(_Symbol,PERIOD_CURRENT,1)  - iOpen(_Symbol,PERIOD_CURRENT,11));
   umap_model_inputs[1] = (float)(iHigh(_Symbol,PERIOD_CURRENT,1)  - iHigh(_Symbol,PERIOD_CURRENT,11));
   umap_model_inputs[2] = (float)(iLow(_Symbol,PERIOD_CURRENT,1)   - iLow(_Symbol,PERIOD_CURRENT,11));
   umap_model_inputs[3] = (float)(iClose(_Symbol,PERIOD_CURRENT,1) - iClose(_Symbol,PERIOD_CURRENT,11));
   umap_model_inputs[4] = (float)(iOpen(_Symbol,PERIOD_CURRENT,1)  - iHigh(_Symbol,PERIOD_CURRENT,1));
   umap_model_inputs[5] = (float)(iOpen(_Symbol,PERIOD_CURRENT,1)  - iLow(_Symbol,PERIOD_CURRENT,1));
   umap_model_inputs[6] = (float)(iOpen(_Symbol,PERIOD_CURRENT,1)  - iClose(_Symbol,PERIOD_CURRENT,1));
   umap_model_inputs[7] = (float)(iHigh(_Symbol,PERIOD_CURRENT,1)  - iLow(_Symbol,PERIOD_CURRENT,1));
   umap_model_inputs[8] = (float)(iHigh(_Symbol,PERIOD_CURRENT,1)  - iClose(_Symbol,PERIOD_CURRENT,1));
   umap_model_inputs[9] = (float)(iLow(_Symbol,PERIOD_CURRENT,1)   - iClose(_Symbol,PERIOD_CURRENT,1));

   return(umap_model_inputs);
  }
//+------------------------------------------------------------------+

Lembre-se de que, nas Fig 4 e 5, excluímos todos os dados históricos que se sobrepunham às datas que estamos utilizando na Fig 11 abaixo. Isso nos permite observar como nosso modelo provavelmente se comportará ao lidar com dados out-of-sample e nos fornece uma estimativa real de quão rentável a estratégia pode ser.

Período do backtest

Fig 11: Nosso período de backtest para avaliar nosso Ensemble de modelos UMAP

Agora decidiremos as condições sob as quais a estratégia será testada. Para obter os resultados mais confiáveis, realizaremos um stress test do Expert Advisor em condições desafiadoras, aplicando um atraso aleatório entre a execução da ordem e seu preenchimento durante o backtest.

Configurações do backtest

Fig 12: As condições que estamos simulando acima reproduzem cenários reais de trading

No log do nosso testador de estratégias, podemos visualizar as entradas para nossos modelos ONNX, e a cadeia de modelos UMAP está gerando saídas válidas. O primeiro modelo reduziu corretamente as 10 entradas que registramos a partir dos dados de mercado para 3 entradas, que foram posteriormente utilizadas para obter uma previsão do mercado.

Nossa cadeia de modelos está funcionando

Fig 13: Tudo parece estar funcionando bem em conjunto nos bastidores

Nossa curva de capital parece estar nos fornecendo um feedback positivo sobre o desempenho do nosso sistema. Isso é encorajador porque, como o leitor deve se lembrar, apenas aproximamos o algoritmo UMAP e seus embeddings.

Curva de lucros e perdas

Fig 14: Nossa estratégia parece ser lucrativa até o momento

Vamos examinar mais detalhadamente o desempenho da nossa estratégia. Nosso sistema possui um Sharpe Ratio de 0,42 com um resultado esperado por operação de 7,05; essas são estatísticas positivas. Nossa proporção de operações lucrativas está em 64%, enquanto, no total, realizamos 25 operações. 

Detalhes estatísticos do backtest

Fig 15: Uma análise detalhada do nosso desempenho histórico utilizando regressão UMAP

Em média, cada uma de nossas operações foi mantida por 1274 horas, cerca de 54 dias. Isso sugere que nosso Expert Advisor deve estar capturando tendências no mercado, considerando que nosso tempo médio de permanência em uma posição está na faixa de 54 dias.

Duração das operações no backtest

Fig 16: Visualizando a distribuição das durações de nossas operações

Ao inspecionarmos o backtest, constatamos que, de fato, o Expert Advisor estava capturando tendências sustentadas no mercado. Na captura de tela abaixo, as linhas brancas verticais representam intervalos de 1 dia, e as operações observadas foram realizadas pelo nosso UMAP Regression EA durante seu backtest. Podemos observar que a primeira operação foi aberta em abril de 2020 e encerrada no mês seguinte, em maio. Enquanto a operação subsequente permaneceu aberta do final de maio até o início de setembro.

Fig 17: Visualizando as operações realizadas pelo nosso Expert Advisor



Conclusão

Neste artigo, demonstramos como o leitor pode empregar técnicas de redução de dimensionalidade para ajudar seu modelo estatístico a aprender as características dominantes do mercado presentes nos dados disponíveis. Mostramos que o algoritmo UMAP pode reduzir nossas taxas de erro ao utilizar modelos estatísticos em até 40%, quando comparado a um modelo idêntico treinado com os dados originais de mercado sem transformações UMAP. Por fim, o leitor conheceu uma nova estrutura que permite aproximar com segurança algoritmos que não podem ser implementados nativamente. Isso pode lhe proporcionar uma vantagem competitiva em qualquer mercado que decidir operar.
Nome do Arquivo Descrição
EURGBP UMAP Forecast.onnx Arquivo ONNX que recebe como entrada nossos embeddings UMAP aproximados para prever o retorno futuro do EURGBP.
EURGBP UMAP.onnx Arquivo ONNX responsável por receber nossos dados de mercado como entrada e aproximar os embeddings UMAP corretos.
UMAP Candlestick Recognition.ipynb O Jupyter Notebook que utilizamos para analisar nossos dados de mercado do MetaTrader 5 e criar nossos arquivos ONNX.
UMAP Candlestick Recognition.mq5  O arquivo de script MQL5 que criamos para obter nossos dados detalhados de mercado.
UMAP Regression.mq5 O Expert Advisor que criamos para operar o EURGBP utilizando nossa arquitetura de dois modelos. 

Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/17631

Últimos Comentários | Ir para discussão (9)
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 16 abr. 2025 em 12:48
linfo2 #:
Obrigado, essa é uma aplicação realmente interessante. Algumas observações que podem ser úteis: o erro “o módulo 'umap' não possui o atributo 'UMAP'” significa que você precisa ter o `umap-learn` instalado; você pode fazer isso com o comando `!pip install umap-learn`. Se você receber a mensagem “NameError: o nome 'FloatTensorType' não está definido”, é preciso instalar ou atualizar o onnxmltools usando o comando !pip install onnxmltools. Meus dados ficaram bem diferentes dos mostrados aqui; eu gostaria de saber como os outros estão se saindo com o código.

Ei, Niel.

Obrigado por compartilhar essas soluções para problemas comuns.

Além disso, você levantou um ponto importante. O mesmo símbolo pode ser mais fácil de modelar em uma corretora e mais difícil em outra.

Isso se deve, em parte, às diferenças nos feeds de dados que cada corretora possui.

Algumas corretoras coletam ticks em tempo real com latência mínima; outras coletam atualizações de preço a cada minuto; algumas coletam ticks apenas se os níveis de preço tiverem mudado; e outras extrapolam os níveis de preço periodicamente.

Em resumo, a dificuldade de prever o EURUSD depende da corretora que você escolheu.

Cada corretora oferece uma fatia diferente do mesmo mercado.
Timotheus Kupembona Mahupe
Timotheus Kupembona Mahupe | 17 abr. 2025 em 11:11
Quero um EA para o MT5. Estou usando a corretora Exness.
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 14 jul. 2025 em 23:11
Timothy Kupembona Mahupe # :
Quero o EA para o MT5 e uso a corretora Exness
Qual é o símbolo?
Khai Cao
Khai Cao | 4 ago. 2025 em 09:42
É possível usar um EA no MT5 para negociar XAUUSD?
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 4 ago. 2025 em 11:20
Khai Cao XAUUSD?
Sim
Caminhe em novos trilhos: Personalize indicadores no MQL5 Caminhe em novos trilhos: Personalize indicadores no MQL5
Vou agora listar todas as possibilidades novas e recursos do novo terminal e linguagem. Elas são várias, e algumas novidades valem a discussão em um artigo separado. Além disso, não há códigos aqui escritos com programação orientada ao objeto, é um tópico muito importante para ser simplesmente mencionado em um contexto como vantagens adicionais para os desenvolvedores. Neste artigo vamos considerar os indicadores, sua estrutura, desenho, tipos e seus detalhes de programação em comparação com o MQL4. Espero que este artigo seja útil tanto para desenvolvedores iniciantes quanto para experientes, talvez alguns deles encontrem algo novo.
Automatizando Estratégias de Trading em MQL5 (Parte 14): Estratégia de Layering de Trades com MACD, RSI e Métodos Estatísticos Automatizando Estratégias de Trading em MQL5 (Parte 14): Estratégia de Layering de Trades com MACD, RSI e Métodos Estatísticos
Neste artigo, apresentamos uma estratégia de trade layering (escalonamento de posições) que combina os indicadores MACD e RSI com métodos estatísticos para automatizar o trading dinâmico em MQL5. Exploramos a arquitetura dessa abordagem em cascata, detalhamos sua implementação por meio dos principais segmentos de código e orientamos os leitores sobre o backtesting para otimizar o desempenho. Por fim, concluímos destacando o potencial da estratégia e preparando o terreno para melhorias futuras no trading automatizado.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
Desenvolvendo uma Estratégia de Pullback: Validação Desenvolvendo uma Estratégia de Pullback: Validação
A etapa de validação de um pullback avança com inclinação das EMAs, limites mínimo e máximo de distância por ATR, janela flexível do gatilho ATR e Walk-Forward Efficiency. As baterias comparam componentes e medem continuidade fora da amostra, dependência exclusiva da gestão. O resultado destaca plataformas paramétricas e critérios práticos para selecionar candidatos e evitar sobreajuste.