English Русский Deutsch 日本語
preview
Escrevemos o primeiro modelo de caixa de vidro (Glass Box) em Python e MQL5

Escrevemos o primeiro modelo de caixa de vidro (Glass Box) em Python e MQL5

MetaTrader 5Sistemas de negociação | 14 maio 2024, 09:47
76 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Introdução

Os algoritmos de caixa de vidro (ou branca) são modelos de aprendizado de máquina transparentes que levam em conta os mecanismos internos do sistema. Eles questionam a noção de que o aprendizado de máquina deve se balancear entre a precisão da previsão e a facilidade de interpretação. Para isso, esses modelos oferecem um alto nível de precisão e transparência, o que os torna significativamente mais simples de depurar, manter e aprimorar em comparação aos modelos de caixa preta. Os modelos de caixa preta são aqueles em que um processo excessivamente complexo em um sistema fica oculto do ambiente externo. Esses modelos geralmente representam relações multidimensionais e não lineares que não são fáceis de entender para nós, seres humanos.

Como regra geral, os modelos de caixa preta só devem ser usados quando um modelo de caixa de vidro não puder entregar o mesmo nível de precisão. Neste artigo, criaremos um modelo transparente e exploraremos os possíveis benefícios de usá-lo. Vamos considerar duas formas de trabalhar com o terminal MetaTrader 5:

  1. A abordagem tradicional, que é a mais direta. Simplesmente integraremos o modelo ao terminal MetaTrader 5 usando a biblioteca Python embutida no MetaTrader 5. Em seguida, programaremos um Expert Advisor em MQL5 que operará com este modelo.
  2. A abordagem moderna, que é a maneira recomendada de integrar modelos de aprendizado de máquina em um EA. Exportaremos nosso modelo de caixa de vidro para o formato ONNX (Open Neural Network Exchange) e, em seguida, o carregaremos diretamente no EA como um recurso. Isso nos permitirá aproveitar todas as funcionalidades disponíveis no MetaTrader 5 e integrá-las com as vantagens do nosso modelo de caixa de vidro.

IA

Figura 1. Simulação do cérebro humano por meio de inteligência artificial


Modelos de caixa preta e de caixa de vidro

A maioria dos modelos tradicionais de aprendizado de máquina é difícil de interpretar ou explicar. Essa classe de modelos é conhecida como modelos de caixa preta. Os modelos de caixa preta podem se referir a todos os modelos com funcionamento interno complexo e de difícil interpretação. Isso representa um problema sério, pois será muito difícil melhorar as principais métricas de desempenho do modelo dessa forma. Os modelos de caixa de vidro, por outro lado, são um conjunto de modelos de aprendizado de máquina cujo funcionamento interno é transparente e de fácil compreensão e, ao mesmo tempo, entregam mesma precisão e confiabilidade quanto às previsões. 

Dentro deste contexto, a equipe de especialistas da Microsoft Research disponibilizou publicamente o código do Interpret ML, um pacote Python que inclui explicadores de modelos de caixa preta e implementações de modelos de caixa de vidro. Os explicadores de caixa preta do Interpret ML são um conjunto de algoritmos projetados para desvendar o funcionamento desses modelos complexos, sendo em grande parte independentes do modelo específico, permitindo sua aplicação a diversas estruturas de caixa preta. No entanto, estes explicadores apenas estimam as funções dos modelos. Na próxima seção do artigo, veremos onde está o problema. Já os modelos de caixa de vidro incluídos no pacote proporcionam uma vantagem em termos de transparência e precisão, representando uma solução ideal para aqueles que valorizam a interpretabilidade, independentemente da área de aplicação ou experiência.

Informações adicionais:

1. Você pode ler mais sobre isso na documentação do Interpret ML.

2. Também pode ser útil familiarizar-se com o White Paper do Interpret ML. 

Neste artigo, usaremos o Interpret ML para criar um modelo de caixa de vidro em Python. Veremos como o modelo de caixa de vidro pode fornecer informações importantes que ajudarão a orientar o processo de desenvolvimento de funções e a melhorar nosso entendimento do funcionamento interno do nosso modelo.

O problema dos modelos de caixa preta: a discordância

Um motivo para abandonar o uso deses modelos é o problema da discordância. Em resumo, diferentes métodos de explicação podem gerar explicações muito variadas, mesmo que avaliem o mesmo modelo. Esses métodos tentam entender a estrutura por trás do modelo de caixa preta. Existem muitas abordagens diferentes, e cada uma delas pode se concentrar em diferentes aspectos do comportamento do modelo e, portanto, cada uma pode resultar em diferentes avaliações do modelo básico de caixa-preta. O problema da discordância representa um campo ainda em aberto para pesquisa, sendo crucial reconhecê-lo e abordá-lo de todas as maneiras possíveis.

Neste artigo, darei uma demonstração real do problema da discordância, caso você ainda não tenha se deparado com ele.

Informações adicionais:

1. Recomendo a leitura de um excelente trabalho de pesquisa de uma equipe de ex-alunos de Harvard, MIT, Drexel e Carnegie Mellon.

Vejamos rapidamente como esse problema de discordância se manifesta:

Primeiro, importamos os pacotes Python para análise.

#Import MetaTrader5 Python package
#pip install --upgrade MetaTrader5, if you don't have it installed
import MetaTrader5 as mt5

#Import datetime for selecting data
#Standard python package, no installation required
from datetime import datetime

#Plotting Data
#pip install --upgrade matplotlib, if you don't have it installed
import matplotlib.pyplot as plt

#Import pandas for handling data
#pip install --upgrade pandas, if you don't have it installed
import pandas as pd

#Import library for calculating technical indicators
#pip install --upgrade pandas-ta, if you don't have it installed
import pandas_ta as ta

#Scoring metric to assess model accuracy
#pip install --upgrade scikit-learn, if you don't have it installed
from sklearn.metrics import precision_score

#Import mutual information, a black-box explanation technique
from sklearn.feature_selection import mutual_info_classif

#Import permutation importance, another black-box explanation technique
from sklearn.inspection import permutation_importance

#Import our model
#pip install --upgrade xgboost, if you don't have it installed
from xgboost import XGBClassifier

#Plotting model importance
from xgboost import plot_importance

Depois disso, passamos a integrá-los ao terminal MetaTrader 5, mas, antes disso, precisamos especificar as credenciais de login.

#Enter your account number
login = 123456789

#Enter your password
password = '_enter_your_password_'

#Enter your Broker's server
server = 'Deriv-Demo'

Agora podemos inicializar o terminal MetaTrader 5 e fazer login na conta de negociação.

#We can initialize the MT5 terminal and login to our account in the same step
if mt5.initialize(login=login,password=password,server=server):
    print('Logged in successfully')
else:
    print('Failed To Log in')

Autorização bem-sucedida.

Agora temos acesso total ao terminal MetaTrader 5 e podemos solicitar dados de gráficos, de ticks, de cotações atuais e muito mais.

#To view all available symbols from your broker
symbols = mt5.symbols_get()

for index,value in enumerate(symbols):
    print(value.name)

Volatility 10 Index

Volatility 25 Index

Volatility 50 Index

Volatility 75 Index

Volatility 100 Index

Volatility 10 (1s) Index

Boom 1000 Index

Boom 500 Index

Crash 1000 Index

Crash 500 Index

Step Index

...

Depois de determinar o símbolo a ser modelado, podemos solicitar os dados do gráfico para o símbolo, mas primeiro precisamos especificar o intervalo de datas para o qual os dados devem ser recuperados.

#We need to specify the dates we want to use in our dataset
date_from = datetime(2019,4,17)
date_to = datetime.now()

Agora você pode solicitar os dados do gráfico para o símbolo.
#Fetching historical data
data = pd.DataFrame(mt5.copy_rates_range('Boom 1000 Index',mt5.TIMEFRAME_D1,date_from,date_to))

A coluna de tempo precisa ser formatada em nosso frame de dados para a plotagem.

#Let's convert the time from seconds to year-month-date
data['time'] = pd.to_datetime(data['time'],unit='s')

data

Dataframe após a conversão de tempo

Fig. 2. Nosso dataframe agora exibe a hora em um formato legível. Note que a coluna real_volume é preenchida com zeros.

Agora vamos criar uma função auxiliar que ajudará a adicionar novas funções ao nosso frame de dados, calcular indicadores técnicos e limpar os dados.

#Let's create a function to preprocess our data
def preprocess(df):
    #All values of real_volume are 0 in this dataset, we can drop the column
    df.drop(columns={'real_volume'},inplace=True) 
    #Calculating 14 period ATR
    df.ta.atr(length=14,append=True)
    #Calculating the growth in the value of the ATR, the second difference
    df['ATR Growth'] = df['ATRr_14'].diff().diff()
    #Calculating 14 period RSI
    df.ta.rsi(length=14,append=True)    
    #Calculating the rolling standard deviation of the RSI
    df['RSI Stdv'] = df['RSI_14'].rolling(window=14).std()
    #Calculating the mid point of the high and low price
    df['mid_point'] = ( ( df['high'] + df['low'] ) / 2 )  
    #We will keep track of the midpoint value of the previous day
    df['mid_point - 1'] = df['mid_point'].shift(1) 
    #How far is our price from the midpoint?
    df['height'] = df['close'] - df['mid_point']  
    #Drop any rows that have missing values
    df.dropna(axis=0,inplace=True)

Em seguida, chamamos a função de pré-processamento.

preprocess(data)

data

Frame de dados após o pré-processamento

Fig. 3. Frame após o pré-processamento.

Depois, precisamos determinar o target (se o próximo preço de fechamento será maior do que o preço de fechamento de hoje). Vamos programar assim: se o preço de fechamento de amanhã for maior que o de hoje, o target = 1. Caso contrário, o target = 0.

#We want to predict whether tomorrow's close will be greater than today's close
#We can encode a dummy variable for that: 
#1 means tomorrow's close will be greater.
#0 means today's close will be greater than tomorrow's.

data['target'] = (data['close'].shift(-1) > data['close']).astype(int)

data

#The first date is 2019-05-14, and the first close price is 9029.486, the close on the next day 2019-05-15 was 8944.461
#So therefore, on the first day, 2019-05-14, the correct forecast is 0 because the close price fell the following day.


Criação de Target

Fig. 4. Criamos target

Agora, definimos explicitamente o objetivo e os preditores. Em seguida, dividimos os dados em uma amostra de treinamento e uma amostra de teste. Observe que se trata de dados de série temporal, por isso não podemos dividi-los aleatoriamente em dois grupos.

#Seperating predictors and target
predictors = ['open','high','low','close','tick_volume','spread','ATRr_14','ATR Growth','RSI_14','RSI Stdv','mid_point','mid_point - 1','height']
target     = ['target']

#The training and testing split definition
train_start = 27
train_end = 1000

test_start = 1001

Criamos amostras para treinamento e teste.

#Train set
train_x = data.loc[train_start:train_end,predictors]
train_y = data.loc[train_start:train_end,target]

#Test set
test_x = data.loc[test_start:,predictors]
test_y = data.loc[test_start:,target]

Agora podemos treinar o modelo.

#Let us fit our model
black_box = XGBClassifier()
black_box.fit(train_x,train_y)

Verificamos as previsões do nosso modelo sobre o conjunto de teste.

#Let's see our model predictions
black_box_predictions = pd.DataFrame(black_box.predict(test_x),index=test_x.index)

Avaliamos a precisão do nosso modelo.

#Assesing model prediction accuracy
black_box_score = precision_score(test_y,black_box_predictions)

#Model precision score
black_box_score

0.4594594594594595

Nosso modelo tem uma precisão de 45%. Quais características funcionam para a precisão e quais não funcionam? O XGBoost vem com um recurso integrado para medir a importância das características, o que facilita nossa vida. Entretanto, isso se aplica especificamente a essa implementação do XGBoost. Nem todas as caixas pretas facilitam a medição da importância das características. Por exemplo, as redes neurais e as máquinas de vetor de suporte não têm uma função equivalente, e você terá de analisar e interpretar cuidadosamente os pesos do modelo para entendê-lo melhor. A função plot\_importance do XGBoost nos permite examinar o interior do nosso modelo.

plot_importance(black_box)

Importância das características no XGBoost

Fig. 5. Importância das características no XGBClassifier. Observe que não há termos de interação na tabela. Isso significa que eles não existem? Não necessariamente.

Agora vamos dar uma olhada na primeira técnica de explicação de caixa preta, a importância da permutação (Permutation Importance). Ela procura estimar a importância de cada recurso embaralhando aleatoriamente os valores de cada característica e, em seguida, medindo a alteração na função de perda do modelo. A explicação aqui é que, quanto mais o seu modelo depender desse atributo, pior será o desempenho se embaralharmos aleatoriamente esses valores. Vamos dar uma olhada nas vantagens e desvantagens da importância da permutação.

Vantagens

  1. Independência do modelo — a importância da permutação pode ser usada em qualquer modelo de caixa preta sem nenhum pré-processamento requerido para o modelo ou recurso de importância da permutação, o que facilita a integração em um fluxo de trabalho de aprendizado de máquina existente. 
  2. Interpretabilidade — os resultados são facilmente interpretados de forma consistente, independentemente do modelo base que está sendo avaliado. É relativamente fácil de usar.
  3. Tratamento da não linearidade - adequado para capturar relações não lineares entre preditores e resposta. 
  4. Manuseio de outliers — a importância da permutação é independente dos valores brutos dos preditores; isso se refere ao efeito das características no desempenho do modelo. Essa abordagem o torna robusto em relação aos valores discrepantes que podem estar presentes nos dados brutos.

Desvantagens:

  1. Custo computacional — para grandes conjuntos de dados com muitas características, o cálculo da importância das permutações pode ser computacionalmente caro, pois requer a análise de cada característica, seu reordenamento e a estimativa do modelo, passando para a próxima característica e repetindo o processo.
  2. Problema de características correlacionadas — pode produzir resultados tendenciosos ao estimar características altamente correlacionadas.
  3. Sensibilidade à complexidade do modelo — é possível que um modelo muito complexo apresente alta variação quando seus atributos forem permutados, dificultando a obtenção de conclusões confiáveis.
  4. Independência das características — o método pressupõe que as características do conjunto de dados são independentes e podem ser reorganizadas aleatoriamente sem nenhuma consequência. Isso simplifica os cálculos, mas, no mundo real, a maioria das características depende uma da outra e tem interações que não são levadas em conta pelo método de importância de permutação. 

Vamos calcular a importância da permutação para nosso classificador de caixa preta.

#Now let us observe the disagreement problem
black_box_pi = permutation_importance(black_box,train_x,train_y)

# Get feature importances and standard deviations
perm_importances = black_box_pi.importances_mean
perm_std = black_box_pi.importances_std

# Sort features based on importance
sorted_idx = perm_importances.argsort()

Agora vamos plotar os valores calculados da importância da permutação.

#We're going to utilize a bar histogram
plt.barh(range(train_x.shape[1]), perm_importances[sorted_idx], xerr=perm_std[sorted_idx])
plt.yticks(range(train_x.shape[1]), train_x.columns[sorted_idx])
plt.xlabel('Permutation Importance')
plt.title('Permutation Importances')
plt.show()

Importância de permutação

Fig. 6. A importância de permutar nossa caixa preta.

De acordo com os cálculos feitos pelo algoritmo de importância da permutação, a leitura do ATR é o recurso mais informativo. Mas sabemos que isso não é verdade: o ATR ficou em sexto lugar. A característica importante é o aumento do ATR! O segundo mais importante foi a altura, mas a importância de permutação determinou que o aumento do ATR era mais importante. A terceira característica mais importante foi o valor RSI, mas o algoritmo indicou que a altura era mais importante.

Esse é o problema dos métodos de explicação de caixa preta: eles fornecem estimativas muito boas da importância das características, mas tendem a estar errados porque, na melhor das hipóteses, são apenas estimativas. Além disso, eles podem divergir uns dos outros ao estimar o mesmo modelo. Vamos analisar isso por nós mesmos.

Vamos usar o algoritmo de informações mútuas como um segundo método de explicação. A informação mútua mede a redução da incerteza causada pela percepção do valor de uma característica.

#Let's see if our black-box explainers will disagree with each other by calculating mutual information
black_box_mi = mutual_info_classif(train_x,train_y)
black_box_mi = pd.Series(black_box_mi, name="MI Scores", index=train_x.columns)
black_box_mi = black_box_mi.sort_values(ascending=False)

black_box_mi

RSI_14:              0.014579

open:                0.010044

low:                  0.005544

mid_point - 1:    0.005514

close:                0.002428

tick_volume :    0.001402

high:                 0.000000

