![Construa Expert Advisors Auto-Otimizáveis em MQL5](https://c.mql5.com/2/74/Build_Self_Optmising_Expert_Advisors_in_MQL5_600x314.jpg)
Construa Expert Advisors Auto-Otimizáveis em MQL5
Introdução
Sistemas automatizados auto-otimizáveis são essenciais nos mercados financeiros dinâmicos de hoje. Na era digital, os mercados se tornaram notavelmente mais voláteis devido à adoção generalizada de negociação algorítmica, especialmente por traders de alta frequência. De acordo com este documento de trabalho da SEC, Papel da SEC sobre Negociação de Alta Frequência, os traders de alta frequência são responsáveis por quase metade de todas as negociações na Europa e nos EUA.
O MQL5 é ideal para essa tarefa, ao contrário do que alguns acreditam. Sua API oferece funções extensivas de matriz e vetor, permitindo a criação de modelos compactos de aprendizado de máquina. Esta introdução enfatiza o uso de MQL5 para construir bots auto-otimizáveis. Uma abordagem de Programação Orientada a Objetos reduz a codificação repetitiva e melhora a adaptabilidade em diferentes prazos e condições de mercado.
Optar pelas capacidades de matriz e vetor do MQL5 em vez de alternativas como ONNX e Python tem consideráveis benefícios. Usar um modelo ONNX exigiria instâncias de modelo separadas para cada símbolo de negociação e novos modelos para quaisquer pequenas mudanças de parâmetro, como ajustes de intervalo de tempo. O MQL5, no entanto, oferece adaptabilidade sem a necessidade de gerenciar inúmeros modelos para diferentes condições.
Sinopse: Para desenvolver Expert Advisors auto-otimizáveis
Precisamos de uma estrutura para avaliar a eficácia do nosso expert advisor. Uma vez que uma métrica de desempenho definitiva seja definida, podemos então maximizar ou minimizar nossa métrica escolhida de acordo. Ao construir modelos de aprendizado de máquina supervisionados para previsão de preços, nosso objetivo é minimizar o erro entre os valores previstos e as observações reais. Por outro lado, para problemas de aprendizado por reforço, o objetivo é maximizar as recompensas totais esperadas descontadas.
Neste artigo, minimizaremos a diferença entre o preço futuro esperado previsto pelo nosso consultor especialista e o preço real observado no futuro. Isso pode ser alcançado calculando as diferenças absolutas entre esses preços.
Este artigo explora os aspectos fundamentais da construção de um consultor especialista auto-otimizável. Artigos futuros abordarão metodologias mais avançadas para criar consultores especialistas auto-otimizáveis usando recursos mais avançados da API MQL5.
Após ler este artigo, o leitor entenderá:
- Uma seleção de funções úteis de matrizes e vetores
- Conceitos fundamentais de Programação Orientada a Objetos em MQL5
- Um framework para construir consultores especialistas dinâmicos e auto-adaptáveis em MQL5
Auto-otimização com Descida do Gradiente
Nosso objetivo é projetar um consultor especialista capaz de se realinhar consistentemente com as condições atuais do mercado. Para alcançar isso, implementaremos o algoritmo de descida do gradiente em MQL5. Para os leitores que não estão familiarizados com o algoritmo de descida do gradiente, pode ser útil compará-lo com o processo pelo qual um DJ configura seu equipamento de som. Imagine que você é um DJ se preparando para uma apresentação. Você liga seu equipamento e o volume está muito alto. O que você faria em seguida? Você provavelmente reduziria o volume. No entanto, agora o volume está muito baixo, então você o aumentará. Essa oscilação entre aumentar e diminuir acontece até que você encontre um nível equilibrado.
O algoritmo de descida do gradiente funciona de maneira semelhante. Começamos com coeficientes aleatórios no modelo do mercado em que estamos. Em seguida, medimos o erro produzido pelos nossos coeficientes atuais. Semelhante ao que o DJ faz, ajustamos iterativamente os coeficientes do nosso modelo na direção oposta ao aumento do erro. Tomamos derivadas em relação às entradas, como coeficientes em um modelo linear, para deduzir em qual direção o erro está aumentando.
Além dos coeficientes, outro parâmetro crucial na descida do gradiente é a taxa de aprendizado. A taxa de aprendizado é análoga ao tamanho da mudança de volume que o DJ faz a cada ajuste do controle de volume. A taxa de aprendizado governa o tamanho do passo que daremos cada vez que ajustarmos os parâmetros do nosso modelo. Se a nossa taxa de aprendizado for muito grande ou muito pequena, nosso modelo não aprenderá de forma otimizada.
Cada mercado pode ter uma taxa de aprendizado ideal diferente. A abordagem ideal é projetar nosso consultor especialista para otimizar dinamicamente as taxas de aprendizado e coeficientes para cada cenário de mercado, mesmo que mudemos os intervalos de tempo e os escopos de dados. Essa adaptabilidade, esperamos, nos permitirá estar efetivamente do lado certo do mercado, aproveitando todo o potencial de soluções nativas para negociar sem limitações.
Estratégia de Negociação
Nossa estratégia de negociação será uma abordagem híbrida que emprega análise técnica e aprendizado de máquina. Usaremos uma média móvel para nos ajudar a determinar a tendência predominante do mercado. Se o preço estiver acima da média móvel, assumimos que a tendência predominante é de alta; caso contrário, se o preço estiver abaixo da média móvel, assumiremos que a tendência do mercado é de baixa. Uma vez que deduzimos a tendência do mercado, buscaremos confirmação em 2 indicadores de suporte. O Índice de Força Relativa (RSI) e o William's Percent Range (WPR).
O indicador RSI fornece leituras entre 0 e 100. Normalmente, quando a leitura do RSI está acima de 70, o ativo é considerado sobrecomprado e deve ser vendido; se a leitura do RSI estiver abaixo de 30, o ativo é considerado sobrevendido e deve ser comprado. Essa estratégia funciona bem ao negociar ativos que existem em números limitados, como ações ou commodities; no entanto, ao negociar pares de moedas, essa estratégia não faz sentido intuitivo. Moedas não podem ser sobrevendidas ou sobrecompradas, os bancos centrais podem criar tanto quanto ou tão pouco quanto considerem necessário; portanto, em nossa estratégia, quando a leitura do RSI estiver acima de 50, vamos querer comprar em vez de vender; da mesma forma, quando a leitura do RSI estiver abaixo de 50, venderemos em vez de comprar.
O indicador WPR fornece leituras entre 0 e -100. Assim como o RSI, o indicador WPR identifica zonas de sobrecompra e sobrevenda. No entanto, as moedas não podem ser sobrecompradas ou sobrevendidas, a oferta de moeda é ilimitada; portanto, em nossa estratégia, interpretaremos o WPR de forma ligeiramente diferente. Em nossa estratégia, quando o indicador WPR estiver acima de -20, registraremos isso como um sinal de compra e, se o indicador WPR estiver abaixo de -80, registraremos isso como um sinal de venda.
Se os 3 indicadores estiverem alinhados no mesmo lado do mercado, finalmente consultaremos nosso modelo para prever o preço esperado no futuro. Se a previsão do nosso modelo estiver alinhada com nosso sentimento ao analisar nossos indicadores, abriremos a posição; caso contrário, se nosso modelo e nossos indicadores estiverem dando sinais contraditórios, esperaremos até que estejam alinhados.
Nossos níveis de take profit e stop loss também serão definidos dinamicamente usando os níveis atuais de volatilidade do mercado. Tomaremos o valor absoluto da diferença entre o preço e a média móvel. Nosso stop loss e take profit serão 2 vezes o valor absoluto da altura entre a média móvel e o preço de fechamento. Nosso raciocínio é que, sob condições de mercado letárgicas, nosso stop loss e take profit serão apertados, e em dias de mercado volátil, nosso stop loss e take profit serão adequadamente amplos. Em resumo, todo o nosso sistema se ajustará dinamicamente sem qualquer intervenção de nossa parte.
Implementação no MQL5
Para começar, primeiro precisamos definir uma classe para nosso modelo de aprendizado de máquina. Usar Programação Orientada a Objetos (POO) tem muitas vantagens, especialmente para projetos de ciência de dados. Imagine se você tivesse construído um modelo de aprendizado de máquina e, em seguida, apenas copiado manualmente esse código e inserido em cada consultor especialista que você possui. Dias depois, você percebe um erro que cometeu em uma das funções em seu código. Se você não estivesse usando princípios de design POO, então teria que passar manualmente por cada instância do código copiado e fazer as correções uma por uma. No entanto, se você estivesse empregando princípios de design POO, então só precisaria corrigir a classe e depois compilar novamente os outros programas. Em resumo, os princípios de design POO podem proporcionar controle definido e preciso sobre milhares de diferentes instâncias do seu código.
Começamos construindo uma nova classe em nosso Editor MetaTrader 5.
Fig 1: Construindo uma nova classe em MQL5.
A partir daí, definimos o nome da classe. Certifique-se de que sua classe esteja sendo salva na pasta "Include", além disso, eu recomendaria que você atribua a cada classe sua própria pasta e dê à pasta o mesmo nome da classe. Fazendo isso, será mais fácil encontrar essas classes no futuro.
Fig 2: Construindo nossa classe de regressão linear.
Se você seguiu os passos acima, o assistente MQL5 ajudará você a produzir um código semelhante a este.
class LinearRegegression { private: public: LinearRegegression(); ~LinearRegegression(); }; LinearRegegression::LinearRegegression() { } LinearRegegression::~LinearRegegression() { }
Se esta é a sua primeira vez lidando com POO em MQL5, então vamos passar pelo código acima juntos. No topo, temos a definição da classe. A palavra-chave "class" define todo esse código como uma classe; após a palavra-chave "class" está o nome da classe. A partir daí, entramos no corpo da classe. A palavra-chave "private" define variáveis e funções que não podem ser acessadas fora da classe, enquanto a palavra-chave Observe que já temos 2 funções em nossas definições de classe.
A primeira função "LinearRegression()" é chamada de construtor. Essa é a primeira função chamada sempre que lançamos uma nova instância de nossa classe, e a última função "~LinearRegression()" é chamada de destrutor. O destrutor é a última função chamada sempre que removemos a classe de nosso gráfico.
Agora podemos prosseguir para definir as variáveis que usaremos para calcular nosso modelo de regressão linear.
- O max learning rate power define até que ponto buscaremos uma boa taxa de aprendizado.
- Fetch é simplesmente o número de velas que queremos analisar do mercado.
- Start e predict definem quando começaremos a buscar dados e o ponto a partir do qual faremos nossa previsão.
- Look ahead define quantos passos no futuro desejamos prever.
- mae_array é o array que armazenará nossa métrica de erro.
- Trained é uma bandeira que nos diz se nosso modelo foi treinado e está pronto para uso.
- Epochs power define o número de épocas que usaremos para treinar nosso modelo.
- Temos 2 vetores mae_train e mae_validation que armazenam nossas métricas de erro de treinamento e validação.
- Temos 4 vetores x e y validation, x e y train. Esses vetores contêm nossos dados de treinamento e validação.
- Os vetores m e b contêm estimativas para os coeficientes m e b apropriados para nosso modelo.
- O 'double forecast' é simplesmente a previsão do nosso modelo.
- Learning rate power é a potência à qual elevaremos 0,1 para definir nossa taxa de aprendizado.
- Epochs é o número de vezes que treinaremos o modelo.
- n é o número de linhas em nossos dados, sempre igual a fetch.
- "output_end,output_start,input_end,input_start" definem nossa divisão de treino/teste.
private: //This is the highest power that we will raise ten to, as we are searching for coefficients ulong max_learning_rate_power; //This is how many bars we should fetch int fetch; //This is where we will start collecting data, it is the end of our validation data set. datetime start,predict; //This is how many steps into the future we want to forecast int look_ahead; //This is the array that will contain our MAE readings from testing different learning rates on the validation data double mae_array[30]; //Trained flag to inform us if the model has been fit and optimised succesfuly and is ready for use bool trained; //The number to raise the power of 10 buy when calculating the number of epochs int epochs_power; //Our error metrics vector mae_train,mae_validation; //This vector contains our inputs validation and training set vector x_validation,x_train; //This vector contains our outputs validation and training set vector y_validation,y_train; //This vector contains our predictions on the validation set vector y_hat_validation,y_hat_train; //This vector contains our gradient coefficient vector m; //This vector contains our model bias vector b; //This is our model's forecast double forecast; //This is our current learning rate power ulong learning_rate_power; //This is the learning rate power we are currently evaluating int lr_error_index; //This is our current learning rate double learning_rate; //This is the number of rounds we will allow whilst training our model double epochs; //This is used in calculations, it is the number of rows in our data, or the fetch size. ulong n; //These are the times for our input and output data datetime output_end,output_start,input_end,input_start; //These are the index times for our input and output data int index_output_end,index_output_start,index_input_end,index_input_start; //This is the value we will use to scale our data double first_reading; bool allowed_to_evaluate; //Update the learning rate bool UpdateLearningRate(void); //Update the number of epochs bool UpdateEpochs(void); //Set the number of epochs bool SetEpochs(int _epochs_power); //Reset the number of epochs bool ResetEpochs(void); //Reset the learning rate bool ResetLearningRate(void); //This function will fit the coeffeicients bool Fit(void); //This function evaluates the current settings bool Evaluate(ulong _index,int _epochs_power); //This function will scale the input data bool ScaleInputs(void); //This function sets the learning rate bool SetLearningRate(ulong _learning_rate_power);
Agora, vamos avançar para as definições públicas em nossa classe.
public: //Constructor LinearRegression(); //Fetch Current Validation Data bool GetCurrentValidationData(void); //Initialise the LinearRegressor Model void Init(int _fetch,int _look_ahead); //Function to determine if the model has been trained and is ready for use. bool Trained(void); //A function to train the model using the best learning rate and the most recent prices bool Train(void); //A function to predict future price using the current price. double Predict(void); //Destructor ~LinearRegression();
O código acima definiu as funções que temos em nossa classe e a assinatura de cada função; no entanto, ainda precisamos implementar cada função. Vamos começar implementando o construtor.
Observe que nosso construtor não recebe entradas, isso é chamado de construtor padrão ou não paramétrico. Além disso, observe que o construtor não tem tipo de retorno, nem mesmo void.
LinearRegression::LinearRegression() { Print("Current Symbol: ",_Symbol); }
Nosso construtor, por design, não realiza nenhuma ação além de exibir o símbolo de negociação atual. Essa escolha de design intencional nos permite recuperar entradas dos inputs do nosso consultor especialista e usá-las para inicializar nosso objeto de regressão linear com base nessas entradas. Notavelmente, o construtor se abstém de definir quaisquer variáveis ou valores padrão, uma tarefa reservada para um método separado definido na função "Init()". Em resumo, separar o construtor da função Init() é altamente vantajoso, pois nos permite coletar dinamicamente entradas das configurações do consultor especialista. Se o construtor fosse responsável pela inicialização de variáveis, essa coleta dinâmica de entradas seria restringida.
Vamos agora definir a função Init(), responsável por inicializar nossas variáveis com seus valores padrão. Após inicializar nossas variáveis, o método Init tentará automaticamente escalar as entradas e treinar o modelo para nós.
void LinearRegression::Init(int _fetch,int _look_ahead) { //Clear The Chart ObjectsDeleteAll(0); //Allow evaluations allowed_to_evaluate = true; //Epochs power epochs_power =4; //Set the number of epochs epochs = 5 * MathPow(10,epochs_power); //Has the model been trained? trained = false; //Set the maximum learning rate power max_learning_rate_power = 30; //Set the end of our validation data start = iTime(_Symbol,PERIOD_CURRENT,1); //This is how much data we're going to fetch this.fetch = _fetch - 1; //This is how far into the future we want to forecast this.look_ahead = _look_ahead + 1; //Set the gradient coefficient to a random value m = vector::Zeros(1); //Set the bias to a random value b = vector::Zeros(1); //Set the forecast to 0 forecast = 0; //Our model's learning rate will start at 0 learning_rate_power = 0; //This is the learning rate we are evaluting lr_error_index = 0; mae_train = vector::Full(1,MathPow(10,100)); mae_validation = vector::Full(30,MathPow(10,10000)); //Set the initial learning rate learning_rate = MathPow(0.1,(learning_rate_power)); //Set the number of rows n = fetch; if(GetCurrentValidationData()) { //Scale the data ScaleInputs(); //Fit the model Fit(); } }
A função predict é projetada para retornar um tipo de dado double sem parâmetros, por isso ela tem entrada void. É importante notar que, para designar uma função como membro de uma classe, a prefixamos com o nome da classe seguido de dois pontos duplos e o nome da função.
A função predict verificará primeiro se o modelo passou por treinamento, chamando a função "Trained()". Uma vez confirmado o status de treinamento do modelo, prosseguimos para coletar dados em tempo real, especificamente o preço atual e o preço de fechamento, juntamente com dados de timestamp para o contexto da previsão. Calculamos o preço previsto multiplicando o preço atual por m e adicionando b. Em seguida, retornamos a previsão ou retornamos 0 se o modelo não estiver treinado.
double LinearRegression::Predict(void) { if(Trained()) { double _current_reading = iClose(_Symbol,PERIOD_CURRENT,0); predict = iTime(_Symbol,PERIOD_CURRENT,0); double prediction = (m[0]*_current_reading)+b[0]; if(prediction > _current_reading) { Comment("Buy, forecast: ",prediction); } else if(prediction < _current_reading) { Comment("Sell, forecast: ",prediction); } ObjectCreate(0,"prediction point",OBJ_VLINE,0,predict,0); ObjectCreate(0,"forecast",OBJ_HLINE,0,predict,prediction); return(prediction); } return(0); }
Nossa próxima função é responsável por coletar dados de treinamento e validação, sempre buscando os dados de mercado mais recentes disponíveis. Esse processo é realizado por meio da função de vetor copy_rates, especificamente projetada para transferir dados de preços históricos para um vetor.
Depois de buscar os dados, precisamos garantir que os vetores tenham o mesmo tamanho, utilizando a função de tamanho do vetor.
bool LinearRegression::GetCurrentValidationData(void) { //Indexes index_output_end = 1; index_output_start = index_output_end + fetch; index_input_end = index_output_end + look_ahead; index_input_start = index_output_start + look_ahead; //Assigning time stamps output_end = iTime(Symbol(),PERIOD_CURRENT,index_output_end); output_start = iTime(Symbol(),PERIOD_CURRENT,index_output_start); input_end = iTime(Symbol(),PERIOD_CURRENT,index_input_end); input_start = iTime(Symbol(),PERIOD_CURRENT,index_input_start); //Get the output data if(!y_validation.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,output_end,fetch)) { Print("Failed to get market data: ",GetLastError()); return(false); } //Get the input data if(!x_validation.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,input_end,fetch)) { Print("Failed to get market data: ",GetLastError()); return(false); } //Print the vectors we have if(x_validation.Size() != y_validation.Size()) { Print("Failed to get market data: Our vectors aren't the same length."); return(false); } //Print the vectors and plot the data points Print("X validation: ",x_validation); ObjectCreate(0,"X validation end",OBJ_VLINE,0,input_end,0); ObjectCreate(0,"X validation start",OBJ_VLINE,0,input_start,0); //Print the vectors and plot the data points Print("y validation: ",y_validation); ObjectCreate(0,"y validation end",OBJ_VLINE,0,output_end,0); ObjectCreate(0,"y validation start",OBJ_VLINE,0,output_start,0); //Set the training data index_output_end = index_input_start + (look_ahead * 2); index_output_start = index_output_end + fetch; index_input_end = index_output_end + look_ahead; index_input_start = index_output_start + look_ahead; //Assigning time stamps output_end = iTime(Symbol(),PERIOD_CURRENT,index_output_end); output_start = iTime(Symbol(),PERIOD_CURRENT,index_output_start); input_end = iTime(Symbol(),PERIOD_CURRENT,index_input_end); input_start = iTime(Symbol(),PERIOD_CURRENT,index_input_start); //Copy the training data if(!y_train.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,output_end,fetch)) { Print("Error fetching training data ",GetLastError()); } //Copy the training data if(!x_train.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,input_end,fetch)) { Print("Error fetching training data ",GetLastError()); } //Check if the data matches if(x_train.Size() != y_train.Size()) { Print("Error fetching training dataL: The x and y vectors are not the same size"); } //Print the vectors and plot the data points Print("X training: ",x_train); ObjectCreate(0,"X training end",OBJ_VLINE,0,input_end,0); ObjectCreate(0,"X training start",OBJ_VLINE,0,input_start,0); Print("y training: ",y_train); ObjectCreate(0,"y training end",OBJ_VLINE,0,output_end,0); ObjectCreate(0,"y training start",OBJ_VLINE,0,output_start,0); return(true); }
Agora, definimos nossa função fit. A função começa usando os valores atuais de m e b para gerar previsões nos dados de treinamento. Subsequentemente, avalia o erro nos dados de treinamento, calculando as diferenças absolutas entre as observações reais de Y e nossas previsões de Y.
Uma vez determinado o erro, calculamos o erro médio utilizando outra função eficiente de vetor, 'Mean', para calcular a média aritmética do vetor de erro.
Após isso, implementamos o algoritmo de descida do gradiente aproximando as derivadas de nosso erro em relação a m e b. Essas aproximações das derivadas nos orientam na atualização de nossos coeficientes por uma fração das derivadas obtidas.
Após a atualização dos coeficientes, é imperativo validar os novos coeficientes, pois certos cenários podem gerar coeficientes inválidos, como NaN ou infinito. Essa etapa de validação é crucial para garantir a integridade e a usabilidade dos coeficientes atualizados.
bool LinearRegression::Fit() { Print("Fitting a linear regression on the training set with learning rate ",learning_rate_power); Print("Evalutaions: ",allowed_to_evaluate); for(int i =0; i < epochs;i++) { //Measure error y_hat_train = (m[0]*x_train) + b[0]; vector y_minus_y_hat = (y_train - y_hat_train); vector y_minus_y_hat_sqaured = MathAbs((y_train - y_hat_train)); mae_train.Set(0,( y_minus_y_hat_sqaured.Mean())); vector x_times_y_minus_y_hat = (x_train*(y_train -y_hat_train)); //Aproximate the derivatives double derivative_m = (-2.0/n) * x_times_y_minus_y_hat.Sum(); double derivative_b = (-2.0/n) * y_minus_y_hat.Sum(); //Update the linear parameters m[0] = m[0] - (learning_rate * derivative_m); b[0] = b[0] - (learning_rate * derivative_b); } //Finished fitting the coefficients Print("Fit on training data complete.\nm: ",m[0]," b: ",b[0]," mae ",mae_train[0],"\nlearning rate: ",learning_rate); if(allowed_to_evaluate) { Evaluate(learning_rate_power,epochs_power); } //Return true return(true); }
Vamos avançar para a definição de nossa função evaluate. A função é responsável por selecionar a melhor taxa de aprendizado para cada símbolo que negociamos. Começamos verificando a validade de nossos coeficientes. Se os coeficientes forem zero ou contiverem valores NaN, eles são redefinidos.
O raciocínio por trás desse processo de validação é selecionar meticulosamente coeficientes que resultem no menor erro de validação em diferentes taxas de aprendizado. Coeficientes inválidos, marcados por altos erros de validação, são excluídos da consideração durante a fase de seleção de coeficientes. Por outro lado, coeficientes válidos são armazenados para análise posterior.
Posteriormente, usamos esses coeficientes armazenados para gerar previsões em nossos dados de validação e avaliar o erro. Esse processo iterativo envolve a atualização das taxas de aprendizado, ajuste do modelo e avaliação dos erros. O limite máximo de iteração é definido em 30, o que corresponde ao poder máximo da taxa de aprendizado.
Durante toda a função evaluate, uma verificação contínua garante que o índice permaneça dentro dos limites do poder máximo da taxa de aprendizado. Coletamos os dados de erro absoluto em um vetor para processamento eficiente, empregando funções de vetor como vector.Min() e Argmin() para identificar o poder da taxa de aprendizado associado ao menor erro de validação.
//This function evaluates the current coefficient settings and learning rate bool LinearRegression::Evaluate(ulong _index) { Print("Evaluating the coefficients m:",m[0]," b: ",b[0]," at learning rate: ",learning_rate); //First check if the coefficient and learning rate are valid if((m.HasNan() > 0 || b.HasNan() > 0 || m[0] == 0 || b[0] == 0 || _index > max_learning_rate_power) && (_index < max_learning_rate_power)) { Print("Coefficients are invalid"); m[0] = 0; b[0] = 0 ; mae_array[_index] = MathPow(10,100000); //Update the learning rate UpdateLearningRate(); //Fit the model again Fit(); } else { //Validation predictions if(_index < max_learning_rate_power) { Print("Coefficients are valid, solution at index ",_index); y_hat_validation = (m[0] * x_validation) + b[0]; vector y_minus_y_hat_squared = MathAbs(y_validation - y_hat_validation); //If everything is fine, let's assess the validation mae mae_array[_index] = (1.0/n) * y_minus_y_hat_squared.Sum(); //What was the validation error? Print("Validation error: ",(1.0/n) * y_minus_y_hat_squared.Sum()); //Update the learning rate UpdateLearningRate(); //Fit the model again Fit(); } } if(_index == max_learning_rate_power) { for(int i = 0; i < max_learning_rate_power;i++) { mae_validation[i] = mae_array[i]; } allowed_to_evaluate = false; trained = true; Print("Validation mae: \n",mae_validation); Print("Lowest validation mae: ",mae_validation.Min()); ulong chosen_learning_rate = mae_validation.ArgMin(); Print("Chosen learning rate ",MathPow(0.1,(chosen_learning_rate))); SetLearningRate(chosen_learning_rate); Fit(); } return(true); }
Também definiremos uma função para escalar nossas entradas. Essa função é fácil de entender, ela divide todas as nossas entradas pelo primeiro valor em nosso vetor de treinamento.
//This function will scale our inputs bool LinearRegression::ScaleInputs(void) { //Set the first reading first_reading = x_train[0]; x_train = x_train / first_reading; x_validation = x_validation / first_reading; return(true); }
A partir daí, definimos o destrutor, que redefine todos os coeficientes que acabamos de otimizar.
LinearRegression::~LinearRegression()
{
ResetLearningRate();
ResetLastError();
}
As funções chamadas pelo destruidor são definidas da seguinte forma
bool LinearRegression::ResetLearningRate(void) { learning_rate_power = 0; learning_rate = MathPow(0.1,learning_rate_power); return(true); }
Quando juntamos tudo, isso se torna a definição da nossa classe:
#property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com" #property version "1.00" class LinearRegression { private: //This is the highest power that we will raise ten to, as we are searching for coefficients ulong max_learning_rate_power; //This is how many bars we should fetch int fetch; //This is where we will start collecting data, it is the end of our validation data set. datetime start,predict; //This is how many steps into the future we want to forecast int look_ahead; //This is the array that will contain our MAE readings from testing different learning rates on the validation data double mae_array[30]; //Trained flag to inform us if the model has been fit and optimised succesfuly and is ready for use bool trained; //The number to raise the power of 10 buy when calculating the number of epochs int epochs_power; //Our error metrics vector mae_train,mae_validation; //This vector contains our inputs validation and training set vector x_validation,x_train; //This vector contains our outputs validation and training set vector y_validation,y_train; //This vector contains our predictions on the validation set vector y_hat_validation,y_hat_train; //This vector contains our gradient coefficient vector m; //This vector contains our model bias vector b; //This is our model's forecast double forecast; //This is our current learning rate power ulong learning_rate_power; //This is the learning rate power we are currently evaluating int lr_error_index; //This is our current learning rate double learning_rate; //This is the number of rounds we will allow whilst training our model double epochs; //This is used in calculations, it is the number of rows in our data, or the fetch size. ulong n; //These are the times for our input and output data datetime output_end,output_start,input_end,input_start; //These are the index times for our input and output data int index_output_end,index_output_start,index_input_end,index_input_start; //This is the value we will use to scale our data double first_reading; bool allowed_to_evaluate; //Update the learning rate bool UpdateLearningRate(void); //Update the number of epochs bool UpdateEpochs(void); //Set the number of epochs bool SetEpochs(int _epochs_power); //Reset the number of epochs bool ResetEpochs(void); //Reset the learning rate bool ResetLearningRate(void); //This function will fit the coeffeicients bool Fit(void); //This function evaluates the current settings bool Evaluate(ulong _index); //This function will scale the input data bool ScaleInputs(void); //This function sets the learning rate bool SetLearningRate(ulong _learning_rate_power); public: //Constructor LinearRegression(); //Fetch Current Validation Data bool GetCurrentValidationData(void); //Initialise the LinearRegressor Model void Init(int _fetch,int _look_ahead); //Function to determine if the model has been trained and is ready for use. bool Trained(void); //A function to train the model using the best learning rate and the most recent prices bool Train(void); //A function to predict future price using the current price. double Predict(void); //Destructor ~LinearRegression(); }; bool LinearRegression::UpdateEpochs(void) { epochs_power = epochs_power + 1; epochs = MathPow(10,epochs_power); return(true); } bool LinearRegression::ResetEpochs(void) { epochs_power = 0 ; epochs = MathPow(10,epochs_power); return(true); } bool LinearRegression::SetEpochs(int _epochs_power) { epochs_power = _epochs_power; epochs = MathPow(10,epochs_power); return(true); } double LinearRegression::Predict(void) { if(Trained()) { double _current_reading = iClose(_Symbol,PERIOD_CURRENT,0); predict = iTime(_Symbol,PERIOD_CURRENT,0); double prediction = (m[0]*_current_reading)+b[0]; if(prediction > _current_reading) { Comment("Buy, forecast: ",prediction); } else if(prediction < _current_reading) { Comment("Sell, forecast: ",prediction); } ObjectCreate(0,"prediction point",OBJ_VLINE,0,predict,0); ObjectCreate(0,"forecast",OBJ_HLINE,0,predict,prediction); return(prediction); } return(0); } bool LinearRegression::GetCurrentValidationData(void) { //Indexes index_output_end = 1; index_output_start = index_output_end + fetch; index_input_end = index_output_end + look_ahead; index_input_start = index_output_start + look_ahead; //Assigning time stamps output_end = iTime(Symbol(),PERIOD_CURRENT,index_output_end); output_start = iTime(Symbol(),PERIOD_CURRENT,index_output_start); input_end = iTime(Symbol(),PERIOD_CURRENT,index_input_end); input_start = iTime(Symbol(),PERIOD_CURRENT,index_input_start); //Get the output data if(!y_validation.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,output_end,fetch)) { Print("Failed to get market data: ",GetLastError()); return(false); } //Get the input data if(!x_validation.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,input_end,fetch)) { Print("Failed to get market data: ",GetLastError()); return(false); } //Print the vectors we have if(x_validation.Size() != y_validation.Size()) { Print("Failed to get market data: Our vectors aren't the same length."); return(false); } //Print the vectors and plot the data points Print("X validation: ",x_validation); ObjectCreate(0,"X validation end",OBJ_VLINE,0,input_end,0); ObjectCreate(0,"X validation start",OBJ_VLINE,0,input_start,0); //Print the vectors and plot the data points Print("y validation: ",y_validation); ObjectCreate(0,"y validation end",OBJ_VLINE,0,output_end,0); ObjectCreate(0,"y validation start",OBJ_VLINE,0,output_start,0); //Set the training data index_output_end = index_input_start + (look_ahead * 2); index_output_start = index_output_end + fetch; index_input_end = index_output_end + look_ahead; index_input_start = index_output_start + look_ahead; //Assigning time stamps output_end = iTime(Symbol(),PERIOD_CURRENT,index_output_end); output_start = iTime(Symbol(),PERIOD_CURRENT,index_output_start); input_end = iTime(Symbol(),PERIOD_CURRENT,index_input_end); input_start = iTime(Symbol(),PERIOD_CURRENT,index_input_start); //Copy the training data if(!y_train.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,output_end,fetch)) { Print("Error fetching training data ",GetLastError()); } //Copy the training data if(!x_train.CopyRates(_Symbol,PERIOD_CURRENT,COPY_RATES_CLOSE,input_end,fetch)) { Print("Error fetching training data ",GetLastError()); } //Check if the data matches if(x_train.Size() != y_train.Size()) { Print("Error fetching training dataL: The x and y vectors are not the same size"); } //Print the vectors and plot the data points Print("X training: ",x_train); ObjectCreate(0,"X training end",OBJ_VLINE,0,input_end,0); ObjectCreate(0,"X training start",OBJ_VLINE,0,input_start,0); Print("y training: ",y_train); ObjectCreate(0,"y training end",OBJ_VLINE,0,output_end,0); ObjectCreate(0,"y training start",OBJ_VLINE,0,output_start,0); return(true); } bool LinearRegression::Train(void) { m = vector::Zeros(1); //Set the bias to a random value b = vector::Zeros(1); forecast = 0; if(GetCurrentValidationData()) { if(Fit()) { Print("Model last updated: ",iTime(_Symbol,PERIOD_CURRENT,0)); return(true); } } return(false); } void LinearRegression::Init(int _fetch,int _look_ahead) { //Clear The Chart ObjectsDeleteAll(0); //Allow evaluations allowed_to_evaluate = true; //Epochs power epochs_power =4; //Set the number of epochs epochs = 5 * MathPow(10,epochs_power); //Has the model been trained? trained = false; //Set the maximum learning rate power max_learning_rate_power = 30; //Set the end of our validation data start = iTime(_Symbol,PERIOD_CURRENT,1); //This is how much data we're going to fetch this.fetch = _fetch - 1; //This is how far into the future we want to forecast this.look_ahead = _look_ahead + 1; //Set the gradient coefficient to a random value m = vector::Zeros(1); //Set the bias to a random value b = vector::Zeros(1); //Set the forecast to 0 forecast = 0; //Our model's learning rate will start at 0 learning_rate_power = 0; //This is the learning rate we are evaluting lr_error_index = 0; mae_train = vector::Full(1,MathPow(10,100)); mae_validation = vector::Full(30,MathPow(10,10000)); //Set the initial learning rate learning_rate = MathPow(0.1,(learning_rate_power)); //Set the number of rows n = fetch; if(GetCurrentValidationData()) { //Scale the data ScaleInputs(); //Fit the model Fit(); } } bool LinearRegression::Trained(void) { return(trained); } bool LinearRegression::SetLearningRate(ulong _learning_rate_power) { learning_rate_power = _learning_rate_power; learning_rate = MathPow(0.1,(learning_rate_power)); return(true); } bool LinearRegression::UpdateLearningRate(void) { learning_rate_power = learning_rate_power + 1; learning_rate = MathPow(0.1,(learning_rate_power)); Print("New learning rate: ",learning_rate," learning rate power: ",learning_rate_power); return(true); } bool LinearRegression::ResetLearningRate(void) { learning_rate_power = 0; learning_rate = MathPow(0.1,learning_rate_power); return(true); } LinearRegression::LinearRegression() { Print("Current Symbol: ",_Symbol); } bool LinearRegression::Fit() { Print("Fitting a linear regression on the training set with learning rate ",learning_rate_power); Print("Evalutaions: ",allowed_to_evaluate); for(int i =0; i < epochs;i++) { //Measure error y_hat_train = (m[0]*x_train) + b[0]; vector y_minus_y_hat = (y_train - y_hat_train); vector y_minus_y_hat_sqaured = MathAbs((y_train - y_hat_train)); mae_train.Set(0,( y_minus_y_hat_sqaured.Mean())); vector x_times_y_minus_y_hat = (x_train*(y_train -y_hat_train)); //Aproximate the derivatives double derivative_m = (-2.0/n) * x_times_y_minus_y_hat.Sum(); double derivative_b = (-2.0/n) * y_minus_y_hat.Sum(); //Update the linear parameters m[0] = m[0] - (learning_rate * derivative_m); b[0] = b[0] - (learning_rate * derivative_b); } //Finished fitting the coefficients Print("Fit on training data complete.\nm: ",m[0]," b: ",b[0]," mae ",mae_train[0],"\nlearning rate: ",learning_rate); if(allowed_to_evaluate) { Evaluate(learning_rate_power); } //Return true return(true); } //This function evaluates the current coefficient settings and learning rate bool LinearRegression::Evaluate(ulong _index) { Print("Evaluating the coefficients m:",m[0]," b: ",b[0]," at learning rate: ",learning_rate); //First check if the coefficient and learning rate are valid if((m.HasNan() > 0 || b.HasNan() > 0 || m[0] == 0 || b[0] == 0 || _index > max_learning_rate_power) && (_index < max_learning_rate_power)) { Print("Coefficients are invalid"); m[0] = 0; b[0] = 0 ; mae_array[_index] = MathPow(10,100000); //Update the learning rate UpdateLearningRate(); //Fit the model again Fit(); } else { //Validation predictions if(_index < max_learning_rate_power) { Print("Coefficients are valid, solution at index ",_index); y_hat_validation = (m[0] * x_validation) + b[0]; vector y_minus_y_hat_squared = MathAbs(y_validation - y_hat_validation); //If everything is fine, let's assess the validation mae mae_array[_index] = (1.0/n) * y_minus_y_hat_squared.Sum(); //What was the validation error? Print("Validation error: ",(1.0/n) * y_minus_y_hat_squared.Sum()); //Update the learning rate UpdateLearningRate(); //Fit the model again Fit(); } } if(_index == max_learning_rate_power) { for(int i = 0; i < max_learning_rate_power;i++) { mae_validation[i] = mae_array[i]; } allowed_to_evaluate = false; trained = true; Print("Validation mae: \n",mae_validation); Print("Lowest validation mae: ",mae_validation.Min()); ulong chosen_learning_rate = mae_validation.ArgMin(); Print("Chosen learning rate ",MathPow(0.1,(chosen_learning_rate))); SetLearningRate(chosen_learning_rate); Fit(); } return(true); } //This function will scale our inputs bool LinearRegression::ScaleInputs(void) { //Set the first reading first_reading = x_train[0]; x_train = x_train / first_reading; x_validation = x_validation / first_reading; return(true); } LinearRegression::~LinearRegression() { ResetLearningRate(); ResetEpochs(); ResetLastError(); }
Agora que definimos nossa classe LinearRegression, estamos prontos para usá-la em nosso consultor especialista.
Começamos criando um novo consultor especialista e incluindo a classe em nosso consultor especialista.
#property copyright "Gamuchirai Zororo Ndawana" #property link "https://www.mql5.com" #property version "1.00" //Include our linear regression class #include <LinearRegression/LinearRegression.mqh> LinearRegression ExtLinearRegression;
O código acima chama o construtor padrão da nossa classe LinearRegression.
A partir daí, também incluímos outras classes úteis.
//Include the trade class #include <Trade/Trade.mqh> CTrade Trade;
Definimos as entradas necessárias para o nosso consultor especialista.
//Inputs int input look_ahead = 10; //How many steps into the future should we forecast? int input fetch_data = 100; //How much data should we fetch? int input ma_period = 10; //Moving Average Period int input rsi_period = 10; //RSI Period int input wr_period = 10; //Williams Percent R Period
Também definiremos outras variáveis úteis para análise técnica, como o volume mínimo de negociação permitido e vetores para armazenar nossos buffers de indicadores.
//Technical Analysis double min_volume =SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); //Indicator Handlers int ma_handler,rsi_handler,wr_handler,total_time; vector ma_vector,rsi_vector,wr_vector; double _price; ulong _ticket;
Uma vez concluído, estamos prontos para definir o manipulador OnInit() para o nosso consultor especialista. Nosso manipulador inicializa nosso objeto de regressão linear usando os parâmetros que o usuário passou para o consultor especialista e, em seguida, configura nossos indicadores técnicos.
int OnInit() { //Setup our model ExtLinearRegression.Init(fetch_data,look_ahead); //Keep Track Of Time total_time = 0; //Set up our technical indicators ma_handler = iMA(_Symbol,PERIOD_CURRENT,ma_period,0,MODE_EMA,PRICE_CLOSE); rsi_handler = iRSI(_Symbol,PERIOD_CURRENT,rsi_period,PRICE_CLOSE); wr_handler = iWPR(_Symbol,PERIOD_CURRENT,wr_period); return(INIT_SUCCEEDED); }
Agora chegamos à função on tick. A função on tick rastreia o tempo, permitindo-nos realizar certas ações após cada nova vela e algumas ações após cada tick. Em cada nova vela, se o número total de velas que se passaram for maior que o horizonte de previsão que o usuário selecionou, precisamos treinar nosso modelo novamente usando a função Train que implementamos. Além disso, gostaríamos de atualizar os valores dos indicadores que temos registrados usando outra função de vetor útil, CopyIndicatorBuffer. Criamos uma função responsável por isso. Por fim, se tivermos uma posição aberta, criamos uma função responsável por gerenciar posições abertas.
void OnTick() { //--- static datetime time_stamp; datetime current_time = iTime(_Symbol,PERIOD_CURRENT,0); if(time_stamp != current_time) { //Update the values of the indicators update_vectors(); total_time += 1; if(total_time > look_ahead) { total_time = 0; //Let the model adapt to the market dynamically ExtLinearRegression.Train(); } //If our model is ready then let's start trading if(ExtLinearRegression.Trained()) { if(PositionsTotal() == 0) { analyse_indicators(); } } if(PositionsTotal() == 1) { //Get position ticket _ticket = PositionGetTicket(0); //Manage the position manage_position(_ticket); } time_stamp = current_time; } }
Esta função é responsável por buscar as barras mais atualizadas disponíveis em nosso corretor.
void update_vectors(void) { //Get the current reading of our indicators ma_vector.CopyIndicatorBuffer(ma_handler,0,1,1); rsi_vector.CopyIndicatorBuffer(rsi_handler,0,1,1); wr_vector.CopyIndicatorBuffer(wr_handler,0,1,1); _price = iClose(_Symbol,PERIOD_CURRENT,1); }
Esta função é responsável por interpretar nossos indicadores e a previsão do nosso modelo. Se todos estiverem alinhados, podemos abrir uma negociação; caso contrário, esperamos que eles se alinhem.
void analyse_indicators(void) { double forecast = ExtLinearRegression.Predict(); Comment("Forecast: ",forecast," Price: ",_price); //If price is above the moving average, check if the other indicators also confirm the buy signal if(_price - ma_vector[0] > 0) { if(rsi_vector[0] > 50) { if(wr_vector[0] > -20) { if(forecast > _price) { Trade.Buy(min_volume,_Symbol,SymbolInfoDouble(_Symbol,SYMBOL_ASK),0,0); } } } } //If price is below the moving average, check if the other indicators also confirm the sell signal if(_price - ma_vector[0] < 0) { if(rsi_vector[0] < 50) { if(wr_vector[0] < -80) { if( forecast < _price) { Trade.Sell(min_volume,_Symbol,SymbolInfoDouble(_Symbol,SYMBOL_BID),0,0); } } } }
Esta função é responsável por gerenciar quaisquer posições abertas que tenhamos e configurar o stop loss e take profit dinamicamente com base nos níveis atuais de volatilidade do mercado. Observe que ela só modificará a posição se a posição não tiver stop loss ou take profit.
void manage_position(ulong m_ticket) { if(PositionSelectByTicket(m_ticket)) { double volatility = 2 * MathAbs(ma_vector[0] - _price); double entry = PositionGetDouble(POSITION_PRICE_OPEN); double current_sl = PositionGetDouble(POSITION_SL); double current_tp = PositionGetDouble(POSITION_TP); if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { double new_sl = _price - volatility; double new_tp = _price + volatility; if(current_sl == 0 || current_tp == 0) { Trade.PositionModify(m_ticket,new_sl,new_tp); } } if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { double new_sl = _price + volatility; double new_tp = _price - volatility; if(current_sl == 0 || current_tp == 0) { Trade.PositionModify(m_ticket,new_sl,new_tp); } } } }
Nosso consultor especialista agora se parece com isso:
Fig 3: EA especialista auto-otimizável.
Fig 4: Entradas para nosso EA auto-otimizável.
Sempre que você aplicar o Consultor em qualquer Símbolo, você poderá ver os cálculos que ele está realizando em seu nome na guia de especialistas.
Fig 5: Os cálculos realizados pelo nosso consultor especialista.
Fig 6: Testando nosso EA.
Recomendações
Este artigo ensinou o método mais simples possível para construir EAs auto-otimizáveis. No entanto, essa não é a melhor abordagem possível, pois é uma busca manual por coeficientes ótimos. A solução ideal empregaria cálculos de matrizes e vetores mais avançados para encontrar automaticamente os coeficientes ótimos. Na verdade, quando usamos funções de matrizes e vetores, podemos construir nosso modelo de regressão linear sem nunca usar um único loop for. Nosso código será mais compacto e nossos coeficientes serão mais estáveis numericamente. Buscas manuais nem sempre garantem soluções.
Conclusão
Construir consultores especialistas auto-adaptáveis em MQL5 é fácil graças às poderosas funções de matrizes e vetores na API MQL5. Na verdade, o que podemos construir em MQL5 é limitado apenas pela nossa compreensão da API.
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/14630
![Técnicas do MQL5 Wizard que você deve conhecer (Parte 15): Máquinas de Vetores de Suporte com o Polinômio de Newton](https://c.mql5.com/2/75/MQL5_Wizard_Techniques_You_Should_Know_1Part_15c____LOGO.png)
![Desenvolvimento de robô em Python e MQL5 (Parte 1): Pré-processamento de dados](https://c.mql5.com/2/74/Robot_development_in_Python_and_MQL5_oPart_1z_Data_preprocessing____LOGO.png)
![Rede neural na prática: O primeiro neurônio](https://c.mql5.com/2/91/Rede_neural_na_pr4tica_O_primeiro_neur6nio___LOGO.png)
![Negociação de Notícias Simplificada (Parte 1): Criando um Banco de Dados](https://c.mql5.com/2/74/News_Trading_Made_Easy_iPart_13_Creating_a_Database___LOGO__2.png)
![MQL5 - Linguagem para estratégias de negociação inseridas no terminal do cliente MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso