Como exportar cotações do MetaTrader5 para aplicações .NET usando serviços WCF
Introdução
Programadores que usam o serviço DDE no MetaTrader 4 provavelmente já ouviram que na quinta versão este serviço não será mais suportado. E não há uma solução padrão para a exportação de cotações. Como solução para este problema, os desenvolvedores do MQL5 sugerem usar seu próprio dll, que o implementa. Então se temos que escrever a implementação, vamos fazer direito!Por que .NET?
Para mim, com minha extensa experiência em programação no .NET, seria mais lógico, interessante e simples implementar a exportação de cotações usando esta plataforma. Infelizmente, não há um nenhum suporte nativo do .NET no MQL5 na quinta versão. Tenho certeza que os desenvolvedores devem ter algum motivo para isso. Por isso, usaremos o dll win32 como um wrapper para o suporte .NET.
Por que WCF?
Eu escolhi o Windows Communication Foundation Technology (WCF) por diversos motivos: por um lado, é fácil estender e adaptar, por outro lado, eu queria testá-lo com trabalho pesado. Além disso, de acordo com a Microsoft, o WCF se desempenha um pouco melhor em comparação com o .NET Remoting.
Requerimentos do sistema
Vamos pensar no que queremos do nosso sistema. Eu acho que são duas exigências:
- Obviamente, precisamos exportar ticks, usando melhor a estrutura nativa MqlTick;
- é preferível saber a lista dos símbolos exportados atualmente.
Vamos começar...
1. Contratos e classes gerais
Primeiramente, vamos criar uma nova biblioteca de classe e nomear como QExport.dll. Definimos a estrutura MqlTick como um DataContract:
[StructLayout(LayoutKind.Sequential)] [DataContract] public struct MqlTick { [DataMember] public Int64 Time { get; set; } [DataMember] public Double Bid { get; set; } [DataMember] public Double Ask { get; set; } [DataMember] public Double Last { get; set; } [DataMember] public UInt64 Volume { get; set; } }
Depois, definiremos os contratos do serviço. Não gosto de usar as classes de configuração e classes proxy geradas, então você não vai encontrar muitos recursos aqui.
Vamos definir o primeiro contrato do servidor de acordo com os requerimentos descritos acima:
[ServiceContract(CallbackContract = typeof(IExportClient))] public interface IExportService { [OperationContract] void Subscribe(); [OperationContract] void Unsubscribe(); [OperationContract] String[] GetActiveSymbols(); }
Como vemos, há um esquema padrão de assinatura e cancelamento de assinatura das notificações do servidor. Os detalhes resumidos das operações são descritos abaixo:
Operação | Descrição |
---|---|
Subscribe() | Assinar para exportação de ticks |
Unsubscribe() | Cancelar assinatura para exportação de ticks |
GetActiveSymbols() | Retorna a lista de símbolos exportados |
E as seguintes informações devem ser enviadas para o callback do cliente: a cotação em si e a notificação sobre as alterações das listas dos símbolos exportados. Vamos definir as operações necessárias como "Operações One Way" para aumentar o desempenho:
[ServiceContract] public interface IExportClient { [OperationContract(IsOneWay = true)] void SendTick(String symbol, MqlTick tick); [OperationContract(IsOneWay = true)] void ReportSymbolsChanged(); }
Operação | Descrição |
---|---|
SendTick(String, MqlTick) | Envia o tick |
ReportSymbolsChanged() | Notifica o cliente sobre as alterações na lista de símbolos exportados |
2. Implementação do servidor
Vamos criar um novo build com o nome Qexport.Service.dll para o serviço com a implementação do contrato do servidor.
Vamos escolher o NetNamedPipesBinding para uma ligação, porque possui o maior desempenho em comparação com as ligações padrão. Se precisarmos transmitir as cotações, por exemplo, por uma rede, o NetTcpBinding deve ser usado.
Aqui estão alguns detalhes sobre a implementação do contrato do servidor:
A definição de classe. Primeiro de tudo, ela deve ser marcada com o atributo ServiceBehavior com os seguintes modificadores:
- InstanceContextMode = InstanceContextMode.Single- para fornecer o uso de uma instância de serviço para todas as solicitações processadas, isso aumentará o desempenho da solução. Além disso, teremos a possibilidade de servir e gerenciar a lista de símbolos exportados;
- ConcurrencyMode = ConcurrencyMode.Multiple -significa o processamento paralelo para todas as solicitações do cliente;
- UseSynchronizationContext = false– significa que não anexamos à thread GUI para evitar situações de travamento. Isso não é necessário aqui para a nossa tarefa, mas é necessário se quisermos hospedar o serviço usando as aplicações Windows;
- IncludeExceptionDetailInFaults = true– para incluir os detalhes de exceção ao objeto FaultExceptionquando passado para o cliente.
O ExportService sozinho contém duas interfaces: IExportService, IDisposable. O primeiro implementa todas as funções do serviço, o segundo implementa o modelo padrão do release de recursos .NET.
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false, IncludeExceptionDetailInFaults = true)] public class ExportService : IExportService, IDisposable {
Vamos descrever as variáveis do serviço:
// full address of service in format net.pipe://localhost/server_name private readonly String _ServiceAddress; // service host private ServiceHost _ExportHost; // active clients callbacks collection private Collection _Clients = new Collection(); // active symbols list private List _ActiveSymbols = new List<string>(); // object for locking private object lockClients = new object();
Vamos definir os métodos Open() e Close(), que abre e fecha nosso serviço:
public void Open() { _ExportHost = new ServiceHost(this); // point with service _ExportHost.AddServiceEndpoint(typeof(IExportService), // contract new NetNamedPipeBinding(), // binding new Uri(_ServiceAddress)); // address // remove the restriction of 16 requests in queue ServiceThrottlingBehavior bhvThrot = new ServiceThrottlingBehavior(); bhvThrot.MaxConcurrentCalls = Int32.MaxValue; _ExportHost.Description.Behaviors.Add(bhvThrot); _ExportHost.Open(); } public void Close() { Dispose(true); } private void Dispose(bool disposing) { try { // closing channel for each client // ... // closing host _ExportHost.Close(); } finally { _ExportHost = null; } // ... }
Depois, a implementação dos métodos IExportService:
public void Subscribe() { // get the callback channel IExportClient cl = OperationContext.Current.GetCallbackChannel(); lock (lockClients) _Clients.Add(cl); } public void Unsubscribe() { // get the callback chanell IExportClient cl = OperationContext.Current.GetCallbackChannel(); lock (lockClients) _Clients.Remove(cl); } public String[] GetActiveSymbols() { return _ActiveSymbols.ToArray(); }
Agora, precisamos adicionar métodos para enviar ticks, registrar e excluir os símbolos exportados.
public void RegisterSymbol(String symbol) { if (!_ActiveSymbols.Contains(symbol)) _ActiveSymbols.Add(symbol); // sending notification to all clients about changes in the list of active symbols //... } public void UnregisterSymbol(String symbol) { _ActiveSymbols.Remove(symbol); // sending notification to all clients about the changes in the list of active symbols //... } public void SendTick(String symbol, MqlTick tick) { lock (lockClients) for (int i = 0; i < _Clients.Count; i++) try { _Clients[i].SendTick(symbol, tick); } catch (CommunicationException) { // it seems that connection with client has lost - we just remove the client _Clients.RemoveAt(i); i--; } }
Vamos resumir a lista das principais funções do servidor (apenas as que precisamos):
Métodos | Descrição |
---|---|
Open() | Executa o servidor |
Close() | Para o servidor |
RegisterSymbol(String) | Adiciona o símbolo à lista de símbolos exportados |
UnregisterSymbol(String) | Exclua o símbolo da lista de símbolos exportados |
GetActiveSymbols() | Retorna o número de símbolos exportados |
SendTick(String, MqlTick) | Envia o tick para os clientes |
3. Implementação do cliente
Temos considerado o servidor, acho que isso está claro, então é hora de considerar o cliente. Vamos criar o Qexport.Client.dll. O contrato do cliente será implementado aqui. Primeiro, deve ser marcado com o atributo CallbackBehavior, que define seu comportamento. Tem os seguintes modificadores:
- ConcurrencyMode = ConcurrencyMode.Multiple - significa o processamento paralelo para todos os callbacks e respostas do servidor. Este modificador é muito importante. Imagine que o servidor queira notificar o cliente sobre as alterações na lista de símbolos exportados chamando callback ReportSymbolsChanged(). E o cliente (em seu callback) deseja receber a nova lista de símbolos exportados por chamado do método do servidor GetActiveSymbols(). Acontece que o cliente não pode receber a resposta do servidor porque realiza o callback esperando pela resposta do servidor. Como resultado, o cliente cairá por causa do limite de tempo.
- UseSynchronizationContext = false - especifica que não anexamos à GUI para evitar situações de travamento. Predefinidamente, os callbacks do WCF são anexados à thread aliada. Se a thread aliada possui uma GUI, a situação é possível quando o callback aguarda a conclusão do método pelo qual foi chamado, mas o método não pode concluir porque aguarda o callback terminar. é um pouco similar ao caso anterior, embora os dois sejam coisas diferentes.
Como para o caso do servidor, o cliente também implementa duas interfaces: IExportClient e IDisposable:
[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false)] public class ExportClient : IExportClient, IDisposable {
Vamos descrever as variáveis do serviço:
// full service address private readonly String _ServiceAddress; // service object private IExportService _ExportService; // Returns service instance public IExportService Service { get { return _ExportService; } } // Returns communication channel public IClientChannel Channel { get { return (IClientChannel)_ExportService; } }
Agora, vamos criar eventos para nossos métodos de callback. Isso é necessário para a aplicação de cliente ser capaz de assinar os eventos e obter as notificações sobre as alterações do estado do cliente.
// calls when tick received public event EventHandler TickRecieved; // call when symbol list has changed public event EventHandler ActiveSymbolsChanged;
Também define os métodos Open() e Close() para o cliente:
public void Open() { // creating channel factory var factory = new DuplexChannelFactory( new InstanceContext(this), new NetNamedPipeBinding()); // creating server channel _ExportService = factory.CreateChannel(new EndpointAddress(_ServiceAddress)); IClientChannel channel = (IClientChannel)_ExportService; channel.Open(); // connecting to feeds _ExportService.Subscribe(); } public void Close() { Dispose(true); } private void Dispose(bool disposing) { try { // unsubscribe feeds _ExportService.Unsubscribe(); Channel.Close(); } finally { _ExportService = null; } // ... }
Observe que a conexão e desconexão dos feeds são chamadas quando um cliente é aberto ou fechado, então não é necessário chamá-las diretamente.
E agora, vamos escrever o contrato do cliente. Sua implementação leva a geração dos seguintes eventos:
public void SendTick(string symbol, MqlTick tick) { // firing event TickRecieved } public void ReportSymbolsChanged() { // firing event ActiveSymbolsChanged }
Finalmente, as principais propriedades e métodos do cliente são definidos da seguinte forma:
Propriedade | Descrição |
---|---|
Serviço | Canal de comunicação do serviço |
Canal | Instância do contrato de serviço IExportService |
Método | Descrição |
---|---|
Open() | Conecta ao servidor |
Close() | Desconecta do servidor |
Evento | Descrição |
---|---|
TickRecieved | Gerado após o recebimento de uma nova cotação |
ActiveSymbolsChanged | Gerado após as alterações na lista dos símbolos ativos |
4. Velocidade de transferência entre duas aplicações .NET
Foi interessante, para eu, medir a velocidade de transferência entre duas aplicações .NET, na verdade, é a taxa de transferência, que é medida em ticks por segundo. Escrevi diversas aplicações de console para medir o desempenho do serviço: a primeira para o servidor e a segunda para o cliente. Escrevi o seguinte código na função Main() do servidor:
ExportService host = new ExportService("mt5"); host.Open(); Console.WriteLine("Press any key to begin tick export"); Console.ReadKey(); int total = 0; Stopwatch sw = new Stopwatch(); for (int c = 0; c < 10; c++) { int counter = 0; sw.Reset(); sw.Start(); while (sw.ElapsedMilliseconds < 1000) { for (int i = 0; i < 100; i++) { MqlTick tick = new MqlTick { Time = 640000, Bid = 1.2345 }; host.SendTick("GBPUSD", tick); } counter++; } sw.Stop(); total += counter * 100; Console.WriteLine("{0} ticks per second", counter * 100); } Console.WriteLine("Average {0:F2} ticks per second", total / 10); host.Close();
Como vemos, o código realiza dez medições de taxa de transferência. Obtive os seguintes resultados de teste no meu Athlon 3000+:
2600 ticks per second 3400 ticks per second 3300 ticks per second 2500 ticks per second 2500 ticks per second 2500 ticks per second 2400 ticks per second 2500 ticks per second 2500 ticks per second 2500 ticks per second Average 2670,00 ticks per second
2500 ticks por segundo - Acho que é o suficiente para exportar cotações para 100 símbolos (claro, virtualmente, porque parece que ninguém quer abrir tantos gráficos e anexar especialistas =)). Além disso, com o número crescente de clientes, o número máximo de símbolos exportados para cada cliente é reduzido.
5. Criando um "estrato"
Agora é hora de pensar como conectar isso com o terminal do cliente. Vamos ver o que temos na primeira chamada da função no MetaTrader 5: o ambiente runtime .NET (CLR) é carregado ao processo e o domínio da aplicação é criado por predefinição. O interessante é que não seja descarregado após a execução do código.
A única maneira de descarregar o CLR do processo é finalizá-lo, (fechar o terminal do cliente) o que irá forçar o Windows a limpar todos os recursos do processo. Então, podemos criar nossos objetos e eles existirão até que o domínio da aplicação seja descarregado ou até ser destruído pelo Garbage Collector.
Você pode dizer que isso parece bom, mas mesmo se impedirmos a destruição do objeto pelo Garbage Collector, não podemos acessar os objetos a partir do MQL5. Felizmente, tal acesso pode ser organizado facilmente. O truque é o seguinte: para cada domínio de aplicação há uma tabela de identificadores de Garbage Collector (tabela de identificação GC), que é usada pela aplicação para rastrear a vida útil do objeto e permitir o gerenciamento manual.
A aplicação adiciona e exclui elementos da tabela usando o tipo System.Runtime.InteropServices.GCHandle. Tudo que precisamos é acondicionar nosso objeto com tal descritor e teremos um acesso a ele por toda a propriedade GCHandle.Target. Dessa forma, podemos ter a referência para o objeto GCHandle, que está na tabela de identificadores e é garantido que ele não será movido ou excluído pelo Garbage Collector. O objeto acondicionado irá também evitar a reciclagem, por causa da referência do descritor.
Agora é a hora de testar a teoria na prática. Para isso, vamos criar um novo dll win 32 com o nome QExpertWrapper.dll e adicionar o suporte CLR, System.dll, QExport.dll, Qexport.Service.dll para referência build. Também criamos uma classe auxiliar ServiceManaged para propósitos de gerenciamento – para realizar a organização, receber objetos por identificadores, etc.
ref class ServiceManaged { public: static IntPtr CreateExportService(String^); static void DestroyExportService(IntPtr); static void RegisterSymbol(IntPtr, String^); static void UnregisterSymbol(IntPtr, String^); static void SendTick(IntPtr, String^, IntPtr); };
Vamos considerar a implementação destes métodos. O método CreateExportService cria o serviço, e o acondiciona ao GCHandle usando GCHandle.Alloc e retorna sua referência. Se algo der errado, ele mostra a Caixa de mensagens com um erro. Eu o usei com o propósito de debug, então não tenho certeza se é realmente necessário, mas o deixei aqui por precaução.
IntPtr ServiceManaged::CreateExportService(String^ serverName) { try { ExportService^ service = gcnew ExportService(serverName); service->Open(); GCHandle handle = GCHandle::Alloc(service); return GCHandle::ToIntPtr(handle); } catch (Exception^ ex) { MessageBox::Show(ex->Message, "CreateExportService"); } }
O método DestroyExportService obtém o indicador para o GCHandle do serviço, obtém o serviço da propriedade alvo e chama seu método Close(). É importante liberar o objeto de serviço chamando seu método Free(). Caso contrário, irá permanecer na memória, o Garbage Collector não o remove.
void ServiceManaged::DestroyExportService(IntPtr hService) { try { GCHandle handle = GCHandle::FromIntPtr(hService); ExportService^ service = (ExportService^)handle.Target; service->Close(); handle.Free(); } catch (Exception^ ex) { MessageBox::Show(ex->Message, "DestroyExportService"); } }
O método RegisterSymbol adiciona o símbolo à lista de símbolos exportados:
void ServiceManaged::RegisterSymbol(IntPtr hService, String^ symbol) { try { GCHandle handle = GCHandle::FromIntPtr(hService); ExportService^ service = (ExportService^)handle.Target; service->RegisterSymbol(symbol); } catch (Exception^ ex) { MessageBox::Show(ex->Message, "RegisterSymbol"); } }
O método UnregisterSymbol exclui um símbolo da lista:
void ServiceManaged::UnregisterSymbol(IntPtr hService, String^ symbol) { try { GCHandle handle = GCHandle::FromIntPtr(hService); ExportService^ service = (ExportService^)handle.Target; service->UnregisterSymbol(symbol); } catch (Exception^ ex) { MessageBox::Show(ex->Message, "UnregisterSymbol"); } }
E agora, o método SendTick. Como vemos, o ponteiro é transformado na estrutura MqlTick usando a classe Marshal. Outro ponto: não há nenhum código no bloco catch - isso é feito para evitar os lags da fila de tick geral em caso de erro.
void ServiceManaged::SendTick(IntPtr hService, String^ symbol, IntPtr hTick) { try { GCHandle handle = GCHandle::FromIntPtr(hService); ExportService^ service = (ExportService^)handle.Target; MqlTick tick = (MqlTick)Marshal::PtrToStructure(hTick, MqlTick::typeid); service->SendTick(symbol, tick); } catch (...) { } }
Vamos considerar a implementação das funções, que serão chamadas de nossos programas ex5.
#define _DLLAPI extern "C" __declspec(dllexport) // --------------------------------------------------------------- // Creates and opens service // Returns its pointer // --------------------------------------------------------------- _DLLAPI long long __stdcall CreateExportService(const wchar_t* serverName) { IntPtr hService = ServiceManaged::CreateExportService(gcnew String(serverName)); return (long long)hService.ToPointer(); } // ----------------------------------------- ---------------------- // Closes service // --------------------------------------------------------------- _DLLAPI void __stdcall DestroyExportService(const long long hService) { ServiceManaged::DestroyExportService(IntPtr((HANDLE)hService)); } // --------------------------------------------------------------- // Sends tick // --------------------------------------------------------------- _DLLAPI void __stdcall SendTick(const long long hService, const wchar_t* symbol, const HANDLE hTick) { ServiceManaged::SendTick(IntPtr((HANDLE)hService), gcnew String(symbol), IntPtr((HANDLE)hTick)); } // --------------------------------------------------------------- // Registers symbol to export // --------------------------------------------------------------- _DLLAPI void __stdcall RegisterSymbol(const long long hService, const wchar_t* symbol) { ServiceManaged::RegisterSymbol(IntPtr((HANDLE)hService), gcnew String(symbol)); } // --------------------------------------------------------------- // Removes symbol from list of exported symbols // --------------------------------------------------------------- _DLLAPI void __stdcall UnregisterSymbol(const long long hService, const wchar_t* symbol) { ServiceManaged::UnregisterSymbol(IntPtr((HANDLE)hService), gcnew String(symbol)); }
O código está pronto, agora precisamos compilá-lo e criá-lo. Vamos especificar o diretório de saída "C:\Program Files\MetaTrader 5\MQL5\Libraries" nas opções de projeto. Após a compilação, três bibliotecas aparecerão na pasta especificada.
O programa mql5 usa apenas uma delas, ou seja, a QExportWrapper.dll, as duas outras bibliotecas são usadas por ele. Por causa disso, precisamos colocar as bibliotecas Qexport.dll e Qexport.Service.dll na pasta raiz do MetaTrader. Não é conveniente.
A solução é criar o arquivo de configuração e especificar o caminho para as bibliotecas lá. Vamos criar o arquivo com o nome terminal.exe.config na pasta raiz do MetaTrader e escreva as seguintes strings lá:
<configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <probing privatePath="mql5\libraries" /> </assemblyBinding> </runtime> </configuration>
Está pronto. Agora o CLR irá procurar pelas bibliotecas na pasta que especificamos.
6. Implementação da parte do servidor no MQL5
Finalmente, conseguimos a programação da parte do servidor no mql5. Vamos criar um novo arquivo QService.mqh e definir as funções importadas do QExpertWrapper.dll:
#import "QExportWrapper.dll" long CreateExportService(string); void DestroyExportService(long); void RegisterSymbol(long, string); void UnregisterSymbol(long, string); void SendTick(long, string, MqlTick&); #import
É ótimo que o mql5 possua classes porque é um recurso ideal para encapsular todas as lógicas internas, que significativamente simplificam o trabalho e entendimento do código. Por isso, vamos projetar uma classe que será uma shell para os métodos da biblioteca.
Além disso, para evitar um problema com a criação de serviço para cada símbolo, vamos organizar a verificação do serviço de trabalho com tal nome, e trabalharemos por isso em tal caso. Um método ideal para fornecer essa informações são variáveis globais, pelos seguintes motivos:
- As variáveis globais desaparecem após o fechamento do terminal do cliente. O mesmo acontece com o serviço;
- Podemos fornecer o número de objetos Qservice que usam o serviço. Isso permite fechar o serviço físico apenas após o último objeto ser fechado.
Então, vamos criar uma classe Qservice:
class QService { private: // service pointer long hService; // service name string serverName; // name of the global variable of the service string gvName; // flag that indicates is service closed or not bool wasDestroyed; // enters the critical section void EnterCriticalSection(); // leaves the critical section void LeaveCriticalSection(); public: QService(); ~QService(); // opens service void Create(const string); // closes service void Close(); // sends tick void SendTick(const string, MqlTick&); }; //-------------------------------------------------------------------- QService::QService() { wasDestroyed = false; } //-------------------------------------------------------------------- QService::~QService() { // close if it hasn't been destroyed if (!wasDestroyed) Close(); } //-------------------------------------------------------------------- QService::Create(const string serviceName) { EnterCriticalSection(); serverName = serviceName; bool exists = false; string name; // check for the active service with such name for (int i = 0; i < GlobalVariablesTotal(); i++) { name = GlobalVariableName(i); if (StringFind(name, "QService|" + serverName) == 0) { exists = true; break; } } if (!exists) // if not exists { // starting service hService = CreateExportService(serverName); // adding a global variable gvName = "QService|" + serverName + ">" + (string)hService; GlobalVariableTemp(gvName); GlobalVariableSet(gvName, 1); } else // the service is exists { gvName = name; // service handle hService = (int)StringSubstr(gvName, StringFind(gvName, ">") + 1); // notify the fact of using the service by this script // by increase of its counter GlobalVariableSet(gvName, NormalizeDouble(GlobalVariableGet(gvName), 0) + 1); } // register the chart symbol RegisterSymbol(hService, Symbol()); LeaveCriticalSection(); } //-------------------------------------------------------------------- QService::Close() { EnterCriticalSection(); // notifying that this script doen't uses the service // by decreasing of its counter GlobalVariableSet(gvName, NormalizeDouble(GlobalVariableGet(gvName), 0) - 1); // close service if there isn't any scripts that uses it if (NormalizeDouble(GlobalVariableGet(gvName), 0) < 1.0) { GlobalVariableDel(gvName); DestroyExportService(hService); } else UnregisterSymbol(hService, Symbol()); // unregistering symbol wasDestroyed = true; LeaveCriticalSection(); } //-------------------------------------------------------------------- QService::SendTick(const string symbol, MqlTick& tick) { if (!wasDestroyed) SendTick(hService, symbol, tick); } //-------------------------------------------------------------------- QService::EnterCriticalSection() { while (GlobalVariableCheck("QService_CriticalSection") > 0) Sleep(1); GlobalVariableTemp("QService_CriticalSection"); } //-------------------------------------------------------------------- QService::LeaveCriticalSection() { GlobalVariableDel("QService_CriticalSection"); }
A classe contém os seguintes métodos:
Método | Descrição |
---|---|
Create(const string) | Inicia o serviço |
Close() | Fecha o serviço |
SendTick(const string, MqlTick&) | Envia a cotação |
Observe também que os métodos privados EnterCriticalSection() e LeaveCriticalSection() permitem que você execute as seções críticas do código entre eles.
Isso nos poupará de casos de chamadas simultâneas da função Create() e a criação de novos serviços para cada QService.
Então, descrevemos a classe para trabalhar com o serviço, agora vamos escrever um Expert Advisor para a transmissão de cotações. O Expert Advisor foi escolhido por causa da sua possibilidade de processar todos os ticks que chegaram.
//+------------------------------------------------------------------+ //| QExporter.mq5 | //| Copyright GF1D, 2010 | //| garf1eldhome@mail.ru | //+------------------------------------------------------------------+ #property copyright "GF1D, 2010" #property link "garf1eldhome@mail.ru" #property version "1.00" #include "QService.mqh" //--- input parameters input string ServerName = "mt5"; QService* service; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { service = new QService(); service.Create(ServerName); return(0); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { service.Close(); delete service; service = NULL; } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { MqlTick tick; SymbolInfoTick(Symbol(), tick); service.SendTick(Symbol(), tick); } //+------------------------------------------------------------------+
7. Teste do desempenho da comunicação entre ex5 e o cliente .NET
É evidente que o desempenho total do serviço irá diminuir se as cotações chegarem diretamente do terminal do cliente, então me interessei em medi-lo. Eu estava certo que deveria diminuir devido a perda inevitável do tempo do CPU para organização e conversão de tipo.
Por este motivo, escrevi um simples script que é o mesmo que o do primeiro teste. A função Start() parece com o seguinte:
QService* serv = new QService(); serv.Create("mt5"); MqlTick tick; SymbolInfoTick("GBPUSD", tick); int total = 0; for(int c = 0; c < 10; c++) { int calls = 0; int ticks = GetTickCount(); while(GetTickCount() - ticks < 1000) { for(int i = 0; i < 100; i++) serv.SendTick("GBPUSD", tick); calls++; } Print(calls * 100," calls per second"); total += calls * 100; } Print("Average ", total / 10," calls per second"); serv.Close(); delete serv;
Eu obtive os seguintes resultados:
1900 calls per second 2400 calls per second 2100 calls per second 2300 calls per second 2000 calls per second 2100 calls per second 2000 calls per second 2100 calls per second 2100 calls per second 2100 calls per second Average 2110 calls per second
2500 ticks/seg. vs 1900 ticks/seg. 25% é o preço que deveria ser pago pelo uso dos serviços do MT5, mas de qualquer forma é insuficiente. é interessante observar que o desempenho pode diminuir usando a pool de threads e o método estático System.Threading.ThreadPool.QueueUserWorkItem.
Usando este método, obtive a velocidade de transferência de até 10000 ticks por segundo. Mas seu trabalho em um teste severo foi instável devido ao fato de que o Garbage Collector não tem tempo para excluir objetos - como resultado, a memória alocada pelo MetaTrader cresce rapidamente e finalmente trava. Mas foi um teste severo, bem mais intenso do que a realidade, então não há perigo em usar a pool de threads.
8. Teste de tempo real
Criei um exemplo de tabela de ticks usando o serviço. O projeto está em anexo no arquivo com o nome WindowsClient. O resultado de seu trabalho é apresentado abaixo:
Fig 1. Janela principal da aplicação WindowsClient com a tabela de cotações
Conclusão
Neste artigo, descrevi um dos métodos de exportação de cotações para aplicações .NET. Tudo que era necessário foi implementado e agora temos classes prontas que podem ser usadas em suas próprias aplicações. A única coisa que não é conveniente é anexar scripts para cada um dos gráficos necessários.
No presente momento acho que este problema pode ser resolvido usando os perfis do MetaTrader. Por outro lado, se você não precisa de todas as cotações, você pode organizar isso com um script que transmite as cotações para os símbolos necessários. Como você pode entender, a transmissão de profundidade de mercado ou até mesmo o acesso pelos dois lados podem ser organizados da mesma maneira.
Descrição dos arquivos:
Bin.rar - arquivo com uma solução pronta. Para os usuários que querem ver como isso funciona. Observe ainda que o .NET Framework 3.5 (talvez também funcione com a versão 3.0) deve estar instalado no seu computador.
Src.rar - código-fonte completo do projeto. Para trabalhar com isso você precisará do MetaEditor e Visual Studio 2008.
QExportDemoProfile.rar - Perfil MetaTrader, que anexa o script a 10 gráficos, como mostrado na Fig. 1.
Traduzido do Russo pelo artigo original MetaQuotes Software Corp.
: https://www.mql5.com/pt/articles/27
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/27
- 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