English Русский Español Deutsch 日本語
preview
Regressões Espúrias em Python

Regressões Espúrias em Python

MetaTrader 5Estatística e análise | 25 setembro 2024, 09:23
32 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Sinopse

Antes de mergulhar no campo do trading algorítmico com aprendizado de máquina, é crucial verificar se existe uma relação significativa entre as entradas do modelo e a variável que desejamos prever. Este artigo ilustra a utilidade de empregar testes de raiz unitária nos resíduos do modelo para validar a presença de tal relação em nossos conjuntos de dados.

Infelizmente, é possível construir modelos usando conjuntos de dados que não têm nenhuma relação genuína. Lamentavelmente, esses modelos podem apresentar métricas de erro impressionantemente baixas, reforçando uma falsa sensação de controle e perspectivas excessivamente otimistas. Esses modelos falhos são comumente chamados de "regressões espúrias."

Este artigo começará primeiro cultivando uma compreensão intuitiva das regressões espúrias. Depois, vamos gerar dados sintéticos de séries temporais para simular uma regressão espúria e observar seus efeitos característicos. Subsequentemente, exploraremos métodos para identificar regressões espúrias, baseando-nos em nossos insights para validar um modelo de aprendizado de máquina desenvolvido em Python. Finalmente, se o nosso modelo for validado, exportaremos para ONNX e implementaremos uma estratégia de negociação em MQL5.


Introdução: Regressões Espúrias Acontecem o Tempo Todo

Durante meados do século 19, Ignaz Semmelweis era um médico praticante em Viena. Ele estava profundamente frustrado com as estatísticas que observava no hospital onde trabalhava.

Ignaz Semmelweis

Fig 1: Ignaz Semmelweis


O problema era que ⅕ das mulheres saudáveis, que davam à luz no hospital, morriam de febres contraídas durante o trabalho de parto. Ignaz estava determinado a entender o porquê. A maioria dos médicos da época atribuía isso ao "ar ruim", que eles acreditavam carregar espíritos malignos que causavam esses problemas. Por mais cômico que isso possa parecer hoje, era amplamente aceito na época. Mas isso não satisfez Ignaz. Com o passar do tempo, Ignaz um dia observou que médicos e estudantes de medicina realizando autópsias em um necrotério de um lado do hospital corriam para realizar partos do outro lado do hospital, sem lavar as mãos entre uma atividade e outra. Após convencer a equipe de seu hospital local a praticar a higiene das mãos, a taxa de mortalidade materna caiu de 20% para 1%.

Infelizmente, as descobertas de Ignaz passaram despercebidas. Todos os esforços que ele fez para compartilhar suas descobertas com outros médicos e instituições médicas só o afastaram ainda mais da comunidade médica da época e de sua crença concreta no "ar ruim". Ignaz Semmelweis morreu como um pária social, em um asilo, aos 46 anos. O que podemos aprender com os médicos que ignoraram as sábias palavras de Semmelweis, e por que foi tão difícil para eles verem seu erro?

O problema é que é possível construir um modelo usando dados que não têm nenhuma relação, além disso, esse modelo pode, por acaso, produzir métricas de erro baixas e falsamente provar relações que não existem. Esses modelos são chamados de regressões espúrias.

Uma regressão espúria é um modelo que falsamente prova uma relação que não existe. Veja, os médicos poderiam ter dito a si mesmos: “Há muitos espíritos malignos no ar hoje, portanto mais mães morrerão amanhã.” Como previsto, mais mulheres morreram no dia seguinte, no entanto, o médico estava certo pelas razões erradas. Ao construir modelos de aprendizado de máquina, nossos modelos também podem estar certos pelas razões erradas.

Se você estiver explicitamente ciente de que existe uma relação entre seus dados de entrada e saída, então você não tem motivo para se preocupar. No entanto, o que você pode fazer se estiver em dúvida? Ou se nunca verificou e simplesmente presumiu que deveria haver uma relação?

A solução mais conhecida é realizar testes especializados nos resíduos do seu modelo. Esses testes são chamados de testes de raiz unitária. Não tentaremos definir raízes unitárias nesta discussão, pois esse é outro tópico por si só. No entanto, para alcançar nossos objetivos, basta saber que se conseguirmos encontrar raízes unitárias para nossos resíduos, então nossa regressão é espúria.

Há apenas uma limitação material com a solução de raiz unitária que estamos considerando hoje: podemos falhar em encontrar raízes unitárias, embora elas existam, erro de classe 1. E, alternativamente, podemos falsamente encontrar raízes unitárias que não existem, um erro de classe 2.

Existem muitos testes que podemos usar para verificar se nossos resíduos possuem raízes unitárias, como o Teste Aumentado de Dickey Fuller e o Teste de Kwiatkowski-Phillips-Schmidt-Shin. Cada teste tem seus pontos fortes e fracos e falhará em diferentes condições. Para ver as regressões espúrias em ação, vamos gerar nossos próprios dados de séries temporais. Vamos criar dois conjuntos de dados de séries temporais que não têm relação entre si e observar o que acontece quando treinamos um modelo usando esses dois conjuntos de dados independentes.


Simulando Regressões Espúrias

Regressões espúrias podem ocorrer por uma grande variedade de razões, mas a razão mais comum é modelar duas séries temporais independentes e não estacionárias. Vamos entender essa definição técnica. Uma série temporal é simplesmente observações registradas de forma uniforme de uma variável aleatória. Quando dizemos que uma série temporal é estacionária, suas propriedades estatísticas, como média, variância e estrutura de autocorrelação, permanecem relativamente constantes ao longo do tempo. Uma série temporal é não estacionária quando suas propriedades estatísticas flutuam ao longo do tempo.

Em nossa discussão, adotaremos uma abordagem prática, simulando nossos próprios dados para entender a verdade fundamental em cada etapa. Essa abordagem nos permite observar os efeitos em primeira mão. Começamos importando os pacotes necessários.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller , kpss
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

Em seguida, definiremos as propriedades estatísticas para nossos dois conjuntos de dados, um irá simular nossos dados de entrada e o outro nossos dados de saída. Ambos os conjuntos de dados conterão números aleatórios e normalmente distribuídos.

size = 1000
mu , sigma = 0 , 1
mu_y , sigma_y = 2 , 4

O código abaixo gera duas séries temporais, x_non_stationary e y_non_stationary, representando passeios aleatórios. A aleatoriedade é introduzida através da distribuição normal, e a soma cumulativa garante que cada valor na série dependa dos valores anteriores, criando um comportamento dinâmico e não estacionário.

Os conjuntos de dados x_non_stationary e y_non_stationary estão sendo gerados aleatoriamente pela função numpy.random.normal. Portanto, não há relação entre os dois conjuntos de dados.

steps = np.random.normal(mu,sigma,size)
steps_y = np.random.normal(mu_y,sigma_y,size)
x_non_stationary =  pd.DataFrame(100 + np.cumsum(steps),index= np.arange(0,1000))
x_non_stationary_lagged = x_non_stationary.shift(1)
x_non_stationary_lagged.dropna(axis=0,inplace=True)
y_non_stationary =   pd.DataFrame(100 + np.cumsum(steps_y),index= np.arange(0,1000))

Vamos ver nossos dois conjuntos de dados aleatórios.

plt.plot(x_non_stationary)

Random walk x

Fig 2: Random walk x




Vamos traçar nossa série temporal y não estacionária.

plt.plot(y_non_stationary)

Random walk y

Fig 3: Random walk y.

Agora, vamos observar os resultados da regressão das duas séries temporais independentes e não estacionárias.

ols = sm.OLS(y_non_stationary,x_non_stationary)
lm = ols.fit()
print(lm.summary())

x_y_non_stationary_regression

Fig 4: Os Resultados Resumidos da Regressão de Duas Variáveis Não Estacionárias Independentes.

Em muitas séries temporais, há um componente de tendência aleatória conhecido como tendência estocástica. Mesmo quando duas séries temporais são independentes, suas tendências estocásticas podem exibir correlações breves e locais. Infelizmente, essas correlações momentâneas podem, às vezes, enganar nossos modelos, levando-os a concluir erroneamente que existe uma relação entre duas séries temporais independentes. Regressões espúrias resultantes de tais casos frequentemente produzem métricas de R-quadrado elevadas. É crucial lembrar que o R-quadrado mede a proporção da variância na resposta explicada pela variância nas variáveis independentes. Portanto, se duas variáveis têm tendências estocásticas correlacionadas, a confiabilidade da métrica R-quadrado pode ser comprometida. A questão da regressão espúria é particularmente relevante no contexto de dados de séries temporais e merece consideração cuidadosa.

