English Русский 中文 Español Deutsch 日本語
preview
Guia Prático MQL5 — Serviços

Guia Prático MQL5 — Serviços

MetaTrader 5Exemplos | 28 fevereiro 2023, 09:58
809 0
Denis Kirichenko
Denis Kirichenko

Introdução

Recentemente, a MetaTrader 5 apresentou um novo tipo de programa conhecido como serviço. De acordo com o desenvolvedor, os serviços permitem que os usuários criem feeds de preços personalizados para a plataforma, ou seja, implementam a entrega de dados de mercado de sistemas externos em tempo real, assim como é implementado nos servidores de negociação das corretoras. Esta não é a única característica dos serviços.

Neste artigo, eu considerarei as nuances de trabalhar com os serviços. O artigo é focado principalmente aos iniciantes. Com base nisso, eu tentei tornar o código totalmente reproduzível e mais complicado de um exemplo para outro.



1. Processos Daemons

Os serviços em MQL5 têm semelhanças com os Serviços do Windows. A Wikipédia dá a seguinte definição de um serviço:

Serviço do Windows é um programa de computador que opera em segundo plano. A partir da definição, fica claro que ele tem muito em comum com o conceito de daemons no Unix.

No nosso caso, o ambiente externo para serviços não é o próprio sistema operacional, mas o shell da MetaTrader5.

Algumas palavras sobre os daemons.

Um daemon é um programa de computador que é executado como um processo em segundo plano, em vez de estar sob o controle direto de um usuário interativo.

O termo foi cunhado pelos programadores do Projeto MAC do MIT. рус.Segundo Fernando J. Corbató, que trabalhou no Projeto MAC em 1963, sua equipe foi a primeira a usar o termo daemon, inspirado no demônio de Maxwell, um agente imaginário da física e da termodinâmica que ajudava a classificar as moléculas.Os sistemas UNIX herdaram essa terminologia.

O demônio de Maxwell é consistente com a interpretação da mitologia grega de um daemon como um ser sobrenatural trabalhando em segundo plano. Conforme declarado no Unix System Administration Handbook, o conceito dos antigos gregos de um "daemon pessoal" era semelhante ao conceito moderno de um "anjo da guarda".

Embora os gregos antigos não tivessem computadores, as relações das entidades eram claras para eles.



2. Serviços – Informações sobre a documentação

Antes de aprofundarmos o assunto, sugiro dar uma olhada nos materiais da documentação e ver como o desenvolvedor descreve os recursos dos serviços.

2.1 Tipos de aplicativos

Na primeira página da Documentação, ou seja, na seção Tipos de aplicativos em MQL5, um serviço é definido como um tipo de programa MQL5:

  • Um serviço é um programa que, ao contrário dos indicadores, Expert Advisors e scripts, não estão vinculados a um gráfico para funcionar. Assim como os scripts, os serviços não manipulam nenhum evento, exceto o seu gatilho. Para iniciar um serviço, seu código deve conter a função do manipulador OnStart. Os serviços não aceitam nenhum outro evento, exceto a OnStart, mas eles podem enviar eventos personalizados para os gráficos usando a EventChartCustom. Os serviços são armazenados em <diretório_do_terminal>\MQL5\Services.

Observe aqui que os serviços são semelhantes aos scripts. A diferença fundamental é que eles não estão vinculados a nenhum gráfico.


2.2Execução do programa

A seção do Execução do Programa fornece um resumo dos programas em MQL5:

Programa
Execução Nota
  Serviço
 Em sua própria thread, existem tantas threads de execução quantos serviços
 Um serviço em loop não pode interromper a execução de outros programas
  Script
 Em sua própria thread, existem tantas threads de execução quanto scripts
 Um script em loop não pode interromper a execução de outros programas
  Expert Advisor
 Em sua próprio thread, existem tantas threads de execução quantos os EAs
 Um Expert Advisor em loop não pode interromper a execução de outros programas
 Indicador
 Uma thread para todos os indicadores em um símbolo. O número de threads é igual ao número de símbolos com os indicadores
 Um loop infinito em um indicador irá parar todos os outros indicadores neste símbolo

Em outras palavras, os serviços não diferem de scripts e EAs quanto ao método de ativação do fluxo de execução. Os serviços também são semelhantes aos scripts e EAs, pois a presença de blocos de código em loop não afeta a operação de outros programas em mql5.


2.3 Proibição do uso de funções em serviços

O desenvolvedor fornece uma lista exaustiva de funções não permitidas para uso:

Isso é razoável, pois os serviços não podem parar o Expert Advisor e trabalhar com o timer, pois eles lidam com um único evento de Start. Eles também não podem trabalhar com as funções de indicadores personalizados.

 
2.4 Loading and unloading services

A seção da Documentação apresenta vários pontos importantes. Vamos considerar cada um deles.

Os serviços são carregados imediatamente após o início da plataforma, se ainda estiverem em execução no momento do desligamento da mesma. Os serviços são descarregados imediatamente após a conclusão do trabalho.

Esta é uma das propriedades notáveis de um serviço. Ele não deve ser monitorado. Ele executa automaticamente suas tarefas após ser iniciado uma vez.

Os serviços têm um único manipulador OnStart(), no qual você pode implementar um loop infinito de recebimento e manipulação de dados, por exemplo, criando e atualizando símbolos personalizados usando as funções de rede.

Nós podemos tirar uma conclusão simples. Se o serviço deve executar um conjunto de ações únicas, não é necessário repetir nenhum bloco de código. Se a tarefa envolver a operação constante ou regular do serviço, será necessário agrupar o bloco de código em um loop. Consideraremos exemplos de tais tarefas mais tarde.

Ao contrário dos Expert Advisors, indicadores e scripts, os serviços não estão vinculados a um gráfico específico, portanto, um mecanismo separado é fornecido para iniciá-los.

 Talvez esta seja a segunda característica notável do serviço. Ele não requer nenhum agendamento para funcionar.

Uma nova instância de serviço é criada a partir do Navegador usando o comando "Adicionar serviço". Uma instância de serviço pode ser iniciada, interrompida e removida usando o menu da instância apropriado. Para gerenciar todas as instâncias, use o menu de serviço.

Esta é a terceira propriedade notável do serviço. Tendo apenas um arquivo de programa, você pode executar várias instâncias dele ao mesmo tempo. Isso geralmente é feito quando você precisa usar parâmetros diferentes (variáveis de entrada).


3. Protótipo do Serviço

A Ajuda da plataforma que pode ser aberta pressionando a tecla F1 descreve o mecanismo para iniciar e gerenciar os serviços. Portanto, nós não vamos nos deter nisso agora.

No MetaEditor,criamos o modelo de serviço chamado dEmpty.mq5.

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   
  }
//+------------------------------------------------------------------+

Após a compilação, poderemos ver o nome do serviço no Navegador (Fig. 1).


Serviço dEmpty

Fig. 1. Serviço dEmpty na subjanela do Navegador

Depois de adicionar e iniciar o serviço dEmpty na subjanela do Navegador, nós obtemos as seguintes entradas no Diário:

CS      0       19:54:18.590    Services        service 'dEmpty' started
CS      0       19:54:18.592    Services        service 'dEmpty' stopped

Os logs mostram que o serviço foi iniciado e interrompido. Como o seu código não contém comandos, não haverá alterações na plataforma. Nós não notamos nada após o lançamento do serviço.

Vamos preencher o modelo de serviço com alguns comandos. Criamos o serviço dStart.mq5 e escreva as seguintes linhas:

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));   
  }
//+------------------------------------------------------------------+

