Cálculos paralelos no MetaTrader 5

ds2 | 10 fevereiro, 2014

Instrução ao paralelismo de processador

Quase todos os PCs modernos são capazes de realizar múltiplas tarefas simultaneamente - devido à presença de diversos núcleos de processador. Seu número está crescendo a cada ano - 2, 3, 4, 6 núcleos... A Intel demonstrou recentemente um processador experimental funcional de 80 núcleos (sim, não é um erro de digitação - infelizmente, este computador não aparecerá em lojas, uma vez que este processador foi criado apenas com o propósito de estudar as capacidades potenciais da tecnologia).

Nem todos os usuários de computador (e nem mesmo todos os programadores novatos) entendem como funciona. Por isso, alguém, com certeza, fará a questão: por que precisamos de um processador com tantos núcleos, quando mesmo antes (com um único núcleo), o computador conseguia executar muitos programas simultaneamente e todos eles funcionavam? A verdade é que não é assim. Vamos observar o seguinte diagrama.

Figura 1. Execução paralela de aplicações

Figura 1. Execução paralela de aplicações

Caso A o diagrama mostra o que acontece quando um único programa é executado em um processador de núcleo simples. O processador dedica todo seu tempo a sua implementação, e o programa realiza alguma quantidade de trabalho sobre o tempo T.

Caso B - 2 programas executados. Mas o processador é organizado de tal forma que fisicamente, em qualquer ponto do tempo, um de seus núcleos pode executar apenas um comando, então, ele tem que constantemente alternar entre os dois programas: ele executará o primeiro, então, o segundo, etc. Isto acontece muito rapidamente, muitas vezes por segundo, então dá a impressão que o processador executa ambos os programas simultaneamente. Na realidade, entretanto, sua execução levará duas vezes mais tempo do que se cada programa fosse executado separadamente no processador.

Caso C mostra que seu problema é efetivamente resolvido se o número de núcleos em um processador corresponder ao número de programas sendo executados. Cada programa possui a sua disposição um núcleo separado, e a velocidade de sua execução aumenta, como no caso A.

Caso D é uma resposta ao engano comum de muitos usuários. Eles acreditam que se um programa está sendo executado em um processador de múltiplos núcleos, então ele é executado muitas vezes mais rápido. Em geral, isso pode não ser verdade, porque o processador não é capaz de dividir independentemente o programa em partes separadas e executá-las todas simultaneamente.

Por exemplo, se o programa pede primeiro uma senha, e então sua verificação é realizada, seria inaceitável realizar o pedido de senha em um núcleo e a verificação em outro, ao mesmo tempo. A verificação simplesmente nunca terá sucesso, porque a senha, no momento de seu início, ainda não foi inserida.

O processador não conhece todos os projetos que o programador implementou, nem a total lógica do trabalho do programa e, portanto, ele não pode separar independentemente o programa entre os núcleos. Então se executarmos um único programa em um sistema de múltiplos núcleos, ele usará apenas um núcleo, e será executado à mesma velocidade que se fosse executado em um processador de núcleo único.

Caso E explica o que precisa ser feito para fazer o programa usar todos os seus núcleos e ser executado mais rápido. Uma vez que o programador sabe a lógica do programa, ele deve, durante seu desenvolvimento, marcando de alguma maneira as partes do programa que podem ser executadas ao mesmo tempo. O programa, durante sua execução, comunicará esta informação ao processador, e o processador então alocará o programa ao número necessário de núcleos.


Paralelismo no MetaTrader

No capítulo anterior, descobrimos o que precisa ser feito para utilizar todos os núcleos da CPU e para acelerar a execução dos programas: precisamos alocar de alguma maneira o código paralelizável do programa em threads separadas. Em muitas linguagens de programação, há classes ou operadores especiais para isso. Mas não existe tal instrumento embutido na linguagem MQL5. Então o que podemos fazer?

Há duas maneiras de resolver este problema:

1. Usar DLL 2. Usar recursos de não-linguagem do MetaTrader
Ao criar uma DLL em uma linguagem que possui uma ferramenta embutida para paralelização, também obteremos a paralelização no EA MQL5. De acordo com a informação dos desenvolvedores MetaTrader, a arquitetura do terminal cliente é multi-threaded. Por isso, sob certas condições, os dados de mercado de entrada são processados em threads separadas. Assim, se pudermos descobrir uma maneira para separar o código de nosso programa para um número de EAs ou indicadores, então o MetaTrader será capaz de usar um número de núcleos de CPU para sua execução.