Nosso modelo tem uma métrica de R-quadrado ajustado que quase atinge 1, significando que o modelo percebe o ajuste como quase perfeito. O modelo afirma que aproximadamente 90% da variação na variável de resposta pode ser explicada pela variância nos preditores. No entanto, esta demonstração serve como um lembrete crucial dos riscos associados às regressões espúrias. Sabemos que não há relação entre entradas e saídas, ambas são aleatórias e não têm nada em comum.

Os desafios persistem, pois o valor de P parece significativo, e o valor 0 está conspicuamente ausente de nossos intervalos de confiança. Embora isso possa normalmente ser interpretado como um indicativo de um ajuste excelente, é necessário ter cautela. O modelo, neste caso, indica erroneamente uma forte relação que na verdade não existe. Nós mesmos criamos os dados de entrada e saída, portanto, sabemos que não há relação.


Defasando o Preditor

Um sinal revelador de uma regressão espúria surge quando incluímos uma versão defasada do preditor. Nesses casos, o coeficiente antes significativo de repente se torna insignificante, como veremos a seguir. Esse fenômeno serve como um indicador crucial, nos guiando para longe de conclusões errôneas e destacando a importância de compreender e lidar com regressões espúrias na análise de séries temporais.

Repetiremos o mesmo procedimento acima, mas desta vez incluiremos também uma versão defasada dos dados de entrada.

ols = sm.OLS(y_non_stationary.iloc[0:998,0],x_matrix.loc[0:998,['current_x','lagged_x']])
lm = ols.fit()
print(lm.summary())

X_y non stationry regression

Fig 5: Estatísticas Resumidas da Nossa Regressão Espúria.

Adicionando mais uma camada de cautela, observe que nosso valor de R-quadrado supera nosso valor de Durbin-Watson. Essa observação pode ser considerada um sinal de alerta adicional que aponta para uma possível regressão espúria. A estatística Durbin-Watson, empregada na análise de regressão, serve para detectar a presença de autocorrelação nos resíduos de um modelo de regressão. A autocorrelação se manifesta quando os resíduos em uma série temporal ou modelo de regressão apresentam correlação entre si. No contexto de dados de séries temporais, onde as observações dependem das anteriores, o teste de Durbin-Watson torna-se particularmente relevante. Seus resultados fornecem insights valiosos sobre a presença de autocorrelação, guiando-nos ainda mais na interpretação do desempenho do modelo.


Estatística de Durbin-Watson

  •     Intervalo: A estatística de Durbin-Watson possui valores entre 0 e 4.
  •     Interpretação: Um valor próximo de 2 indica ausência de autocorrelação significativa. Valores significativamente abaixo de 2 sugerem autocorrelação positiva (os resíduos estão positivamente correlacionados). Valores significativamente acima de 2 sugerem autocorrelação negativa (os resíduos estão negativamente correlacionados).

Embora um R-quadrado alto e um valor de Durbin-Watson baixo possam levantar suspeitas de uma regressão espúria, é importante notar que esses indicadores, por si só, não confirmam conclusivamente sua presença. No campo da análise de séries temporais, testes diagnósticos adicionais e conhecimento do domínio tornam-se componentes essenciais de uma avaliação completa. 


Testes de Raiz Unitária

O método mais confiável para identificar uma regressão espúria está na análise dos resíduos. Se os resíduos do nosso modelo não forem estacionários, isso fornece uma forte indicação de que a regressão é realmente espúria. No entanto, determinar se uma série temporal é estacionária não é uma tarefa simples. No nosso caso, sabendo que a regressão é espúria e, portanto, os resíduos são não estacionários, o Teste Aumentado de Dickey Fuller pode falhar em rejeitar a hipótese nula. Em outras palavras, pode não demonstrar que os dados não são estacionários, mesmo que a regressão seja espúria, ilustrando as sutilezas e desafios envolvidos na identificação de regressões espúrias. Isso ressalta a importância de uma abordagem cuidadosa, combinando testes estatísticos e conhecimento do domínio para navegar eficazmente nas complexidades da análise de séries temporais.

Agora usaremos o sklearn para ajustar um modelo em nosso conjunto de treinamento.

lm = LinearRegression()
lm.fit(x[train_start:train_end],y[train_start:train_end])

Em seguida, calcularemos os resíduos.

residuals = y[test_start:test_end] - lm.predict(x[test_start:test_end])

Vamos traçar os resíduos

residuals.plot()

Residuals plot

Fig 6: Os Resíduos do Nosso Modelo de Regressão Construído com Scikit-Learn.


Agora, vamos submeter nossos resíduos ao Teste Aumentado de Dickey-Fuller para determinar sua estacionariedade.


Teste Aumentado de Dickey-Fuller

O Teste Aumentado de Dickey-Fuller (ADF) serve como um instrumento estatístico fundamental projetado para avaliar a estacionariedade de séries temporais ou identificar uma raiz unitária, indicativa de não estacionariedade. No campo da análise de séries temporais, a estacionariedade assume um papel primordial, significando a constância de atributos estatísticos, como média e variância, ao longo do tempo. A presença de uma raiz unitária em uma série temporal, denotando não estacionariedade, sugere que as observações dentro da série podem ser caracterizadas como estocásticas ou aleatórias. O teste ADF, portanto, oferece uma metodologia robusta para examinar o comportamento temporal de um conjunto de dados, contribuindo para uma compreensão detalhada de suas características inerentes e potenciais implicações para análises subsequentes.

  1. Hipótese Nula: A hipótese nula do teste ADF postula que a série temporal possui uma raiz unitária, indicando não estacionariedade.
  2. Hipótese Alternativa: A hipótese alternativa do teste ADF é que a série temporal não tem uma raiz unitária e é estacionária.
  3. Regra de Decisão: A regra de decisão no teste ADF envolve a comparação do estatístico do teste com os valores críticos. Se o estatístico do teste for menor que o valor crítico, a hipótese nula (presença de uma raiz unitária) é rejeitada, indicando estacionariedade. Por outro lado, se o estatístico do teste for maior que o valor crítico, não há evidências suficientes para rejeitar a hipótese nula, sugerindo não estacionariedade.

Ao realizar o Teste Aumentado de Dickey-Fuller (ADF) nos resíduos gerados a partir do nosso modelo, os resultados servirão como um determinante crucial para discernir a presença de características estacionárias ou não estacionárias nos resíduos. O teste ADF tem uma importância significativa no processo de validação dos resultados de nossa regressão, desempenhando um papel fundamental na garantia da confiabilidade do nosso arcabouço analítico. Ao elucidar as propriedades de estacionariedade dos resíduos, esse teste contribui substancialmente para o aprimoramento da robustez interpretativa de nossa análise de séries temporais.

adfuller(residuals)

(-12.753804093890963, 8.423533501802878e-24, 2, 497, {'1%': -3.4435761493506294, '5%': -2.867372960189225, '10%': -2.5698767442886696}, 1366.9343966932422)

Nosso foco principal está no valor de p derivado deste experimento, especificamente o segundo valor na lista fornecida, que é 8.423533501802878e-24. Notavelmente, esse valor de p se aproxima de zero e excede significativamente qualquer valor crítico razoável. No contexto do Teste ADF, se o estatístico ADF for menor que o valor crítico, torna-se pertinente rejeitar a hipótese nula, significando a presença de estacionariedade.

É imperativo reconhecer que o teste ADF, como qualquer teste estatístico, possui limitações e suposições inerentes. Existem vários fatores que podem contribuir para o fracasso do teste ADF em aceitar a hipótese nula, levando à rejeição da presença de uma raiz unitária, mesmo quando os dados subjacentes são não estacionários. Compreender essas sutilezas é crucial para uma interpretação abrangente dos resultados do teste.

  1. Tamanho da Amostra Pequeno: O desempenho do teste ADF pode ser afetado por tamanhos pequenos de amostra. Nesses casos, o teste pode carecer de poder suficiente para detectar a não estacionariedade.
  2. Ordem de Defasagem Inadequada: A escolha da ordem de defasagem no teste ADF é crucial. Se a ordem de defasagem for especificada incorretamente, pode levar a resultados imprecisos. Usar poucas ou muitas defasagens pode impactar a capacidade do teste de capturar a estrutura subjacente dos dados.
  3. Presença de Tendências Determinísticas: Se os dados contêm tendências determinísticas (por exemplo, tendências lineares, quadráticas) que não são levadas em conta no modelo do teste, o teste ADF pode falhar em rejeitar a hipótese nula. Nesses casos, etapas de pré-processamento como a remoção de tendência podem ser necessárias.
  4. Diferenciação Inadequada: Se a ordem de diferenciação usada no teste ADF for insuficiente para tornar os dados estacionários, o teste pode falhar em rejeitar a hipótese nula.


Kwiatkowski-Phillips-Schmidt-Shin

O teste Kwiatkowski-Phillips-Schmidt-Shin (KPSS) se apresenta como uma alternativa viável ao Teste ADF para avaliar a estacionariedade de dados de séries temporais. Embora ambos os testes sejam prevalentes na análise de séries temporais, eles divergem em suas hipóteses nula e alternativa, bem como em seus modelos subjacentes. A seleção entre os testes ADF e KPSS depende das características específicas da série temporal em exame e da questão de pesquisa em geral. Utilizar ambos os testes em conjunto muitas vezes oferece uma análise mais abrangente da estacionariedade, proporcionando aos pesquisadores uma compreensão detalhada da dinâmica das séries temporais.

  1. Hipótese Nula: A hipótese nula do teste KPSS é que a série temporal é estacionária em torno de uma tendência. A estacionariedade em torno de uma tendência implica que a série exibe uma raiz unitária, sugerindo a presença de uma tendência determinística.
  2. Hipótese Alternativa: A hipótese alternativa do teste KPSS é que a série temporal não é estacionária em torno de uma tendência, indicando que ela é estacionária em torno de uma tendência estocástica.
  3. Regra de Decisão: A regra de decisão para o teste KPSS envolve a comparação do estatístico do teste com os valores críticos em um nível de significância escolhido (por exemplo, 1%, 5% ou 10%). Se o estatístico do teste for maior que o valor crítico, a hipótese nula é rejeitada, sugerindo que a série temporal não é estacionária em torno de uma tendência. Por outro lado, se o estatístico do teste for menor que o valor crítico, a hipótese nula não pode ser rejeitada, implicando estacionariedade em torno de uma tendência.

No caso do teste KPSS, um limiar comumente adotado é um nível de significância de 0,05. Se o estatístico KPSS for inferior a esse limiar, sugere não estacionariedade nos dados. Em nossa análise, o estatístico KPSS produziu um valor de 0.016, afirmando sua divergência em relação ao limite crítico e indicando uma tendência à não estacionariedade no conjunto de dados. Esse resultado ressalta ainda mais a importância de considerar várias ferramentas de diagnóstico, como os testes ADF e KPSS, para garantir uma avaliação completa e precisa das características das séries temporais.

kpss(residuals)

(0.6709994557854182, 0.016181867655871072, 1, {'10%': 0.347, '5%': 0.463, '2.5%': 0.574, '1%': 0.739})

O teste KPSS pode rejeitar falsamente a hipótese nula (H0) em determinadas circunstâncias, levando a um erro de Tipo I. Um erro de Tipo I ocorre quando o teste conclui incorretamente que a série temporal não é estacionária em torno de uma tendência, quando, na realidade, ela é.

Aqui estão algumas situações em que o teste KPSS pode rejeitar incorretamente a hipótese nula:

  1. Padrões Sazonais: O teste KPSS é sensível tanto à tendência quanto à sazonalidade. Se uma série temporal exibir um padrão sazonal forte, o teste pode interpretá-lo como uma tendência não estacionária. Nesses casos, pode ser necessário aplicar a diferenciação para lidar com a sazonalidade.
  2. Quebras Estruturais: Se houver quebras estruturais na série temporal, como mudanças repentinas e significativas no processo gerador de dados subjacente, o teste KPSS pode detectá-las como tendências não estacionárias. Quebras estruturais podem levar à rejeição da hipótese nula.
  3. Outliers: A presença de outliers nos dados pode influenciar o desempenho do teste KPSS. Outliers podem ser percebidos como desvios de tendência, levando à rejeição da estacionariedade em torno da tendência. A robustez contra outliers é uma consideração importante ao interpretar os resultados do teste KPSS.
  4. Tendências Não Lineares: O teste KPSS assume uma tendência linear. Se a tendência subjacente na série temporal for não linear, o teste pode produzir resultados enganosos. Tendências não lineares podem não ser capturadas adequadamente pelo teste, levando a uma falsa rejeição da estacionariedade.

É crucial interpretar os resultados do teste KPSS com cautela e considerar as características específicas da série temporal sendo analisada. Além disso, combinar o teste KPSS com outros testes de estacionariedade, como o Teste ADF, pode fornecer uma avaliação mais abrangente das propriedades de estacionariedade da série temporal.


Unindo Tudo

Tendo estabelecido uma base de confiança, agora é oportuno redirecionarmos nosso foco dos dados de controle sintéticos para a análise de dados de mercado autênticos obtidos diretamente do nosso terminal MetaTrader 5. Para facilitar essa transição, propomos o desenvolvimento de um script em MetaQuotes Language 5 (MQL5). Este script será especificamente projetado para recuperar dados de nosso terminal de negociação, formatando-os e exportando-os em formato CSV.

Iniciando nosso script, o primeiro passo envolve a declaração de variáveis globais, com o primeiro conjunto dedicado a armazenar os identificadores de nossos indicadores técnicos. Essas variáveis desempenharão um papel fundamental na gestão eficiente e no acesso aos indicadores relevantes durante a execução do script, contribuindo para a coerência e organização geral do nosso programa em MetaQuotes Language 5 (MQL5).

//---Our handlers for our indicators
int ma_handle;
int rsi_handle;
int cci_handle;
int ao_handle;

Subsequentemente, precisamos de estruturas de dados cuidadosamente projetadas para acomodar e organizar as leituras de nossos indicadores técnicos. Essas estruturas de dados serão usadas ao longo da execução do script. 

//---Data structures to store the readings from our indicators
double ma_reading[];
double rsi_reading[];
double cci_reading[];
double ao_reading[];

Em seguida, criamos um nome para o arquivo.

//---File name
string file_name = "Market Data.csv";

Agora vamos definir a quantidade de dados a serem buscados.

//---Amount of data requested
int size = 3000;

Iniciando o desenvolvimento de nosso manipulador de eventos OnStart, a primeira chamada a ser feita envolve a inicialização de nossos indicadores técnicos designados.

//---Setup our technical indicators
ma_handle = iMA(_Symbol,PERIOD_CURRENT,20,0,MODE_EMA,PRICE_CLOSE);
rsi_handle = iRSI(_Symbol,PERIOD_CURRENT,60,PRICE_CLOSE);
cci_handle = iCCI(_Symbol,PERIOD_CURRENT,10,PRICE_CLOSE);
ao_handle = iAO(_Symbol,PERIOD_CURRENT);

Prosseguindo com a execução do script, a próxima tarefa envolve a transferência de valores de nossos identificadores de indicadores para as estruturas de dados correspondentes. Este processo essencial envolve o mapeamento meticuloso das saídas dos indicadores para as estruturas de dados previamente estabelecidas.

//---Set the values as series
CopyBuffer(ma_handle,0,0,size,ma_reading);
ArraySetAsSeries(ma_reading,true);
CopyBuffer(rsi_handle,0,0,size,rsi_reading);
ArraySetAsSeries(rsi_reading,true);
CopyBuffer(cci_handle,0,0,size,cci_reading);
ArraySetAsSeries(cci_reading,true);
CopyBuffer(ao_handle,0,0,size,ao_reading);
ArraySetAsSeries(ao_reading,true);

Ao nos prepararmos para iniciar o processo de gravação de arquivos, um precursor crítico envolve o estabelecimento de um manipulador de arquivos dentro do nosso script.

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

Subsequentemente, uma fase fundamental do nosso script se desenrola à medida que percorremos sistematicamente o conjunto de dados, orquestrando o processo meticuloso de gravação dos dados no arquivo CSV designado. Este procedimento iterativo envolve uma análise detalhada e extração de cada ponto de dados, aderindo a uma sequência cuidadosamente orquestrada que está alinhada com os parâmetros estabelecidos.

for(int i=-1;i<=size;i++){
      if(i == -1){
            FileWrite(file_handle,"Open","High","Low","Close","MA 20","RSI 60","CCI 10","AO");
      }
      
        else{
                FileWrite(file_handle,iOpen(_Symbol,PERIOD_CURRENT,i),
                                        iHigh(_Symbol,PERIOD_CURRENT,i),
                                        iLow(_Symbol,PERIOD_CURRENT,i),
                                        iClose(_Symbol,PERIOD_CURRENT,i),
                                        ma_reading[i],
                                        rsi_reading[i],
                                        cci_reading[i],
                                        ao_reading[i]);
      } 
}

Após a conclusão da configuração do script, prossiga para executá-lo no símbolo de sua preferência. Posteriormente, o script gerará um arquivo CSV, espelhando o formato exemplificado abaixo, fornecendo uma representação abrangente e estruturada dos dados pertinentes associados ao símbolo selecionado.

Trading data

Fig 7: Nosso Arquivo CSV de Dados de Mercado.

Com nosso foco agora voltado para a análise de dados de mercado autênticos, nosso objetivo é construir um modelo de regressão projetado para prever o crescimento esperado de preço nos próximos 15 minutos. O critério crucial para nossa busca analítica é a validação da autenticidade do modelo de regressão. Uma vez estabelecida essa autenticidade, pretendemos exportar o modelo validado para o formato ONNX, aproveitando-o posteriormente para o desenvolvimento de um Expert Advisor.

Iniciando esta fase, nosso primeiro passo envolve o carregamento das dependências essenciais. Entre essas dependências, destaca-se o pacote 'Arch', conhecido por seu conjunto abrangente de ferramentas de análise estatística. A integração do 'Arch' nos fornece uma série de recursos inestimáveis para empregar em nossos empreendimentos analíticos, aumentando a profundidade e sofisticação de nossa abordagem à análise de dados de mercado.

import pandas as pd
import numpy as np
import statsmodels.api as sm
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from arch.unitroot import PhillipsPerron , ADF , KPSS
import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import DoubleTensorType

Em seguida, lemos o CSV que criamos usando nosso script MQL5.

csv = pd.read_csv("/enter/your/path/here")

Preparamos então nosso alvo, que é o crescimento no preço de fechamento.

csv["Target"] = csv["Close"] / csv["Close"].shift(-15)
csv.dropna(axis=0,inplace=True)

Podemos examinar nosso dataframe.

data frame

Fig 8: Lendo Nosso Arquivo CSV de Dados de Mercado com Pandas

A partir daí, vamos preparar nossa divisão de treino e teste.

train = np.arange(0,20000)
test = np.arange(20020,89984)


Agora ajustaremos uma regressão linear múltipla usando statsmodels.

ols = sm.OLS(csv.loc[:,"Target"],csv.loc[:,["Open","High","Low","Close","MA 20","RSI 60","CCI 10","AO"]])
lm = ols.fit()
print(lm.summary())

Market Data OLS

Fig 9: Resultados da Regressão de Todas as Variáveis para Prever o Crescimento no Preço.

Vamos nos aprofundar na interpretação dos resultados do nosso modelo. O intervalo de confiança para o coeficiente associado à variável de preço de abertura abrange 0, o que implica uma falta de significância estatística. Consequentemente, a decisão de excluir essa variável do nosso modelo é informada pela possível insignificância de sua contribuição. Um exame mais detalhado revela que tanto o Índice de Força Relativa (RSI) quanto o Índice de Canal de Commodities (CCI) exibem coeficientes próximos de 0, com seus respectivos intervalos de confiança mostrando uma proximidade semelhante a esse valor. Dado isso, optamos por eliminar essas variáveis, supondo que sua contribuição informativa seja limitada.

Embora nosso modelo exiba um valor de R-quadrado notavelmente alto, sugerindo que uma parte substancial da variância é explicada pelas variáveis incluídas, uma análise concomitante da estatística Durbin-Watson (DW) revela um valor baixo. Isso nos leva a adotar uma abordagem cuidadosa, pois o baixo valor de DW indica a possibilidade de correlação residual, o que pode comprometer a validade do nosso modelo. Consequentemente, recomendamos uma análise meticulosa dos resíduos, com foco específico em sua estacionariedade. Essa camada adicional de escrutínio é imperativa para garantir a robustez e confiabilidade do nosso modelo ao capturar os padrões subjacentes nos dados.

Vamos criar uma estrutura de dados para armazenar as variáveis que consideramos importantes.

predictors = ["High","Low","Close","MA 20","AO"]

Vamos registrar os resíduos.

residuals = csv.loc[test[0]:test[-1],"Target"] - lm.predict(csv.loc[test[0]:test[-1],predictors])

Vamos prosseguir com a geração dos gráficos de resíduos, uma etapa crítica no processo de avaliação do modelo. 

plt.plot(residuals)

Market data residuals

Fig 10: Gráfico dos Resíduos da Previsão de Dados de Mercado.

Analisar os resíduos é uma etapa crucial para entender quão bem nosso modelo se ajusta aos dados. Os resíduos podem revelar se nosso modelo possui erros sistemáticos ou se está cometendo erros consistentes. Isso é importante porque nos ajuda a verificar se nosso modelo segue certas regras básicas, como manter variação consistente em suas previsões e não depender de erros anteriores.

Uma coisa que precisamos observar ao analisar os resíduos é algo chamado "raiz unitária". Isso basicamente significa que os resíduos mostram um padrão de mudança ao longo do tempo que não desaparece. É como ter uma tendência persistente em nossos erros. Encontrar uma raiz unitária nos resíduos é um grande problema porque prejudica algumas das suposições básicas que fazemos sobre nosso modelo, como a independência entre as previsões.

Lidar com raízes unitárias é importante porque, se não o fizermos, isso pode afetar negativamente nossas estimativas de quão bom nosso modelo é e dificultar a confiança nas conclusões que tiramos dele.

Portanto, ao analisar os resíduos, não estamos apenas cumprindo uma formalidade. Estamos garantindo que nosso modelo resista a um escrutínio rigoroso e corrigindo quaisquer problemas, como raízes unitárias, para tornar nossas previsões mais confiáveis.

Pode ser útil pensar dessa maneira: Imagine a tarefa de classificar um gato, uma empreitada aparentemente simples. Em um cenário ideal, onde nossa compreensão de um gato simboliza uma verdade imutável, cada classificação seria impecável, e, se traçássemos o erro de tal modelo ideal, obteríamos uma linha reta e estacionária, representada por y = uma constante. Essa linha simboliza a constância da verdade de que um gato é sempre um gato em todos os momentos, e se o classificássemos corretamente, o erro seria zero.

No entanto, se a proficiência do classificador diminuir, introduzindo classificações incorretas, os resíduos começam a se desviar desse estado de estacionariedade perfeita. Esse desvio corresponde à divergência do classificador em relação à verdade e se manifesta como flutuações nos resíduos. A não estacionariedade surge nos dias em que um cachorro pode ser rotulado erroneamente como um gato ou vice-versa, refletindo a incerteza do classificador em relação às características definidoras de um gato.

Para aprofundar ainda mais o rigor estatístico, considere o vínculo entre uma raiz unitária nos resíduos e a não estacionariedade na análise de séries temporais. A presença de uma raiz unitária nos resíduos, análoga à falta de conhecimento do classificador, indica não estacionariedade, levando a um aumento da volatilidade nos resíduos. Quanto maior a falta de conhecimento, mais pronunciadas se tornam as flutuações, semelhante a classificar erroneamente cães como gatos e vice-versa.

Entender esses conceitos é importante porque nos ajuda a ajustar melhor nosso modelo para fazer previsões mais precisas. Portanto, mesmo que tudo pareça bem à primeira vista, vale a pena verificar duas vezes para garantir que nossos resíduos estejam se comportando como deveriam. Lembre-se, não existe um teste único que resolva todos os problemas, então é melhor usar vários métodos e observar as tendências gerais nos resíduos.


Teste Phillip-Perron

O Teste Phillips-Perron é um teste estatístico usado em econometria e análise de séries temporais para avaliar a presença de uma raiz unitária em um conjunto de dados de série temporal. Uma raiz unitária implica que uma variável de série temporal possui uma tendência estocástica, tornando-a não estacionária. A estacionariedade é uma suposição crucial em muitas análises estatísticas, e dados de séries temporais não estacionários podem levar a resultados de regressão espúrios.

O Teste Phillips-Perron é uma variação do teste Dickey-Fuller e foi proposto por Peter C.B. Phillips e Pierre Perron em 1988. Assim como o teste Dickey-Fuller, o Teste Phillips-Perron foi projetado para detectar a presença de uma raiz unitária, examinando o comportamento de uma variável de série temporal ao longo do tempo.

A ideia básica por trás do Teste Phillips-Perron é regredir a variável da série temporal diferenciada em seus valores defasados. O estatístico do teste é então usado para avaliar se o coeficiente na variável defasada é significativamente diferente de zero. Se o coeficiente for significativamente diferente de zero, sugere evidência contra a presença de uma raiz unitária e implica que a série temporal é estacionária.

