Guia prático do MQL5: O Histórico de transações e a biblioteca de função para obter propriedades de posição
Introdução
É hora de brevemente resumir a informação fornecida nos artigos anteriores sobre as propriedades de posição. Neste artigo, criaremos algumas funções adicionais para obter propriedades que podem apenas serem obtidas após acessar o histórico de transações. Também nos familiarizaremos com estruturas de dados que nos permitirá acessar propriedades de posição e símbolo de forma mais conveniente.
Sistemas de negociação onde volumes de posição permanecem o mesmo durante sua existência não requerem realmente o uso de funções que serão fornecidas neste artigo. Mas se você planeja implementar um sistema de administração de dinheiro e controlar um tamanho da posição de um lote em sua estratégia de negociação em um momento mais tarde, estas funções serão indispensáveis.
Antes de começarmos, eu gostaria de fazer uma sugestão para aqueles leitores que seguiram um link para este artigo, enquanto visitantes de primeira viagem ao website, ou apenas começaram a aprender a linguagem MQL5, para começar com os primeiros artigos da série "Guia prático do MQL5".
Desenvolvimento do Consultor Especializado
Para ser possível visualizar a operação de novas funções no Consultor Especializado modificado no artigo anterior chamado "Guia prático do MQL5: Como Evitar Erros Quando Configurando/Modificando Níveis de Negociação", adicionaremos a possibilidade de aumentar o volume da posição se um sinal para abertura ocorrer novamente, enquanto a posição já estiver lá.
Podem haver várias transações no histórico de posição, e se houve mudanças no volume da posição no curso das negociações, devem ter ocorrido mudanças no preço atual da posição. Para descobrir o preço do primeiro ponto de entrada, precisamos acessar o histórico de transações com respeito aquela posição específica. A figura abaixo é uma demonstração do caso onde a posição tem apenas uma transação (um ponto de entrada):
Fig. 1. Primeira transação na posição.
A próxima figura mostra uma mudança no preço da posição seguida da segunda transação:
Fig. 2. Segunda transação na posição.
Como demonstrando em artigos anteriores, identificadores padrão permitem a você obter apenas o preço da posição atual (POSITION_PRICE_OPEN) e o preço atual de um símbolo (POSITION_PRICE_CURRENT) para os quais uma posição é aberta.
Entretanto, em alguns sistemas de negócios precisamos saber a distância coberta pelo preço desde o primeiro ponto de entrada, assim como o preço da última transação. Toda esta informação está disponível no histórico de transações/ordens da conta. Abaixo está a lista de transações associadas com a figura anterior:
Fig. 3. O histórico de transações na conta.
Acredito que a situação agora seja clara e todos os objetivos estão definidos. Vamos continuar a modificar o Consultor Especialista apresentado nos artigos anteriores. Primeiro, adicionaremos novos identificadores numerados 0, 6, 9, 12 e 16 para a enumeração de propriedades de posição:
//--- Enumeration of position properties enum ENUM_POSITION_PROPERTIES { P_TOTAL_DEALS = 0, P_SYMBOL = 1, P_MAGIC = 2, P_COMMENT = 3, P_SWAP = 4, P_COMMISSION = 5, P_PRICE_FIRST_DEAL= 6, P_PRICE_OPEN = 7, P_PRICE_CURRENT = 8, P_PRICE_LAST_DEAL = 9, P_PROFIT = 10, P_VOLUME = 11, P_INITIAL_VOLUME = 12, P_SL = 13, P_TP = 14, P_TIME = 15, P_DURATION = 16, P_ID = 17, P_TYPE = 18, P_ALL = 19 };
Comentários para cada das propriedades serão dados em uma estrutura que será revisada logo abaixo.
Vamos aumentar o número de parâmetros externos. Agora, seremos capazes de especificar:
- MagicNumber - uma ID única do Consultor Especialista (número mágico);
- Deviation - desvio;
- VolumeIncrease - um valor pelo qual o volume da posição será aumentado;
- InfoPanel - um parâmetro que lhe permite ativar/desativar a exibição do painel de informação.
Assim é como é implementado:
//--- External parameters of the Expert Advisor sinput long MagicNumber=777; // Magic number sinput int Deviation=10; // Slippage input int NumberOfBars=2; // Number of Bullish/Bearish bars for a Buy/Sell input double Lot=0.1; // Lot input double VolumeIncrease=0.1; // Position volume increase input double StopLoss=50; // Stop Loss input double TakeProfit=100; // Take Profit input double TrailingStop=10; // Trailing Stop input bool Reverse=true; // Position reversal sinput bool ShowInfoPanel=true; // Display of the info panel
Por favor note os parâmetros cujo modificador sinput é definido. Este modificador lhe permite desabilitar a otimização no Testador de Estratégia. De fato, quando desenvolvemos um programa para uso próprio, você tem uma perfeita compreensão de quais parâmetros afetarão o resultado final, então você simplesmente os desmarca da otimização. Mas quando se trata de um grande número de parâmetros, este método te permite os separar visualmente de outros enquanto eles ficam esmaecidos:
Fig. 4. Parâmetros desativados para otimização estão esmaecidos.
Vamos agora substituir as variáveis globais que armazenaram valores de propriedade de função e símbolo com estruturas de dados (struct):
//--- Position properties struct position_properties { uint total_deals; // Number of deals bool exists; // Flag of presence/absence of an open position string symbol; // Symbol long magic; // Magic number string comment; // Comment double swap; // Swap double commission; // Commission double first_deal_price; // Price of the first deal in the position double price; // Current position price double current_price; // Current price of the position symbol double last_deal_price; // Price of the last deal in the position double profit; // Profit/Loss of the position double volume; // Current position volume double initial_volume; // Initial position volume double sl; // Stop Loss of the position double tp; // Take Profit of the position datetime time; // Position opening time ulong duration; // Position duration in seconds long id; // Position identifier ENUM_POSITION_TYPE type; // Position type };
//--- Symbol properties struct symbol_properties { int digits; // Number of decimal places in the price int spread; // Spread in points int stops_level; // Stops level double point; // Point value double ask; // Ask price double bid; // Bid price double volume_min; // Minimum volume for a deal double volume_max; // Maximum volume for a deal double volume_limit; // Maximum permissible volume for a position and orders in one direction double volume_step; // Minimum volume change step for a deal double offset; // Offset from the maximum possible price for a transaction double up_level; // Upper Stop level price double down_level; // Lower Stop level price }
Agora, para acessar um certo elemento da estrutura, precisamos criar uma variável deste tipo de estrutura. O procedimento é similar a criar um objeto para uma classe de negócios que foi considerada no artigo chamado "Guia prático do MQL5: Analisando propriedades de posição no testador de estratégia do MetaTrader 5".
//--- variables for position and symbol properties
position_properties pos;
symbol_properties symb;
Você pode acessar os elementos da mesma maneira quando lidando com métodos de classe. Em outras palavras, é suficiente colocar um ponto após o nome da variável da estrutura para exibir a lista de elementos contidos naquela estrutura específica. Isto é muito conveniente. No caso de comentários de uma linha são fornecidos para os campos da estrutura (assim como em nosso exemplo), eles serão mostrados em uma dica na direita.
Fig. 5. Lista de campos de estrutura.
Outro ponto importante. Ao modificar o Consultor Especialista, mudamos virtualmente todas as suas variáveis globais usadas em muitas funções, então, agora precisamos substituí-las com campos de estrutura correspondentes para propriedades de símbolo e posição. Por exemplo, a variável global pos_open que foi usada para armazenar a sinalização de presença/ausência de uma posição aberta foi substituída com o campo exists do tipo de estrutura position_properties. Portanto, sempre que a variável pos_open for usada, ela precisa ser substituída por pos.exists.
Será um longo e exaustivo processo se você fazê-lo manualmente. Então será melhor automatizar a solução para esta tarefa usando os recursos do MetaEditor: Encontrar e Substituir -> Substituir no menu Editar ou na combinação de teclas Ctrl+H:
Fig. 6. Encontrando e substituindo o texto.
Precisamos encontrar e substituir todas as variáveis globais para as propriedades de posição e símbolo para executar um teste mais profundo, tendo compilado o arquivo. Se nenhum erro é detectado, significará que fizemos tudo corretamente. Não fornecerei o código aqui para não fazer este artigo desnecessariamente longo. Além disso, um código fonte pronto para uso está disponível no final deste artigo para download.
Agora que resolvemos as coisas com as variáveis, vamos proceder para modificar as funções existentes e criar novas.
Nos parâmetros externos, você pode agora definir o número mágico e desvio em pontos. Nós, portanto, também precisaremos fazer mudanças relevantes no código do Consultor Especialista. Criaremos uma função auxiliar definida pelo usuário OpenPosition(), onde estas propriedades será definidas usando as funções da classe CTrade antes de enviar uma ordem para abertura de posição.
//+------------------------------------------------------------------+ //| Opening a position | //+------------------------------------------------------------------+ void OpenPosition(double lot, ENUM_ORDER_TYPE order_type, double price, double sl, double tp, string comment) { trade.SetExpertMagicNumber(MagicNumber); // Set the magic number in the trading structure trade.SetDeviationInPoints(CorrectValueBySymbolDigits(Deviation)); // Set the slippage in points //--- If the position failed to open, print the relevant message if(!trade.PositionOpen(_Symbol,order_type,lot,price,sl,tp,comment)) { Print("Error opening the position: ",GetLastError()," - ",ErrorDescription(GetLastError())); } }
Precisamos apenas fazer algumas pequenas mudanças no código da função de negociação principal do Consultor Especialista - TradingBlock(). Abaixo está a parte do código da função que sofreu mudanças:
//--- If there is no position if(!pos.exists) { //--- Adjust the volume lot=CalculateLot(Lot); //--- Open a position OpenPosition(lot,order_type,position_open_price,sl,tp,comment); } //--- If there is a position else { //--- Get the position type GetPositionProperties(P_TYPE); //--- If the position is opposite to the signal and the position reversal is enabled if(pos.type==opposite_position_type && Reverse) { //--- Get the position volume GetPositionProperties(P_VOLUME); //--- Adjust the volume lot=pos.volume+CalculateLot(Lot); //--- Reverse the position OpenPosition(lot,order_type,position_open_price,sl,tp,comment); return; } //--- If the signal is in the direction of the position and the volume increase is enabled, increase the position volume if(!(pos.type==opposite_position_type) && VolumeIncrease>0) { //--- Get the Stop Loss of the current position GetPositionProperties(P_SL); //--- Get the Take Profit of the current position GetPositionProperties(P_TP); //--- Adjust the volume lot=CalculateLot(Increase); //--- Increase the position volume OpenPosition(lot,order_type,position_open_price,pos.sl,pos.tp,comment); return; }
O código acima foi melhorado com o bloco onde a direção da posição atual é verificada contra a direção do sinal. Se as direções coincidem e o aumento do volume da posição é ativado nos parâmetros externos (o valor do parâmetro VolumeIncrease é maior do que zero), nós verificamos/ajustamos um dado lote e enviados a ordem relevante. Agora, tudo que você precisa fazer para enviar uma ordem para abrir ou reverter uma posição ou para aumentar o volume da posição é escrever uma linha de código.
Vamos criar funções para obter propriedades de posição do histórico de transações. Começaremos com a função CurrentPositionTotalDeals() que retorna o número de transações na posição atual:
//+------------------------------------------------------------------+ //| Returning the number of deals in the current position | //+------------------------------------------------------------------+ uint CurrentPositionTotalDeals() { int total =0; // Total deals in the selected history list int count =0; // Counter of deals by the position symbol string deal_symbol =""; // symbol of the deal //--- If the position history is obtained if(HistorySelect(pos.time,TimeCurrent())) { //--- Get the number of deals in the obtained list total=HistoryDealsTotal(); //--- Iterate over all the deals in the obtained list for(int i=0; i<total; i++) { //--- Get the symbol of the deal deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL); //--- If the symbol of the deal and the current symbol are the same, increase the counter if(deal_symbol==_Symbol) count++; } } //--- return(count); }
O código acima é dado com comentários bastante detalhados. Mas devemos dizer algumas palavras sobre como o histórico é selecionado. Em nosso caso, obtemos a lista do ponto de abertura da posição atual determinado pelo horário de abertura para o ponto atual do tempo usando a função HistorySelect(). Após o histórico ser selecionado, podemos descobrir o número de transações na lista usando a função HistoryDealsTotal(). O resto deve estar claro pelos comentários.
O histórico de uma posição particular também pode ser selecionado pelo seu identificador usando a função HistorySelectByPosition(). Aqui, você precisa considerar que o identificador da posição permanece o mesmo quando a posição é revertida, como acontece as vezes em nosso Consultor Especialista. Entretanto, o horário de abertura de posição realmente muda com a reversão, portanto esta variante é mais fácil de implementar. Mas se você tem que lidar com o histórico de transações que não se aplica apenas a posição aberta atualmente, você precisa utilizar identificadores. Retornaremos ao histórico de transações em artigos futuros.
Vamos continuar criando uma função CurrentPositionFirstDealPrice() que retorna o preço da primeira transação na posição, ou seja, o preço da transação no qual a posição foi aberta.
//+------------------------------------------------------------------+ //| Returning the price of the first deal in the current position | //+------------------------------------------------------------------+ double CurrentPositionFirstDealPrice() { int total =0; // Total deals in the selected history list string deal_symbol =""; // symbol of the deal double deal_price =0.0; // Price of the deal datetime deal_time =NULL; // Time of the deal //--- If the position history is obtained if(HistorySelect(pos.time,TimeCurrent())) { //--- Get the number of deals in the obtained list total=HistoryDealsTotal(); //--- Iterate over all the deals in the obtained list for(int i=0; i<total; i++) { //--- Get the price of the deal deal_price=HistoryDealGetDouble(HistoryDealGetTicket(i),DEAL_PRICE); //--- Get the symbol of the deal deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL); //--- Get the time of the deal deal_time=(datetime)HistoryDealGetInteger(HistoryDealGetTicket(i),DEAL_TIME); //--- If the time of the deal equals the position opening time, // and if the symbol of the deal and the current symbol are the same, exit the loop if(deal_time==pos.time && deal_symbol==_Symbol) break; } } //--- return(deal_price); }
O princípio aqui é o mesmo que na função anterior. Obtemos o histórico do ponto da abertura da posição e então verificamos o horário da transação e horário da abertura de posição a cada iteração. Junto com o preço da transação, obtemos o nome do símbolo e o horário da transação. A primeira transação é identificada quando o horário da transação coincide com o horário de abertura da posição. Como seu preço já foi atribuído para a variável relevante, apenas precisamos retornar o valor.
Vamos continuar. Às vezes, você pode precisar obter o preço da última transação na posição atual. Para este propósito, criaremos a função CurrentPositionLastDealPrice():
//+------------------------------------------------------------------+ //| Returning the price of the last deal in the current position | //+------------------------------------------------------------------+ double CurrentPositionLastDealPrice() { int total =0; // Total deals in the selected history list string deal_symbol =""; // Symbol of the deal double deal_price =0.0; // Price //--- If the position history is obtained if(HistorySelect(pos.time,TimeCurrent())) { //--- Get the number of deals in the obtained list total=HistoryDealsTotal(); //--- Iterate over all the deals in the obtained list from the last deal in the list to the first deal for(int i=total-1; i>=0; i--) { //--- Get the price of the deal deal_price=HistoryDealGetDouble(HistoryDealGetTicket(i),DEAL_PRICE); //--- Get the symbol of the deal deal_symbol=HistoryDealGetString(HistoryDealGetTicket(i),DEAL_SYMBOL); //--- If the symbol of the deal and the current symbol are the same, exit the loop if(deal_symbol==_Symbol) break; } } //--- return(deal_price); }
Desta vez o ciclo (loop) começou com a última transação na lista e é, frequentemente, o caso em que a transação requerida é identificada na primeira iteração do ciclo. Mas se você negociar em vários símbolos, o ciclo continuará até o símbolo da transação coincidir ao símbolo atual.
O volume atual de posição pode ser obtido usando o identificador padrão POSITION_VOLUME. Para encontrar o volume da posição inicial (o volume da primeira transação), criaremos a função CurrentPositionInitialVolume():
//+------------------------------------------------------------------+ //| Returning the initial volume of the current position | //+------------------------------------------------------------------+ double CurrentPositionInitialVolume() { int total =0; // Total deals in the selected history list ulong ticket =0; // Ticket of the deal ENUM_DEAL_ENTRY deal_entry =WRONG_VALUE; // Position modification method bool inout =false; // Flag of position reversal double sum_volume =0.0; // Counter of the aggregate volume of all deals, except for the first one double deal_volume =0.0; // Volume of the deal string deal_symbol =""; // Symbol of the deal datetime deal_time =NULL; // Deal execution time //--- If the position history is obtained if(HistorySelect(pos.time,TimeCurrent())) { //--- Get the number of deals in the obtained list total=HistoryDealsTotal(); //--- Iterate over all the deals in the obtained list from the last deal in the list to the first deal for(int i=total-1; i>=0; i--) { //--- If the order ticket by its position is obtained, then... if((ticket=HistoryDealGetTicket(i))>0) { //--- Get the volume of the deal deal_volume=HistoryDealGetDouble(ticket,DEAL_VOLUME); //--- Get the position modification method deal_entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket,DEAL_ENTRY); //--- Get the deal execution time deal_time=(datetime)HistoryDealGetInteger(ticket,DEAL_TIME); //--- Get the symbol of the deal deal_symbol=HistoryDealGetString(ticket,DEAL_SYMBOL); //--- When the deal execution time is less than or equal to the position opening time, exit the loop if(deal_time<=pos.time) break; //--- otherwise calculate the aggregate volume of deals by the position symbol, except for the first one if(deal_symbol==_Symbol) sum_volume+=deal_volume; } } } //--- If the position modification method is a reversal if(deal_entry==DEAL_ENTRY_INOUT) { //--- If the position volume has been increased/decreased // I.e. the number of deals is more than one if(fabs(sum_volume)>0) { //--- Current volume minus the volume of all deals except for the first one double result=pos.volume-sum_volume; //--- If the resulting value is greater than zero, return the result, otherwise return the current position volume deal_volume=result>0 ? result : pos.volume; } //--- If there are no more deals, other than the entry, if(sum_volume==0) deal_volume=pos.volume; // return the current position volume } //--- Return the initial position volume return(NormalizeDouble(deal_volume,2)); }
Esta função saiu mais complexa do que as anteriores. Tenho tentado levar em consideração todas as possíveis situações que pode resultar no valor errado. Um teste cuidadoso não revelou quaisquer problemas. Os comentários detalhados fornecidos no código deve ajudá-lo a entender a ideia.
Também será útil ter uma função que retorna a duração da posição. Organizaremos para permitir ao usuário selecionar o formato apropriado do valor retornado: segundos, minutos, horas ou dias. Para este propósito, vamos criar outra enumeração:
//--- Position duration enum ENUM_POSITION_DURATION { DAYS = 0, // Days HOURS = 1, // Hours MINUTES = 2, // Minutes SECONDS = 3 // Seconds };
Abaixo está o código da função CurrentPositionDuration() responsável por todos os cálculos relevantes:
//+------------------------------------------------------------------+ //| Returning the duration of the current position | //+------------------------------------------------------------------+ ulong CurrentPositionDuration(ENUM_POSITION_DURATION mode) { ulong result=0; // End result ulong seconds=0; // Number of seconds //--- Calculate the position duration in seconds seconds=TimeCurrent()-pos.time; //--- switch(mode) { case DAYS : result=seconds/(60*60*24); break; // Calculate the number of days case HOURS : result=seconds/(60*60); break; // Calculate the number of hours case MINUTES : result=seconds/60; break; // Calculate the number of minutes case SECONDS : result=seconds; break; // No calculations (number of seconds) //--- default : Print(__FUNCTION__,"(): Unknown duration mode passed!"); return(0); } //--- Return result return(result); }
Vamos criar a função CurrentPositionDurationToString() para o painel de informação onde as propriedades de função são exibidas. A função converterá a duração da posição em segundos para um formato facilmente compreendido pelo usuário. O número de segundos será passado para a função, e a função por sua vez retornará um string contendo a duração da posição em dias, horas, minutos e segundos:
//+------------------------------------------------------------------+ //| Converting the position duration to a string | //+------------------------------------------------------------------+ string CurrentPositionDurationToString(ulong time) { //--- A dash if there is no position string result="-"; //--- If the position exists if(pos.exists) { //--- Variables for calculation results ulong days=0; ulong hours=0; ulong minutes=0; ulong seconds=0; //--- seconds=time%60; time/=60; //--- minutes=time%60; time/=60; //--- hours=time%24; time/=24; //--- days=time; //--- Generate a string in the specified format DD:HH:MM:SS result=StringFormat("%02u d: %02u h : %02u m : %02u s",days,hours,minutes,seconds); } //--- Return result return(result); }
Está tudo pronto agora. Não fornecerei os códigos das funções GetPositionProperties() e GetPropertyValue() que precisam ser modificados de acordo com todas as mudanças acima. Se você leu todos os artigos anteriores da série, você não deve encontrar qualquer dificuldade em fazer isso por conta própria. Qualquer que seja o caso, o arquivo do código-fonte está em anexo a este artigo.
Como resultado, o painel de informação deve parecer como mostrado abaixo:
Fig. 7. Demonstração de todas as propriedades de posição no painel de informação.
Deste modo, agora temos a biblioteca de função para obter propriedades de função e provavelmente continuaremos a trabalhar nela em artigos futuros, como e quando necessário.
Otimizando Parâmetros e Testando o Consultor Especialista
Como um experimento, vamos tentar otimizar os parâmetros do Consultor Especialista. Embora o que temos atualmente não pode ainda ser chamado um sistema de transações completo e cheio de recursos, o resultado que alcançaremos abrirá nossos olhos em relação a algumas coisas e melhorar nossa experiência como desenvolvedores de sistemas de transações.
Configuraremos o Testador de Estratégia como mostrado abaixo:
Fig. 8. Configurações do Testador de Estratégia para otimização de parâmetro.
Configurações dos parâmetros externos do Consultor Especialista devem ser como seguem:
Fig. 9. Configurações de parâmetro do Consultor Especialista para otimização.
Seguindo a otimização, classificamos os resultados alcançados pelo fator máximo de recuperação:
Fig. 10. Resultados ordenados pelo fator máximo de recuperação.
Vamos agora testar o grupo de parâmetros do topo com o valor do Fator de Recuperação sendo igual a 4.07. Mesmo dado o fato que a otimização foi feita para EURUSD, podemos ver os resultados positivos para muitos símbolos:
Resultados para EURUSD:
Fig. 11. Resultados para EURUSD.
Resultados para AUDUSD:
Fig. 12. Resultados para AUDUSD.
Resultados para NZDUSD:
Fig. 13. Resultados para NZDUSD.
Conclusão
Virtualmente qualquer ideia pode ser desenvolvida e melhorada. Todo sistema de transações deve ser cuidadosamente testado antes de ser rejeitado como defeituoso. Em artigos futuros, olharemos para vários mecanismos e esquemas que podem desempenhar um papel muito positivo na personalização e adaptação de quase qualquer sistema de negociações.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/644
- 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