Não discutiremos o primeiro método neste artigo. Está claro que no caso da DLL, podemos implementar aquilo que quisermos. Tentaremos encontrar uma solução que envolva apenas os meios padrão do MetaTrader e que não necessitem o uso de outras linguagens além do MQL5.

E, então, mais sobre o segundo método. Precisaremos realizar uma série de experimentos para descobrir exatamente quantos múltiplos núcleos são suportados no MetaTrader. Para fazer isso, vamos criar um indicador de teste e EAs de teste, que realizarão qualquer trabalho contínuo, que carregará pesadamente a CPU.

Escrevi o seguinte indicador i-flood:

//+------------------------------------------------------------------+
//|                                                      i-flood.mq5 |
//+------------------------------------------------------------------+
#property indicator_chart_window

input string id;
//+------------------------------------------------------------------+
void OnInit()
  {
   Print(id,": OnInit");
  }
//+------------------------------------------------------------------+
int OnCalculate(const int rt,const int pc,const int b,const double &p[])
  {
   Print(id,": OnCalculate Begin");
   
   for (int i=0; i<1e9; i++)
     for (int j=0; j<1e1; j++);
     
   Print(id,": OnCalculate End");
   return(0);   
  }
//+------------------------------------------------------------------+

E o EA e-flood análogo a ele:

//+------------------------------------------------------------------+
//|                                                      e-flood.mq5 |
//+------------------------------------------------------------------+
input string id;
//+------------------------------------------------------------------+
void OnInit()
  {
   Print(id,": OnInit");
  }
//+------------------------------------------------------------------+
void OnTick()
  {
   Print(id,": OnTick Begin");
   
   for (int i=0; i<1e9; i++)
     for (int j=0; j<1e1; j++);
     
   Print(id,": OnTick End");
  }
//+------------------------------------------------------------------+

Além disso, abrindo diversas combinações de janelas de gráficos (um gráfico, dois gráficos com o mesmo símbolo, dois gráficos com diferentes símbolos), e colocando nelas uma ou duas cópias deste indicador ou EA, podemos observar como o terminal utiliza os núcleos da CPU.

Estes indicadores e EA também mandam mensagens para o log, e é interessante observar a sequência de sua aparição. Não fornecerei estes logs, já que você pode gerá-los por si mesmo, mas neste artigo estamos interessados em descobrir quantos núcleos e em quais combinações de gráficos são usados pelo terminal.

Podemos medir o número de núcleos operantes por meio do "Gerenciador de tarefas" do Windows.

Figura 2. Núcleos da CPU

Figura 2. Núcleos da CPU


Os resultados de todas as medições estão reunidos na tabela abaixo:


combinação
Conteúdo do terminal
Utilização da CPU
1
2 indicadores em um gráfico 1 núcleo
2
2 indicadores em diferentes gráficos, o mesmo par 1 núcleo
3
2 indicadores em diferentes gráficos, diferentes pares 2 núcleos
4
2 EAs no mesmo gráfico - esta situação é impossível -
5
2 EAs em diferentes gráficos, o mesmo par 2 núcleos
6
2 EAs em diferentes gráficos, diferentes pares 2 núcleos
7
2 indicadores em diferentes pares, criados a partir do EA 2 núcleos


A sétima combinação é uma maneira comum de criar um indicador, usada em muitas estratégias de comércio.

A única característica especial é que eu criei dois indicadores em dois pares de moedas diferentes, uma vez que as combinações 1 e 2 tornam claro que não faz sentido colocar os indicadores no mesmo par. Para esta combinação, usei o EA e-flood starter, que produziu duas cópias de i-flood:

//+------------------------------------------------------------------+
//|                                              e-flood-starter.mq5 |
//+------------------------------------------------------------------+
void OnInit() 
  {
   string s="EURUSD";
   for(int i=1; i<=2; i++) 
     {
      Print("Indicator is created, handle=",
            iCustom(s,_Period,"i-flood",IntegerToString(i)));
      s="GBPUSD";
     }
  }
//+------------------------------------------------------------------+

Assim, todos os cálculos de núcleos foram realizados, e agora sabemos para quais combinações o MetaTrader usa múltiplos núcleos. A seguir, tentaremos aplicar este conhecimento para implementar as ideias de computações paralelas.


Projetamos um sistema paralelo

No que diz respeito ao terminal de comércio para o sistema paralelo, pretendemos que um grupo de indicadores ou EAs (ou uma mistura de ambos) que realizam juntos alguma tarefa comum, por exemplo, realizem comércio ou desenhem no gráfico. Pretende-se que este grupo trabalhe como um único grande indicador ou como um grande EA. Mas, ao mesmo tempo, distribua a carga computacional por todos dos núcleos de processador disponíveis.

Tal sistema consistem em dois tipos de componentes de software:

Por exemplo, para um EA MM e um processador de dois núcleos, o esquema de trabalho do sistema será semelhante ao seguinte:

Figura 3. Esquema do sistema com 2 núcleos de CPU.

Figura 3. Esquema do sistema com 2 núcleos de CPU.

Deve ser entendido que o sistema desenvolvido por nós não é um programa tradicional, onde você pode apenas chamar o necessário em determinado procedimento de momento. O MM e CM são EAs ou indicadores, ou seja, estes são programas autônomos e independentes. Não há conexão direta entre eles, eles operam independentemente, e não podem comunicar-se diretamente entre si.

A execução de qualquer um destes programas começa apenas com a aparição no terminal de qualquer evento (por exemplo, a chegada de cotações ou um tick temporizador). E entre os eventos, todos os dados que estes programas precisam para transmitir um para o outro, devem ser armazenados em algum lugar externo, em um local publicamente acessado (vamos chamá-lo de "Buffer de troca de dados"). Então o esquema acima é implementado no terminal da seguinte maneira:

Figura 4. Detalhes de implementação

Figura 4. Detalhes de implementação

Para a implementação deste sistema, precisamos responder às seguintes questões:

Para cada uma destas questões há mais de uma resposta, e elas são fornecidas abaixo. Na prática, as opções específicas devem ser selecionadas com base na situação em particular. Faremos isso no próximo capítulo. Enquanto isso, vamos considerar todas as respostas possíveis.

Combinação

A Combinação 7 é a mais conveniente para uso prático regular (todas as outras combinações estão listadas no capítulo anterior), porque não há necessidade de abrir janelas adicionais no terminal e colocar nelas EAs ou indicadores. Todo o sistema está localizado em uma única janela, e todos os indicadores (CM-1 e CM-2) são criados no EA (MM) automaticamente. A falta de janelas extras e ações manuais eliminaram a confusão para o negociante, e assim, os erros relacionados a tal confusão.

Em algumas estratégias de comércio, outras combinações podem ser mais úteis. Por exemplo, na base de qualquer uma delas, podemos criar sistemas de software completos, operando pelo princípio "cliente-servidor". Onde os mesmos CMs serão comuns a diversos MMs. Tais CMs comuns podem realizar não apenas um papel secundário de "computadores", mas serem um "servidor" que armazena algum tipo de informação unificada para todas as estratégias, ou mesmo os coordenadores de seu trabalho coletivo. Um servidor CM pode, por exemplo, controlar centralmente a distribuição de meios em algum portfólio de estratégias e pares de moedas, enquanto mantém o nível geral de risco desejado.

Troca de dados

Podemos transmitir a informação entre o MM e CM usando uma das 3 maneiras:

  1. variáveis globais do terminal;
  2. arquivos;
  3. buffers de indicador.

O primeiro método é ótimo quando há um pequeno número de variáveis numéricas sendo transferido. Se houver a necessidade de transferir dados de texto, ele tem que ser codificado em números de alguma maneira, porque variáveis globais possuem apenas o tipo double.

A alternativa é o segundo método, porque qualquer coisa pode ser escrita em arquivo(s). E este é um método conveniente (e, possivelmente, mais rápido do que o primeiro) para a circunstância na qual você precisa transferir uma grande quantidade de dados.

O terceiro método é adequado se o MM e o CM são indicadores. Apenas os dados do tipo double podem ser transferidos, mas é mais conveniente transferir arrays numéricos grandes. Mas há uma desvantagem: durante a formação de uma nova barra, a numeração dos elementos nos buffers é deslocada. Pelo fato de que o MM e o CM estão em pares de moedas diferentes, as novas barras não aparecem simultaneamente. Devemos levar estes deslocamentos em conta.

Sincronização

Quando o terminal recebe uma cotação para o MM, e começa a processá-la, ele não pode transferir o controle imediatamente ao CM. Ele pode apenas (como mostrado no diagrama acima) formar uma tarefa (colocando-a nas variáveis globais, arquivo ou buffer de indicador), e aguardar o CM ser executado. Uma vez que todos os CMs estão localizados em diferentes pares de moeda, a espera pode levar algum tempo. Isto é porque um par pode receber a cotação, enquanto o outro ainda não recebeu, e chegará apenas em alguns segundos ou mesmo minutos (por exemplo, isto pode ocorrer durante a noite em pares não-líquidos).

Por isso, para o CM obter o controle, não devemos usar os eventos OnTick e OnCalculate, que dependem de cotações. Em vez deles, precisamos usar o evento OnTimer (inovação no MQL5), que é executado com uma frequência especificada (por exemplo, 1 segundo). Neste caso, os atrasos no sistema serão severamente limitados.

Além disso, em vez do OnTimer, podemos usar a técnica de ciclagem: ou seja, colocando um ciclo infinito para o CM em OnInit ou OnCalculate. Cada uma de suas iterações é análoga a um tick temporizador.

Aviso. Realizei alguns experimentos e descobri que quando utilizada a Combinação 7, o evento OnTimer não funciona nos indicadores (por alguma razão), ainda que os temporizadores tenham sido criados com sucesso.

Você deve ser cuidadoso com os loops infinitos no OnInit e OnCalculate: se mesmo um indicador CM estiver localizado no mesmo par de moedas que o EA MM, então o preço deixa de se mover no gráfico, e o EA para de funcionar (ele para de gerar os eventos OnTick). Os desenvolvedores do terminal explicaram os motivos deste comportamento.

Dos desenvolvedores: scripts e EAs funcionam em suas próprias threads separadas, enquanto todos os indicadores, em um símbolo único, funcionam na mesma thread. No mesmo fluxo que os indicadores, todas as outras ações neste símbolo também são consecutivamente executadas: o processamento de ticks, a sincronização do histórico e o cálculo dos indicadores. Então, se o indicador realiza uma ação infinita, todos os outros eventos para seu símbolo nunca serão executadas.

Programa Execução Nota
Script Em sua própria thread, há tantas threads de execução quantos forem os scripts. Um script em ciclo não pode quebrar o trabalho de outros programas
Expert Advisor Em sua própria thread, há tantas threads de execução quantos forem os EAs. Um script em ciclo não pode quebrar o trabalho de outros programas
Indicador Uma thread de execução para todos os indicadores em um símbolo. Há tantos símbolos com indicadores quantas forem as threads de execução para eles. Um ciclo infinito em um indicador interromperá o trabalho de todos os outros indicadores naquele símbolo


Criando um Expert Advisor de teste

Vamos selecionar alguma estratégia de comércio que faria sentido paralelizar, e um algoritmo que seja adequado para isso.

Por exemplo, esta pode ser uma estratégia simples: compilar a sequência de N das últimas barras, e descobrir a sequência mais similar a esta no histórico. Sabendo onde o preço foi alterado no histórico, abrimos o negócio relevante.

Se o comprimento da sequência for relativamente pequeno, esta estratégia funcionará bastante rapidamente no MetaTrader5 - em segundos. Mas se tomarmos um comprimento grande - por exemplo, todas as barras do quadro de tempo M1 para as últimas 24 horas (que seriam 1440 barras), - e se buscarmos no histórico até um ano atrás (cerca de 375.000 barras), isto requirirá uma quantidade significante de tempo. E, ainda, esta busca pode ser facilmente paralelizada: basta dividir o histórico em partes iguais correspondentes ao número de núcleos de processador disponíveis, e atribuir a cada núcleo a busca de uma localização específica.

Os parâmetros do sistema paralelo serão os seguintes:

Para a conveniência do desenvolvimento e uso subsequente, criaremos o EA de maneira que, dependendo das configurações, pode operar como um EA paralelo (com cálculo nos indicadores), e como usual (ou seja, sem o uso de indicadores). O código do Expert Advisor e-MultiThread:

//+------------------------------------------------------------------+
//|                                                e-MultiThread.mq5 |
//+------------------------------------------------------------------+
input int Threads=1; // How many cores should be used
input int MagicNumber=0;

// Strategy parameters
input int PatternLen  = 1440;   // The length of the sequence to analyze (pattern)
input int PrognozeLen = 60;     // Forecast length (bars)
input int HistoryLen  = 375000; // History length to search

input double Lots=0.1;
//+------------------------------------------------------------------+
class IndData
  {
public:
   int               ts,te;
   datetime          start_time;
   double            prognoze,rating;
  };