Depois de lançar o serviço, nós veremos a seguinte entrada na Experts:

CS      0       20:04:28.347    dStart       Service "dStart" starts at: 2022.11.30 20:04:28.

Então o serviço dStart nos informou sobre o seu lançamento e depois parou.

Vamos expandir os recursos do serviço anterior e nomear o novo dStartStop.mq5.

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
   ::Sleep(1000);
   now=::TimeLocal();
   ::PrintFormat("Service \"%s\" stops at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
  }
//+------------------------------------------------------------------+

O serviço atual já informa não só do seu início, mas também do término de sua atividade.

Após o início do serviço no diário, nós veremos as seguintes entradas:

2022.12.01 22:49:10.324 dStartStop   Service "dStartStop" starts at: 2022.12.01 22:49:10
2022.12.01 22:49:11.336 dStartStop   Service "dStartStop" stops at: 2022.12.01 22:49:11

É fácil ver que a primeira e a segunda vez diferem por um segundo. A função Sleep() nativa foi acionada entre o primeiro e o último comando.

Agora vamos estender os recursos do serviço atual para que ele seja executado até que ele seja interrompido à força.Vamos nomear o novo serviço de dStandBy.mq5.

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
   do
     {
      ::Sleep(1);
     }
   while(!::IsStopped());
   now=::TimeLocal();
   ::PrintFormat("Service \"%s\" stops at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
//--- final goodbye
   for(ushort cnt=0; cnt<5; cnt++)
     {
      ::PrintFormat("Count: %hu", cnt+1);
      ::Sleep(10000);
     }
  }
//+------------------------------------------------------------------+

Depois de sair do loop do while devido ao término do programa, o serviço ainda gravará vários valores de contador no log. Após cada uma dessas entradas, a Sleep() é chamada com um intervalo de atraso de 10 segundos.

O diário contém os seguintes registros:

CS      0       23:20:44.478    dStandBy     Service "dStandBy" starts at: 2022.12.01 23:20:44
CS      0       23:20:51.144    dStandBy     Service "dStandBy" stops at: 2022.12.01 23:20:51
CS      0       23:20:51.144    dStandBy     Count: 1
CS      0       23:20:51.159    dStandBy     Count: 2
CS      0       23:20:51.175    dStandBy     Count: 3
CS      0       23:20:51.191    dStandBy     Count: 4
CS      0       23:20:51.207    dStandBy     Count: 5

O serviço foi iniciado às 23:20:44 e interrompido à força às 23:20:51. Também é fácil ver que os intervalos entre os valores do contador não ultrapassam 0.02 segundos. Embora um atraso de 10 segundos tenha sido definido anteriormente para esses intervalos.

De acordo com a Documentação a respeito da função Sleep():

Nota

A função Sleep() não pode ser chamada a partir de indicadores personalizados, pois os indicadores são executados na thread da interface e não devem retardá-la. A função possui uma verificação integrada do do estado da flag de término do EA a cada 0.1 segundos.

Então, no nosso caso, a Sleep() detectou rapidamente que o serviço foi interrompido à força, atrasando a execução do programa mql5.

Para completar, vamos dar uma olhada no que a Documentação diz sobre o valor de retorno da função de verificação do estado IsStopped():

Valor de Retorno

Retorna true, se a variável _StopFlag do sistema contém um valor diferente de 0. Um valor diferente de zero é gravado em _StopFlag, se um programa mql5 tiver sido comandado para concluir sua operação. Neste caso, você deve encerrar imediatamente o programa, caso contrário, o programa será concluído à força do lado de fora após 3 segundos.

Assim, após um término forçado, o serviço tem 3 segundos para fazer outra coisa antes de ser totalmente desativado. Vamos verificar esse momento na prática. Vamos adicionar um cálculo de matriz ao código do serviço anterior após o loop. O cálculo leva cerca de um minuto. Nós veremos se o serviço tem tempo para calcular tudo depois de ter sido interrompido à força. Vamos nomear o novo serviço srvcStandByMatrixMult.mq5.

Depois do loop de calcular os valores do contador, nós precisamos adicionar o seguinte bloco ao código anterior:

//--- Matrix mult
//--- matrix A 1000x2000
   int rows_a=1000;
   int cols_a=2000;
//--- matrix B 2000x1000
   int rows_b=cols_a;
   int cols_b=1000;
//--- matrix C 1000x1000
   int rows_c=rows_a;
   int cols_c=cols_b;
//--- matrix A: size=rows_a*cols_a
   int size_a=rows_a*cols_a;
   int size_b=rows_b*cols_b;
   int size_c=rows_c*cols_c;
//--- prepare matrix A
   double matrix_a[];
   ::ArrayResize(matrix_a, rows_a*cols_a);
   for(int i=0; i<rows_a; i++)
      for(int j=0; j<cols_a; j++)
         matrix_a[i*cols_a+j]=(double)(10*::MathRand()/32767);
//--- prepare matrix B
   double matrix_b[];
   ::ArrayResize(matrix_b, rows_b*cols_b);
   for(int i=0; i<rows_b; i++)
      for(int j=0; j<cols_b; j++)
         matrix_b[i*cols_b+j]=(double)(10*::MathRand()/32767);
//--- CPU: calculate matrix product matrix_a*matrix_b
   double matrix_c_cpu[];
   ulong time_cpu=0;
   if(!MatrixMult_CPU(matrix_a, matrix_b, matrix_c_cpu, rows_a, cols_a, cols_b, time_cpu))
     {
      ::PrintFormat("Error in calculation on CPU. Error code=%d", ::GetLastError());
      return;
     }
   ::PrintFormat("time CPU=%d ms", time_cpu);

Iniciar o serviço dStandByMatrixMult e pará-lo à força após alguns segundos. As seguintes linhas aparecem no log:

CS      0       15:17:23.493    dStandByMatrixMult   Service "dStandByMatrixMult" starts at: 2022.12.02 15:17:23
CS      0       15:18:17.282    dStandByMatrixMult   Service "dStandByMatrixMult" stops at: 2022.12.02 15:18:17
CS      0       15:18:17.282    dStandByMatrixMult   Count: 1
CS      0       15:18:17.297    dStandByMatrixMult   Count: 2
CS      0       15:18:17.313    dStandByMatrixMult   Count: 3
CS      0       15:18:17.328    dStandByMatrixMult   Count: 4
CS      0       15:18:17.344    dStandByMatrixMult   Count: 5
CS      2       15:18:19.771    dStandByMatrixMult   Abnormal termination

Como nós vemos, o comando para encerrar a execução do programa mql5 chegou às 15:18:17.282. O serviço em si foi encerrado à força às 15:18:19.771. Com efeito, decorreram 2489 segundos desde o momento do encerramento até o término forçado do serviço. O fato do serviço ter sido interrompido à força e, além disso, como resultado da rescisão de emergência, é demonstrado pela entrada "Abnormal termination".

Como não restam mais de 3 segundos antes de interromper o serviço à força (_StopFlag == true), não é recomendado fazer os cálculos sérios ou ações de negociação para o loop interrompido.

Aqui está um simples exemplo. Suponha que a plataforma possua um serviço que fecha todas as posições quando a própria plataforma for fechada. A plataforma fecha e o serviço tenta liquidar todas as posições ativas. Como resultado, a plataforma é fechada e algumas posições permanecem abertas, embora não saibamos disso. 


4. Exemplos de uso