Uma característica notável do Teste Phillips-Perron é que ele permite certas formas de correlação serial e heterocedasticidade nos dados, algo que o teste Dickey-Fuller original não considera. Isso torna o Teste Phillips-Perron robusto contra certas violações de suposições que podem estar presentes em dados de séries temporais do mundo real.

  1. Hipótese Nula: A hipótese nula do Teste Phillips-Perron é que a variável da série temporal contém uma raiz unitária, implicando que ela é não estacionária.
  2. Hipótese Alternativa: A hipótese alternativa é que a variável da série temporal não contém uma raiz unitária, indicando que ela é estacionária.
  3. Regra de Decisão: Se o estatístico do teste calculado for menor que o valor crítico, rejeite a hipótese nula de que uma raiz unitária está presente, sugerindo evidências de que a série temporal é estacionária. Se o estatístico do teste calculado for maior que o valor crítico, não rejeite a hipótese nula, indicando evidências insuficientes para concluir que a série temporal é estacionária.

Usaremos a biblioteca arch para realizar o Teste Phillips-Perron.

pp = PhillipsPerron(residuals)

Podemos obter os resultados resumidos. Estatísticas Resumidas do Teste Phillips-Perron

pp.summary()

Teste Phillips-Perron
(Z-tau)

Estatística do teste -73,916

Valor de p 0,000

Atrasos 62

Tendência: Constante
Valores Críticos: -3,43 (1%), -2,86 (5%), -2,57 (10%)

Hipótese Nula: O processo contém uma raiz unitária.
Hipótese Alternativa: O processo é fracamente estacionário.

Vamos interpretar os resultados juntos. O estatístico do teste é -73.916. Esse valor representa quantos desvios-padrão o coeficiente estimado está afastado do valor hipotético de 1 (indicando a presença de uma raiz unitária). Neste caso, um valor muito negativo do estatístico do teste sugere fortes evidências contra a presença de uma raiz unitária, apoiando a estacionariedade da série temporal.

O valor de p associado ao estatístico do teste é 0,000. O valor de p é uma medida das evidências contra a hipótese nula. Um valor de p de 0,000 significa que o estatístico do teste observado é extremamente improvável sob a suposição de que a hipótese nula seja verdadeira. Em termos práticos, esse valor de p extremamente pequeno fornece fortes evidências contra a presença de uma raiz unitária.

Dado o valor de p muito baixo (0,000), você normalmente rejeitaria a hipótese nula em níveis convencionais de significância (por exemplo, 0,05). As evidências sugerem que a série temporal provavelmente é estacionária, já que o valor de p está abaixo do nível de significância escolhido. Em resumo, com base nos resultados fornecidos, você tem fortes evidências para rejeitar a hipótese nula de uma raiz unitária, indicando que a série temporal provavelmente é estacionária. No entanto, não podemos basear nossas decisões em um único teste; queremos observar medidas de tendência central de diferentes testes.

Teste Aumentado de Dickey-Fuller

Usaremos a biblioteca 'arch' para realizar o Teste Aumentado de Dickey-Fuller.

adf = ADF(residuals)

Podemos obter estatísticas resumidas. Estatísticas Resumidas do Teste Aumentado de Dickey-Fuller

adf.summary()
Resultados do Teste Aumentado de Dickey-Fuller

Estatístico do Teste: -31.300
Valor de p: 0,000
Defasagens: 60

Tendência: Constante
Valores Críticos: -3,43 (1%), -2,86 (5%), -2,57 (10%)

Hipótese Nula: O processo contém uma raiz unitária.


Hipótese Alternativa: O processo é fracamente estacionário.


Vamos interpretar os resultados. O estatístico do teste é -31.300. Assim como o Teste Phillips-Perron, o estatístico do teste ADF é usado para avaliar a presença de uma raiz unitária na série temporal. Neste caso, o valor muito negativo indica fortes evidências contra a hipótese nula de uma raiz unitária, apoiando a ideia de que a série temporal é estacionária.

O valor de p associado é 0,000. Semelhante ao Teste Phillips-Perron, um valor de p de 0,000 significa que o estatístico do teste observado é extremamente improvável sob a suposição de que a hipótese nula (presença de uma raiz unitária) seja verdadeira. O valor de p muito pequeno fornece fortes evidências contra a hipótese nula.

Dado o valor de p baixo (0,000), você normalmente rejeitaria a hipótese nula em níveis convencionais de significância. As evidências do teste ADF apoiam a conclusão de que a série temporal provavelmente é estacionária.

Tanto o Teste Phillips-Perron quanto o Teste Aumentado de Dickey-Fuller forneceram fortes evidências contra a presença de uma raiz unitária, indicando que a série temporal provavelmente é estacionária. A semelhança nos resultados entre esses dois testes é esperada, pois ambos são projetados para avaliar a estacionariedade em dados de séries temporais. A escolha entre eles muitas vezes depende das características específicas dos dados e das suposições dos testes. No seu caso, ambos os testes sugerem que a série temporal é estacionária.


Vamos exportar nosso modelo para o formato Open Neural Network Exchange (ONNX).

ONNX, ou Open Neural Network Exchange, é um formato aberto e interoperável para representar modelos de aprendizado de máquina. Desenvolvido por uma comunidade colaborativa, o ONNX permite a troca fácil de modelos entre várias ferramentas e frameworks, promovendo a interoperabilidade no ecossistema de aprendizado de máquina. Ele fornece uma maneira padronizada de representar e transferir modelos treinados, facilitando a implantação em diferentes plataformas e a integração em diversas aplicações. O ONNX oferece suporte a uma ampla gama de modelos e frameworks de aprendizado de máquina, promovendo flexibilidade e eficiência nos fluxos de trabalho de desenvolvimento e implantação de modelos.

Este código está definindo um tipo inicial para uma variável chamada 'double_input' que será usada no contexto de gerar um arquivo ONNX (Open Neural Network Exchange). O tipo especificado é 'DoubleTensorType', indicando que os dados de entrada devem estar em precisão dupla. A forma do tensor de entrada é determinada pelo número de colunas em um DataFrame (presumivelmente chamado 'csv') correspondente às variáveis usadas para previsão (recuperadas usando 'csv.loc[:, predictors].shape[1]'). O 'None' na forma indica que o tamanho da primeira dimensão (provavelmente representando o número de amostras) não está fixado neste estágio.

initial_type_double = [('double_input', DoubleTensorType([None, csv.loc[:,predictors].shape[1]]))]

Este código está utilizando a função 'convert_sklearn' para converter nosso modelo de regressão linear treinado ('lm') em sua representação ONNX. A variável 'initial_type_double', definida anteriormente, especifica o tipo esperado para os dados de entrada como precisão dupla. Além disso, o parâmetro 'target_opset' é definido como 12, indicando a versão desejada do conjunto de operadores ONNX. O 'onnx_model_double' resultante será uma representação ONNX do modelo de regressão linear fornecido, adequada para implantação e interoperabilidade com outros frameworks que suportam o formato ONNX.

onnx_model_double = convert_sklearn(lm, initial_types=initial_type_double, target_opset=12)

Este código está especificando o nome do arquivo ("EURUSD_ONNX") para salvar a representação ONNX de um modelo de regressão linear. O modelo ONNX resultante, convertido usando a função 'convert_sklearn' mencionada anteriormente, será armazenado com esse nome de arquivo, tornando-o facilmente identificável e acessível para uso ou implantação futura.

onnx_model_filename = "EURUSD_ONNX"

Este código está combinando o nome do arquivo definido anteriormente ("EURUSD_ONNX") com o sufixo "_Double.onnx" para criar um novo nome de arquivo ("EURUSD_ONNX_Double.onnx"). Posteriormente, a função 'onnx.save_model' é usada para salvar o modelo ONNX ('onnx_model_double') em um arquivo com o nome construído. Este processo garante que o modelo ONNX, representando o modelo de regressão linear em precisão dupla, seja armazenado e possa ser facilmente referenciado usando o nome de arquivo especificado.

onnx_filename=onnx_model_filename+"_Double.onnx"
onnx.save_model(onnx_model_double, onnx_filename)


Construindo um Expert Advisor para Nosso Arquivo ONNX em MetaQuotes Language 5 (MQL5) Utilizando o MetaEditor Integrado e Versátil.

Desenvolver um Expert Advisor (EA) para nosso arquivo ONNX em MetaQuotes Language 5 (MQL5) envolve aproveitar as capacidades do MetaEditor integrado e versátil. Um Expert Advisor é um script escrito em MQL5 que permite negociação automatizada na plataforma MetaTrader 5. Neste contexto, o EA irá interagir com o arquivo ONNX, facilitando a integração de modelos de aprendizado de máquina nas estratégias de negociação, aprimorando os processos de tomada de decisão com base em análises preditivas. O MetaEditor fornece um ambiente completo para codificação, testes e otimização de EAs, garantindo implantação e execução eficientes dentro da estrutura do MetaTrader 5.

Primeiro, incluímos a biblioteca Trade em MQL5, que é uma biblioteca padrão para lidar com operações de negociação no MetaTrader 5 (MT5). A biblioteca Trade fornece funções e estruturas predefinidas que facilitam a execução de várias atividades de negociação, como abertura e fechamento de posições, gerenciamento de ordens e tratamento de eventos relacionados a negociações. Incluir esta biblioteca em um Expert Advisor (EA) permite uma implementação eficiente da lógica de negociação e operações dentro do código MQL5.

//Our trade class helps us open trades
#include <Trade\Trade.mqh>
CTrade Trade;

Este trecho de código em MQL5 envolve a utilização de um Expert Advisor (EA) e incorpora um modelo ONNX para análises preditivas. A diretiva #resource é utilizada para embutir o arquivo do modelo ONNX, "EURUSD_ONNX_Double.onnx," nos recursos do EA como um array de bytes chamado ONNXModel. Isso facilita o acesso e a utilização do modelo de aprendizado de máquina dentro do EA.

A variável ONNXHandle é inicializada como INVALID_HANDLE, indicando que ela será usada para armazenar o identificador associado ao modelo ONNX assim que ele for carregado durante a execução do EA.

Além disso, PredictedMove é inicializada com o valor -1, sugerindo que o movimento ou resultado previsto com base no modelo ONNX ainda não foi determinado. É provável que essa variável seja atualizada com o valor previsto uma vez que o EA processe os dados relevantes através do modelo ONNX durante sua execução. Os detalhes da lógica preditiva e processamento posterior dependerão das seções subsequentes do código do EA.

//Loading our ONNX model
#resource "ONNX\\EURUSD_ONNX_Double.onnx" as uchar EURUSD_ONNX_MODEL[]
long     ONNXHandle=INVALID_HANDLE;

Nesta seção do código MQL5 para um Expert Advisor, dois conjuntos de variáveis são declarados: ma_handle e ma_reading[] para uma média móvel, e ao_handle e ao_reading[] para um Oscilador Impressionante (Awesome Oscillator).

A variável ma_handle serve como referência ou identificador para o indicador de média móvel, permitindo que o EA interaja e obtenha informações sobre essa ferramenta de análise técnica específica. O array ma_reading[] é destinado a armazenar os valores calculados da média móvel, permitindo que o EA acesse e analise seus valores históricos para tomada de decisões.

Da mesma forma, a variável ao_handle é esperada para representar um identificador para o indicador Oscilador Impressionante, enquanto o array ao_reading[] é designado para armazenar os valores calculados correspondentes. 

//Handles for our technical indicators and dynamic arrays to store their readings
int ma_handle;
double ma_reading[];
int ao_handle;
double ao_reading[];

//Inputs
int input sl_width = 150; //How wide should the stoploss be?
int input  positions = 1; //How many positions should the we open?
int input lot_multiple = 1; //How many times greater than minimum lot should each position be?

//Symbol variables
double min_volume;
double bid,ask;

//We'll use this time stamp to keep track of the number of candles passing
static datetime time_stamp;

//Our model's forecast will be stored here
vector model_forecast(1);


Função OnInit

A função OnInit() é uma parte crucial de um Expert Advisor (EA) em MQL5 e serve como a função de inicialização que é automaticamente executada quando o EA é anexado a um gráfico no MetaTrader 5. Nessa função, várias tarefas relacionadas à configuração e preparação do EA são geralmente realizadas. O restante do código que examinaremos está aninhado dentro do nosso manipulador OnInit().

Esta declaração condicional dentro da função OnInit() do Expert Advisor verifica se o símbolo de negociação é "EURUSD" e se o período gráfico está definido para M1 (intervalos de um minuto) Se as condições não forem atendidas, o EA imprime uma mensagem no console usando a função Print(), afirmando que o modelo deve operar especificamente com o par de moedas "EURUSD" no período de um minuto. Subsequentemente, a declaração return(INIT_FAILED) é utilizada para encerrar o processo de inicialização do EA, indicando uma falha de inicialização se as condições especificadas não forem satisfeitas.

int OnInit(){
    //Validating trading conditions
   if(_Symbol!="EURUSD" || _Period!=PERIOD_M1)
     {
      Print("Model must work with EURUSD on the M1 timeframe");
      return(INIT_FAILED);
     }

Este segmento de código dentro do Expert Advisor (EA) em MQL5 é responsável por criar um modelo ONNX a partir de um buffer estático. A função OnnxCreateFromBuffer é utilizada para esse propósito, recebendo como parâmetros os dados do modelo ONNX armazenados no buffer ONNXModel e utilizando as configurações padrão para a criação do modelo.

Após a execução, a variável ONNXHandle é atribuída com o identificador associado ao modelo ONNX criado. Subsequentemente, uma declaração condicional verifica se o ONNXHandle é um identificador válido (diferente de INVALID_HANDLE). Se o identificador não for válido, o EA imprime uma mensagem de erro no console usando a função Print(), fornecendo informações sobre o erro encontrado (via GetLastError()) e, em seguida, sinaliza uma falha de inicialização retornando INIT_FAILED.

Esta seção de código é crucial para inicializar o EA com um modelo ONNX funcional, garantindo que o modelo seja criado com sucesso a partir do buffer fornecido. Qualquer falha nesse processo é prontamente comunicada.

ONNXHandle=OnnxCreateFromBuffer(ONNXModel,ONNX_DEFAULT);

if(ONNXHandle==INVALID_HANDLE)
{
   Print("OnnxCreateFromBuffer error ",GetLastError());
   return(INIT_FAILED);
}

A linha de código declara um array constante chamado input_shape com dois elementos. Este array é do tipo long e armazena inteiros. Os elementos do array representam o formato dos dados de entrada para um modelo ou algoritmo de aprendizado de máquina. Neste caso específico, o array input_shape é inicializado com os valores {1, 5}, indicando que os dados de entrada devem ter dimensões de uma linha e cinco colunas.

//Defining the model's input shape
const long input_shape[] = {1,5};

Este bloco de código, dentro do Expert Advisor em MQL5, verifica se a configuração do formato de entrada para o modelo ONNX é bem-sucedida usando a função OnnxSetInputShape. O formato de entrada é especificado pelo array input_shape, que indica as dimensões esperadas dos dados de entrada.

A declaração if avalia se a negação da condição OnnxSetInputShape (retornando verdadeiro se a configuração não for bem-sucedida) é verdadeira. Se a condição for verdadeira, o EA imprime uma mensagem de erro no console usando a função Print(), transmitindo detalhes sobre o erro encontrado, obtidos através de GetLastError(). Subsequentemente, a função retorna INIT_FAILED, indicando uma falha de inicialização.

if(!OnnxSetInputShape(ONNXHandle,ONNX_DEFAULT,input_shape)) 
{
    Print("OnnxSetInputShape error ",GetLastError());
    return(INIT_FAILED); 
}

Esta linha de código declara um array constante chamado output_shape com dois elementos. Semelhante à declaração anterior para input_shape, este array é do tipo long e armazena inteiros. Neste caso, os valores {1, 1} são atribuídos a output_shape, indicando que o formato esperado dos dados de saída do modelo de aprendizado de máquina é um array unidimensional com um único elemento.

A especificação do formato de saída é crucial para lidar e interpretar adequadamente os resultados gerados pelo modelo de aprendizado de máquina. 

//Defining the model's output shape
const long output_shape[] = {1,1};

Este bloco de código, dentro do Expert Advisor em MQL5, verifica se a configuração do formato de saída para o modelo ONNX é bem-sucedida usando a função OnnxSetOutputShape. O formato de saída é especificado pelo array output_shape, que indica as dimensões esperadas dos dados de saída.

A declaração if avalia se a negação da condição OnnxSetOutputShape (retornando verdadeiro se a configuração não for bem-sucedida) é verdadeira. Se a condição for verdadeira, o EA imprime uma mensagem de erro no console usando a função Print(), transmitindo detalhes sobre o erro encontrado, obtidos através de GetLastError(). Subsequentemente, a função retorna INIT_FAILED, indicando uma falha de inicialização.

Assim como a configuração do formato de entrada, configurar o formato de saída é essencial para alinhar o formato esperado da saída do modelo de aprendizado de máquina com os requisitos de processamento subsequente no EA. 

if(!OnnxSetOutputShape(ONNXHandle,0,output_shape))
{
    Print("OnnxSetOutputShape error ",GetLastError());
    return(INIT_FAILED);
}

Nesta parte da função OnInit() do Expert Advisor em MQL5, dois indicadores técnicos são inicializados.

  1. ma_handle é atribuído o identificador para uma Média Móvel Exponencial (EMA) de 20 períodos, calculada com base nos preços de fechamento (PRICE_CLOSE) do gráfico de um minuto (PERIOD_M1) para o símbolo especificado por _Symbol.
  2. ao_handle é atribuído o identificador para o indicador Oscilador Impressionante (AO) calculado no gráfico de um minuto para o mesmo símbolo.
  3. min_volume é o menor tamanho de contrato permitido pelo corretor.
//Setting up our technical indicators
ma_handle = iMA(_Symbol,PERIOD_M1,20,0,MODE_EMA,PRICE_CLOSE);
ao_handle = iAO(_Symbol,PERIOD_M1);
min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
return(INIT_SUCCEEDED);
}