IndData Calc[];
double CurPattern[];
double Prognoze;
int  HistPatternBarStart;
int  ExistsPrognozeLen;
uint TicksStart,TicksEnd;
//+------------------------------------------------------------------+
#include <ThreadCalc.mqh>
#include <Trade\Trade.mqh>
//+------------------------------------------------------------------+
int OnInit()
  {

   double rates[];

//--- Make sure there is enough history
   int HistNeed=HistoryLen+Threads+PatternLen+PatternLen+PrognozeLen-1;
   if(TerminalInfoInteger(TERMINAL_MAXBARS)<HistNeed)
     {    
      Print("Change the terminal setting \"Max. bars in chart\" to the value, not lesser than ",
            HistNeed," and restart the terminal");
      return(1);      
     }
   while(Bars(_Symbol,_Period)<HistNeed)
     {      
      Print("Insufficient history length (",Bars(_Symbol,_Period),") in the terminal, upload...");
      CopyClose(_Symbol,_Period,0,HistNeed,rates);
     }
   Print("History length in the terminal: ",Bars(_Symbol,_Period));

//--- For a multi-core mode create computational indicators
   if(Threads>1)
     {
      GlobalVarPrefix="MultiThread_"+IntegerToString(MagicNumber)+"_";
      GlobalVariablesDeleteAll(GlobalVarPrefix);

      ArrayResize(Calc,Threads);

      // Length of history for each core
      int HistPartLen=MathCeil(HistoryLen/Threads);
      // Including the boundary sequences
      int HistPartLenPlus=HistPartLen+PatternLen+PrognozeLen-1;

      string s;
      int snum=0;
      // Create all computational indicators
      for(int t=0; t<Threads; t++)
        {      
         // For each indicator - its own currency pair,
         // it should not be the same as for the EA
         do
            s=SymbolName(snum++,false);
         while(s==_Symbol);

         int handle=iCustom(s,_Period,"i-Thread",
                            GlobalVarPrefix,t,_Symbol,PatternLen,
                            PatternLen+t*HistPartLen,HistPartLenPlus);

         if(handle==INVALID_HANDLE) return(1);
         Print("Indicator created, pair ",s,", handle ",handle);
        }
     }

   return(0);
  }
//+------------------------------------------------------------------+
void OnTick()
  {
   TicksStart=GetTickCount();

   // Fill in the sequence with the last bars
   while(CopyClose(_Symbol,_Period,0,PatternLen,CurPattern)<PatternLen) Sleep(1000);

   // If there is an open position, measure its "age"
   // and modify the forecast range for the remaining 
   // planned life time of the deal
   CalcPrognozeLen();

   // Find the most similar sequence in the history
   // and the forecast of the movement of its price on its basis
   FindHistoryPrognoze();

   // Perform the necessary trade actions
   Trade();

   TicksEnd=GetTickCount();
   // Debugging information in
   PrintReport();
  }
//+------------------------------------------------------------------+
void FindHistoryPrognoze()
  {
   Prognoze=0;
   double MaxRating;

   if(Threads>1)
     {
      //--------------------------------------
      // USE COMPUTATIONAL INDICATORS
      //--------------------------------------
      // Look through all of the computational indicators 
      for(int t=0; t<Threads; t++)
        {
         // Send the parameters of the computational task
         SetParam(t,"PrognozeLen",ExistsPrognozeLen);
         // "Begin computations" signal 
         SetParam(t,"Query");
        }

      for(int t=0; t<Threads; t++)
        {
         // Wait for results
         while(!ParamExists(t,"Answer"))
            Sleep(100);
         DelParam(t,"Answer");

         // Obtain results
         double progn        = GetParam(t, "Prognoze");
         double rating       = GetParam(t, "Rating");
         datetime time[];
         int start=GetParam(t,"PatternStart");
         CopyTime(_Symbol,_Period,start,1,time);
         Calc [t].prognoze   = progn;
         Calc [t].rating     = rating;
         Calc [t].start_time = time[0];
         Calc [t].ts         = GetParam(t, "TS");
         Calc [t].te         = GetParam(t, "TE");

         // Select the best result
         if((t==0) || (rating>MaxRating))
           {
            MaxRating = rating;
            Prognoze  = progn;
           }
        }
     }
   else
     {
      //----------------------------
      // INDICATORS ARE NOT USED
      //----------------------------
      // Calculate everything in the EA, into one stream
      FindPrognoze(_Symbol,CurPattern,0,HistoryLen,ExistsPrognozeLen,
                   Prognoze,MaxRating,HistPatternBarStart);
     }
  }