Antes de prosseguir com os exemplos práticos, eu proponho discutir o que os serviços da plataforma de negociação podem fazer. Por um lado, nós podemos introduzir quase qualquer código no serviço (exceto o que for proibido) e, por outro lado, provavelmente vale a pena delimitar poderes e dar aos serviços um nicho próprio no ambiente da plataforma de negociação.

Primeiro, os serviços não devem duplicar o trabalho de outros programas ativos em MQL5: Expert Advisors, indicadores e scripts. Digamos que haja um Expert Advisor que coloque ordens limitadas por um sinal no final de uma sessão de negociação. Além disso, existe um serviço que coloca essas ordens limitadas. Como resultado, o sistema de contabilidade das ordens limitadas no próprio EA poderá ser interrompido , ou no caso de números mágicos diferentes, o EA pode perder a visibilidade das ordens realizadas pelo serviço.

Em segundo lugar, nós precisamos evitar a situação oposta - o conflito de serviços com outros programas em MQL5. Digamos que haja um Expert Advisor que coloque ordens limitadas por um sinal no final de uma sessão de negociação. E existe um serviço que controla que no final do dia de negociação todas as posições sejam fechadas e as ordens pendentes sejam removidas.Há um conflito de interesses: o EA colocará as ordens e o serviço os removerá imediatamente. Tudo isso pode acabar em uma espécie de ataque DDoS a um servidor de negociação.

Em geral, os serviços devem ser harmoniosamente integrados ao funcionamento da plataforma de negociação, sem interferir nos programas em mql5, enquanto interage com eles para um uso mais eficiente dos algoritmos de negociação.


4.1 Limpando os logs

Suponha que o serviço tenha a tarefa de limpar a pasta de logs (diários) gerados por um ou mais Expert Advisors no passado (ontem, anteontem, etc.) no início de um novo dia de negociação.

Quais ferramentas nós precisamos aqui? Nós vamos precisar das operações de arquivo e da definição de uma nova barra.Saiba mais sobre a nova classe de detecção de barras no artigo Handlre de evento "nova barra".

Agora vamos lidar com as operações de arquivo. As operações de arquivo nativo não funcionarão aqui, pois nos deparamos com as limitações da caixa de proteção do arquivo (sandbox). De acordo com a Documentação:

Por motivos de segurança, o trabalho com arquivos é estritamente controlado na linguagem MQL5. Os arquivos usados nas operações de arquivo por meio da linguagem MQL5 não podem ser localizados fora da caixa de proteção do arquivo (sandbox).

Os arquivos de log gravados em disco pelos programas MQL5 estão localizados em %MQL5\Logs. Felizmente, nós podemos usar o WinAPI com as operações de arquivo.

A WinAPI é incluída usando a seguinte diretiva:

#include <WinAPI\winapi.mqh>

Nós usaremos oito funções no arquivo WinAPI:

  1. FindFirstFileW(),
  2. FindNextFileW(),
  3. CopyFileW(),
  4. GetFileAttributesW(),
  5. SetFileAttributesW(),
  6. DeleteFileW(),
  7. FindClose(),
  8. GetLastError().

A primeira função procura na pasta especificada o primeiro arquivo com o nome fornecido. É possível substituir uma máscara como um nome. Assim, para localizar os arquivos de log em uma pasta, basta especificar a string ".log" como nome.

A segunda função continua a busca iniciada pela primeira função.

A terceira função copia um arquivo existente para um novo.

A quarta função obtém os atributos do sistema de arquivos para o arquivo ou diretório especificado.

A quinta função define tais atributos.

A sexta função exclui o arquivo com o nome fornecido.

A sétima função fecha o manipulador de busca do arquivo.

A oitava função recupera o último valor do código de erro.

Vamos dar uma olhada no código de serviço dClearTradeLogs.mq5.

//--- include
#include <WinAPI\winapi.mqh>
#include "Include\CisNewBar.mqh"
//--- defines
#define ERROR_FILE_NOT_FOUND 0x2
#define ERROR_NO_MORE_FILES 0x12
#define INVALID_FILE_ATTRIBUTES 0xFFFFFFFF
#define FILE_ATTRIBUTE_READONLY 0x1
#define FILE_ATTRIBUTE_DIRECTORY 0x10
#define FILE_ATTRIBUTE_ARCHIVE 0x20
//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input string InpDstPath="G:" ; // Destination drive
//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS));
//--- new bar
   CisNewBar daily_new_bar;
   daily_new_bar.SetPeriod(PERIOD_D1);
   daily_new_bar.SetLastBarTime(1);
//--- logs path
   string logs_path=::TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Logs\\";
   string mask_path=logs_path+"*.log";
