![Guia Prático MQL5 — Serviços](https://c.mql5.com/2/50/Recipes_Services_600x314.jpg)
Guia Prático MQL5 — Serviços
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:
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:
- ExpertRemove(),
- EventSetMillisecondTimer(),
- EventSetTimer(),
- EventKillTimer(),
- SetIndexBuffer(),
- IndicatorSetDouble(),
- IndicatorSetInteger(),
- IndicatorSetString(),
- PlotIndexSetDouble(),
- PlotIndexSetInteger(),
- PlotIndexSetString(),
- PlotIndexGetInteger().
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).
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:
- FindFirstFileW(),
- FindNextFileW(),
- CopyFileW(),
- GetFileAttributesW(),
- SetFileAttributesW(),
- DeleteFileW(),
- FindClose(),
- 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).
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.
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).
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:
- EUR (57.6%);
- JPY (13.6%);
- GBP (11.9%);
- CAD (9.1%);
- SEK (4.2%);
- 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).
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.
//--- 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
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.
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
![DoEasy. Controles (Parte 27): Continuamos a trabalhar no objeto WinForms "ProgressBar"](https://c.mql5.com/2/50/MQL5-avatar-doeasy-library-2__4.png)
![Redes neurais de maneira fácil (Parte 33): regressão quantílica em aprendizado Q distribuído,](https://c.mql5.com/2/50/Neural_Networks_Made_Easy_q-learning_avatar.png)
![Desenvolvendo um sistema de Replay — Simulação de mercado (Parte 01): Primeiros experimentos (I)](https://c.mql5.com/2/52/replay-p1-avatar.png)
![Integrando modelos de ML ao Testador de estratégias (Parte 3): Gerenciamento de Arquivos CSV(II)](https://c.mql5.com/2/51/Avatar_Perceptron_Multicamadas_e_o-Algoritmo_Backpropagation_Parte_3_02.png)
![MQL5 - Linguagem para estratégias de negociação inseridas no terminal do cliente MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso