Uma solução livre de DLL para comunicação entre os terminais MetaTrader utilizando pipes nomeados
Introdução
Imaginei por um tempo formas possíveis de comunicação entre os terminais MetaTrader 5. Meu objetivo foi utilizar um indicador de pontos e exibir pontos de diferentes fornecedores de cotações em um dos terminais.
A solução natural foi separar os arquivos em um disco rígido. Um terminal poderia escrever dados para o arquivo e o outro poderia ler. Esse método apesar de relevante para enviar mensagens únicas não parece ser o mais eficiente para transmissão de cotações.
Então eu me deparei com um bom artigo por Alexander sobre como exportar cotações para aplicações .NET utilizando serviços WCF e quando eu estava prestes a terminar apareceu outro artigo por Sergeev.
Ambos os artigos estavam próximos do que eu precisava mas eu procurava por uma solução sem DLL que poderia ser utilizada por diferentes terminais, um servindo como um servidor e outro servindo como um cliente. Enquanto procurando na internet encontrei uma observação sugerindo que alguém poderia utilizar pipes nomeados para comunicação e li cuidadosamente a especificação MSDN para a comunicação interprocessos utilizando pipes.
Descobri que pipes nomeados suportam comunicação sobre o mesmo computador ou sobre diferentes computadores sobre a intranet, decidi seguir essa abordagem.
Esse artigo introduz comunicação por pipes nomeados e descreve um processo de projeto da classe CNamedPipes. Ele também inclui o streaming do indicador do tick de teste entre os terminais do MetaTrader 5 e rendimento do sistema no geral.
1. Interprocesso de comunicação utilizando pipes nomeados
Quando pensamos em um pipe típico imaginamos um tipo de cilindro que é utilizado para transportar mídia. Esse também é um termo utilizado para um dos meios de comunicação por interprocessos em um sistema operacional. Você poderia simplesmente imaginar um pipe que conecta dois processos, em nosso caso os terminais MetaTrader 5 que trocam dados.
Pipes podem ser anônimos ou nomeados. Há duas diferenças principais entre eles: primeira é que os pipes anônimos não podem ser utilizados sobre uma rede e o segundo que dois processos devem ser relacionados. Isto é, um processo deve ser um processo pai e o outro o filho. Pipes nomeados não possuem essa limitação.
De forma a se comunicar utilizando pipes um processo de servidor deve configurar um pipe com um nome conhecido. O nome pipe é uma string e deve ser na forma de \\servername\pipe\pipename. Se pipes forem usados no mesmo computador, o nome do servidor pode ser omitido e o ponto pode ser colocado em seu lugar: \\.\pipe\pipename.
O cliente que tentar se conectar a um pipe deve saber o nome dele. Eu estou utilizando uma convenção de nome de \\.\pipe\mt[número_conta] de forma a distinguir terminais, mas a convenção de nomes pode ser modificada arbitrariamente.
2. Implementando a classe CNamedPipes
Começarei com uma curta descrição de mecanismos de baixo nível de criar e conectar a um pipe nomeado. Nos sistemas operacionais Windows todas as funções que lidam com pipes estão disponíveis através da biblioteca kernel32.dll. A função iniciando um pipe nomeado no lado do servidor é CreateNamedPipe().
Após o pipe ser criado, o servidor chama a função ConnectNamedPipe() para esperar que um cliente se conecte. Se a conexão é bem sucedida, ConnectNamedPipe() retorna um inteiro diferente de zero. É possível, no entanto, que o cliente tenha se conectado com sucesso após chamar CreateNamedPipe() e antes que ConnectNamedPipe() fosse chamada. Nesse caso ConnectNamedPipe() retorna a zero, e GetLastError() retorna com o erro 535 (0X217) : ERROR_PIPE_CONNECTED.
É possível ler e escrever a partir de um pipe com as mesmas funções que para acesso de arquivo:
BOOL WINAPI ReadFile( __in HANDLE hFile, __out LPVOID lpBuffer, __in DWORD nNumberOfBytesToRead, __out_opt LPDWORD lpNumberOfBytesRead, __inout_opt LPOVERLAPPED lpOverlapped );
BOOL WINAPI WriteFile( __in HANDLE hFile, __in LPCVOID lpBuffer, __in DWORD nNumberOfBytesToWrite, __out_opt LPDWORD lpNumberOfBytesWritten, __inout_opt LPOVERLAPPED lpOverlapped );
Tendo aprendido sobre pipes nomeados, projetei a classe CNamedPipes de forma a esconder as instruções de baixo nível subjacentes.
Agora é o suficiente colocar o arquivo CNamedPipes.mqh na pasta apropriada (/include) do terminal e incluir ela no código fonte e declarar um objeto CNamedPipe.
A classe que projetei expõe alguns métodos básicos para lidar com pipes nomeados.
Create(), Connect(), Disconnect(), Open(), Close(), WriteUnicode(), ReadUnicode(), WriteANSI(), ReadANSI(), WriteTick(), ReadTick()
A classe pode ser estendida adicionalmente de acordo com exigências adicionais.
O método Create() tenta criar um pipe com um nome dado. Para simplificar a conexão entre os terminais, a 'conta' do parâmetro de entrada é o número da conta de um cliente que usará um pipe.
Se um nome de conta não é inserido o método tenta abrir um pipe com uma número de conta do terminal atual. A função Create() retorna verdadeira se o pipe for criado com sucesso.
//+------------------------------------------------------------------+ /// Create() : try to create a new instance of Named Pipe /// \param account - source terminal account number /// \return true - if created, false otherwise | //+------------------------------------------------------------------+ bool CNamedPipe::Create(int account=0) { if(account==0) pipeNumber=IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)); else pipeNumber=IntegerToString(account); string fullPipeName=pipeNamePrefix+pipeNumber; hPipe=CreateNamedPipeW(fullPipeName, (int)GENERIC_READ|GENERIC_WRITE|(ENUM_PIPE_ACCESS)PIPE_ACCESS_DUPLEX, (ENUM_PIPE_MODE)PIPE_TYPE_RW_BYTE,PIPE_UNLIMITED_INSTANCES, BufferSize*sizeof(ushort),BufferSize*sizeof(ushort),0,NULL); if(hPipe==INVALID_HANDLE_VALUE) return false; else return true; }
O método Connect() espera um cliente se conectar ao pipe. Ele retorna verdadeiro se o cliente se conectou com sucesso a um pipe.
//+------------------------------------------------------------------+ /// Connect() : wait for a client to connect to a pipe /// \return true - if connected, false otherwise. //+------------------------------------------------------------------+ bool CNamedPipe::Connect(void) { if(ConnectNamedPipe(hPipe,NULL)==false) return(kernel32::GetLastError()==ERROR_PIPE_CONNECTED); else return true; }
O método Disconnect() desconecta o servidor de um pipe.
//+------------------------------------------------------------------+ /// Disconnect(): disconnect from a pipe /// \return true - if disconnected, false otherwise //+------------------------------------------------------------------+ bool CNamedPipe::Disconnect(void) { return DisconnectNamedPipe(hPipe); }
O método Open() deve ser utilizado por um cliente, ele tenta abrir um pipe criado anteriormente. Ele retorna verdadeiro se a abertura do pipe foi bem sucedida. Ele retorna falso se por alguma razão ele não pôde se conectar ao pipe criado dentro de um tempo de 5 segundos ou se a abertura do pipe falhou.
//+------------------------------------------------------------------+ /// Open() : try to open previously created pipe /// \param account - source terminal account number /// \return true - if successfull, false otherwise. //+------------------------------------------------------------------+ bool CNamedPipe::Open(int account=0) { if(account==0) pipeName=IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)); else pipeName=IntegerToString(account); string fullPipeName=pipeNamePrefix+pipeName; if(hPipe==INVALID_HANDLE_VALUE) { if(WaitNamedPipeW(fullPipeName,5000)==0) { Print("Pipe "+fullPipeName+" not available..."); return false; } hPipe=CreateFileW(fullPipeName,GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL); if(hPipe==INVALID_HANDLE_VALUE) { Print("Pipe open failed"); return false; } } return true; }
O método Close() fecha o pipe nomeado.
//+------------------------------------------------------------------+ /// Close() : close pipe handle /// \return 0 if successfull, non-zero otherwise //+------------------------------------------------------------------+ int CNamedPipe::Close(void) { return CloseHandle(hPipe); }
Os próximos seis métodos são utilizados para ler e escrever através dos pipes. Os primeiros dois pares lidam com cadeias (strings) nos formatos Unicode e ANSI, ambos podem ser utilizados para enviar comandos ou mensagens entre os terminais.
A variável da string no MQL5 é armazenada como um objeto que contém Unicode, então, o modo natural foi fornecer métodos Unicode, mas já que o MQL5 fornece métodos UnicodeToANSI, também implementei a comunicação de string ANSI. Os dois últimos métodos lidam com enviar e receber o objeto MqlTick através de um pipe nomeado.
O método WriteUnicode() escreve a mensagem consistindo em caracteres Unicode. Uma vez que todo caractere consiste de dois bytes, ele envia um arranjo de ushort para um pipe.
//+------------------------------------------------------------------+ /// WriteUnicode() : write Unicode string to a pipe /// \param message - string to send /// \return number of bytes written to a pipe //+------------------------------------------------------------------+ int CNamedPipe::WriteUnicode(string message) { int ushortsToWrite, bytesWritten; ushort UNICODEarray[]; ushortsToWrite = StringToShortArray(message, UNICODEarray); WriteFile(hPipe,ushortsToWrite,sizeof(int),bytesWritten,0); WriteFile(hPipe,UNICODEarray,ushortsToWrite*sizeof(ushort),bytesWritten,0); return bytesWritten; }
O método ReadUnicode() recebe array de ushorts e retorna um objeto string.
//+------------------------------------------------------------------+ /// ReadUnicode(): read unicode string from a pipe /// \return unicode string (MQL5 string) //+------------------------------------------------------------------+ string CNamedPipe::ReadUnicode(void) { string ret; ushort UNICODEarray[STR_SIZE*sizeof(uint)]; int bytesRead, ushortsToRead; ReadFile(hPipe,ushortsToRead,sizeof(int),bytesRead,0); ReadFile(hPipe,UNICODEarray,ushortsToRead*sizeof(ushort),bytesRead,0); if(bytesRead!=0) ret = ShortArrayToString(UNICODEarray); return ret; }
O método WriteANSI() escreve arranjo uchar ANSI em um pipe.
//+------------------------------------------------------------------+ /// WriteANSI() : write ANSI string to a pipe /// \param message - string to send /// \return number of bytes written to a pipe | //+------------------------------------------------------------------+ int CNamedPipe::WriteANSI(string message) { int bytesToWrite, bytesWritten; uchar ANSIarray[]; bytesToWrite = StringToCharArray(message, ANSIarray); WriteFile(hPipe,bytesToWrite,sizeof(int),bytesWritten,0); WriteFile(hPipe,ANSIarray,bytesToWrite,bytesWritten,0); return bytesWritten; }
O método ReadANSI() lê o arranjo uchar de um pipe e retorna um objeto em cadeia.
//+------------------------------------------------------------------+ /// ReadANSI(): read ANSI string from a pipe /// \return unicode string (MQL5 string) //+------------------------------------------------------------------+ string CNamedPipe::ReadANSI(void) { string ret; uchar ANSIarray[STR_SIZE]; int bytesRead, bytesToRead; ReadFile(hPipe,bytesToRead,sizeof(int),bytesRead,0); ReadFile(hPipe,ANSIarray,bytesToRead,bytesRead,0); if(bytesRead!=0) ret = CharArrayToString(ANSIarray); return ret; }
O método WriteTick() escreve um único objeto MqlTick para um pipe.
//+------------------------------------------------------------------+ /// WriteTick() : write MqlTick to a pipe /// \param MqlTick to send /// \return true if tick was written correctly, false otherwise //+------------------------------------------------------------------+ int CNamedPipe::WriteTick(MqlTick &outgoing) { int bytesWritten; WriteFile(hPipe,outgoing,MQLTICK_SIZE,bytesWritten,0); return bytesWritten; }
O método ReadTick() lê um único MqlTick objeto de um pipe. Se um pipe está vazio ele retorna 0, se não ele retorna o número de bytes de um objeto MqlTick.
//+------------------------------------------------------------------+ /// ReadTick() : read MqlTick from a pipe /// \return true if tick was read correctly, false otherwise //+------------------------------------------------------------------+ int CNamedPipe::ReadTick(MqlTick &incoming) { int bytesRead; ReadFile(hPipe,incoming,MQLTICK_SIZE,bytesRead,NULL); return bytesRead; } //+------------------------------------------------------------------+
Uma vez que os métodos básicos para se lidar com pipes nomeados são conhecidos podemos começar com dois programas MQL: script simples para recebimento de cotações e um indicador para enviar cotações.
3. Scripts do servidor para receber cotações
O servidor do exemplo inicia um pipe nomeado e espera para um cliente se conectar. Após o cliente desconectar ele mostra quantos pontos foram recebidos por aquele cliente no total e espera um novo cliente se conectar. Se um cliente desconectou e o servidor encontra uma variedade global 'gvar0' ele sai. Se a variável 'gvar0' não existir, uma pessoa pode parar manualmente o servidor clicando com o botão direito em um gráfico e selecionando a opção Expert List.
//+------------------------------------------------------------------+ //| NamedPipeServer.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #include CNamedPipe pipe; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { bool tickReceived; int i=0; if(pipe.Create()==true) while (GlobalVariableCheck("gvar0")==false) { Print("Waiting for client to connect."); if (pipe.Connect()==true) Print("Pipe connected"); while(true) { do { tickReceived=pipe.ReadTick(); if(tickReceived==false) { if(GetError()==ERROR_BROKEN_PIPE) { Print("Client disconnected from pipe "+pipe.Name()); pipe.Disconnect(); break; } } else i++; Print(IntegerToString(i) + "ticks received."); } while(tickReceived==true); if (i>0) { Print(IntegerToString(i) + "ticks received."); i=0; }; if(GlobalVariableCheck("gvar0")==true || (GetError()==ERROR_BROKEN_PIPE)) break; } } pipe.Close(); }
4. Indicador simples para enviar cotações
O indicador para enviar cotações abre um pipe dentro do método OnInit() e envia um único MqlTick cada vez que o método OnCalculate() é acionado://+------------------------------------------------------------------+ //| SendTickPipeIndicator.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #property indicator_chart_window #include CNamedPipe pipe; int ctx; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { while (!pipe.Open(AccountInfoInteger(ACCOUNT_LOGIN))) { Print("Pipe not created, retrying in 5 seconds..."); if (GlobalVariableCheck("gvar1")==true) break; } ctx = 0; return(0); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], const double& high[], const double& low[], const double& close[], const long& tick_volume[], const long& volume[], const int& spread[]) { ctx++; MqlTick outgoing; SymbolInfoTick(Symbol(), outgoing); pipe.WriteTick(outgoing); Print(IntegerToString(ctx)+" tick send to server by SendTickPipeClick."); return(rates_total); } //+------------------------------------------------------------------+
5. Executando indicadores de pontos para provedores múltiplos em um único terminal de cliente
A situação ficou mais complicada conforme eu quis exibir cotações que chegavam em indicadores de pontos separados. Alcancei isso implementando um servidor de pipe que transmite pontos de entrada a um indicador de pontos acionando o método EventChartCustom().
Cotações de compra ou venda são enviadas como uma única cadeia dividida por um ponto e vírgula por exemplo '1.20223;120225'. O indicador apropriado lida com um evento personalizado dentro de OnChartEvent() e exibe um gráfico de pontos.
//+------------------------------------------------------------------+ //| NamedPipeServerBroadcaster.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #property script_show_inputs #include input int account = 0; CNamedPipe pipe; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { bool tickReceived; int i=0; if(pipe.Create(account)==true) while(GlobalVariableCheck("gvar0")==false) { if(pipe.Connect()==true) Print("Pipe connected"); i=0; while(true) { do { tickReceived=pipe.ReadTick(); if(tickReceived==false) { if(kernel32::GetLastError()==ERROR_BROKEN_PIPE) { Print("Client disconnected from pipe "+pipe.GetPipeName()); pipe.Disconnect(); break; } } else { i++; Print(IntegerToString(i)+" ticks received BY server."); string bidask=DoubleToString(pipe.incoming.bid)+";"+DoubleToString(pipe.incoming.ask); long currChart=ChartFirst(); int chart=0; while(chart<100) { EventChartCustom(currChart,6666,0,(double)account,bidask); currChart=ChartNext(currChart); if(currChart==0) break; // Reached the end of the charts list chart++; } if(GlobalVariableCheck("gvar0")==true || (kernel32::GetLastError()==ERROR_BROKEN_PIPE)) break; } } while(tickReceived==true); if(i>0) { Print(IntegerToString(i)+"ticks received."); i=0; }; if(GlobalVariableCheck("gvar0")==true || (kernel32::GetLastError()==ERROR_BROKEN_PIPE)) break; Sleep(100); } } pipe.Close(); }
Para exibir os ticks que eu escolhi no indicador de ticks colocados em MQLmagazine, mas, ao invés de OnCalculate() implementei um método de processamento dentro do OnChartEvent() e adicionei instruções condicionais. Uma cotação é aceita para processar apenas se o parâmetro dparam for igual ao número do pipe e a identificação do evento for igual a CHARTEVENT_CUSTOM+6666:
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (dparam==(double)incomingPipe) if(id>CHARTEVENT_CUSTOM) { if(id==CHARTEVENT_CUSTOM+6666) { // Process incoming tick } } else { // Handle the user event } }
Na captura de tela abaixo há três indicadores de pontos.
Dois deles exibem pontos recebidos através de pipes e um terceiro indicador que não utiliza pipes foi executado para verificar se nenhum ponto havia sido perdido.
Fig. 1 Cotas recebidas através de um pipe nomeado
Por favor, observe anexo um vídeo com comentários sobre como eu executo os indicadores:
Fig. 2 Vídeo descrevendo a configuração de indicadores
6. Testando a vazão do sistema
Uma vez que pipes utilizam memória compartilhada a comunicação é muito rápida. Conduzi testes de enviar 100 000 e 1 000 000 pontos consecutivos entre dois terminais do MetaTrader 5. O script de envio utiliza a função WriteTick() e mede o intervalo de tempo utilizando GetTickCount():
Print("Sending..."); uint start = GetTickCount(); for (int i=0;i<100000;i++) pipe.WriteTick(outgoing); uint stop = GetTickCount(); Print("Sending took" + IntegerToString(stop-start) + " [ms]"); pipe.Close();
O servidor lê as cotações de entrada. O período de tempo é medido da primeira cotação de chegada até que o cliente de desconecte:
//+------------------------------------------------------------------+ //| SpeedTestPipeServer.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #property script_show_inputs #include input int account=0; bool tickReceived; uint start,stop; CNamedPipe pipe; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { int i=0; if(pipe.Create(account)==true) if(pipe.Connect()==true) Print("Pipe connected"); do { tickReceived=pipe.ReadTick(); if(i==0) start=GetTickCount(); if(tickReceived==false) { if(kernel32::GetLastError()==ERROR_BROKEN_PIPE) { Print("Client disconnected from pipe "+pipe.GetPipeName()); pipe.Disconnect(); break; } } else i++; } while(tickReceived==true); stop=GetTickCount(); if(i>0) { Print(IntegerToString(i)+" ticks received."); i=0; }; pipe.Close(); Print("Server: receiving took "+IntegerToString(stop-start)+" [ms]"); } //+------------------------------------------------------------------+
Os resultados para 10 execuções de amostra foram como segue:
Executar | Cotações | Tempo de envio [ms] | Tempo de recebimento [ms] |
---|---|---|---|
1 | 100000 | 624 | 624 |
2 | 100000 | 702 | 702 |
3 | 100000 | 687 | 687 |
4 | 100000 | 592 | 608 |
5 | 100000 | 624 | 624 |
6 | 1000000 | 5616 | 5616 |
7 | 1000000 | 5788 | 5788 |
8 | 1000000 | 5928 | 5913 |
9 | 1000000 | 5772 | 5756 |
10 | 1000000 | 5710 | 5710 |
Tabela 1 Medições da velocidade de processamento
A velocidade média de envio de 1 000 000 cotações foi de cerca de 170 000 pontos/segundo em um notebook com Windows Vista com uma CPU 2.0GHz T4200 e 3GB RAM.
Conclusão
Apresentei um método de comunicação entre os terminais MetaTrader 5 utilizando pipes nomeados. O método acabou por ser suficiente pra enviar cotações em tempo real entre terminais.
A classe CNamedPipes pode ser ainda estendida de acordo com as necessidades adicionais, por exemplo, tornar possível a limitação em duas contas independentes. Por favor, encontre o código-fonte da classe CNamedPipe com a documentação no formato chm e outro código-fonte que implementei para escrever o artigo anexos.
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/115
- 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