spread:             0.000000

ATRr_14:           0.000000

ATR Growth:     0.000000

RSI Stdv:          0.000000

mid_point:       0.000000

height:             0.000000

Name: MI Scores, dtype: float64

Como você pode ver, temos classificações de importância completamente diferentes. A informação mútua classifica os recursos quase na ordem inversa em comparação com nossa base e cálculo de importância de permutação. Se você não tivesse uma base com valores verdadeiros, como neste exemplo, em qual explicação você confiaria mais? E se você usasse cinco métodos diferentes de explicação, e cada um deles desse avaliações diferentes? Se você escolhe vieses que correspondem às suas crenças sobre como o mundo real funciona, isso abre a porta para outro problema, chamado viés de confirmação. O viés de confirmação é quando você ignora quaisquer evidências que contradigam suas crenças existentes e ativamente busca confirmar o que você acredita ser verdade, mesmo que não seja!

Vantagens dos modelos de caixa de vidro

Os modelos de caixa de vidro substituem perfeitamente a necessidade de métodos de explicação de caixa preta, pois são totalmente transparentes e compreensíveis. Eles podem potencialmente resolver o problema de discordâncias em muitas áreas, incluindo o setor financeiro. Depurar um modelo de caixa de vidro é exponencialmente mais fácil do que depurar uma caixa preta do mesmo nível de flexibilidade. Isso economiza o recurso mais importante, o tempo! A melhor parte é que não compromete a precisão do modelo, oferecendo o melhor de ambos os mundos. Em geral, as caixas pretas só devem ser usadas quando a caixa branca não pode fornecer o mesmo nível de precisão. 

Vamos agora passar para a criação de nosso primeiro modelo de caixa de vidro. Analisaremos suas características e tentaremos aumentar sua precisão. Em seguida, veremos como integrar nosso modelo ao terminal MetaTrader 5 e começar a negociar usando modelos brancos. Depois, criaremos um Expert Advisor baseado no modelo de caixa branca no MQL5. E, finalmente, exportaremos nosso modelo de caixa de vidro para o formato ONNX, para liberar todo o potencial do MetaTrader 5 e nosso modelo.

Criando o primeiro modelo de caixa de vidro em Python

Para que o código seja fácil de ler, criaremos nossa caixa de vidro em um script Python separado do que usamos para construir o modelo de caixa preta. No entanto, a maioria das coisas permanecerá a mesma, como login, obtenção de dados, etc., bem como o pré-processamento de dados. Portanto, não os repetiremos, mas nos concentraremos nos passos únicos para o modelo de caixa de vidro.

Primeiro, você precisa instalar o Interpret ML.

#Installing Interpret ML
pip install --upgrade interpret

Em seguida, carregamos nossas dependências. Neste artigo, nos concentraremos em três módulos do pacote de interpretação. O primeiro é o próprio modelo de caixa de vidro, o segundo é o módulo que nos permite olhar dentro do modelo e apresentar essa informação em um painel informativo interativo com interface gráfica, e o último pacote nos permite visualizar o desempenho de nosso modelo em um único gráfico. Já discutimos os outros pacotes.

#Import MetaTrader5 package
import MetaTrader5 as mt5

#Import datetime for selecting data
from datetime import datetime

#Import matplotlib for plotting
import matplotlib.pyplot as plt

#Intepret glass-box model for classification
from interpret.glassbox import ExplainableBoostingClassifier

#Intepret GUI dashboard utility
from interpret import show

#Visualising our model's performance in one graph
from interpret.perf import ROC

#Pandas for handling data
import pandas as pd

#Pandas-ta for calculating technical indicators
import pandas_ta as ta

#Scoring metric to assess model accuracy
from sklearn.metrics import precision_score

Criamos os dados de entrada e fazemos login no terminal MT5, como antes. Neste passo, não paramos.

Escolhemos novamente o símbolo que precisamos modelar, como antes. Neste passo, não paramos.

Em seguida, especificamos o intervalo de datas para os dados que precisamos modelar, como fizemos anteriormente. Neste passo, não paramos.

Depois, podemos obter os dados históricos. Neste passo, não paramos.

Em seguida, executamos os mesmos passos de pré-processamento descritos acima. Neste passo, não paramos.

Após o pré-processamento dos dados, adicionamos o target, como antes. Neste passo, não paramos.

Em seguida, dividimos os dados em conjuntos de treinamento e teste. Neste passo, não paramos. A divisão dos dados em conjuntos de treinamento e teste não deve ser randomizada. É importante manter a ordem natural do tempo, caso contrário, os resultados podem ser prejudicados, e você pode acabar com uma visão excessivamente otimista dos resultados futuros.

Vamos treinar o modelo.

#Let us fit our glass-box model
#Please note this step can take a while, depending on your computational resources
glass_box = ExplainableBoostingClassifier()
glass_box.fit(train_x,train_y)

Agora podemos olhar para dentro do nosso modelo de caixa de vidro.

#The show function provides an interactive GUI dashboard for us to interface with out model
#The explain_global() function helps us find what our model found important and allows us to identify potential bias or unintended flaws
show(glass_box.explain_global())


Estado global da caixa de vidro

Fig. 7. O estado global da caixa de vidro

A interpretação das estatísticas resumidas é muito importante. Mas antes disso, vamos começar com os conceitos importantes. O "estado global" resume o estado de todo o modelo. Isso permite entender quais características o modelo considera informativas. Não deve ser confundido com o estado local. Os estados locais são usados para explicar as previsões individuais do modelo, ajudam a entender por que o modelo fez determinada previsão e quais características influenciaram as previsões individuais.

Vamos voltar ao estado global do nosso modelo. Como podemos ver, o modelo encontrou o valor da média móvel com deslocamento muito informativo, como esperávamos. Além disso, ele também encontrou um possível fator de interação entre o aumento da ATR e o valor da mid_point. A altura foi a terceira característica mais importante, seguida pelo fator de interação entre o preço de fechamento e a altura (distância entre a média móvel e o fechamento). Observe que não precisamos de nenhuma ferramenta adicional para entender o modelo de caixa branca. Com isso, evitamos completamente o problema de discordância e o viés de confirmação. A informação sobre o estado global é inestimável do ponto de vista do desenvolvimento de características, pois mostra onde futuros esforços podem ser direcionados para o desenvolvimento de melhores características. Avançando, vamos ver como funciona nosso caixa branca.

Obtendo previsões do modelo de caixa branca

#Obtaining glass-box predictions
glass_box_predictions = pd.DataFrame(glass_box.predict(test_x))

Agora vamos medir a precisão do modelo.

glass_box_score = precision_score(test_y,glass_box_predictions)

glass_box_score

0.49095022624434387

Bem, o modelo de caixa branca mostrou uma precisão de 49%. Obviamente, o modelo EBC (Explainable Boosting Classifier) pode ter mais peso em comparação com o XGBClassifier. Isso demonstra as capacidades dos modelos de caixa branca, proporcionando alta precisão sem comprometer a compreensão.

Também é possível obter explicações individuais para cada previsão de nosso modelo de caixa branca, para entender quais características influenciaram a previsão em um nível detalhado — isso é chamado de explicações locais. Elas são bastante fáceis de obter.

#We can also obtain individual explanations for each prediction
show(glass_box.explain_local(test_x,test_y))

Explicações locais

Fig. 8. Explicações locais do EBC (Explainable Boosting Classifier).

O primeiro menu suspenso permite percorrer todas as previsões e escolher aquela que queremos entender melhor. 

Em seguida, observamos a classe real e a classe prevista. Neste caso, a classe real era 0, o que significa que o preço de fechamento caiu, mas classificamos como 1. Também são apresentadas as probabilidades calculadas para cada classe. Nosso modelo estimou incorretamente uma probabilidade de 53% de que a próxima vela fechasse mais alta. Há também um detalhamento da contribuição de cada característica para a probabilidade prevista. As características destacadas em azul trabalham contra a previsão feita pelo nosso modelo, enquanto as características laranjas formaram a base para a previsão. Acontece que o RSI contribuiu mais para essa classificação incorreta, enquanto a interação entre o spread e a altura apontava na direção correta. Essas características podem merecer mais desenvolvimento, mas é necessário um exame mais detalhado das explicações locais antes que possamos tirar conclusões.