//+------------------------------------------------------------------+
void CalcPrognozeLen()
  {
   ExistsPrognozeLen=PrognozeLen;

   // If there is an opened position, determine 
   // how many bars have passed since its opening
   if(PositionSelect(_Symbol))
     {
      datetime postime=PositionGetInteger(POSITION_TIME);
      datetime curtime,time[];
      CopyTime(_Symbol,_Period,0,1,time);
      curtime=time[0];
      CopyTime(_Symbol,_Period,curtime,postime,time);
      int poslen=ArraySize(time);
      if(poslen<PrognozeLen)
         ExistsPrognozeLen=PrognozeLen-poslen;
      else
         ExistsPrognozeLen=0;
     }
  }
//+------------------------------------------------------------------+
void Trade()
  {

   // Close the open position, if it is against the forecast
   if(PositionSelect(_Symbol))
     {
      long type=PositionGetInteger(POSITION_TYPE);
      bool close=false;
      if((type == POSITION_TYPE_BUY)  && (Prognoze <= 0)) close = true;
      if((type == POSITION_TYPE_SELL) && (Prognoze >= 0)) close = true;
      if(close)
        {
         CTrade trade;
         trade.PositionClose(_Symbol);
        }
     }

   // If there are no position, open one according to the forecast
   if((Prognoze!=0) && (!PositionSelect(_Symbol)))
     {
      CTrade trade;
      if(Prognoze > 0) trade.Buy (Lots);
      if(Prognoze < 0) trade.Sell(Lots);
     }
  }
//+------------------------------------------------------------------+
void PrintReport()
  {
   Print("------------");
   Print("EA: started ",TicksStart,
         ", finished ",TicksEnd,
         ", duration (ms) ",TicksEnd-TicksStart);
   Print("EA: Forecast on ",ExistsPrognozeLen," bars");

   if(Threads>1)
     {
      for(int t=0; t<Threads; t++)
        {
         Print("Indicator ",t+1,
               ": Forecast ", Calc[t].prognoze,
               ", Rating ", Calc[t].rating,
               ", sequence from ",TimeToString(Calc[t].start_time)," in the past");
         Print("Indicator ",t+1,
               ": started ",  Calc[t].ts,
               ", finished ",   Calc[t].te,
               ", duration (ms) ",Calc[t].te-Calc[t].ts);
        }
     }
   else
     {
      Print("Indicators were not used");
      datetime time[];
      CopyTime(_Symbol,_Period,HistPatternBarStart,1,time);
      Print("EA: sequence from ",TimeToString(time[0])," in the past");
     }

   Print("EA: Forecast ",Prognoze);
   Print("------------");
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   // Send the "finish" command to the indicators
   if(Threads>1)
      for(int t=0; t<Threads; t++)
         SetParam(t,"End");
  }
//+------------------------------------------------------------------+

O código do indicador computacional i-Thread, usado pelo Expert Advisor:

//+------------------------------------------------------------------+
//|                                                     i-Thread.mq5 |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1

//--- input parameters
input string VarPrefix;  // Prefix for global variables (analog to MagicNumber)
input int    ThreadNum;  // Core number (so that indicators on different cores could
                        // differentiate their tasks from the tasks of the "neighboring" cores)
input string DataSymbol; // On what pair is the MM-EA working
input int    PatternLen; // Length of the sequence for analysis
input int    BarStart;   // From which bar in the history the search for a similar sequence began
input int    BarCount;   // How many bars of the history to perform a search on

//--- indicator buffers
double Buffer[];
//---
double CurPattern[];

//+------------------------------------------------------------------+
#include <ThreadCalc.mqh>
//+------------------------------------------------------------------+
void OnInit()
  {
   SetIndexBuffer(0,Buffer,INDICATOR_DATA);

   GlobalVarPrefix=VarPrefix;

   // Infinite loop - so that the indicator always "listening", 
   // for new commands from the EA
   while(true)
     {
      // Finish the work of the indicator, if there is a command to finish
      if(ParamExists(ThreadNum,"End"))
         break;

      // Wait for the signal to begin calculations
      if(!ParamExists(ThreadNum,"Query"))
        {
         Sleep(100);
         continue;
        }
      DelParam(ThreadNum,"Query");

      uint TicksStart=GetTickCount();

      // Obtain the parameters of the task
      int PrognozeLen=GetParam(ThreadNum,"PrognozeLen");

      // Fill the sequence from the last bars
      while(CopyClose(DataSymbol,_Period,0,PatternLen,CurPattern)
            <PatternLen) Sleep(1000);

      // Perform calculations
      int HistPatternBarStart;
      double Prognoze,Rating;
      FindPrognoze(DataSymbol,CurPattern,BarStart,BarCount,PrognozeLen,
                   Prognoze,Rating,HistPatternBarStart);

      // Send the results of calculations
      SetParam(ThreadNum,"Prognoze",Prognoze);
      SetParam(ThreadNum,"Rating",Rating);
      SetParam(ThreadNum,"PatternStart",HistPatternBarStart);
      SetParam(ThreadNum,"TS",TicksStart);
      SetParam(ThreadNum,"TE",GetTickCount());
      // Signal "everything is ready"
      SetParam(ThreadNum,"Answer");
     }
  }
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
   // The handler of this event is required
   return(0);
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   SetParam(ThreadNum,"End");
  }
