
Aprendendo MQL5 do iniciante ao profissional (Parte VI): Fundamentos da criação de EAs
Introdução
Finalmente chegamos à criação de EAs. Pode-se dizer que o Rubicão foi cruzado.
Para que este artigo seja útil para você, é necessário que domine bem conceitos como:
- variáveis (locais e globais),
- funções e seus parâmetros (passados por referência e por valor),
- arrays (incluindo o entendimento de arrays-seriados),
- operadores básicos, incluindo lógicos e aritméticos, além de operadores condicionais (escolha, ramificação, ternário) e operadores de laço (principalmente for, mas while e do... while também serão úteis).
Do ponto de vista do programador, os EAs não são muito mais difíceis do que o indicador tratado no artigo anterior da série. Para operar, também é necessário verificar as condições para uma operação e, se essas condições forem satisfeitas, executar alguma ação (geralmente enviar uma ordem de trade para o servidor). O principal é entender a estrutura das próprias ordens e conhecer as funções para envio dessas ordens, assim como para obter os dados necessários para a operação.
Vnimanie! Todos os EAs apresentados neste artigo servem apenas para ilustrar princípios de programação, não para obtenção de lucro. Se você usar esse código em contas reais, provavelmente os algoritmos de tomada de decisão precisarão ser aprimorados, caso contrário poderá haver perdas.
Na verdade, o código dos EAs deste artigo não pode ser utilizado diretamente para operações reais, mesmo que os algoritmos de entrada sejam aprimorados. O motivo é que, no primeiro exemplo de EA, não há verificação de erros, nem na solicitação nem na resposta. Isso facilita muito a compreensão do código, o que é o objetivo agora, mas torna o EA adequado apenas para a prototipagem rápida e o teste da viabilidade de estratégias. No segundo EA, há mais verificações... No entanto, para publicação no Mercado ou mesmo para uma operação confiável — quando problemas são resolvidos à medida que surgem (se é que é possível resolver esses problemas, ou ao menos relatar e encerrar corretamente a operação) —, essas verificações ainda serão insuficientes.
O EA totalmente funcional, que tecnicamente pode ser aceito no Mercado, incluindo as verificações necessárias e um algoritmo de operação um pouco mais complexo do que o apresentado neste artigo, será analisada no próximo artigo. Aqui, quero apresentar apenas os fundamentos da operação. Serão escritos dois EAs: um sem indicadores e outro usando o indicador interno de média móvel. O primeiro irá operar com ordens pendentes, enquanto o segundo realizará operações a preço atual.
Template de EA
Todo EA começa quando o programador cria um template vazio, por exemplo, usando o assistente de criação de arquivos (figura 1).
Figura 1. Primeira janela do assistente de criação de arquivos
O assistente de criação de arquivos oferece a opção de criar um Expert a partir de um template (linha superior) ou gerar uma versão mais completa. Mas recomendo fortemente aos programadores iniciantes que usem apenas a opção superior (a partir do template).
Ao gerar, é criada uma versão orientada a objetos do EA, dividida em vários arquivos, e para um iniciante sem ferramentas adicionais será bastante difícil entender essa estrutura, e ainda mais difícil criar seu próprio conjunto de instruções de operação com base nela. Portanto, recomendo usar a opção inferior apenas depois que você entender muito bem os conceitos e práticas de POO, não antes. Entender o código gerado pelo assistente será, provavelmente, interessante do ponto de vista de “ver como pode ser feito”, mas trata-se apenas de uma das possíveis implementações, otimizada para uso automático. É provável que, quando você já tiver experiência suficiente para entender os meandros das classes dessa implementação, você já tenha desenvolvido seus próprios templates de código. E claro, é muito mais confortável editar seu próprio código do que tentar entender o código de terceiros. Além disso, é bastante possível que seus templates não fiquem nada atrás dos que o assistente oferece.
A maioria das funções que o assistente pode criar (figuras 2, 3) não é obrigatória, mas muitas vezes são bastante úteis. Assim, com as funções da terceira janela do assistente (figura 2) é possível tratar eventos que ocorrem durante a operação, como o recebimento de um sinal pelo servidor de trade, a abertura de uma posição e assim por diante (OnTrade e OnTradeTransaction), além de tratar eventos de timer (OnTimer), eventos do gráfico como cliques em botões ou criação de novos objetos gráficos (OnChartEvent) e eventos de alteração do livro de preços (OnBookEvent).
Figura 2. Criação de EA — terceira tela do assistente (funções adicionais do EA).
Às vezes, também é possível usar funções especiais que são processadas apenas no testador de estratégias, mas não no modo normal (figura 3). As situações em que pode ser útil separar esses dois modos geralmente estão relacionadas a versões de demonstração, que funcionam apenas no testador, mas não em contas reais. Além disso, às vezes, no testador, é necessário gerar logs mais detalhados ou obter dados de outras fontes, diferentes daquelas usadas em operações normais. Em resumo, embora eu use essas funções raramente, elas ainda podem ser úteis.
Figura 3. Criação de EA — quarta tela do assistente (funções de EA para funcionamento apenas no testador).
Algumas funções da figura 2 vamos analisar detalhadamente em artigos futuros, enquanto as funções da figura 3 deixo totalmente para seu estudo independente.
Ao criar um EA usando o assistente, sempre é gerado um arquivo contendo no mínimo três funções (exemplo 1):
//+------------------------------------------------------------------+ //| FirstExpert.mq5 | //| Oleg Fedorov (aka certain) | //| mailto:coder.fedorov@gmail.com | //+------------------------------------------------------------------+ #property copyright "Oleg Fedorov (aka certain)" #property link "mailto:coder.fedorov@gmail.com" #property version "1.00" //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+
Exemplo 1. Template mínimo de EA criado pelo assistente
- OnInit — função de preparação prévia, conhecida dos indicadores, executada uma única vez no início do nosso programa;
- OnDeinit — também já deve ser familiar, acionada ao término da execução do EA. Lembrando que essa função serve para excluir todos os objetos que o EA criou e que não são mais necessários, além de realizar ações finais como fechar arquivos e liberar recursos de indicadores — entre outras tarefas;
- OnTick — executada a cada tick (semelhante à função OnCalculate em indicadores). É nela que ocorre o trabalho principal.
A única função obrigatória para um EA é OnTick, enquanto OnInit e OnDeinit podem ser omitidas caso seu EA seja simples.
Termos principais que precisam ser compreendidos ao operar no MetaTrader 5
Na negociação automática no MetaTrader 5, os desenvolvedores utilizam termos específicos. A lógica de operação e os nomes das funções estão ligados a esses termos. Vamos entender melhor.
Order (ordem) — mensagem para o servidor informando nossa intenção de comprar ou vender um determinado instrumento a um determinado preço.
Todas as alterações na operação — seja uma compra imediata ou a modificação do preço do Stop Loss — são realizadas por meio de ordens de trade. É importante lembrar que existem ordens de execução imediata (por exemplo, uma ordem para comprar ou vender a preço atual) e "ordens pendentes", que assumem que o preço da operação ainda não foi alcançado, mas será em um período aceitável para nosso algoritmo (stop ou limite de ordens).
Entre as ordens pendentes estão Stop Loss, Take Profit, além de Buy/Sell Stop, Buy/Sell Limit, Buy/Sell Stop Limit. Exemplos de funções que trabalham com ordens: OrderSend (envia a ordem ao servidor), OrderGetInteger (retorna um número inteiro, que é um parâmetro dessa ordem, como o ticket ou o horário de criação.).
Deal (negociação) — o momento em que a ordem é executada.
A negociação no MetaTrader 5 é, na verdade, um fenômeno mais histórico. Não podemos influenciar diretamente nela, pois ela ocorre no servidor, após a ordem já ter sido enviada. Mas podemos verificar no histórico exatamente quando a negociação foi realizada e a que preço. Exemplos de nomes de funções: HistoryDealGetDouble (permite obter algum parâmetro da negociação do tipo double, como o preço), HistoryDealsTotal (retorna o número total de negociações no histórico).
Position (posição) — o conteúdo final da sua carteira após todas as negociações de um determinado instrumento.
Inicialmente, a ideia era que no MetaTrader 5 pudesse haver apenas uma posição por símbolo, mas na prática surgiu uma situação um pouco mais complexa. Atualmente, dependendo do tipo de conta aberta pelo trader, todas as negociações ou alteram apenas uma única posição (netting), ou cada negociação cria sua própria posição (hedging), exceto se for uma negociação de ordem stop. Assim, em contas do tipo hedging podem existir várias posições para o mesmo instrumento, às vezes até em direções opostas simultaneamente. Nesse caso, se necessário, pode-se calcular a posição líquida de compra e de venda. Com o uso de ordens de trade, é possível alterar posições: fechá-las total ou parcialmente, modificar seus níveis de Stop Loss e Take Profit. Exemplos de nomes de funções que trabalham com posições: PositionSelectByTicket (seleciona uma posição pelo seu ticket), PositionGetString (obtém um parâmetro de texto, como o nome do símbolo ou o comentário definido no momento da abertura da posição).
Cada negociação é resultado da execução de uma ordem de trade, cada posição é o resultado final de uma ou mais negociações.
Event (evento) — qualquer mudança significativa que ocorre no ambiente da nossa aplicação.
Envio de uma solicitação de trade — evento. O servidor aceitou a solicitação, o usuário clicou no gráfico com o mouse, o gráfico mudou de escala, o servidor enviou uma notificação de que a solicitação foi processada, chegou um novo tick... Tudo isso e muito mais são eventos. Para tratar os mais importantes entre eles, existem funções padrão chamadas handlers, cujo nome começa com "On", como OnInit (executada no evento Init) ou OnTick (que trata o evento Tick).
Existem também outros eventos que são tratados apenas através de uma lista de constantes. Isso significa que primeiro uma das funções de tratamento de evento global precisa ser acionada. Por exemplo, a função OnChartEvent é chamada para qualquer evento de gráfico. E dentro dessa função, para entender exatamente qual evento ocorreu, é necessário comparar a variável que recebe o evento com um valor de referência. Normalmente, as informações para identificação dos eventos são passadas para essas funções como parâmetros. Nesta parte do artigo, esses "pequenos" eventos não serão abordados.
Princípios básicos do trading automático no terminal MetaTrader 5
Vamos observar o processo de trading de um ponto de vista mais amplo. E começaremos com um fato óbvio, mas que traz implicações importantes.
O programa terminal instalado em seu computador e o programa que realmente opera seu dinheiro (o servidor) são dois programas diferentes. É importante destacar que eles só podem se comunicar apenas pela rede (normalmente, pela internet).
Isso significa que, ao clicar no botão "comprar" ou "vender", ocorrem vários eventos sequenciais:
- o seu terminal forma um pacote especial de dados, preenchendo uma estrutura especial MqlTradeRequest;
- em seguida, a estrutura preenchida é enviada ao servidor usando a função OrderSend (envio síncrono padrão) ou OrderSendAsinc (modo assíncrono), formando uma ordem (order) para negociação;
- o servidor recebe esse pacote e verifica seus dados para garantir que estão de acordo com as condições reais: se existe uma oferta de outros traders que atenda ao seu preço, se há saldo suficiente para a negociação, etc.;
- se estiver tudo correto, a ordem é colocada na fila junto com outras ordens (de outros traders) aguardando execução (atingir o preço desejado);
- uma mensagem é então enviada ao terminal;
- se o preço atinge o nível necessário, o servidor executa a negociação (deal) e registra essa informação em seu próprio log;
- após a negociação ser realizada, o servidor envia ao terminal os resultados da operação;
- o terminal recebe a mensagem resultante do servidor, que será registrada na estrutura MqlTradeResult, e, em consequência, serão gerados os eventos Trade e TradeTransaction;
- depois, o terminal deve verificar se ocorreram erros do lado do servidor (o programador faz isso conferindo o campo retcode da estrutura MqlTradeRequest),
- e, se estiver tudo certo, o terminal atualiza suas variáveis, bem como o log e a representação gráfica dos eventos que aconteceram;
- no final, temos uma nova posição (position) criada ou atualizada para um determinado instrumento.
Se representarmos essa sequência de forma simplificada, teremos o diagrama mostrado na figura 4:
Figura 4. Diagrama de processamento de uma ordem de trading
Modo de transmissão de dados assíncrono
Você pode perceber que, nessa interação, o terminal precisa se comunicar com a rede pelo menos duas vezes: para enviar os dados e para receber a resposta do servidor. Quando enviamos os dados usando a função OrderSend, esse processo é "compactado": o EA permanece aguardando a resposta, o que ocupa recursos, pelo menos o canal de internet e, provavelmente, também o tempo do processador.
No entanto, a transmissão de dados pela rede é uma operação muito demorada do ponto de vista do processador. Se a execução de um script de trading normalmente dura algumas centenas de microssegundos (digamos, 200 μs = 2e-4 s), a transmissão de dados pela rede é geralmente medida em milissegundos (por exemplo, 20 ms = 2e-2 s), ou seja, pelo menos 100 vezes mais demorada. Além disso, é preciso considerar o tempo de processamento no servidor. E, ocasionalmente, o servidor pode passar por manutenção técnica ou enfrentar outras situações imprevistas... Em resumo, nos piores casos, podem se passar segundos ou até minutos entre o envio da ordem de trading e a recepção da resposta. Se durante todo esse tempo o EA ficar aguardando a resposta, e se houver dezenas desses EAs... muito tempo de processador será gasto de forma improdutiva.
Por isso, para o recebimento das respostas do servidor, os desenvolvedores adicionaram no MetaTrader um modo especial assíncrono. A palavra "assíncrono" significa que o EA pode enviar uma ordem e continuar fazendo suas tarefas, como simplesmente "dormir" ou realizar cálculos complexos. Quando a resposta do servidor chegar, o terminal gerará um evento TradeTransaction (e depois Trade), e o EA poderá ser reativado para usar essa resposta na tomada de novas decisões de trading. As vantagens dessa abordagem são evidentes.
Tanto no modo síncrono quanto no modo assíncrono, a função OnTradeTransaction é utilizada para o tratamento de erros de trading. A complexidade do código não aumenta, apenas parte das funções é transferida de OnTick para OnTradeTransaction. E, se esse código for organizado em funções separadas, essa transferência nem sequer causará dificuldades. Assim, o programador pode escolher livremente qual dos modos usar para cada tarefa de trading específica. As estruturas de dados para ambos os modos são idênticas.
Começamos a operar
Suponhamos que precisamos de um Expert para FOREX que procure por um inside bar. Lembrando que um inside bar é aquele cuja faixa de preço está completamente contida dentro da faixa do candle anterior. O Expert atua uma vez por candle e, se encontrar o padrão desejado, envia simultaneamente duas ordens pendentes:
- uma de compra, posicionando o preço alguns pontos (configurável) acima da máxima do candle maior,
- e uma de venda, posicionando o preço a mesma distância abaixo da mínima do mesmo candle,
- o tempo de vida da ordem será de dois candles, se nesse tempo a ordem não for executada, será cancelada,
- as ordens de proteção (Stop Loss) para ambas as ordens serão colocadas no meio do candle maior,
- as ordens de realização de lucro (Take Profit) serão posicionadas no nível de 7/8 do tamanho do candle maior,
- o volume da operação será calculado como o lote mínimo possível.
Nesta versão do EA, para facilitar a compreensão do código, não haverá outras condições. Vamos escrever a "estrutura básica", que poderá ser aprimorada posteriormente. Primeiro, vamos criar o template do EA, deixando todas as opções desmarcadas na terceira janela (neste artigo, não vamos tratar as respostas do servidor). Nesse caso, o código gerado será semelhante ao apresentado no exemplo 1. E, para que possamos configurar nosso EA e permitir sua otimização, adicionaremos quatro parâmetros: a distância dos extremos para posicionar as ordens (inp_PipsToExtremum), a distância para colocar os stops e os take profits (inp_StopCoefficient e inp_TakeCoefficient, respectivamente), e o número de candles que devem passar para cancelar uma ordem não executada (inp_BarsForOrderExpired). Além disso, vamos declarar o número mágico do nosso EA, para no futuro identificar “nossas” ordens de trading das “ordens alheias”.
//--- declare and initialize input parameters input int inp_PipsToExtremum = 2; input double inp_TakeCoeffcient = 0.875; input double inp_StopCoeffcient = 0.5; input int inp_BarsForOrderExpired = 2; //--- declare and initialize global variables #define EXPERT_MAGIC 11223344
Exemplo 2. Definição dos parâmetros de entrada e do número mágico do EA
Lembrando que o código do exemplo 2 deve ser inserido logo no topo do arquivo do EA, imediatamente após as diretivas #property.
O restante do código neste exemplo ficará concentrado dentro da função OnTick. Todas as outras funções permanecerão vazias por enquanto. Aqui está o código que precisa ser colocado dentro do corpo do OnTick:
/**************************************************************** * Please note: this Expert Advisor uses standard functions * * to access price/time data. Therefore, it's convenient to * * work with series as arrays (time and prices). * ****************************************************************/ string symbolName = Symbol(); ENUM_TIMEFRAMES period = PERIOD_CURRENT; //--- Define a new candlestick (Operations only at the start of a new candlestick) static datetime timePreviousBar = 0; // Time of the previous candlestick datetime timeCurrentBar; // Time of the current candlestick // Get the time of the current candlestick using the standard function timeCurrentBar = iTime( symbolName, // Symbol name period, // Period 0 // Candlestick index (remember it's series) ); if(timeCurrentBar==timePreviousBar) { // If the time of the current and previous candlesticks match return; // Exit the function and do nothing } // Otherwise the current candlestick becomes the previous one, // so as not to trade on the next tick timePreviousBar = timeCurrentBar; //--- Prepare data for trading double volume=SymbolInfoDouble(symbolName,SYMBOL_VOLUME_MIN); // Volume (lots) - get minimum allowed volume // Candlestick extrema double high[],low[]; // Declare arrays // Declare that arrays are series ArraySetAsSeries(high,true); ArraySetAsSeries(low,true); // Fill arrays with values of first two closed candlesticks // (start copying with index 1 // as we only need closed candlesticks; use 2 values) CopyHigh(symbolName,period,1,2,high); CopyLow(symbolName,period,1,2,low); double lengthPreviousBar; // The range of the "long" bar MqlTradeRequest request; // Request structure MqlTradeResult result; // Server response structure if( // If the first closed bar is inside high[0]<high[1] && low[0]>low[1] ) { // Calculate the range lengthPreviousBar=high[1]-low[1]; // Timeseries have right-to-left indexing //--- Prepare data for a buy order request.action =TRADE_ACTION_PENDING; // order type (pending) request.symbol =symbolName; // symbol name request.volume =volume; // volume deal request.type =ORDER_TYPE_BUY_STOP; // order action (buy) request.price =high[1] + inp_PipsToExtremum*Point(); // buy price // Optional parameters request.deviation =5; // acceptable deviation from the price request.magic =EXPERT_MAGIC; // EA's magic number request.type_time =ORDER_TIME_SPECIFIED; // Parameter is required to set the lifetime request.expiration =timeCurrentBar+ PeriodSeconds()*inp_BarsForOrderExpired; // Order lifetime request.sl =high[1]-lengthPreviousBar*inp_StopCoeffcient; // Stop Loss request.tp =high[1]+lengthPreviousBar*inp_TakeCoeffcient; // Take Profit //--- Send a buy order to the server OrderSend(request,result); // For asynchronous mode you need to use OrderSendAsync(request,result); //--- Clear the request and response structures for reuse ZeroMemory(request); ZeroMemory(result); //--- Prepare data for a sell order. Parameers are the same as in the previous function. request.action =TRADE_ACTION_PENDING; // order type (pending) request.symbol =symbolName; // symbol name request.volume =volume; // volume request.type =ORDER_TYPE_SELL_STOP; // order action (sell) request.price =low[1] - inp_PipsToExtremum*Point(); // sell price // Optional parameters request.deviation =5; // acceptable deviation from the price request.magic =EXPERT_MAGIC; // EA's magic number request.type_time =ORDER_TIME_SPECIFIED; // Parameter is required to set the lifetime request.expiration =timeCurrentBar+ PeriodSeconds()*inp_BarsForOrderExpired; // Order lifetime request.sl =low[1]+lengthPreviousBar*inp_StopCoeffcient; // Stop Loss request.tp =low[1]-lengthPreviousBar*inp_TakeCoeffcient; // Take Profit //--- Send a sell order to the server OrderSend(request,result); }
Exemplo 3. A função OnTick deste EA contém toda a lógica de trading
A função padrão Point retorna o tamanho do ponto para o gráfico atual. Assim, se o provedor fornecer cotações com cinco casas decimais, para o par EURUSD o ponto será 0.00001, enquanto para o USDJPY, nesse caso, será 0.001. As funções iTime, iHigh, iLow permitem obter, respectivamente, o horário, o valor máximo e o valor mínimo de preço de uma vela específica (pelo número da vela, contando da direita). Neste exemplo, usamos apenas iTime para capturar o horário atual e verificar a troca de vela. Para obter os valores de High e Low, utilizamos as funções de cópia de arrays CopyHigh e CopyLow.
O código possui dois blocos: verificação de nova vela e execução da operação (que começa com a fase de preparação). O bloco de trading, por sua vez, se divide em dois blocos quase idênticos: um para compra e outro para venda. Fica claro que o preenchimento da estrutura e o envio da solicitação são tão semelhantes nos dois casos que seria possível criar uma função separada, que preenche os campos comuns e "ramifica" apenas no preenchimento dos campos específicos, como tipo da ordem e preços de execução (price, tp, sl). Neste exemplo, a universalidade e a compactação do código foram sacrificadas em favor da clareza.
Cada etapa do processo foi destacada por mim com o comentário "//---", enquanto dentro de cada etapa os comentários são comuns, sem os três traços. A operação consiste em duas partes: o preenchimento da estrutura de solicitação e o envio dessa estrutura, sendo que no preenchimento da estrutura apenas os cinco primeiros campos são obrigatórios.
É importante observar que, se você precisar utilizar o tempo de expiração da ordem, como feito neste exemplo, será necessário preencher dois campos: request.type_time e request.expiration. Se o primeiro campo não for preenchido, o segundo será ignorado por padrão.
Para ver como esse EA funciona, você pode executá-lo em qualquer intervalo de tempo em uma conta demo (funciona até em gráficos de minuto, embora, claro, isso dependa do spread do símbolo em que o EA será executado) ou pressionar <Ctrl>+<F5> no MetaEditor, o que iniciará a depuração usando dados históricos no testador de estratégias. O texto completo do exemplo está no arquivo anexado TrendPendings.mq5.
Uso de indicadores padrão
O EA do exemplo 3 não utilizava indicadores, mas isso nem sempre é o caso. Para estratégias baseadas em indicadores padrão, temos dois caminhos: usar funções padrão ou usar classes que descrevem esses indicadores. Vamos analisar ambos os casos, começando pelas funções embutidas.
Suponhamos que precisemos criar um EA baseado em uma média móvel. Lembrando novamente que nesta série de artigos não estamos criando EAs lucrativos, mas demonstrando princípios de funcionamento, por isso quero simplificar o código ao máximo e torná-lo o mais legível possível. Tendo isso em mente, vamos definir as regras de operação:
- operamos, como no EA anterior, apenas quando uma nova vela se forma;
- para comprar, a vela anterior deve fechar acima da média móvel;
- para vender, a vela anterior deve fechar abaixo da média móvel;
- vamos utilizar a inclinação da média como filtro: se da segunda vela (penúltima) para a primeira (a mais recente fechada) a média estiver subindo, compramos; se estiver caindo, vendemos;
- saída da posição será pelo sinal oposto;
- o Stop Loss será colocado no máximo (para venda) ou no mínimo (para compra) da vela de sinal;
- para cada símbolo só poderá existir uma posição aberta, mesmo se a conta for do tipo hedging; se houver um novo sinal mas já houver uma posição aberta, não operamos.
Na figura 5 está mostrado o princípio de filtragem usado por este EA.
Figura 5. Princípio de seleção das velas no EA para trading com médias móveis
Neste EA, continuarei tentando deixar o código o mais claro possível, porém usarei mais verificações de erro para aproximar um pouco mais o programa dos exemplos práticos reais.
Nos parâmetros de entrada, vamos colocar todos os parâmetros da média, bem como a máxima divergência de preço permitida em relação ao preço da solicitação. Além disso, declararemos uma variável global para o handle do indicador (vou explicar mais adiante) e descreveremos o número mágico do nosso EA:
//--- declare and initialize global variables #define EXPERT_MAGIC 3345677 input int inp_maPeriod = 3; // MA period input int inp_maShift = 0; // Shift input ENUM_MA_METHOD inp_maMethod = MODE_SMA; // Calculation mode input ENUM_APPLIED_PRICE inp_maAppliedPrice = PRICE_CLOSE; // Applied price input int inp_deviation = 5; // Max price deviation from the request price in points //--- MA indicator handle int g_maHandle;
Exemplo 4. Variáveis globais do EA para trading com médias móveis
Antes de utilizar qualquer indicador em um EA ou em outro indicador, precisamos realizar três ações:
Inicializar o indicador e obter uma referência para ele. Para isso, geralmente utilizamos funções específicas para indicadores incorporados (por exemplo, iMA para a média móvel) ou a função iCustom para indicadores desenvolvidos por usuários. Normalmente, essa etapa é feita dentro da função OnInit.
Em inglês, a palavra handle tem dois significados: "alça" (como a de uma ferramenta ou mala) e "pseudônimo", uma forma de referência. Em resumo, algo que permite "lidar" com o objeto desejado. Em português, não encontrei um equivalente exato, então usarei "referência ao indicador" ou simplesmente o termo handle.- Antes de cada novo uso, precisamos ler os dados atualizados. Para isso, geralmente criamos um array para cada buffer do indicador e depois os preenchemos usando a função CopyBuffer.
- Usar os dados dos arrays preenchidos.
- Ao final da execução do nosso programa, todos os handles de indicadores precisam ser liberados para evitar vazamentos de memória. Para isso, usamos a função IndicatorRelease, que, como se pode imaginar, é chamada dentro da função OnDeinit.
As funções OnInit e OnDeinit deste EA não apresentam nada de especial:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Before an action that could potentially cause an error, reset // built-in _LastError variable to default // (assuming there's no error yet) ResetLastError(); //--- The standard iMA function returns the indicator handle g_maHandle = iMA( _Symbol, // Symbol PERIOD_CURRENT, // Chart period inp_maPeriod, // MA period inp_maShift, // MA shift inp_maMethod, // MA calculation method inp_maAppliedPrice // Applied price ); // inp_maAppliedPrice in general case can be // either a price type as in this example, // (from ENUM_APPLIED_PRICE), // or a handle of another indicator //--- if the handle is not created if(g_maHandle==INVALID_HANDLE) { //--- report failure and output error code PrintFormat("Failed to crate iMA indicator handle for the pair %s/%s, error code is %d", _Symbol, EnumToString(_Period), GetLastError() // Output error code ); //--- If an error occurs, terminate the EA early return(INIT_FAILED); } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Release resources occupied by the indicator if(g_maHandle!=INVALID_HANDLE) IndicatorRelease(g_maHandle); }
Exemplo 5. Inicialização e desinicialização de indicadores no EA
Um detalhe ao qual quero chamar sua atenção é o dueto de funções ResetLastError e GetLastError na função de inicialização. A primeira reseta a variável padrão _LastError para o estado "sem erro", enquanto a segunda permite capturar o código do último erro, caso ele tenha ocorrido.
Fora isso, tudo é bastante trivial. As funções de inicialização de indicadores (inclusive a iMA) retornam ou um handle de indicador ou um valor especial INVALID_HANDLE (handle inválido). Isso nos permite detectar se algo deu errado e tratar o erro (no nosso caso — exibindo uma mensagem). Se o OnInit retornar INIT_FAILED, o EA (ou indicador) não será iniciado. De fato, se não conseguimos obter a referência para a média, encerrar a execução é a única decisão correta.
Agora, no código do OnTick, vamos trabalhar em partes. A primeira parte é a declaração e inicialização de variáveis.
//--- Declare and initialize variables MqlTradeRequest requestMakePosition; // Request structure for opening a new position MqlTradeRequest requestClosePosition; // Request structure for closing an existing position MqlTradeResult result; // Structure for receiving the server's response MqlTradeCheckResult checkResult; // Structure for validating the request before sending bool positionExists = false; // Flag indicating if a position exists bool tradingNeeds = false; // Flag indicating whether trading is allowed ENUM_POSITION_TYPE positionType; // Type of currently open position ENUM_POSITION_TYPE tradingType; // Desired position type (used for comparison) ENUM_ORDER_TYPE orderType; // Desired order type double requestPrice=0; // Entry price for the future position /* The MqlRates structure contains all candle data: open, close, high, and low prices, tick volume, real volume, spread, and time. In this example, I decided to demonstrate how to fill the entire structure at once, instead of retrieving each value separately. */ MqlRates rates[]; // Array of price data used for evaluating trading conditions double maValues[]; // Array of MA values // Declare data arrays as series ArraySetAsSeries(rates,true); ArraySetAsSeries(maValues,true);
Exemplo 6. Variáveis locais da função OnTick
A verificação de uma nova vela permanece a mesma:
//--- Check whether there's a new bar static datetime previousTime = iTime(_Symbol,PERIOD_CURRENT,0); datetime currentTime = iTime(_Symbol,PERIOD_CURRENT,0); if(previousTime==currentTime) { return; } previousTime=currentTime;
Exemplo 7. Verificação de uma nova vela
Em seguida, obtemos todos os dados necessários usando funções específicas. Assumimos que podem ocorrer erros, como, por exemplo, o terminal ainda não ter carregado os dados necessários, portanto, tratamos esses possíveis erros com o uso de uma estrutura condicional.
//--- Prepare data for processing // Copy the quotes of two bars, starting from the first one if(CopyRates(_Symbol,PERIOD_CURRENT,1,2,rates)<=0) { PrintFormat("Data error for symbol %s, error code is %d", _Symbol, GetLastError()); return; } // Copy the values of the moving average indicator buffer if(CopyBuffer(g_maHandle,0,1,2,maValues)<=0) { PrintFormat("Error getting indicator data, error code is %d", GetLastError()); return; }
Exemplo 8. Cópia dos dados atuais do indicador e cotações para arrays locais
Agora selecionamos a posição aberta para o símbolo atual usando a função padrão PositionSelect. Conforme acordado nas regras, pode haver apenas uma posição por símbolo, então não deveria haver problemas, mas ainda assim pensamos cuidadosamente sobre o que poderia dar errado... No mínimo, é necessário verificar se a posição foi aberta pelo nosso EA:
//--- Determine if there is an open position if(PositionSelect(_Symbol)) { // Set the open position flag - for further processing positionExists = true; // Save the type of the open position positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); // Check if the position has been opened by our EA requestClosePosition.magic = PositionGetInteger(POSITION_MAGIC); // I didn't create a separate variable for // the existing position magic number if(requestClosePosition.magic!= EXPERT_MAGIC) { // Some other EA started trading our symbol. Let it do so... return; } // if(requestClosePosition.magic!= EXPERT_MAGIC) } // if(PositionSelect(_Symbol))
Exemplo 9. Obtenção dos dados da posição atual
Agora podemos verificar as condições de operação. Neste exemplo, os resultados das verificações serão armazenados em variáveis separadas, que depois serão usadas para tomar a decisão final: comprar ou vender. Em decisões grandes e complexas, essa abordagem se justifica, primeiro, pela enorme flexibilidade, e segundo, porque o código final acaba ficando mais curto. Aqui, esse método foi utilizado principalmente por causa da segunda razão: como o algoritmo final não é muito visual, tento aumentar sua clareza de diferentes maneiras.
//--- Check trading conditions, if( // Conditions for BUY rates[0].close>maValues[0] // If the first candlestick closed above МА && maValues[0]>maValues[1] // and the MA slope is upwards ) { // Set the trade flag tradingNeeds = true; // and inform the EA about the direction (here - BUY) tradingType = POSITION_TYPE_BUY; // to check the direction of the open direction orderType = ORDER_TYPE_BUY; // to trade in the right direction // calculate the deal price requestPrice = SymbolInfoDouble(_Symbol,SYMBOL_ASK); } else if( // conditions for SELL rates[0].close<maValues[0] && maValues[0]<maValues[1] ) { tradingNeeds = true; tradingType = POSITION_TYPE_SELL; orderType = ORDER_TYPE_SELL; requestPrice = SymbolInfoDouble(_Symbol,SYMBOL_BID); }
Exemplo 10. Verificação das condições de operação
O código a seguir decide se é necessário operar neste momento. A escolha do momento de operação é determinada por três perguntas:
- A situação de trading se formou? Em outras palavras, a vela fechou além da média? A resposta é dada pela variável tradingNeeds. Se a resposta for "não" (tradingNeeds == false), não é necessário operar.
- Existe outra posição aberta? A variável positionExists responderá. Se não houver, operamos sem problemas. Se houver, analisamos o próximo ponto.
- Essa posição está na mesma direção ou na direção oposta ao sinal detectado no primeiro ponto? Para responder, é necessário comparar as variáveis tradingType e positionType. Se forem iguais, a posição está na mesma direção do sinal, portanto, não operamos. Se for na direção oposta, primeiro fechamos a posição existente e depois criamos uma nova.
Este algoritmo de operação pode ser representado em um diagrama (figura 6).
Figura 6. Esquema das principais bifurcações do algoritmo de operação
No MQL5, fechar uma posição e abrir uma posição são ações que pressupõem a realização imediata de uma negociação. A abordagem é a mesma que você já conhece: preencher a estrutura do pedido de trading e enviá-la ao servidor. E a diferença dessas duas estruturas está apenas no fato de que, ao fechar uma posição, é necessário especificar seu identificador (ticket) e copiar exatamente os parâmetros da posição existente na solicitação. Ao abrir uma nova posição, não há nada para copiar, portanto, temos mais liberdade, e o identificador da posição antiga não existe, então não é necessário passá-lo.
E a diferença entre essas duas operações está apenas em dois pontos:
- o campo action, que no caso anterior continha o valor TRADE_ACTION_PENDING, agora será TRADE_ACTION_DEAL,
- e o campo type, que agora descreverá ordens "diretas" (ORDER_TYPE_SELL ou ORDER_TYPE_BUY), e não ordens pendentes.
Para facilitar a correspondência dos trechos de código com o diagrama da figura 6, vou colorir o código de exemplo com as mesmas cores usadas nos operadores internos de ramificação no esquema.
Este código ainda apresenta duas diferenças importantes em relação ao exemplo 3. Primeiro, antes de enviar a ordem de trade, a estrutura do pedido é verificada usando a função OrderCheck. Isso informa se os campos da estrutura foram preenchidos incorretamente, permitindo não apenas obter o código de retorno (retcode), mas também sua descrição textual (comment). E, segundo, tentamos controlar se o envio dos dados foi bem-sucedido (e se o servidor os aceitou) ou não. Se ocorrer um erro, o programa exibirá uma mensagem a respeito.
// If the setup is to trade if(tradingNeeds) { // If there is a position if(positionExists) { // And it is opposite to the desired direction of trade if(positionType != tradingType) { //--- Close the position //--- Clear all participating structures, otherwise you may get an "invalid request" error ZeroMemory(requestClosePosition); ZeroMemory(checkResult); ZeroMemory(result); //--- set operation parameters // Get position ticket requestClosePosition.position = PositionGetInteger(POSITION_TICKET); // Closing a position is just a trade requestClosePosition.action = TRADE_ACTION_DEAL; // position type is opposite to current trading direction, // therefore, for the closing deal, we can use the current order type requestClosePosition.type = orderType; // Current price requestClosePosition.price = requestPrice; // Operation volume must match the current position volume requestClosePosition.volume = PositionGetDouble(POSITION_VOLUME); // Set acceptable deviation from the current price requestClosePosition.deviation = inp_deviation; // Symbol requestClosePosition.symbol = Symbol(); // Position magic number requestClosePosition.magic = EXPERT_MAGIC; if(!OrderCheck(requestClosePosition,checkResult)) { // If the structure is filled incorrectly, display a message PrintFormat("Error when checking an order to close position: %d - %s",checkResult.retcode, checkResult.comment); } else { // Send order if(!OrderSend(requestClosePosition,result)) { // If position closing failed, report PrintFormat("Error closing position: %d - %s",result.retcode,result.comment); } // if(!OrderSend) } // else (!OrderCheck) } // if(positionType != tradingType) else { // Position opened in the same direction as the trade signal. Do not trade return; } // else(positionType != tradingType) } // if(positionExists) //--- Open a new position //--- Clear all participating structures, otherwise you may get an "invalid request" error ZeroMemory(result); ZeroMemory(checkResult); ZeroMemory(requestMakePosition); // Fill the request structure requestMakePosition.action = TRADE_ACTION_DEAL; requestMakePosition.symbol = Symbol(); requestMakePosition.volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); requestMakePosition.type = orderType; // While waiting for position to close, the price could have changed requestMakePosition.price = orderType == ORDER_TYPE_BUY ? SymbolInfoDouble(_Symbol,SYMBOL_ASK) : SymbolInfoDouble(_Symbol,SYMBOL_BID) ; requestMakePosition.sl = orderType == ORDER_TYPE_BUY ? rates[0].low : rates[0].high; requestMakePosition.deviation = inp_deviation; requestMakePosition.magic = EXPERT_MAGIC; if(!OrderCheck(requestMakePosition,checkResult)) { // If the structure check fails, report a check error PrintFormat("Error when checking a new position order: %d - %s",checkResult.retcode, checkResult.comment); } else { if(!OrderSend(requestMakePosition,result)) { // If position opening failed, report an error PrintFormat("Error opening position: %d - %s",result.retcode,result.comment); } // if(!OrderSend(requestMakePosition // Trading completed, reset flag just in case tradingNeeds = false; } // else (!OrderCheck(requestMakePosition)) } // if(tradingNeeds)
Exemplo 11. Código principal de trading (a maior parte do espaço é ocupada pelo preenchimento da estrutura e verificação de erros)
O texto completo do exemplo está no arquivo anexo MADeals.mq5.
Uso de classes de indicadores da biblioteca padrão
As classes dos indicadores padrão estão organizadas na pasta <Include\Indicators>. Você pode incluir todas de uma vez usando o arquivo <Include\Indicators\Indicators.mqh> (note a letra 's' no final do nome do arquivo) ou incluir por grupos ("Trend.mqh", "Oscillators.mqh", "Volumes.mqh", "BillWilliams.mqh"). Também existe a possibilidade de incluir separadamente as classes para acesso às séries temporais ("TimeSeries.mqh") e a classe para trabalhar com indicadores personalizados ("Custom.mqh").
Os demais arquivos dessa pasta são auxiliares e, para quem não domina POO, provavelmente serão de pouca utilidade. Em cada arquivo "útil" dessa pasta, normalmente são descritas várias classes. Todas elas seguem um padrão de nomenclatura semelhante: começam com a letra 'C', seguida pelo mesmo nome da função de criação do indicador. Por exemplo, a classe para trabalhar com médias móveis se chama CiMA e está localizada no arquivo "Trend.mqh".
Trabalhar com classes é muito parecido com trabalhar com as funções "cruas" do MQL5. A diferença está apenas na forma de chamar as funções e em seus nomes. Na primeira etapa (criação), é necessário chamar a função Create e passar os parâmetros do indicador a ser criado. Na segunda etapa (obtenção de dados), é necessário chamar a função Refresh, geralmente sem parâmetros. Se necessário, é possível especificar no parâmetro dessa função quais períodos atualizar, por exemplo (OBJ_PERIOD_D1 | OBJ_PERIOD_H1). Na etapa de uso dos dados, é aplicada a função GetData, normalmente com dois parâmetros: o número do buffer e o índice da vela (a numeração cresce da direita para a esquerda, como nas séries).
No exemplo 12 apresento o código mínimo de um EA que utiliza a classe CiMA. Esse EA simplesmente exibe em forma de comentário o valor da média móvel na primeira vela. Se quiser ver como usar essa abordagem de trabalho com indicadores em trading, copie o EA do segmento anterior (MADeals.mq5) para um novo arquivo e substitua as linhas correspondentes pelas linhas do exemplo 12.
#include <Indicators\Indicators.mqh> CiMA g_ma; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Create the indicator g_ma.Create(_Symbol,PERIOD_CURRENT,3,0,MODE_SMA,PRICE_CLOSE); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Comment(""); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Get data g_ma.Refresh(); //--- Use Comment( NormalizeDouble( g_ma.GetData(0,1), _Digits ) ); } //+------------------------------------------------------------------+
Exemplo 12. Uso da classe CiMA (indicador de média móvel)
Considerações finais
Após a leitura deste artigo, você já deve ser capaz de escrever EAs simples para prototipagem rápida de qualquer estratégia não muito complexa, baseada apenas em candles ou utilizando quaisquer indicadores que desenhem suas marcações através de buffers, e não através de elementos gráficos. Espero que o tema não tenha parecido excessivamente difícil. Caso tenha parecido — talvez valha a pena voltar e reler o material dos artigos anteriores.
No próximo artigo, quero analisar um EA tecnicamente pronto para publicação no Mercado. Ele terá ainda mais verificações do que o segundo EA deste artigo, mas essas verificações tornarão o EA mais confiável. A estrutura do EA mudará um pouco. A função OnTick deixará de ser o único núcleo da lógica de negócios, surgirão outras funções. Mas o principal é que surgirá a capacidade de reagir a erros de colocação de ordens (por exemplo, devido a requotes). Para isso, reestruturaremos a função OnTick de forma que seja possível acessar diretamente cada "etapa" do funcionamento do EA (operar, esperar o candle, calcular o lote...), sem precisar passar por outras etapas. E utilizaremos o evento TradeTransaction para monitorar as respostas do servidor. O resultado será um template funcionalmente orientado, fácil de editar, para você construir seu próprio código de qualquer complexidade (ainda sem precisar mergulhar profundamente em POO, mas plenamente funcional).
Lista dos artigos anteriores desta série:
- Aprendendo MQL5 do iniciante ao profissional (Parte I): Comecemos a programar
- Aprendendo MQL5 do iniciante ao profissional (Parte II): Tipos de dados básicos e uso de variáveis
- Aprendendo MQL5 do iniciante ao profissional (Parte III): Tipos de dados complexos e arquivos inclusos
- Aprendendo MQL5 do iniciante ao profissional (Parte IV): Sobre Arrays, Funções e Variáveis Globais do Terminal
- Aprendendo MQL5 do iniciante ao profissional (Parte V): Principais operadores de redirecionamento do fluxo de comandos (Aqui também foi abordado o princípio de construção de indicadores próprios)
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/15727





- 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
Excelente artigo, muito claro e com muitas coisas explicadas - muito obrigado. Especialmente no final, como usar indicadores por meio de classes! Vou considerar a possibilidade de testar protótipos em meu desenvolvimento de TS simples.