Agora, verifiquemos o desempenho do modelo usando o gráfico ROC. O gráfico ROC permite avaliar o desempenho do nosso classificador. Estamos interessados na área sob a curva ou AUC. Teoricamente, um classificador ideal teria uma área total sob a curva de 1. Isso permite avaliar o classificador com um único gráfico.

glass_box_performance = ROC(glass_box.predict_proba).explain_perf(test_x,test_y, name='Glass Box')
show(glass_box_performance)

Gráfico ROC

Fig. 9: Curva ROC do nosso modelo de caixa de vidro.

No modelo, AUC = 0.49. Essa métrica simples permite avaliar o desempenho do modelo usando unidades que são fáceis de entender. Além disso, a curva é independente do modelo e pode ser usada para comparar diferentes classificadores, independentemente dos métodos de classificação subjacentes.

Integrando o modelo de caixa de vidro ao terminal MT5

Agora vamos ao cerne da questão, a integração de nosso modelo ao terminal, começando com uma abordagem mais simples. 

Primeiro, determinaremos o estado da conta atual.

#Fetching account Info
account_info = mt5.account_info()

# getting specific account data
initial_balance = account_info.balance
initial_equity = account_info.equity

print('balance: ', initial_balance)
print('equity: ', initial_equity)

balance: 912.11 equity: 912.11

Obteremos todos os símbolos.

symbols = mt5.symbols_get()

Configuraremos variáveis globais.

#Trading global variables
#The symbol we want to trade
MARKET_SYMBOL = 'Boom 1000 Index'

#This data frame will store the most recent price update
last_close = pd.DataFrame()

#We may not always enter at the price we want, how much deviation can we tolerate?
DEVIATION = 100

#For demonstrational purposes we will always enter at the minimum volume
#However,we will not hardcode the minimum volume, we will fetch it dynamically
VOLUME = 0
#How many times the minimum volume should our positions be
LOT_MUTLIPLE = 1

#What timeframe are we working on?
TIMEFRAME = mt5.TIMEFRAME_D1

Não especificaremos volumes de negociação no código. Obteremos o volume mínimo permitido da corretora e multiplicaremos por algum coeficiente para garantir o envio de ordens válidas. O tamanho da ordem será definido em relação ao volume mínimo.

No nosso caso, abriremos cada negociação com o volume mínimo ou com um coeficiente de 1.

for index,symbol in enumerate(symbols):
    if symbol.name == MARKET_SYMBOL:
        print(f"{symbol.name} has minimum volume: {symbol.volume_min}")
        VOLUME = symbol.volume_min * LOT_MULTIPLE

O índice Boom 1000 tem um volume mínimo de 0,2.

Agora definiremos uma função auxiliar para abrir negociações.

# function to send a market order
def market_order(symbol, volume, order_type, **kwargs):
    #Fetching the current bid and ask prices
    tick = mt5.symbol_info_tick(symbol)
    
    #Creating a dictionary to keep track of order direction
    order_dict = {'buy': 0, 'sell': 1}
    price_dict = {'buy': tick.ask, 'sell': tick.bid}

    request = {
        "action": mt5.TRADE_ACTION_DEAL,
        "symbol": symbol,
        "volume": volume,
        "type": order_dict[order_type],
        "price": price_dict[order_type],
        "deviation": DEVIATION,
        "magic": 100,
        "comment": "Glass Box Market Order",
        "type_time": mt5.ORDER_TIME_GTC,
        "type_filling": mt5.ORDER_FILLING_FOK,
    }

    order_result = mt5.order_send(request)
    print(order_result)
    return order_result

Em seguida, definiremos uma função auxiliar para fechar negociações com base no ticket.

# Closing our order based on ticket id
def close_order(ticket):
    positions = mt5.positions_get()

    for pos in positions:
        tick = mt5.symbol_info_tick(pos.symbol) #validating that the order is for this symbol
        type_dict = {0: 1, 1: 0}  # 0 represents buy, 1 represents sell - inverting order_type to close the position
        price_dict = {0: tick.ask, 1: tick.bid} #bid ask prices

        if pos.ticket == ticket:
            request = {
                "action": mt5.TRADE_ACTION_DEAL,
                "position": pos.ticket,
                "symbol": pos.symbol,
                "volume": pos.volume,
                "type": type_dict[pos.type],
                "price": price_dict[pos.type],
                "deviation": DEVIATION,
                "magic": 100,
                "comment": "Glass Box Close Order",
                "type_time": mt5.ORDER_TIME_GTC,
                "type_filling": mt5.ORDER_FILLING_FOK,
            }

            order_result = mt5.order_send(request)
            print(order_result)
            return order_result

    return 'Ticket does not exist'

Para não solicitar constantemente muitos dados do servidor, atualizaremos o intervalo de datas.

#Update our date from and date to
date_from = datetime(2023,11,1)
date_to = datetime.now()

Também precisaremos de uma função para obter a previsão do modelo de caixa de vidro e usar a previsão como sinais de negociação.

#Get signals from our glass-box model
def ai_signal():
    #Fetch OHLC data
    df = pd.DataFrame(mt5.copy_rates_range(market_symbol,TIMEFRAME,date_from,date_to))
    #Process the data
    df['time'] = pd.to_datetime(df['time'],unit='s')
    df['target'] = (df['close'].shift(-1) > df['close']).astype(int)
    preprocess(df)
    #Select the last row
    last_close = df.iloc[-1:,1:]
    #Remove the target column
    last_close.pop('target')
    #Use the last row to generate a forecast from our glass-box model
    #Remember 1 means buy and 0 means sell
    forecast = glass_box.predict(last_close)
    return forecast[0]

Agora escreveremos a parte principal do robô de negociação em Python.

#Now we define the main body of our Python Glass-box Trading Bot
if __name__ == '__main__':
    #We'll use an infinite loop to keep the program running
    while True:
        #Fetching model prediction
        signal = ai_signal()
        
        #Decoding model prediction into an action
        if signal == 1:
            direction = 'buy'
        elif signal == 0:
            direction = 'sell'
        
        print(f'AI Forecast: {direction}')
        
        #Opening A Buy Trade
        #But first we need to ensure there are no opposite trades open on the same symbol
        if direction == 'buy':
            #Close any sell positions
            for pos in mt5.positions_get():
                if pos.type == 1:
                    #This is an open sell order, and we need to close it
                    close_order(pos.ticket)
            
            if not mt5.positions_totoal():
                #We have no open positions
                market_order(MARKET_SYMBOL,VOLUME,direction)
        
        #Opening A Sell Trade
        elif direction == 'sell':
            #Close any buy positions
            for pos in mt5.positions_get():
                if pos.type == 0:
                    #This is an open buy order, and we need to close it
                    close_order(pos.ticket)
            
            if not mt5.positions_get():
                #We have no open positions
                market_order(MARKET_SYMBOL,VOLUME,direction)
        
        print('time: ', datetime.now())
        print('-------\n')
        time.sleep(60)

Previsão de AI: Sell

Hora:  2023-12-04 15:31:31.569495

-------

Expert Advisor baseado no caixa de vidro

Fig. 10. O robô investidor baseado no modelo de caixa de vidro em Python mostra lucro

Criando um robô de negociação para trabalhar com o modelo

Vamos criar um assistente no MQL5 para nosso modelo de caixa de vidro. Criaremos um robô investidor que ajustará o stop-loss e o take-profit com base nos valores de ATR. O código abaixo atualizará os valores de TP e SL a cada tick. Realizar essa tarefa usando o módulo de integração Python seria um pesadelo e exigiria atualizações frequentes, como a cada minuto ou hora. No entanto, precisamos atualizar SL e TP a cada tick. O usuário deverá especificar a distância entre a entrada e o nível de SL/TP. Multiplicaremos os valores de ATR pelos dados inseridos pelo usuário para determinar a distância do SL ou TP até o ponto de entrada. O segundo parâmetro que o usuário deve especificar é o período de ATR.

//Meta Properties 
#property copyright "Gamuchirai Ndawana"
#property link "https://twitter.com/Westwood267"

//Classes for managing Trades And Orders
#include  <Trade\Trade.mqh>
#include <Trade\OrderInfo.mqh>

//Instatiating the trade class and order manager
CTrade trade;
class COrderInfo;

//Input variables
input double atr_multiple =0.025;  //How many times the ATR should the SL & TP be?
input int atr_period = 200;      //ATR Period

//Global variables
double ask, bid,atr_stop; //We will use these variables to determine where we should place our ATR
double atr_reading[];     //We will store our ATR readings in this arrays
int    atr;               //This will be our indicator handle for our ATR indicator
int min_volume;