//+------------------------------------------------------------------+

O Expert Advisor e o indicador utilizam uma biblioteca ThreadCalc.mqh comum.

Aqui está seu código:

//+------------------------------------------------------------------+
//|                                                   ThreadCalc.mqh |
//+------------------------------------------------------------------+
string GlobalVarPrefix;
//+------------------------------------------------------------------+
// It finds the price sequence, most similar to the assigned one.
// in the specified range of the history
// Returns the estimation of similarity and the direction 
// of the further changes of prices in history.
//+------------------------------------------------------------------+
void FindPrognoze(
                  string DataSymbol,    // symbol
                  double  &CurPattern[],// current pattern
                  int BarStart,         // start bar
                  int BarCount,         // bars to search
                  int PrognozeLen,      // forecast length

                  // RESULT
                  double  &Prognoze,        // forecast (-,0,+)
                  double  &Rating,          // rating
                  int  &HistPatternBarStart // starting bar of the found sequence
                  ) 
  {

   int PatternLen=ArraySize(CurPattern);

   Prognoze=0;
   if(PrognozeLen<=0) return;

   double rates[];
   while(CopyClose(DataSymbol,_Period,BarStart,BarCount,rates)
         <BarCount) Sleep(1000);

   double rmin=-1;
   // Shifting by one bar, go through all of the price sequences in the history
   for(int bar=BarCount-PatternLen-PrognozeLen; bar>=0; bar--) 
     {
      // Update to eliminate the differences in the levels of price in the sequences
      double dr=CurPattern[0]-rates[bar];

      // Calculate the level of differences between the sequences - as a sum 
      // of squares of price deviations from the sample values
      double r=0;
      for(int i=0; i<PatternLen; i++)
         r+=MathPow(MathAbs(rates[bar+i]+dr-CurPattern[i]),2);

      // Find the sequence with the least difference level
      if((r<rmin) || (rmin<0)) 
        {
         rmin=r;
         HistPatternBarStart   = bar;
         int HistPatternBarEnd = bar + PatternLen-1;
         Prognoze=rates[HistPatternBarEnd+PrognozeLen]-rates[HistPatternBarEnd];
        }
     }
   // Convert the bar number into an indicator system of coordinates
   HistPatternBarStart=BarStart+BarCount-HistPatternBarStart-PatternLen;

   // Convert the difference into the rating of similarity
   Rating=-rmin;
  }
//====================================================================
// A set of functions for easing the work with global variables.
// As a parameter contain the number of computational threads 
// and the names of the variables, automatically converted into unique
// global names.
//====================================================================
//+------------------------------------------------------------------+
string GlobalParamName(int ThreadNum,string ParamName) 
  {
   return GlobalVarPrefix+IntegerToString(ThreadNum)+"_"+ParamName;
  }
//+------------------------------------------------------------------+
bool ParamExists(int ThreadNum,string ParamName) 
  {
   return GlobalVariableCheck(GlobalParamName(ThreadNum,ParamName));
  }
//+------------------------------------------------------------------+
void SetParam(int ThreadNum,string ParamName,double ParamValue=0) 
  {
   string VarName=GlobalParamName(ThreadNum,ParamName);
   GlobalVariableTemp(VarName);
   GlobalVariableSet(VarName,ParamValue);
  }
//+------------------------------------------------------------------+
double GetParam(int ThreadNum,string ParamName) 
  {
   return GlobalVariableGet(GlobalParamName(ThreadNum,ParamName));
  }
//+------------------------------------------------------------------+
double DelParam(int ThreadNum,string ParamName) 
  {
   return GlobalVariableDel(GlobalParamName(ThreadNum,ParamName));
  }
//+------------------------------------------------------------------+

Nosso sistema de comércio, que é capaz de usar mais do que um núcleo de processador em seu trabalho, está pronto!

Quando estiver usando-o, você deve lembrar que neste exemplo utilizamos indicadores CM com loops infinitos.