//--- destination folder (if to copy files)
   string new_folder_name=NULL;
   uint file_attributes=0;
   if(::StringLen(InpDstPath)>0)
     {
      new_folder_name=InpDstPath+"\\Logs";
      //--- check whether a folder exists
      file_attributes=kernel32::GetFileAttributesW(new_folder_name);
      bool does_folder_exist=(file_attributes != INVALID_FILE_ATTRIBUTES) &&
                             ((file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0);
      if(!does_folder_exist)
        {
         //--- create a folder
         int create_res=kernel32::CreateDirectoryW(new_folder_name, 0);
         if(create_res<1)
           {
            ::PrintFormat("Failed CreateDirectoryW() with error: %x", kernel32::GetLastError());
            return;
           }
        }
     }
//--- main processing loop
   do
     {
      MqlDateTime sToday;
      ::TimeTradeServer(sToday);
      sToday.hour=sToday.min=sToday.sec=0;
      datetime dtToday=::StructToTime(sToday);
      if(daily_new_bar.isNewBar(dtToday))
        {
         ::PrintFormat("\nToday is: %s", ::TimeToString(dtToday, TIME_DATE));
         string todays_log_file_name=::TimeToString(dtToday, TIME_DATE);
         int replaced=::StringReplace(todays_log_file_name, ".", "");
         if(replaced>0)
           {
            todays_log_file_name+=".log";
            //--- log files
            FIND_DATAW find_file_data;
            ::ZeroMemory(find_file_data);
            HANDLE hFind=kernel32::FindFirstFileW(mask_path, find_file_data);
            if(hFind==INVALID_HANDLE)
              {
               ::PrintFormat("Failed FindFirstFile (hFind) with error: %x", kernel32::GetLastError());
               continue;
              }
            // List all the files in the directory with some info about them
            int result=0;
            uint files_cnt=0;
            do
              {
               string name="";
               for(int i=0; i<MAX_PATH; i++)
                  name+=::ShortToString(find_file_data.cFileName[i]);
               //--- delete any file except today's
               if(::StringCompare(name, todays_log_file_name))
                 {
                  string file_name=logs_path+name;
                  //--- if to copy a file before deletion
                  if(::StringLen(new_folder_name)>0)
                    {                     
                     string new_file_name=new_folder_name+"\\"+name;
                     if(kernel32::CopyFileW(file_name, new_file_name, 0)==0)
                       {
                        ::PrintFormat("Failed CopyFileW() with error: %x", kernel32::GetLastError());
                       }
                     //--- set READONLY attribute
                     file_attributes=kernel32::GetFileAttributesW(new_file_name);
                     if(file_attributes!=INVALID_FILE_ATTRIBUTES)
                        if(!(file_attributes & FILE_ATTRIBUTE_READONLY))
                          {
                           file_attributes=kernel32::SetFileAttributesW(new_file_name, file_attributes|FILE_ATTRIBUTE_READONLY);
                           if(!(file_attributes & FILE_ATTRIBUTE_READONLY))
                              ::PrintFormat("Failed SetFileAttributesW() with error: %x", kernel32::GetLastError());
                          }
                    }
                  int del_ret=kernel32::DeleteFileW(file_name);
                  if(del_ret>0)
                     files_cnt++;
                 }
               //--- next file
               ::ZeroMemory(find_file_data);
               result= kernel32::FindNextFileW(hFind, find_file_data);
              }
            while(result!=0);
            uint kernel32_last_error=kernel32::GetLastError();
            if(kernel32_last_error>0)
               if(kernel32_last_error!=ERROR_NO_MORE_FILES)
                  ::PrintFormat("Failed FindNextFileW (hFind) with error: %x", kernel32_last_error);
            ::PrintFormat("Deleted log files: %I32u", files_cnt);
            int file_close=kernel32::FindClose(hFind);
           }
        }
      ::Sleep(15000);
     }
   while(!::IsStopped());
   now=::TimeLocal();
   ::PrintFormat("Service \"%s\" stops at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS));
  }
//+------------------------------------------------------------------+

Se a variável de entrada especificar o disco para o qual os arquivos serão copiados, criamos uma pasta para armazenar os arquivos de log após verificar a existência desta pasta.

No loop de processamento principal, verificamos se é um novo dia. Então nós buscamos e excluimos os arquivos de log no mesmo loop, pulando os arquivos de hoje. Se nós precisarmos copiar um arquivo, verificamos esta possibilidade e após copiar definimos o atributo "Read only" para o novo arquivo

Definimos uma pausa de 15 segundos no loop. Esta é provavelmente uma frequência relativamente ideal para determinar um novo dia.

Portanto, antes de iniciar o serviço, a pasta %MQL5\Logs ficou assim no Explorer (Fig. 2).

Pasta "%MQL5\Logs" no Explorer antes de remover os arquivos

Fig. 2. Pasta "%MQL5\Logs" no Explorer antes de remover os arquivos


Após iniciar o serviço, as seguintes mensagens aparecerão no log:

2022.12.05 23:26:59.960 dClearTradeLogs Service "dClearTradeLogs" starts at: 2022.12.05 23:26:59
2022.12.05 23:26:59.960 dClearTradeLogs 
2022.12.05 23:26:59.960 dClearTradeLogs Today is: 2022.12.05
2022.12.05 23:26:59.985 dClearTradeLogs Deleted log files: 6

É fácil perceber que o serviço não escreveu nada no log referente ao final de seu trabalho. A razão é que a operação do serviço ainda não foi finalizada. Ele apenas faz um loop e é executado até ser interrompido.

Pasta "%MQL5\Logs" no Explorer após a remoção dos arquivos

Fig. 3. Pasta "%MQL5\Logs" no Explorer após a remoção dos arquivos

Portanto, após excluir os logs, apenas um arquivo permanece na pasta especificada (Fig. 3). Naturalmente, a tarefa de deletar arquivos pode ser melhorada e flexibilizada. Por exemplo, antes de excluir os arquivos, você pode copiá-los para outro disco para não perder as informações necessárias para sempre. Em geral, a implementação já depende dos requisitos específicos do algoritmo. No exemplo atual, os arquivos foram copiados para a pasta G:\Logs (Fig. 4).

Pasta "G:\Logs" no Explorer após a cópia dos arquivos

Fig. 4. Pasta "G:\Logs" no Explorer após a cópia dos arquivos

Isso conclui o trabalho com os logs. No exemplo a seguir, vamos atribuir a tarefa de exibição de gráficos ao serviço.


4.2Gerenciamento dos gráficos

Vamos imaginar que nos deparamos com a seguinte tarefa. O terminal deve apresentar os gráficos dos símbolos atualmente negociados, ou seja, os que apresentam posições em aberto.

As regras para abrir os gráficos são bem simples. Se houver uma posição aberta para um dos símbolos, abra o gráfico desse símbolo. Se não houver posição, não haverá gráfico. Se houver várias posições para um símbolo, apenas um gráfico será aberto.

Além disso, vamos adicionar algumas cores. Se a posição for lucrativa, a cor de fundo do gráfico será em azul claro e, se estiver no vermelho, será rosa claro. O lucro zero usa a cor lavanda.


Então, para realizar esta tarefa, primeiro nós precisamos de um loop no código de serviço, no qual iremos monitorar o estado das posições e gráficos. O loop ficou grande o suficiente. Então, vamos analisar o seu código bloco por bloco.

O ciclo é dividido em dois blocos.

O primeiro bloco está lidando com a situação quando não há posições:

int positions_num=::PositionsTotal();
//--- if there are no positions
if(positions_num<1)
  {
   // close all the charts
   CChart temp_chart_obj;
   temp_chart_obj.FirstChart();
   long temp_ch_id=temp_chart_obj.ChartId();
   for(int ch_idx=0; ch_idx<MAX_CHARTS && temp_ch_id>-1; ch_idx++)
     {
      long ch_id_to_close=temp_ch_id;
      temp_chart_obj.NextChart();
      temp_ch_id=temp_chart_obj.ChartId();
      ::ChartClose(ch_id_to_close);
     }
  }

No bloco, nós passamos pelos gráficos que estão abertos (se houver) e os fechamos. Eu usarei aqui e abaixo a classe Cchart para lidar com as propriedades do gráfico de preços.

O segundo bloco é mais complexo:

//--- if there are some positions
else
   {
   //--- collect unique position symbols
   CHashSet<string> pos_symbols_set;
   for(int pos_idx=0; pos_idx<positions_num; pos_idx++)
      {
      string curr_pos_symbol=::PositionGetSymbol(pos_idx);
      if(!pos_symbols_set.Contains(curr_pos_symbol))
         {
         if(!pos_symbols_set.Add(curr_pos_symbol))
            ::PrintFormat("Failed to add a symbol \"%s\" to the positions set!", curr_pos_symbol);
         }
      }
   string pos_symbols_arr[];
   int unique_pos_symbols_num=pos_symbols_set.Count();
   if(pos_symbols_set.CopyTo(pos_symbols_arr)!=unique_pos_symbols_num)
      continue;
   //--- collect unique chart symbols and close duplicates
   CHashMap<string, long> ch_symbols_map;
   CChart map_chart_obj;
   map_chart_obj.FirstChart();
   long map_ch_id=map_chart_obj.ChartId();
   for(int ch_idx=0; ch_idx<MAX_CHARTS && map_ch_id>-1; ch_idx++)
      {
      string curr_ch_symbol=map_chart_obj.Symbol();
      long ch_id_to_close=0;
      if(!ch_symbols_map.ContainsKey(curr_ch_symbol))
         {
         if(!ch_symbols_map.Add(curr_ch_symbol, map_ch_id))
            ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_ch_symbol);
         }
      else
         {
         //--- if there's a duplicate
         ch_id_to_close=map_chart_obj.ChartId();
         }
      //--- move to the next chart
      map_chart_obj.NextChart();
      map_ch_id=map_chart_obj.ChartId();
      if(ch_id_to_close>0)
         {
         ::ChartClose(ch_id_to_close);
         }
      }
   map_chart_obj.Detach();
   //--- looking for a chart if there's a position
   for(int s_pos_idx=0; s_pos_idx<unique_pos_symbols_num; s_pos_idx++)
      {
      string curr_pos_symbol=pos_symbols_arr[s_pos_idx];
      //--- if there's no chart of the symbol
      if(!ch_symbols_map.ContainsKey(curr_pos_symbol))
         if(::SymbolSelect(curr_pos_symbol, true))
            {
            //--- open a chart of the symbol
            CChart temp_chart_obj;
            long temp_ch_id=temp_chart_obj.Open(curr_pos_symbol, PERIOD_H1);
            if(temp_ch_id<1)
               ::PrintFormat("Failed to open a chart of the symbol \"%s\"!", curr_pos_symbol);
            else
               {
               if(!ch_symbols_map.Add(curr_pos_symbol, temp_ch_id))
                  ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_pos_symbol);
               temp_chart_obj.Detach();
               }
            }
      }
   string ch_symbols_arr[];
   long ch_ids_arr[];
   int unique_ch_symbols_num=ch_symbols_map.Count();
   if(ch_symbols_map.CopyTo(ch_symbols_arr, ch_ids_arr)!=unique_ch_symbols_num)
      continue;
   //--- looking for a position if there's a chart
   for(int s_ch_idx=0; s_ch_idx<unique_ch_symbols_num; s_ch_idx++)
      {
      string curr_ch_symbol=ch_symbols_arr[s_ch_idx];
      long ch_id_to_close=ch_ids_arr[s_ch_idx];
      CChart temp_chart_obj;
      temp_chart_obj.Attach(ch_id_to_close);
      //--- if there's no position of the symbol
      if(!pos_symbols_set.Contains(curr_ch_symbol))
         {
         temp_chart_obj.Close();
         }
      else
         {
         CPositionInfo curr_pos_info;
         //--- calculate  a position profit
         double curr_pos_profit=0.;
         int pos_num=::PositionsTotal();
         for(int pos_idx=0; pos_idx<pos_num; pos_idx++)
            if(curr_pos_info.SelectByIndex(pos_idx))
               {
               string curr_pos_symbol=curr_pos_info.Symbol();
               if(!::StringCompare(curr_ch_symbol, curr_pos_symbol))
                  curr_pos_profit+=curr_pos_info.Profit()+curr_pos_info.Swap();
               }
         //--- apply a color
         color profit_clr=clrLavender;
         if(curr_pos_profit>0.)
            {
            profit_clr=clrLightSkyBlue;
            }
         else if(curr_pos_profit<0.)
            {
            profit_clr=clrLightPink;
            }
         if(!temp_chart_obj.ColorBackground(profit_clr))
            ::PrintFormat("Failed to apply a profit color for the symbol \"%s\"!", curr_ch_symbol);
         temp_chart_obj.Redraw();
         }
      temp_chart_obj.Detach();
      }
   //--- tile windows (Alt+R)
   uchar vk=VK_MENU;
   uchar scan=0;
   uint flags[]= {0, KEYEVENTF_KEYUP};
   ulong extra_info=0;
   uchar Key='R';
   for(int r_idx=0; r_idx<2; r_idx++)
      {
      user32::keybd_event(vk, scan, flags[r_idx], extra_info);
      ::Sleep(10);
      user32::keybd_event(Key, scan, flags[r_idx], extra_info);
      }
   }