Função OnDeinit

A função OnDeinit neste Expert Advisor MQL5 serve como o manipulador para o processo de desinicialização do EA. Ela é automaticamente executada quando o EA é removido ou quando a plataforma de negociação é fechada.

Dentro desta função, uma declaração condicional verifica se a variável ONNXHandle contém um identificador válido (diferente de INVALID_HANDLE). Se a condição for verdadeira, isso significa que o modelo ONNX foi inicializado durante o tempo de execução do EA. Nesses casos, a função OnnxRelease é chamada para liberar os recursos associados ao modelo ONNX e, posteriormente, o ONNXHandle é definido como INVALID_HANDLE.

Essa rotina de desinicialização garante a liberação adequada dos recursos, prevenindo vazamentos de memória e contribuindo para a eficiência e limpeza do ciclo de vida do EA. Ela reflete uma prática de codificação responsável para gerenciar e liberar os recursos adquiridos durante a execução do EA, aumentando a robustez e confiabilidade do sistema de negociação.

void OnDeinit(const int reason)
  {
      //OnDeinit we should freeup resources we don't require
      //We'll begin by releasing the ONNX models then removing the Expert Advisor
      if(ONNXHandle!=INVALID_HANDLE)
     {
      OnnxRelease(ONNXHandle);
      ONNXHandle=INVALID_HANDLE;
     }
   //Lastly remove the Expert Advisor
   ExpertRemove();
  }


Função OnTick

A função OnTick é um componente vital deste Expert Advisor em MQL5, representando o código executado a cada tick recebido. Nesta função:

Dados históricos dos indicadores Média Móvel (MA) e Oscilador Impressionante (AO) são obtidos usando a função CopyBuffer. Os 10 valores mais recentes são copiados em arrays (ma_reading e ao_reading), e ArraySetAsSeries é aplicado para organizar os arrays em uma forma de série.

O timestamp atual (current_time) é obtido usando a função iTime para o gráfico de um minuto (PERIOD_M1) e o símbolo especificado (_Symbol).

Uma declaração condicional verifica se o timestamp atual difere do timestamp armazenado (time_stamp). Se houver uma diferença, indicando um novo tick, a função ModelForecast é chamada.

Essa estrutura de código permite que o EA capture e organize os valores recentes dos indicadores a cada tick, facilitando a avaliação periódica das previsões do modelo de aprendizado de máquina por meio da função ModelForecast. A função OnTick, portanto, estabelece a base para a tomada de decisões em tempo real, com base nas condições mais recentes do mercado e nas previsões do modelo, contribuindo para a natureza dinâmica e adaptativa do Expert Advisor.

void OnTick()
{
   //Update the arrays storing the technical indicator values to their current values
   //Then set the arrays as series so that the current reading is at the top and the oldest reading is last
   CopyBuffer(ma_handle,0,0,10,ma_reading);
   ArraySetAsSeries(ma_reading,true);
   CopyBuffer(ao_handle,0,0,10,ao_reading);
   ArraySetAsSeries(ao_reading,true);
   ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
//Update time marker
   datetime current_time = iTime(_Symbol,PERIOD_M1,0);
   //Periodically make forecasts if we have no open positions
   if(time_stamp != current_time){
      //No open positions
      if(PositionsTotal() == 0){
               ModelForecast();
      }  
      //We have open positions to manage
      else if(PositionsTotal() > 0){
               ManageTrade();
      }
   }
  }


Inferência do Modelo

A função ModelForecast dentro deste Expert Advisor MQL5 é projetada para executar o modelo de aprendizado de máquina para fazer previsões com base nas condições atuais do mercado. Aqui está uma explicação do código:

Dois vetores são declarados dentro da função:

  1. model_forecast: Um vetor usado para armazenar a saída do modelo de aprendizado de máquina, representando sua previsão ou previsão.
  2. model_input: Um vetor contendo os recursos de entrada necessários para o modelo. Esses recursos incluem os preços máximo, mínimo e de fechamento da vela atual, o valor da média móvel (ma_reading[0]) e o valor do Oscilador Impressionante (ao_reading[0]).

A função OnnxRun é chamada para realizar a inferência usando o modelo ONNX (ONNXHandle). A flag ONNX_NO_CONVERSION indica que nenhuma conversão de tipo de dados é aplicada durante o processo de inferência. Os recursos de entrada (model_input) são fornecidos, e a previsão resultante é armazenada no vetor model_forecast.

Uma declaração condicional verifica se o processo de inferência foi bem-sucedido. Se não for, uma mensagem de erro é impressa no console usando a função Print(), transmitindo detalhes sobre o erro encontrado obtidos através de GetLastError().

Se a inferência for bem-sucedida, a previsão armazenada no vetor model_forecast é impressa no console.

Essa função encapsula os passos essenciais para obter previsões do modelo com base nas condições atuais do mercado, fomentando uma estratégia de negociação dinâmica e adaptativa dentro do Expert Advisor. A inclusão de mecanismos de tratamento de erros melhora a robustez do sistema, fornecendo insights sobre possíveis problemas durante o processo de inferência. Uma vez concluída, a função chama NextMove().

//This function provides these utilities:
//          1) Inferencing using our ONNX model 
//          2) Calling the next function responsible for intepreting model forecasts and other subroutines
void ModelForecast(void){
      //These are the inputs for our ONNX model:
      //          1)High 
      //          2)Low
      //          3)Close
      //          4)20 PERIOD MA MODE: EMA  APPLIED_PRICE:PRICE CLOSE 
      //          5)Awesome Oscilator
      vector model_input{iHigh(_Symbol,PERIOD_M1,0),iLow(_Symbol,PERIOD_M1,0),iClose(_Symbol,PERIOD_M1,0),ma_reading[0],ao_reading[0]};
      //Inferencing with our model
      if(!OnnxRun(ONNXHandle,ONNX_NO_CONVERSION,model_input,model_forecast)){
            Print("Error performing inference: ",GetLastError());
      }
      //Pring model forecast to the terminal
      else{
               Print(model_forecast);
                NextMove();
      }
}


Interpretação do Modelo

Existem 2 tarefas a serem realizadas para interpretar nosso modelo:

  1. Interpretando a Saída do Modelo: A função usa a função InterpretForecast para interpretar a saída de um modelo preditivo. A função InterpretForecast é chamada duas vezes, primeiro com um argumento de 1 e depois com um argumento de -1. O propósito dessas chamadas é verificar se a saída do modelo indica uma direção específica: 1 para uma previsão positiva e -1 para uma previsão negativa.
  2. Agindo com Base nas Interpretações: Dependendo da interpretação da saída do modelo, a função toma ações específicas. Se o modelo prever um resultado positivo (1), ele chama a função CheckOrder com um argumento de 1 e, em seguida, retorna. Se o modelo prever um resultado negativo (-1), ele chama a função CheckOrder com um argumento de -1.

Em resumo, a função NextMove é projetada para processar a saída de um modelo preditivo, interpretá-la com base em valores específicos (1 ou -1) e tomar ações correspondentes, chamando a função CheckOrder com os valores interpretados.

//This function provides these utilities:
//          1) Getting the model output intepreted
//          2) Acting on the intepretations
void NextMove(){
      if(InterpretForecast(1)){
            CheckOrder(1);
            return;
      }     
      else if(InterpretForecast(-1)){
            CheckOrder(-1);
      }
}


A função InterpretForecast tem o propósito de interpretar a saída de um modelo preditivo com base em uma direção especificada. A função recebe um argumento direction que pode ser 1 ou -1. A interpretação depende de o modelo ter previsto um valor maior que 1 ou menor que 1.

Aqui está uma explicação do código:

Se direction for igual a 1, a função verifica se o primeiro elemento (model_forecast[0]) do array model_forecast é maior que 1. Se for, a função retorna true, indicando que o modelo está prevendo um crescimento maior que o preço atual.