int OnInit(){     
                  //Check if we are authorized to use an EA on the terminal
                  if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)){
                           Comment("Press Ctrl + E To Give The Robot Permission To Trade And Reload The Program");
                           //Remove the EA from the terminal
                           ExpertRemove();
                           return(INIT_FAILED);
                  }
                  
                  //Check if we are authorized to use an EA on the terminal
                  else if(!MQLInfoInteger(MQL_TRADE_ALLOWED)){
                            Comment("Reload The Program And Make Sure You Clicked Allow Algo Trading");
                            //Remove the EA from the terminal
                            ExpertRemove();
                            return(INIT_FAILED);
                  }
                  
                  //If we arrive here then we are allowed to trade using an EA on the Terminal                
                  else{
                        //Symbol information
                        //The smallest distance between our point of entry and the stop loss
                        min_volume = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);//SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN)
                        //Setting up our ATR indicator
                        atr = iATR(_Symbol,PERIOD_CURRENT,atr_period);
                        return(INIT_SUCCEEDED);
                  }                       
}

void OnDeinit(const int reason){

}

void OnTick(){
               //Get the current ask
               ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
               //Get the current bid
               bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
               //Copy the ATR reading our array for storing the ATR value
               CopyBuffer(atr,0,0,1,atr_reading);
               //Set the array as series so the natural time ordering is preserved
               ArraySetAsSeries(atr_reading,true); 
               
               //Calculating where to position our stop loss
               //For now we'll keep it simple, we'll add the minimum volume and the current ATR reading and multiply it by the ATR multiple
               atr_stop = ((min_volume + atr_reading[0]) * atr_multiple);

               //If we have open positions we should adjust the stop loss and take profit 
               if(PositionsTotal() > 0){
                        check_atr_stop();          
               }
}

//--- Functions
//This funciton will update our S/L & T/P based on our ATR reading
void check_atr_stop(){
      
      //First we iterate over the total number of open positions                      
      for(int i = PositionsTotal() -1; i >= 0; i--){
      
            //Then we fetch the name of the symbol of the open position
            string symbol = PositionGetSymbol(i);
            
            //Before going any furhter we need to ensure that the symbol of the position matches the symbol we're trading
                  if(_Symbol == symbol){
                           //Now we get information about the position
                           ulong ticket = PositionGetInteger(POSITION_TICKET); //Position Ticket
                           double position_price = PositionGetDouble(POSITION_PRICE_OPEN); //Position Open Price
                           double type = PositionGetInteger(POSITION_TYPE); //Position Type
                           double current_stop_loss = PositionGetDouble(POSITION_SL); //Current Stop loss value
                           
                           //If the position is a buy
                           if(type == POSITION_TYPE_BUY){
                                  //The new stop loss value is just the ask price minus the ATR stop we calculated above
                                  double atr_stop_loss = (ask - (atr_stop));
                                  //The new take profit is just the ask price plus the ATR stop we calculated above
                                  double atr_take_profit = (ask + (atr_stop));
                                  
                                  //If our current stop loss is less than our calculated ATR stop loss 
                                  //Or if our current stop loss is 0 then we will modify the stop loss and take profit
                                 if((current_stop_loss < atr_stop_loss) || (current_stop_loss == 0)){
                                       trade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
                                 }  
                           }
                           
                            //If the position is a sell
                           else if(type == POSITION_TYPE_SELL){
                                     //The new stop loss value is just the bid price plus the ATR stop we calculated above
                                     double atr_stop_loss = (bid + (atr_stop));
                                     //The new take profit is just the bid price minus the ATR stop we calculated above
                                     double atr_take_profit = (bid - (atr_stop));
                                     
                                 //If our current stop loss is greater than our calculated ATR stop loss 
                                 //Or if our current stop loss is 0 then we will modify the stop loss and take profit 
                                 if((current_stop_loss > atr_stop_loss) || (current_stop_loss == 0)){
                                       trade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
                                 }
                           }  
                  }  
            }
}

Robô assistente para nosso caixa de vidro

Fig. 11. Robô assistente para nosso modelo de caixa de vidro

Exportação do modelo de caixa de vidro para o formato Open Neural Network Exchange (ONNX)


ONNX

Fig. 12. Logotipo do Open Neural Network Exchange.

ONNX (Open Neural Network Exchange) é um padrão aberto para representação de modelos de redes neurais. É amplamente suportado graças aos esforços coletivos de empresas de todo o mundo e de diferentes setores. Algumas dessas empresas incluem Microsoft, Facebook, MATLAB, IBM, Qualcomm, Huawei, Intel, AMD, entre outras. No momento da escrita deste artigo, o ONNX é uma forma padrão universal para representar qualquer modelo de aprendizado de máquina, independentemente do ambiente em que foi desenvolvido, e, além disso, permite o desenvolvimento e a implantação de modelos de aprendizado de máquina em diferentes linguagens de programação e ambientes. A ideia principal é que qualquer modelo de aprendizado de máquina pode ser representado como um gráfico de nós e arestas. Cada nó representa uma operação matemática, e cada aresta representa um fluxo de dados. Usando essa representação simples, qualquer modelo de aprendizado de máquina pode ser representado, independentemente do ambiente em que foi criado.

Além disso, precisaremos de um mecanismo que execute modelos ONNX. Por isso, a execução ONNX é responsável. O ambiente de execução ONNX é responsável pela execução eficiente e implantação de modelos ONNX em vários dispositivos: de supercomputadores em centros de dados a telefones celulares no seu bolso e tudo o que há entre eles.

No nosso caso, o ONNX permite integrar o modelo de aprendizado de máquina ao nosso robô investidor e, essencialmente, criar um robô investidor com um cérebro próprio. O terminal MetaTrader 5 oferece um conjunto completo de ferramentas para testar robôs investidores de forma segura e confiável em dados históricos. Além disso, ele permite testá-los em um período futuro. O teste futuro é a execução do robô investidor em tempo real ou durante qualquer período antes da última data de treinamento que o modelo viu. Este é o melhor teste da confiabilidade do modelo ao lidar com dados que não viu durante o treinamento.

Como antes, separaremos o código usado para exportar o modelo ONNX do restante do código que usamos até agora neste artigo. Assim fica mais fácil de ler. Além disso, vamos reduzir o número de parâmetros que o modelo requer como dados de entrada para simplificar sua implementação prática. Para fornecer entrada ao modelo ONNX, usaremos apenas estas características:

1. Lag height — altura com deslocamento; a altura é definida como: (((High + Low) / 2) – Close), e o lag height é o valor anterior da altura.

2. Height growth — avaliação da segunda derivada das leituras de altura. Para o cálculo, a diferença entre os valores históricos sucessivos de altura é feita duas vezes. O valor obtido fornece uma ideia sobre a velocidade de mudança. Simplificando, isso ajuda a entender se o crescimento da altura está acelerando ou desacelerando com o tempo.

3. Midpoint — calculado como ((High + Low) / 2)

4. Midpoint growth — crescimento do ponto médio, uma característica derivada das leituras do ponto médio. Para calcular, a diferença entre valores históricos consecutivos do ponto médio é tomada duas vezes. O valor obtido fornece uma ideia sobre a velocidade de mudança do ponto médio. Isso mostra se o crescimento do ponto médio está acelerando ou desacelerando com o tempo. Falando em termos mais simples e menos técnicos, o valor ajuda a entender se o ponto médio está se afastando do zero com velocidade crescente ou se aproximando do zero com uma velocidade que está aumentando cada vez mais.

Além disso, mudamos os símbolos: na primeira metade do artigo, modelávamos o símbolo Boom 1000 Index, e agora modelaremos o Volatility 75 Index.

O EA também colocará automaticamente os níveis de SL/TP dinamicamente, usando as leituras de ATR. Além disso, permitiremos que ele adicione automaticamente outra posição assim que o lucro exceder um determinado limiar.

A maior parte permaneceu a mesma, exceto por duas novas adições: ONNX e ebm2onnx. Esses dois pacotes permitem converter nossa EBM (máquina explicativa de aumento) para o formato ONNX. 

#Import MetaTrader5 package
import MetaTrader5 as mt5

#Import datetime for selecting data
from datetime import datetime

#Keeping track of time
import time

#Import matplotlib
import matplotlib.pyplot as plt

#Intepret glass-box model
from interpret.glassbox import ExplainableBoostingClassifier