Primeiro, coletamos os valores únicos dos símbolos, posições que estão em aberto. Os recursos da classe CHashSet<T> são adequadas para a tarefa. A classe é uma implementação do conjunto de dados dinâmicos não ordenados do tipo T, com a exclusividade necessária de cada valor. Copiamos os valores exclusivos obtidos em uma matriz de strings para ter acesso simplificado a eles posteriormente.

Na próxima etapa, coletamos os símbolos únicos dos gráficos que estão abertos. Fechamos os gráficos duplicados ao longo do caminho, se houver. Suponha que nós temos dois gráficos EURUSD. Isso significa que deixamos um gráfico e fechamos outro. A instância da classe CHashMap<TKey,TValue> é aplicada aqui. A classe é uma implementação de uma tabela hash dinâmica cujos dados são armazenados como pares chave/valor não ordenados, com a exclusividade necessária de uma chave.

Faltam apenas dois loops. Na primeira,movemos ao longo da matriz de símbolos da posição em aberto e verificamos se há um gráfico para isso. Se não, abrimos ele. Na segunda volta,movemos ao longo da matriz de símbolos do gráfico aberto e verificamos se a posição em aberto corresponde a cada símbolo. Suponha que haja um gráfico do USDJPY aberto, mas ele não apresenta nenhuma posição. Então o USDJPY é fechado. No mesmo loop, calculamos o lucro da posição para definir a cor de fundo, conforme determinado no início da tarefa. Para acessar as propriedades da posição e obter os seus valores, a classe CPositionInfo da Biblioteca Padrão foi usada.

Finalmente, vamos adicionar algum apelo visual colocando as janelas do gráfico como um bloco. Para conseguir isso, usamos a WinAPI, simulando o pressionamento da tecla usando a função keybd_event().

É isso aí. Resta apenas lançar o serviço dActivePositionsCharts.


4.3 Símbolo personalizado, cotações

Uma das vantagens do serviço é a possibilidade de trabalhar em plano de fundo sem utilizar a tabela de preços. Como exemplo, nesta seção, eu mostrarei como o serviço pode ser utilizado para criar um símbolo personalizado e o seu histórico de ticks, bem como gerar os novos ticks.

Eu usarei o índice do dólar americano como um símbolo personalizado.

4.3.1 Índice USD, composição

O índice do dólar americano é um índice sintético que reflete o valor do USD em relação a uma cesta de seis outras moedas:

  1. EUR (57.6%);
  2. JPY (13.6%);
  3. GBP (11.9%);
  4. CAD (9.1%);
  5. SEK (4.2%);
  6. CHF (3.6%).

A equação do Índice 0é a média geométrica ponderada das taxas de câmbio do USD em relação a essas moedas com um fator de correção:

USDX = 50.14348112 * EURUSD-0.576 * USDJPY0.136 * GBPUSD-0.119 * USDCAD0.091 * USDSEK0.042 * USDCHF0.036

Com base na equação, suponha que a cotação do par seja elevada a uma potência negativa quando o USD é uma moeda cotada, e a cotação do par é elevada a uma potência positiva quando o USD e´uma moeda base.

A cesta de moedas pode ser apresentada esquematicamente da seguinte forma (Fig. 5).



Cesta de moedas do índice USD (DXY)

Fig. 5. Cesta de moedas do índice USD (DXY)


O índice USD é o ativo subjacente para os futuros negociados no Intercontinental Exchange (ICE). Os futuros do índice são calculados aproximadamente a cada 15 segundos. Os preços para cálculo são obtidos no preço de compra mais alto e no preço de venda mais baixo na profundidade do mercado do par de moedas incluído no índice.


4.3.2 Índice USD, serviço

Temos tudo o que precisamos para os cálculos, então é hora de iniciar o código do serviço. Mas primeiro, eu observarei que o serviço funcionará em etapas. Na primeira etapa, ele formará o histórico de ticks e barras para sintéticos e, na segunda etapa, processará os novos ticks. Obviamente, o primeiro estágio está relacionado ao passado e o segundo - com o presente.

Vamos criar um modelo de programa (serviço) em MQL5 chamado dDxySymbol.mq5.

Definimos as seguintes variáveis de entrada:

input datetime InpStartDay=D'01.10.2022'; // Start date
input int InpDxyDigits=3;                 // Index digits

A primeira define o início do histórico de cotações, que tentaremos obter para criar o nosso símbolo. Em outras palavras, faremos o download do histórico de cotações a partir de 01 de outubro de 2022.

O segundo define a precisão da cotação do símbolo.

Portanto, para começar a trabalhar com o índice, nós precisamos criar um símbolo personalizado - a base para exibir uma cotação sintética. DXY é um nome para o símbolo do índice. O recurso possui muito material sobre os símbolos personalizados. Eu vou abordar a classe CiCustomSymbol definida no artigo Guia Prático do MQL5 – Teste de estresse de uma estratégia de negociação utilizando os símbolos personalizados.