Se você estiver planejando executar outros programas juntamente com este sistema, no terminal, então você deve ter certeza que estará usando eles em pares de moeda que não estão sendo usados pelos indicadores CM. Uma boa maneira de evitar este conflito é modificar o sistema de maneira que nos parâmetros de entrada do EA MM você possa especificar diretamente os pares de moeda para os indicadores CM.


Medindo a velocidade do trabalho dos EA

Modo regular

Abra o gráfico EURUSD M1 e execute nosso Expert Advisor, criado no capítulo anterior. Nas configurações, especifique o comprimento dos padrões como 24 horas (barras de 1440 min), e a profundidade da busca no histórico como 1 ano (375.000 barras).

Figura 4. Parâmetros de entrada do consultor especialista

Figura 4. Parâmetros de entrada do consultor especialista

Parâmetro "Threads" definido como igual a 1. Isto significa que todos os cálculos do EA serão produzidos em uma thread (em um único núcleo). Enquanto isso, ele não usará indicadores computacionais, mas calculará tudo por si só. Basicamente, pelo princípio de trabalho de um EA regular.

Log de sua execução:

Figura 6. Log do Expert Advisor

Figura 6. Log do Expert Advisor (1 thread)


Modo paralelo

Agora vamos apagar este EA e a posição aberta por ele. Adicione o EA novamente, mas desta vez com o parâmetro "Threads" igual a 2.

Agora o EA precisará criar e usar em seu trabalho 2 indicadores computacionais, ocupando dois núcleos de processador. Log de sua execução:

Figura 7. Log do Expert Advisor (2 threads)

Figura 7. Log do Expert Advisor (2 threads)


Comparação de velocidade

Analisando ambos os registros, concluímos que o tempo aproximado de execução do EA é:

Então, pela paralelização em uma CPU de dois núcleos, fomos capazes de aumentar a velocidade de um EA em 1,9 vezes. Pode-se presumir que quando estamos usando um processador com uma grande quantidade de núcleos, a velocidade de execução aumentará ainda mais, em proporção ao número de núcleos.

Controle da correção do trabalho

Além do tempo de execução, os logs fornecem informações adicionais, que nos permitem verificar se todas as medições foram realizadas corretamente. As linhas do EA: Beginning work ... ending work ... " e "Indicator ...: Beginning work ... ending work ..." mostram que os indicadores iniciaram seus cálculos nem um segundo antes do EA ter dado a eles aquele comando.

Vamos também verificar se não há violações na estratégia de comércio durante a execução do EA no modo paralelo. De acordo com os logs, fica claro que a execução do EA em modo paralelo foi realizada quase imediatamente após sua execução em modo regular. Ou seja, as situações de mercado, em ambos os casos, foram similares. Os logs mostram que as datas, encontradas no histórico de padrões, em ambos os casos foram bastante similares. Então tudo está bem: o algoritmo da estratégia funciona em ambos os casos igualmente bem.

Aqui estão os padrões, descritos nas situações de logs. Esta foi a situação de mercado atual (comprimento - barras de 1440 minutos) no tempo de execução do EA no modo regular:

Figura 8. Situação atual do mercado

Figura 8. Situação atual do mercado

O EA encontrou no histórico o seguinte padrão similar:

Figura 9. Situação de mercado similar

Figura 9. Situação de mercado similar

Quando executamos o EA no modo paralelo, o mesmo padrão foi encontrado pelo "Indicador 1". O "Indicador 2", como segue no log, estava buscando padrões na outra metade do ano do histórico, então encontrou um padrão similar diferente.

Figura 10. Situação de mercado similar

Figura 10. Situação de mercado similar

E isto é como estavam as variáveis globais no MetaTrader 5 durante o trabalho do EA em modo paralelo:

Figura 11. Variáveis globais

Figura 11. Variáveis globais

A troca de dados entre o EA e os indicadores por meio de variáveis globais foi implementada com sucesso.


Conclusão

Neste estudo, descobrimos que é possível paralelizar algoritmos com muitos recursos pelos meios padrão no MetaTrader 5. E descobrimos que a solução para este problema é adequada para uso conveniente em estratégias de comércio no mundo real.

Este programa, em um sistema de múltiplos núcleos, realmente funciona de modo proporcionalmente mais rápido. O número de núcleos em um processador cresce a cada ano, e é bom que os negociantes, que usam MetaTrader, tenham a oportunidade de usar eficazmente estes recursos de hardware. Podemos criar com segurança mais estratégias de comércio com muitos recursos, que ainda serão capazes de analisar o mercado em tempo real.