#Intepret GUI dashboard utility
from interpret import show

#Pandas for handling data
import pandas as pd

#Pandas-ta for calculating technical indicators
import pandas_ta as ta

#Scoring metric to assess model accuracy
from sklearn.metrics import precision_score

#ONNX
import onnx

#Import ebm2onnx
import ebm2onnx

#Path handling
from sys import argv

Abaixo repetimos os passos descritos acima para entrar no sistema e obter dados. Adicionalmente, adicionaremos passos para a preparação de características personalizadas.

#Let's create a function to preprocess our data
def preprocess(data):
    data['mid_point'] = ((data['high'] + data['low']) / 2)

    data['mid_point_growth'] = data['mid_point'].diff().diff()

    data['mid_point_growth_lag'] = data['mid_point_growth'].shift(1)

    data['height'] = (data['mid_point'] - data['close'])

    data['height - 1'] = data['height'].shift(1)

    data['height_growth'] = data['height'].diff().diff()
    
    data['height_growth_lag'] = data['height_growth'].shift(1)
    
    data['time'] = pd.to_datetime(data['time'],unit='s')
    
    data.dropna(axis=0,inplace=True)
    
    data['target'] = (data['close'].shift(-1) > data['close']).astype(int)

Após a coleta de dados, os passos são exatamente os mesmos para dividir os dados em conjuntos de treino e teste e para configurar o modelo.

Após a configuração, estamos prontos para exportar o modelo para o formato ONNX.

Primeiro, precisamos especificar o caminho onde salvaremos o modelo. A cada instalação do MetaTrader 5, uma pasta especial é criada para arquivos que podem ser usados no terminal. É muito fácil obter o caminho absoluto usando a biblioteca Python.

terminal_info=mt5.terminal_info()
print(terminal_info)
TerminalInfo(community_account=False, community_connection=False, connected=True, dlls_allowed=False, trade_allowed=True, tradeapi_disabled=False, email_enabled=False, ftp_enabled=False, notifications_enabled=False, mqid=True, build=4094, maxbars=100000, codepage=0, ping_last=222088, community_balance=0.0, retransmission=0.030435223698894183, company='MetaQuotes Software Corp.', name='MetaTrader 5', language='English', path='C:\\Program Files\\MetaTrader 5', data_path='C:\\Users\\Westwood\\AppData\\Roaming\\MetaQuotes\\Terminal\\D0E8209F77C8CF37AD8BF550E51FF075', commondata_path='C:\\Users\\Westwood\\AppData\\Roaming\\MetaQuotes\\Terminal\\Common')

O caminho desejado é armazenado como data path no objeto terminal_info.

file_path=terminal_info.data_path+"\\MQL5\\Files\\"
print(file_path)

C:\Users\Westwood\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Files\

Em seguida, precisamos preparar o caminho. O código aceita o caminho do arquivo que obtivemos do terminal e isola o diretório do caminho, excluindo quaisquer nomes de arquivos.

data_path=argv[0]
last_index=data_path.rfind("\\")+1
data_path=data_path[0:last_index]
print("data path to save onnx model",data_path)

data path to save onnx model C:\Users\Westwood\AppData\Local\Programs\Python\Python311\Lib\site-packages\

Usamos o pacote ebm2onnx para preparar nosso modelo de vidro para conversão para o formato ONNX. Note que é necessário especificar explicitamente os tipos de dados para cada um dos parâmetros de entrada. É melhor fazer isso dinamicamente, usando a função ebm2onnx.get_dtype_from_pandas. Para isso, passamos a ela o frame de dados de treinamento que usamos anteriormente. 

onnx_model = ebm2onnx.to_onnx(glass_box,ebm2onnx.get_dtype_from_pandas(train_x))
#Save the ONNX model in python
output_path = data_path+"Volatility_75_EBM.onnx"
onnx.save_model(onnx_model,output_path)
#Save the ONNX model as a file to be imported in our MetaEditor
output_path = file_path+"Volatility_75_EBM.onnx"
onnx.save_model(onnx_model,output_path)

Agora estamos prontos para trabalhar com o arquivo ONNX no MetaEditor 5. MetaEditor é um ambiente de desenvolvimento integrado para a linguagem MQL5. 

Abrimos o MetaEditor, clicamos duas vezes no Volatility Doctor 75 EBM e vemos:

Primeira abertura de nosso modelo ONNX

Fig. 13: Dados de entrada e saída de nosso modelo ONNX.


Agora, vamos criar um Expert Advisor e importar nosso modelo ONNX.

Começaremos especificando informações gerais sobre o arquivo.

//+------------------------------------------------------------------+
//|                                                         ONNX.mq5 |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
//Meta properties
#property copyright "Gamuchirai Zororo Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

Definiremos variáveis globais.

//Trade Library
#include <Trade\Trade.mqh>           //We will use this library to modify our positions

//Global variables
//Input variables
input double atr_multiple =0.025;    //How many times the ATR should the SL & TP be?
int input lot_mutliple = 1;          //How many time greater than minimum lot should we enter?
const int atr_period = 200;          //ATR Period

//Trading variables
double ask, bid,atr_stop;            //We will use these variables to determine where we should place our ATR
double atr_reading[];                //We will store our ATR readings in this arrays
int    atr;                          //This will be our indicator handle for our ATR indicator
long min_distance;                   //The smallest distance allowed between our entry position and the stop loss
double min_volume;                   //The smallest contract size allowed by the broker
static double initial_balance;       //Our initial trading balance at the beginning of the trading session
double current_balance;              //Our trading balance at every instance of trading
long     ExtHandle = INVALID_HANDLE; //This will be our model's handler
int      ExtPredictedClass = -1;     //This is where we will store our model's forecast
CTrade   ExtTrade;                   //This is the object we will call to open and modify our positions

//Reading our ONNX model and storing it into a data array
#resource "\\Files\\Volatility_75_EBM.onnx" as uchar ExtModel[] //This is our ONNX file being read into our expert advisor

//Custom keyword definitions
#define  PRICE_UP 1
#define  PRICE_DOWN 0

Especificamos a função OnInit(). Usamos OnInit para configurar nosso modelo ONNX. Para configurar o modelo ONNX, precisamos realizar 3 passos simples. Primeiro, criamos o modelo ONNX a partir do buffer que usamos nas variáveis globais acima, quando precisávamos do modelo ONNX como um recurso. Lemos o modelo, especificamos a forma de cada parâmetro de entrada e saída individual. Depois disso, verificamos se ocorreram erros ao tentar definir a forma. Se tudo estiver bem, obteremos o volume mínimo de contrato permitido pela corretora, a distância mínima entre o stop-loss e a posição de entrada, e configuraremos o indicador ATR.

int OnInit()
  {
   //Check if the symbol and time frame conform to training conditions
   if(_Symbol != "Volatility 75 Index" || _Period != PERIOD_M1)
       {
            Comment("Model must be used with the Volatility 75 Index on the 1 Minute Chart");
            return(INIT_FAILED);
       }
    
    //Create an ONNX model from our data array
    ExtHandle = OnnxCreateFromBuffer(ExtModel,ONNX_DEFAULT);
    Print("ONNX Create from buffer status ",ExtHandle);
    
    //Checking if the handle is valid
    if(ExtHandle == INVALID_HANDLE)
      {
            Comment("ONNX create from buffer error ", GetLastError());
            return(INIT_FAILED);
      }
   
   //Set input shape
   long input_count = OnnxGetInputCount(ExtHandle);   
   const long input_shape[] = {1};
   Print("Total model inputs : ",input_count);
   
   //Setting the input shape of each input
   OnnxSetInputShape(ExtHandle,0,input_shape);
   OnnxSetInputShape(ExtHandle,1,input_shape);
   OnnxSetInputShape(ExtHandle,2,input_shape);
   OnnxSetInputShape(ExtHandle,3,input_shape);
   
   //Check if anything went wrong when setting the input shape
   if(!OnnxSetInputShape(ExtHandle,0,input_shape) || !OnnxSetInputShape(ExtHandle,1,input_shape) || !OnnxSetInputShape(ExtHandle,2,input_shape) || !OnnxSetInputShape(ExtHandle,3,input_shape))
      {
            Comment("ONNX set input shape error ", GetLastError());
            OnnxRelease(ExtHandle);
            return(INIT_FAILED);
      }
      
   //Set output shape
   long output_count = OnnxGetOutputCount(ExtHandle);
   const long output_shape[] = {1};
   Print("Total model outputs : ",output_count);
   //Setting the shape of each output
   OnnxSetOutputShape(ExtHandle,0,output_shape);
   //Checking if anything went wrong when setting the output shape
   if(!OnnxSetOutputShape(ExtHandle,0,output_shape))
      {
            Comment("ONNX set output shape error ", GetLastError());
            OnnxRelease(ExtHandle);
            return(INIT_FAILED);
      }
    //Get the minimum trading volume allowed  
    min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);  
    //Symbol information
    //The smallest distance between our point of entry and the stop loss
    min_distance = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
    //Initial account balance
    initial_balance = AccountInfoDouble(ACCOUNT_BALANCE);
    //Setting up our ATR indicator
    atr = iATR(_Symbol,PERIOD_CURRENT,atr_period);
    return(INIT_SUCCEEDED);
//---
  }

A função DeInit remove o manipulador ONNX para não ocupar recursos que não usamos.

void OnDeinit(const int reason)
  {
//---
   if(ExtHandle != INVALID_HANDLE)
      {
         OnnxRelease(ExtHandle);
         ExtHandle = INVALID_HANDLE;
      }
  }

A função OnTick é o coração do especialista, é chamada toda vez que um novo tick é recebido do corretor. Neste caso, começamos monitorando o tempo. Isso permite separar os processos realizados em cada tick daqueles que precisam ser realizados ao formar uma nova vela. Em cada tick, precisamos atualizar os preços Bid e Ask, bem como as posições de take-profit e stop-loss a cada tick. No entanto, a previsão do modelo só é necessária após a formação de uma nova vela, se não houver posições abertas.

void OnTick()
  {
//---
   //Time trackers
   static datetime time_stamp;
   datetime time = iTime(_Symbol,PERIOD_M1,0);

   //Current bid price
   bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   //Current ask price
   ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   
   //Copy the ATR reading our array for storing the ATR value
   CopyBuffer(atr,0,0,1,atr_reading);
   
   //Set the array as series so the natural time ordering is preserved
   ArraySetAsSeries(atr_reading,true); 
   
   //Calculating where to position our stop loss
   //For now we'll keep it simple, we'll add the minimum volume and the current ATR reading and multiply it by the ATR multiple
   atr_stop = ((min_distance + atr_reading[0]) * atr_multiple);
   
   //Current Session Profit and Loss Position
   current_balance = AccountInfoDouble(ACCOUNT_BALANCE);
   Comment("Current Session P/L: ",current_balance - initial_balance);
   
   //If we have a position open we need to update our stoploss
   if(PositionsTotal() > 0){
        check_atr_stop();          
   }
   
    //Check new bar
     if(time_stamp != time)
      {
         time_stamp = time;
         
         //If we have no open positions let's make a forecast and open a new position
         if(PositionsTotal() == 0){
            Print("No open positions making a forecast");
            PredictedPrice();
            CheckForOpen();
         }
      }
   
  }