Aqui está o bloco de código onde o trabalho de criação doDXY sintético está em andamento:

//--- create a custom symbol
string custom_symbol="DXY",
       custom_group="Dollar Index";
CiCustomSymbol custom_symbol_obj;
const uint batch_size = 1e6;
const bool is_selected = true;
int code = custom_symbol_obj.Create(custom_symbol, custom_group, NULL, batch_size, is_selected);
::PrintFormat("Custom symbol \"%s\", code: %d", custom_symbol, code);
if(code < 0)
   return;

Se o símbolo DXY não tiver sido criado antes e não estiver na lista de símbolos personalizados da plataforma, o método CiCustomSymbol::Create() retornará o código 1. Se o símbolo DXY já estiver entre os símbolos, obtemos o código 0. Se não for possível criar um símbolo, obteremos um erro - código -1. Em caso de erro ao criar um símbolo personalizado, o serviço vai parar de funcionar.

Depois de criar o sintético, vamos definir várias propriedades para ele.

//--- Integer properties
//--- sector
ENUM_SYMBOL_SECTOR symbol_sector = SECTOR_CURRENCY;
if(!custom_symbol_obj.SetProperty(SYMBOL_SECTOR, symbol_sector))
   {
   ::PrintFormat("Failed to set a sector for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- background color
color symbol_background_clr = clrKhaki;
if(!custom_symbol_obj.SetProperty(SYMBOL_BACKGROUND_COLOR, symbol_background_clr))
   {
   ::PrintFormat("Failed to set a background color for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- chart mode
ENUM_SYMBOL_CHART_MODE symbol_ch_mode=SYMBOL_CHART_MODE_BID;
if(!custom_symbol_obj.SetProperty(SYMBOL_CHART_MODE, symbol_ch_mode))
   {
   ::PrintFormat("Failed to set a chart mode for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- digits
if(!custom_symbol_obj.SetProperty(SYMBOL_DIGITS, InpDxyDigits))
   {
   ::PrintFormat("Failed to set digits for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- trade mode
ENUM_SYMBOL_TRADE_MODE symbol_trade_mode = SYMBOL_TRADE_MODE_DISABLED;
if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_MODE, symbol_trade_mode))
   {
   ::PrintFormat("Failed to disable trade for the custom symbol \"%s\"", custom_symbol);
   return;
   }

As seguintes propriedades são do tipo ENUM_SYMBOL_INFO_INTEGER:

  • SYMBOL_SECTOR,
  • SYMBOL_BACKGROUND_COLOR,
  • SYMBOL_CHART_MODE,
  • SYMBOL_DIGITS,
  • SYMBOL_TRADE_MODE.

A última propriedade é responsável pelo modo de negociação. O sintético não terá permissão para negociar, então a propriedade será definida como[SYMBOL_TRADE_MODE_DISABLED. Caso precisemos verificar alguma estratégia por símbolo no Testador, então a propriedade deve ser habilitada (SYMBOL_TRADE_MODE_FULL).

//--- Double properties
//--- point
double symbol_point = 1./::MathPow(10, InpDxyDigits);
if(!custom_symbol_obj.SetProperty(SYMBOL_POINT, symbol_point))
   {
   ::PrintFormat("Failed to to set a point value for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- tick size
double symbol_tick_size = symbol_point;
if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_TICK_SIZE, symbol_tick_size))
   {
   ::PrintFormat("Failed to to set a tick size for the custom symbol \"%s\"", custom_symbol);
   return;
   }

As seguintes propriedades são do tipo ENUM_SYMBOL_INFO_DOUBLE:

  • SYMBOL_POINT,
  • SYMBOL_TRADE_TICK_SIZE.
Como foi previamente determinado que o símbolo não será negociado, há poucas propriedades do tipo double.

//--- String properties
//--- category
string symbol_category="Currency indices";
if(!custom_symbol_obj.SetProperty(SYMBOL_CATEGORY, symbol_category))
   {
   ::PrintFormat("Failed to to set a category for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- country
string symbol_country= "US";
if(!custom_symbol_obj.SetProperty(SYMBOL_COUNTRY, symbol_country))
   {
   ::PrintFormat("Failed to to set a country for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- description
string symbol_description= "Synthetic US Dollar Index";
if(!custom_symbol_obj.SetProperty(SYMBOL_DESCRIPTION, symbol_description))
   {
   ::PrintFormat("Failed to to set a description for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- exchange
string symbol_exchange= "ICE";
if(!custom_symbol_obj.SetProperty(SYMBOL_EXCHANGE, symbol_exchange))
   {
   ::PrintFormat("Failed to to set an exchange for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- page
string symbol_page = "https://www.ice.com/forex/usdx";
if(!custom_symbol_obj.SetProperty(SYMBOL_PAGE, symbol_page))
   {
   ::PrintFormat("Failed to to set a page for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- path
string symbol_path="Custom\\"+custom_group+"\\"+custom_symbol;
if(!custom_symbol_obj.SetProperty(SYMBOL_PATH, symbol_path))
   {
   ::PrintFormat("Failed to to set a path for the custom symbol \"%s\"", custom_symbol);
   return;
   }

As seguintes propriedades são do tipo ENUM_SYMBOL_INFO_STRING:

  • SYMBOL_CATEGORY,
  • SYMBOL_COUNTRY,
  • SYMBOL_DESCRIPTION,
  • SYMBOL_EXCHANGE,
  • SYMBOL_PAGE,
  • SYMBOL_PATH.

A última propriedade é responsável pelo caminho na árvore de símbolos. Ao criar um sintético, um grupo de caracteres e o nome do símbolo foram especificados. Portanto, esta propriedade pode ser ignorada, pois será idêntica.

Claro, eu poderia definir a equação para o sintético diretamente sem coletar ticks. Mas então o significado do exemplo seria perdido. Além disso, o preço do índice é calculado periodicamente. No exemplo atual, o período de contagem é de 10 segundos.

Agora vamos para o próximo bloco - esta é uma verificação do histórico de negociação. Aqui vamos resolver duas tarefas: verificar o histórico das barras e carregar os ticks. Vamos verificar as barras da seguinte maneira:

//--- check quotes history
CBaseSymbol base_symbols[BASE_SYMBOLS_NUM];
const string symbol_names[]=
  {
   "EURUSD", "USDJPY", "GBPUSD", "USDCAD", "USDSEK", "USDCHF"
  };
ENUM_TIMEFRAMES curr_tf=PERIOD_M1;
::Print("\nChecking of quotes history is running...");
for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
  {
   CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
   string curr_symbol_name=symbol_names[s_idx];
   if(ptr_base_symbol.Init(curr_symbol_name, curr_tf, InpStartDay))
     {
      ::PrintFormat("\n   Symbol #%hu: \"%s\"", s_idx+1, curr_symbol_name);
      ulong start_cnt=::GetTickCount64();
      int check_load_code=ptr_base_symbol.CheckLoadHistory();
      ::PrintFormat("   Checking code: %I32d", check_load_code);
      ulong time_elapsed_ms=::GetTickCount64()-start_cnt;
      ::PrintFormat("   Time elapsed: %0.3f sec", time_elapsed_ms/MS_IN_SEC);
      if(check_load_code<0)
        {
         ::PrintFormat("Failed to load quotes history for the symbol \"%s\"", curr_symbol_name);
         return;
        }
     }
  }

Nós teremos 6 símbolos que precisamos percorrer e manipular as suas cotações. A classe CBaseSymbol foi criada por conveniência.

//+------------------------------------------------------------------+
//| Class CBaseSymbol                                                |
//+------------------------------------------------------------------+
class CBaseSymbol : public CObject
  {
      //--- === Data members === ---
   private:
      CSymbolInfo    m_symbol;
      ENUM_TIMEFRAMES m_tf;
      matrix         m_ticks_mx;
      datetime       m_start_date;
      ulong          m_last_idx;
      //--- === Methods === ---
   public:
      //--- constructor/destructor
      void           CBaseSymbol(void);
      void          ~CBaseSymbol(void) {};
      //---
      bool           Init(const string _symbol, const ENUM_TIMEFRAMES _tf, datetime start_date);
      int            CheckLoadHistory(void);
      bool           LoadTicks(const datetime _stop_date, const uint _flags);
      matrix         GetTicks(void) const
        {
         return m_ticks_mx;
        };
      bool           SearchTickLessOrEqual(const double _dbl_time, vector &_res_row);
      bool           CopyLastTick(vector &_res_row);
  };

A classe trata do histórico de barras e ticks, que é uma tarefa extremamente importante, caso contrário não haverá dados para criar os sintéticos. 

Vamos carregar os ticks:

//--- try to load ticks
::Print("\nLoading of ticks is running...");
now=::TimeCurrent();
uint flags=COPY_TICKS_INFO | COPY_TICKS_TIME_MS | COPY_TICKS_BID | COPY_TICKS_ASK;
double first_tick_dbl_time=0.;
for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
   {
   CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
   string curr_symbol_name=symbol_names[s_idx];
   ::PrintFormat("\n   Symbol #%hu: \"%s\"", s_idx+1, curr_symbol_name);
   ulong start_cnt=::GetTickCount64();
   ::ResetLastError();
   if(!ptr_base_symbol.LoadTicks(now, flags))
      {
      ::PrintFormat("Failed to load ticks for the symbol \"%s\" , error: %d", curr_symbol_name, ::GetLastError());
      return;
      }
   ulong time_elapsed_ms=::GetTickCount64()-start_cnt;
   ::PrintFormat("   Time elapsed: %0.3f sec", time_elapsed_ms/MS_IN_SEC);
   //--- looking for the 1st tick
   matrix ticks_mx=ptr_base_symbol.GetTicks();
   double tick_dbl_time=ticks_mx[0][0];
   if(tick_dbl_time>first_tick_dbl_time)
      first_tick_dbl_time=tick_dbl_time;
   }

A função nativa matrix::CopyTicksRange() foi usada para carregar os ticks. A conveniência é que você só pode carregar essas colunas na estrutura de ticks definida por flags. A questão da economia de recursos é extremamente relevante quando solicitamos milhões de ticks.

COPY_TICKS_INFO    = 1,       // ticks caused by Bid and/or Ask changes
COPY_TICKS_TRADE   = 2,       // ticks caused by Last and Volume changes
COPY_TICKS_ALL     = 3,       // all ticks that have changes
COPY_TICKS_TIME_MS = 1<<8,    // time in milliseconds
COPY_TICKS_BID     = 1<<9,    // Bid price
COPY_TICKS_ASK     = 1<<10,   // Ask price
COPY_TICKS_LAST    = 1<<11,   // Last price
COPY_TICKS_VOLUME  = 1<<12,   // volume
COPY_TICKS_FLAGS   = 1<<13,   // tick flags

As etapas de verificação do histórico e carregamento de ticks no log serão descritas em termos de custos de tempo.

CS      0       12:01:11.802    dDxySymbol      Checking of quotes history is running...
CS      0       12:01:11.802    dDxySymbol      
CS      0       12:01:11.802    dDxySymbol         Symbol #1: "EURUSD"
CS      0       12:01:14.476    dDxySymbol         Checking code: 1
CS      0       12:01:14.476    dDxySymbol         Time elapsed: 2.688 sec
CS      0       12:01:14.476    dDxySymbol      
CS      0       12:01:14.476    dDxySymbol         Symbol #2: "USDJPY"
CS      0       12:01:17.148    dDxySymbol         Checking code: 1
CS      0       12:01:17.148    dDxySymbol         Time elapsed: 2.672 sec
CS      0       12:01:17.148    dDxySymbol      
CS      0       12:01:17.148    dDxySymbol         Symbol #3: "GBPUSD"
CS      0       12:01:19.068    dDxySymbol         Checking code: 1
CS      0       12:01:19.068    dDxySymbol         Time elapsed: 1.922 sec
CS      0       12:01:19.068    dDxySymbol      
CS      0       12:01:19.068    dDxySymbol         Symbol #4: "USDCAD"
CS      0       12:01:21.209    dDxySymbol         Checking code: 1
CS      0       12:01:21.209    dDxySymbol         Time elapsed: 2.140 sec
CS      0       12:01:21.209    dDxySymbol      
CS      0       12:01:21.209    dDxySymbol         Symbol #5: "USDSEK"
CS      0       12:01:22.631    dDxySymbol         Checking code: 1
CS      0       12:01:22.631    dDxySymbol         Time elapsed: 1.422 sec
CS      0       12:01:22.631    dDxySymbol      
CS      0       12:01:22.631    dDxySymbol         Symbol #6: "USDCHF"
CS      0       12:01:24.162    dDxySymbol         Checking code: 1
CS      0       12:01:24.162    dDxySymbol         Time elapsed: 1.531 sec
CS      0       12:01:24.162    dDxySymbol      
CS      0       12:01:24.162    dDxySymbol      Loading of ticks is running...
CS      0       12:01:24.162    dDxySymbol      
CS      0       12:01:24.162    dDxySymbol         Symbol #1: "EURUSD"
CS      0       12:02:27.204    dDxySymbol         Time elapsed: 63.032 sec
CS      0       12:02:27.492    dDxySymbol      
CS      0       12:02:27.492    dDxySymbol         Symbol #2: "USDJPY"
CS      0       12:02:32.587    dDxySymbol         Time elapsed: 5.094 sec
CS      0       12:02:32.938    dDxySymbol      
CS      0       12:02:32.938    dDxySymbol         Symbol #3: "GBPUSD"
CS      0       12:02:37.675    dDxySymbol         Time elapsed: 4.734 sec
CS      0       12:02:38.285    dDxySymbol      
CS      0       12:02:38.285    dDxySymbol         Symbol #4: "USDCAD"
CS      0       12:02:43.223    dDxySymbol         Time elapsed: 4.937 sec
CS      0       12:02:43.624    dDxySymbol      
CS      0       12:02:43.624    dDxySymbol         Symbol #5: "USDSEK"
CS      0       12:03:18.484    dDxySymbol         Time elapsed: 34.860 sec
CS      0       12:03:19.596    dDxySymbol      
CS      0       12:03:19.596    dDxySymbol         Symbol #6: "USDCHF"
CS      0       12:03:24.317    dDxySymbol         Time elapsed: 4.719 sec

Após receber os ticks, formamos o histórico de ticks para o DXY sintético. Este processo ocorrerá no seguinte bloco:

//--- create a custom symbol ticks history
::Print("\nCustom symbol ticks history is being formed...");
long first_tick_time_sec=(long)(first_tick_dbl_time/MS_IN_SEC);
long first_tick_time_ms=(long)first_tick_dbl_time%(long)MS_IN_SEC;
::PrintFormat("   First tick time: %s.%d", ::TimeToString((datetime)first_tick_time_sec,
              TIME_DATE|TIME_SECONDS), first_tick_time_ms);
double active_tick_dbl_time=first_tick_dbl_time;
double now_dbl_time=MS_IN_SEC*now;
uint ticks_cnt=0;
uint arr_size=0.5e8;
MqlTick ticks_arr[];
::ArrayResize(ticks_arr, arr_size);
::ZeroMemory(ticks_arr);
matrix base_prices_mx=matrix::Zeros(BASE_SYMBOLS_NUM, 2);
do
   {
   //--- collect base symbols ticks
   bool all_ticks_ok=true;
   for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
      {
      CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
      vector tick_prices_vc;
      bool to_break_loop=false;
      if(!ptr_base_symbol.SearchTickLessOrEqual(active_tick_dbl_time, tick_prices_vc))
         to_break_loop=true;
      else
         {
         if(!base_prices_mx.Row(tick_prices_vc, s_idx))
            to_break_loop=true;
         }
      if(to_break_loop)
         {
         all_ticks_ok=false;
         break;
         }
      }
   //--- calculate index prices
   if(all_ticks_ok)
      {
      MqlTick last_ind_tick;
      CalcIndexPrices(active_tick_dbl_time, base_prices_mx, last_ind_tick);
      arr_size=ticks_arr.Size();
      if(ticks_cnt>=arr_size)
         {
         uint new_size=(uint)(arr_size+0.1*arr_size);
         if(::ArrayResize(ticks_arr, new_size)!=new_size)
            continue;
         }
      ticks_arr[ticks_cnt]=last_ind_tick;
      ticks_cnt++;
      }
   active_tick_dbl_time+=TICK_PAUSE;
   }
while(active_tick_dbl_time<now_dbl_time);
::ArrayResize(ticks_arr, ticks_cnt);
int ticks_replaced=custom_symbol_obj.TicksReplace(ticks_arr, true);

Definimos um ponto temporário (active_tick_dbl_time) e adicionamos 10 segundos para ele no final do loop. Este é um tipo de 'timestamp' para obter os ticks para todos os símbolos que compõem o Índice.

Assim, a busca pelo tick desejado em cada símbolo é baseada em um ponto específico no passado. O método CBaseSymbol::SearchTickLessOrEqual() fornece um tick que chegou antes que o valor active_tick_dbl_time.

Quando os ticks de cada componente do Índice são recebidos, os preços dos ticks já estão na matriz base_prices_mx

A função CalcIndexPrices() retorna o valor já preparado do tick do índice no momento. 

Quando os ticks são criados, o banco de dados de ticks é atualizado usando o método CiCustomSymbol::TicksReplace().

O serviço então lida apenas com o presente no próximo bloco:

//--- main processing loop
::Print("\nA new tick processing is active...");
do
   {
   ::ZeroMemory(base_prices_mx);
   //--- collect base symbols ticks
   bool all_ticks_ok=true;
   for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
      {
      CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
      vector tick_prices_vc;
      bool to_break_loop=false;
      if(!ptr_base_symbol.CopyLastTick(tick_prices_vc))
         to_break_loop=true;
      else
         {
         if(!base_prices_mx.Row(tick_prices_vc, s_idx))
            to_break_loop=true;
         }
      if(to_break_loop)
         {
         all_ticks_ok=false;
         break;
         }
      }
   //--- calculate index prices
   if(all_ticks_ok)
      {
      MqlTick last_ind_tick, ticks_to_add[1];
      now=::TimeCurrent();
      now_dbl_time=MS_IN_SEC*now;
      CalcIndexPrices(now_dbl_time, base_prices_mx, last_ind_tick);
      ticks_to_add[0]=last_ind_tick;
      int ticks_added=custom_symbol_obj.TicksAdd(ticks_to_add, true);
      }
   ::Sleep(TICK_PAUSE);
   }
while(!::IsStopped());

A tarefa do bloco é semelhante à do bloco anterior, embora um pouco mais fácil. A cada 10 segundos, nós precisamos obter os dados de ticks nos símbolos e calcular os preços do índice. No Índice, o preço de bid é calculado com base nos bids de todos os símbolos, e o preço de ask é calculado com base em todos os asks, respectivamente.

Depois de lançar o serviço dDxySymbol, o gráfico do símbolo personalizado DXY pode ser aberto após algum tempo (Fig. 6). 

Fig. 6. Gráfico do símbolo personalizado DXY com feriados

Fig. 6. Gráfico do símbolo personalizado DXY com feriados


No gráfico, os sábados são destacados com segmentos verticais vermelhos. Acontece que aos sábados e domingos o serviço continua a calcular os ticks no histórico, o que provavelmente não está totalmente correto. É necessário complementar o código de serviço com um limite de tempo (dias da semana). Vamos atribuir esta tarefa à função CheckDayOfWeek().

Agora o gráfico sintético se parece com isso (Fig. 7). Parece que o bug foi corrigido.

Gráfico de símbolos personalizados DXY sem feriados

Fig. 7. Gráfico de símbolos personalizados DXY sem feriados

Isso completa o trabalho com o serviço dDxySymbol.


Conclusão

O artigo destacou alguns recursos de um tipo de programa em MQL5 conhecido como serviço. Este tipo de programa dm MQL5 difere por não ter um gráfico de vinculação, mas funciona de forma independente. A natureza dos serviços é tal que eles podem entrar em conflito com outros EAs, scripts e provavelmente, em menor grau, com os indicadores. Portanto, a tarefa de definir os direitos e obrigações dos programas de serviço no ambiente MetaTrader 5 recai sobre o desenvolvedor.

O arquivo contém fontes que podem ser colocadas na pasta %MQL5\Services.

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/11826

Arquivos anexados |
code.zip (23.7 KB)
DoEasy. Controles (Parte 27): Continuamos a trabalhar no objeto WinForms "ProgressBar" DoEasy. Controles (Parte 27): Continuamos a trabalhar no objeto WinForms "ProgressBar"
Neste artigo, continuaremos desenvolvendo o controle ProgressBar. Criaremos a funcionalidade para gerenciar a barra de progresso e os efeitos visuais.
Redes neurais de maneira fácil (Parte 33): regressão quantílica em aprendizado Q distribuído, Redes neurais de maneira fácil (Parte 33): regressão quantílica em aprendizado Q distribuído,
Continuamos a estudar o aprendizado Q distribuído e hoje veremos essa abordagem de outro ponto de vista. Falaremos sobre a possibilidade de usar regressão quantílica para resolver o problema de previsão de movimentos de preços.
Desenvolvendo um sistema de Replay — Simulação de mercado (Parte 01): Primeiros experimentos (I) Desenvolvendo um sistema de Replay — Simulação de mercado (Parte 01): Primeiros experimentos (I)
Que tal criar um sistema para estudar o mercado quando ele está fechado, ou mesmo simular situações de mercado. Aqui vamos iniciar uma nova sequencia de artigos, a fim de tratar deste tema.
Integrando modelos de ML ao Testador de estratégias  (Parte 3): Gerenciamento de Arquivos CSV(II) Integrando modelos de ML ao Testador de estratégias (Parte 3): Gerenciamento de Arquivos CSV(II)
Este artigo fornece uma visão detalhada sobre como construir uma classe em MQL5 para gerenciamento eficiente de arquivos CSV. Ele explica como os métodos de abertura, escrita, leitura e conversão de dados são implementados e como eles podem ser utilizados para armazenar e carregar dados. Além disso, o artigo também discute as limitações e considerações importantes ao usar essa classe. É uma leitura valiosa para aqueles interessados em aprender a trabalhar com arquivos CSV em MQL5.