Se direction for igual a -1, a função verifica se o primeiro elemento (model_forecast[0]) do array model_forecast é menor que 1. Se for, a função retorna true, indicando que o modelo está prevendo um crescimento menor que o preço atual.

Se direction não for nem 1 nem -1, a função retorna false. Isso serve como um caso padrão, indicando que a direção especificada não é reconhecida, e a função não pode fornecer uma interpretação significativa.

Em resumo, a função InterpretForecast verifica o valor previsto pelo modelo com base na direção especificada e retorna true se a condição for atendida, caso contrário, retorna false.

//This function provides these utilities:
//          1) Check whether the model forecasted a reading greater than or less than 1.
bool InterpretForecast(int direction){
      //1 means check if the model is forecasting growth greater than the current price
      if(direction == 1){
            return(model_forecast[0] > 1);
      }
      //-1 means check if the model is forecasting growth less than the current price
      if(direction == -1){
            return(model_forecast[0] < 1);
      }
      //Otherwise return false.
      return false;
}


Execução de Ordens

O código a seguir define a função CheckOrder. Essa função tem a responsabilidade de iniciar posições de negociação com base em uma direção de ordem especificada.

Verificação de Posições Abertas: A declaração condicional inicial (PositionsTotal() == 0) serve como uma verificação pré-requisito, garantindo que novas posições sejam abertas exclusivamente quando não houver negociações ativas no portfólio.

Execução de Ordens de Compra: Caso o parâmetro order_direction seja igual a 1 (indicando uma ordem de compra), a função utiliza um loop for para executar iterativamente o número desejado de posições (positions). Dentro desse loop, a função Trade.PositionOpen é invocada para iniciar posições de compra. Parâmetros relevantes, como símbolo (_Symbol), tipo de ordem (ORDER_TYPE_BUY), volume (min_volume * lot_multiple) e preço de execução (ask), são fornecidos como argumentos para essa função.

Execução de Ordens de Venda: Da mesma forma, se o parâmetro order_direction for igual a -1 (indicando uma ordem de venda), e não houver posições ativas (PositionsTotal() == 0 é reavaliado), a função procede para abrir posições de venda através de um processo iterativo semelhante. A função Trade.PositionOpen é novamente utilizada com parâmetros adaptados para posições de venda.

Em resumo, a função CheckOrder garante a iniciação de posições de negociação de forma disciplinada, considerando a ausência de posições existentes e aderindo à direção da ordem especificada. O código encapsula a lógica de negociação no contexto de estratégias de negociação algorítmica.

//This function is responsible for opening positions
void CheckOrder(int order_direction){
      //Only open new positions if we have no open positions
     if(PositionsTotal() == 0){
            //Buy
            if(order_direction == 1){
                  //Iterate over the desired number of positions
                  for(int i = 0; i < positions; i++){
                           Trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,min_volume * lot_multiple,ask,0,0,"Volatitlity Doctor AI");
                  }
            }
            //Sell
            else if(order_direction == -1 && PositionsTotal() == 0){
                  //Iterate over the desired number of positions
                  for(int i = 0; i < positions; i++){
                        Trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,min_volume  * lot_multiple ,bid,0,0,"Volatitlity Doctor AI");
            }
         }
   }
}


Gerenciamento de Negociações

A função ManageTrade atua como o módulo central de gerenciamento de negociações, delegando a responsabilidade de ajustar os níveis de Stop Loss e Take Profit para a função CheckStop.

A função CheckStop itera sobre todas as posições abertas, extraindo informações relevantes, como símbolo, ticket, tipo de posição, stop loss atual e preço de abertura da posição. Ela garante que o símbolo da posição corresponda ao símbolo sendo negociado ativamente (_Symbol). Para cada posição válida, o código calcula novos níveis de Stop Loss e Take Profit com base em parâmetros predefinidos, como preços bid e ask, a largura do Stop Loss (sl_width) e o valor do ponto (_Point).

A função então distingue entre posições de compra e venda. Para posições de compra, ela calcula os novos níveis de Stop Loss e Take Profit com base no preço ask, ajustando-os apenas se os novos níveis forem mais favoráveis. Da mesma forma, para posições de venda, o cálculo é baseado no preço bid, e ajustes são feitos se os novos níveis forem mais favoráveis.

O uso de NormalizeDouble garante que os níveis calculados estejam em conformidade com o número especificado de dígitos (_Digits). A função Trade.PositionModify é empregada para modificar a negociação existente com os níveis atualizados de Stop Loss e Take Profit, apenas se necessário.

//This function handles our trade management
void ManageTrade(){
   CheckStop();
}

//This funciton will update our S/L & T/P 
void CheckStop(){
      //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
                           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 stop we calculated above
                                  double new_stop_loss = NormalizeDouble(ask - ((sl_width * _Point) / 2) ,_Digits);
                                  //The new take profit is just the ask price plus the stop we calculated above
                                  double new_take_profit = NormalizeDouble(ask + (sl_width * _Point),_Digits);
                                  //If our current stop loss is less than our calculated stop loss 
                                  //Or if our current stop loss is 0 then we will modify the stop loss and take profit
                                 if((current_stop_loss < new_stop_loss) || (current_stop_loss == 0)){
                                       Trade.PositionModify(ticket,new_stop_loss,new_take_profit);
                                 }  
                           }
                            //If the position is a sell
                           else if(type == POSITION_TYPE_SELL){
                                  //The new stop loss value is just the ask price minus the stop we calculated above
                                  double new_stop_loss = NormalizeDouble(bid + ((sl_width * _Point)/2),_Digits);
                                  //The new take profit is just the ask price plus the stop we calculated above
                                  double new_take_profit = NormalizeDouble(bid - (sl_width * _Point),_Digits);
                                 //If our current stop loss is greater than our calculated stop loss 
                                 //Or if our current stop loss is 0 then we will modify the stop loss and take profit 
                                 if((current_stop_loss > new_stop_loss) || (current_stop_loss == 0)){
                                       Trade.PositionModify(ticket,new_stop_loss,new_take_profit);
                                 }
                           }  
                  }  
            }
}


Nosso Expert Advisor agora conterá esses parâmetros em seu menu.


EA

Fig 11: Nosso Expert Advisor.


E deve definir automaticamente stop losses e níveis de take profit para cada negociação que abrir.

EA em ação

Fig 12: Nosso Expert Advisor em ação.


Conclusão

Em conclusão, regressões espúrias representam um desafio significativo ao modelar dados de séries temporais, frequentemente levando a resultados enganosos e não confiáveis. Pesquisadores e profissionais devem ter cautela ao interpretar resultados de regressão, especialmente ao lidar com dados de séries temporais não estacionárias. Para mitigar o risco de regressões espúrias, é crucial empregar técnicas estatísticas adequadas, como testes de raiz unitária, análise de cointegração e utilização de variáveis estacionárias. Além disso, adotar métodos avançados de séries temporais, como modelos de correção de erros, pode aumentar a robustez das análises de regressão e contribuir para interpretações econômicas mais precisas e significativas. Em última análise, uma compreensão sutil das propriedades subjacentes dos dados e uma aplicação rigorosa de métodos estatísticos são essenciais para pesquisadores que buscam produzir resultados de regressão confiáveis e válidos diante de possíveis relações espúrias.

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

Do básico ao intermediário: Array e Strings (II) Do básico ao intermediário: Array e Strings (II)
Neste artigo, irei demostrar que apesar de ainda estamos em um nível iniciante, e bem básico. Já conseguimos implementar algum tipo de aplicação interessante. No caso iremos criar um gerador de senhas bem simples. Isto de modo a conseguir aplicar alguns conceitos que foram explicados até aqui. Além disto, irei mostrar como você pode desenvolver soluções para alguns problemas especiais.
Técnicas do MQL5 Wizard que você deve conhecer (Parte 18): Pesquisa de Arquitetura Neural com Vetores Próprios Técnicas do MQL5 Wizard que você deve conhecer (Parte 18): Pesquisa de Arquitetura Neural com Vetores Próprios
Pesquisa de Arquitetura Neural, uma abordagem automatizada para determinar as configurações ideais de uma rede neural, pode ser um diferencial ao enfrentar muitas opções e grandes conjuntos de dados de teste. Examinamos como, quando emparelhado com Vetores Próprios, esse processo pode se tornar ainda mais eficiente.
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.
Arbitragem Estatística com previsões Arbitragem Estatística com previsões
Vamos explorar a arbitragem estatística, pesquisar com Python símbolos correlacionados e cointegrados, criar um indicador para o coeficiente de Pearson e desenvolver um EA para negociar arbitragem estatística com previsões feitas com Python e modelos ONNX.