Em seguida, definimos uma função que atualizará a posição de take-profit e stop-loss de acordo com o ATR. A função itera por cada posição aberta e verifica se a posição corresponde ao símbolo negociado. Se corresponder, a função obtém informações adicionais sobre a posição, com base nas quais ajusta o stop-loss e o take-profit de acordo com a direção da posição. Note que, se a negociação for contra a posição, o take-profit e o stop-loss permanecerão no lugar.
//--- Functions
//This function will update our S/L & T/P based on our ATR reading
void check_atr_stop(){
      
      //First we iterate over the total number of open positions                      
      for(int i = PositionsTotal() -1; i >= 0; i--){
      
            //Then we fetch the name of the symbol of the open position
            string symbol = PositionGetSymbol(i);
            
            //Before going any further we need to ensure that the symbol of the position matches the symbol we're trading
                  if(_Symbol == symbol){
                           //Now we get information about the position
                           ulong ticket = PositionGetInteger(POSITION_TICKET); //Position Ticket
                           double position_price = PositionGetDouble(POSITION_PRICE_OPEN); //Position Open Price
                           long type = PositionGetInteger(POSITION_TYPE); //Position Type
                           double current_stop_loss = PositionGetDouble(POSITION_SL); //Current Stop loss value
                           
                           //If the position is a buy
                           if(type == POSITION_TYPE_BUY){
                                  //The new stop loss value is just the ask price minus the ATR stop we calculated above
                                  double atr_stop_loss = (ask - (atr_stop));
                                  //The new take profit is just the ask price plus the ATR stop we calculated above
                                  double atr_take_profit = (ask + (atr_stop));
                                  
                                  //If our current stop loss is less than our calculated ATR stop loss 
                                  //Or if our current stop loss is 0 then we will modify the stop loss and take profit
                                 if((current_stop_loss < atr_stop_loss) || (current_stop_loss == 0)){
                                       ExtTrade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
                                 }  
                           }
                           
                            //If the position is a sell
                           else if(type == POSITION_TYPE_SELL){
                                     //The new stop loss value is just the bid price plus the ATR stop we calculated above
                                     double atr_stop_loss = (bid + (atr_stop));
                                     //The new take profit is just the bid price minus the ATR stop we calculated above
                                     double atr_take_profit = (bid - (atr_stop));
                                     
                                 //If our current stop loss is greater than our calculated ATR stop loss 
                                 //Or if our current stop loss is 0 then we will modify the stop loss and take profit 
                                 if((current_stop_loss > atr_stop_loss) || (current_stop_loss == 0)){
                                       ExtTrade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
                                 }
                           }  
                  }  
            }
}
Precisamos de mais uma função para abrir uma nova posição. Note que usamos variáveis globais para os valores de Bid e Ask. Isso garante que todo o programa usará o mesmo preço. Além disso, definimos o stop-loss e o take-profit como 0, já que seus valores serão controlados pela função check_atr_stop.
void CheckForOpen(void)
   {
      ENUM_ORDER_TYPE signal = WRONG_VALUE;
      
      //Check signals
      if(ExtPredictedClass == PRICE_DOWN)
         {
            signal = ORDER_TYPE_SELL;
         }
      else if(ExtPredictedClass == PRICE_UP)
         {
            signal = ORDER_TYPE_BUY;
         }
         
      if(signal != WRONG_VALUE && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
         {
            double price, sl = 0 , tp = 0;
            
            if(signal == ORDER_TYPE_SELL)
               {
                  price = bid;
               }
               
           else
               {
                  price = ask;
               }
               
            Print("Opening a new position: ",signal);  
            ExtTrade.PositionOpen(_Symbol,signal,min_volume,price,0,0,"ONNX Order");
         }
   }
   

Finalmente, precisamos de uma função para fazer previsões com nosso modelo ONNX dentro do nosso Expert Advisor. A função também será responsável pelo pré-processamento dos dados, da mesma forma que durante o treinamento. É essencial garantir que os dados sejam processados consistentemente durante o treinamento e a utilização em massa. Observe que cada entrada no modelo é mantida em seu próprio vetor, e cada vetor é então passado para a função de execução ONNX na mesma ordem em que foram fornecidos ao modelo durante o treinamento. É extremamente importante considerar isso ao longo de todo o projeto, caso contrário, podem ocorrer erros durante a operação, que não geram exceções durante a compilação do modelo. Os tipos de dados de cada vetor de entrada devem corresponder ao tipo esperado pelo modelo, assim como o tipo dos dados de saída deve corresponder ao tipo de saída do modelo.

void PredictedPrice(void)
   {
      long output_data[] = {1};
      
      double lag_2_open = double(iOpen(_Symbol,PERIOD_M1,3));
      double lag_2_high = double(iOpen(_Symbol,PERIOD_M1,3));
      double lag_2_close = double(iClose(_Symbol,PERIOD_M1,3));
      double lag_2_low = double(iLow(_Symbol,PERIOD_M1,3));
      double lag_2_mid_point = double((lag_2_high + lag_2_low) / 2);
      double lag_2_height = double(( lag_2_mid_point - lag_2_close));
      
      double lag_open = double(iOpen(_Symbol,PERIOD_M1,2));
      double lag_high = double(iOpen(_Symbol,PERIOD_M1,2));
      double lag_close = double(iClose(_Symbol,PERIOD_M1,2));
      double lag_low = double(iLow(_Symbol,PERIOD_M1,2));
      double lag_mid_point = double((lag_high + lag_low) / 2);
      double lag_height = double(( lag_mid_point - lag_close));
      
      double   open  =  double(iOpen(_Symbol,PERIOD_M1,1));
      double   high  = double(iHigh(_Symbol,PERIOD_M1,1));
      double   low   = double(iLow(_Symbol,PERIOD_M1,1));
      double   close = double(iClose(_Symbol,PERIOD_M1,1));
      double   mid_point = double( (high + low) / 2 );
      double   height =  double((mid_point - close)); 
      
      double first_height_delta = (height - lag_height);
      double second_height_delta = (lag_height - lag_2_height);
      double height_growth = first_height_delta - second_height_delta;
      
      double first_midpoint_delta = (mid_point - lag_mid_point);
      double second_midpoint_delta = (lag_mid_point - lag_2_mid_point);
      double mid_point_growth = first_midpoint_delta - second_midpoint_delta;
      
      vector input_data_lag_height = {lag_height};
      vector input_data_height_grwoth = {height_growth};
      vector input_data_midpoint_growth = {mid_point_growth};
      vector input_data_midpoint = {mid_point};
      
       if(OnnxRun(ExtHandle,ONNX_NO_CONVERSION,input_data_lag_height,input_data_height_grwoth,input_data_midpoint_growth,input_data_midpoint,output_data))
         {
            Print("Model Inference Completed Successfully");
            Print("Model forecast: ",output_data[0]);
         }
       else
       {
            Print("ONNX run error : ",GetLastError());
            OnnxRelease(ExtHandle);
       }
        
       long predicted = output_data[0];
       
       if(predicted == 1)
         {
            ExtPredictedClass = PRICE_UP;
         }
         
       else if(predicted == 0)
         {
            ExtPredictedClass = PRICE_DOWN;
         }
   }

Em seguida, podemos compilar nosso modelo e testá-lo em uma conta demo no terminal MetaTrader 5.

Teste forward do modelo ONNX

Fig. 14. Teste forward do Expert Advisor Glass-box ONNX.

Para garantir que o modelo esteja funcionando sem erros, precisamos verificar as abas Expert e Diário.

Verificando erros

Fig. 15. Verificando erros na aba Expert

Verificando erros na aba Diário

Fig. 16. Verificando erros na aba Diário

Como você pode ver, o modelo está funcionando. Lembre-se de que as configurações do Expert Advisor podem ser alteradas a qualquer momento.

Configuração dos parâmetros do modelo

Fig. 17. Configuração dos parâmetros do Expert Advisor

Problemas comuns

Vamos olhar para alguns erros que podem ocorrer durante a primeira configuração. Vamos analisar o que causa o erro e ver como esses problemas podem ser resolvidos.

Configuração incorreta de dados de entrada ou saída.

O problema mais comum ocorre devido à forma incorreta dos dados de entrada e saída. É necessário definir a forma de entrada para cada função que o modelo espera. Nesse caso, cada índice deve ser revisado para definir a forma de entrada para cada objeto nesse índice. Se não especificarmos a forma para cada objeto, o modelo ainda pode ser compilado sem erros. Mas ao tentar executar o modelo, receberemos um erro. O código de erro é 5808, descrito na documentação MQL5 como "A dimensão do tensor não é definida ou está incorretamente especificada". Neste exemplo, temos 4 parâmetros de entrada. Por exemplo, vamos definir a forma apenas para um. 

Configuração incorreta da forma de entrada de dados

Fig. 18. O Expert Advisor é compilado sem exceções

Aqui está como o erro aparece na aba Expert. Observo que o código correto está anexado ao artigo.

Mensagem de erro 5808

Fig. 19. Mensagem de erro 5808

Conversão incorreta de tipos

A conversão incorreta de tipos às vezes pode levar à perda total de dados ou simplesmente ao crash do Expert Advisor. No exemplo abaixo, um array de inteiros foi usado para armazenar os dados de saída do modelo ONNX. Lembre-se, originalmente o modelo ONNX tem saída do tipo int64. Por que você acha que isso levaria a um erro? O erro está relacionado ao fato de que o tipo int não tem memória suficiente para armazenar os dados de saída do nosso modelo. Para a saída do modelo, são necessários 8 bytes, mas um array int fornece apenas 4. A solução é simples: certifique-se de usar o tipo de dados correto para armazenar os dados de entrada e saída, e se for necessário fazer conversão de tipos, assegure-se de que tudo aconteça de acordo com as regras de conversão especificadas na Documentação MQL5. O código de erro é 5807, e a descrição é "Tamanho incorreto do parâmetro".

Erro de conversão de tipos

Fig. 20. Conversão incorreta de tipos

Mensagem de erro 5807

Fig. 21. Mensagem de erro 5807.

Erro na chamada ONNX Run

A função ONNX Run espera que cada uma das entradas do modelo seja passada em um array separado. No exemplo abaixo, combinamos todas as entradas em um único array e passamos esse único array para a função de execução ONNX. O código compila, mas ao executar, um erro é exibido na aba Experts. O código de erro é 5804, e na documentação, ele é brevemente descrito como "Número incorreto de parâmetros passados para OnnxRun".

Erro na chamada ONNX Run

Fig. 22. Erro na chamada da função ONNXRun

Mensagem de erro 5804

Fig. 23. Mensagem de erro 5804.

Considerações finais

Neste artigo, vimos quais podem ser os benefícios de usar um modelo de caixa de vidro na programação para mercados financeiros. Esse tipo de opção oferecem informações valiosas com pouco esforço em comparação com a quantidade de trabalho que seria necessário para extrair a mesma informação de um modelo de caixa preta. Além disso, modelos de caixa de vidro são mais fáceis de depurar, manter, interpretar e explicar. Não é suficiente supor que os modelos funcionem conforme o esperado. É necessário verificar isso, olhando “por baixo do capô”. 

Modelos de caixa de vidro têm uma grande desvantagem que ainda não discutimos: eles são menos flexíveis em comparação com os modelos de caixa preta. Modelos de caixa de vidro representam uma área de pesquisa aberta, e com o tempo, certamente surgirão modelos mais flexíveis no futuro, mas, no momento da redação deste artigo, a questão da flexibilidade permanece relevante. Isso significa que existem certas relações que são melhor modeladas por um modelo de caixa preta. Além disso, as implementações atuais de modelos de caixa de vidro são baseadas em árvores de decisão, portanto, a implementação atual dos BoostingClassifiers explicados no InterpretML herda todas as desvantagens das árvores de decisão.

Até a próxima! Desejo-lhe paz, amor, harmonia e negociações lucrativas.

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

Padrões de projeto no MQL5 (Parte 3): Padrões comportamentais 1 Padrões de projeto no MQL5 (Parte 3): Padrões comportamentais 1
Neste novo artigo da série dedicada a padrões de projeto, exploraremos os padrões comportamentais para entender como criar métodos eficazes de interação entre os objetos criados. Ao projetar esses padrões de comportamento, poderemos entender como desenvolver software reutilizável, expansível e testável.
Desenvolvendo um sistema de Replay (Parte 49): Complicando as coisas (I) Desenvolvendo um sistema de Replay (Parte 49): Complicando as coisas (I)
Aqui neste artigo iremos complicar um pouco as coisa. Fazendo uso do que foi visto nos artigos anteriores, iremos começar a liberar o arquivo de Template, para que o usuário possa fazer uso de um template pessoal. No entanto, irei fazer as mudanças aos poucos, visto que também irei modificar o indicador a fim de proporcionar um alivio ao MetaTrader 5.
Anotação de dados na análise de série temporal (Parte 4): Decomposição da interpretabilidade usando anotação de dados Anotação de dados na análise de série temporal (Parte 4): Decomposição da interpretabilidade usando anotação de dados
Esta série de artigos apresenta várias técnicas destinadas a rotular séries temporais, técnicas essas que podem criar dados adequados à maioria dos modelos de inteligência artificial (IA). A rotulação de dados (ou anotação de dados) direcionada pode tornar o modelo de IA treinado mais alinhado aos objetivos e tarefas do usuário, melhorar a precisão do modelo e até mesmo ajudar o modelo a dar um salto qualitativo!
Algoritmos de otimização populacional: Método Nelder-Mead (NM) Algoritmos de otimização populacional: Método Nelder-Mead (NM)
O artigo apresenta um estudo completo do método Nelder-Mead explicando como o simplex — o espaço dos parâmetros da função — muda e se reestrutura a cada iteração para alcançar a solução ótima, e também descreve como melhorar este método.