Cálculos paralelos en MetaTrader 5

ds2 | 1 abril, 2014


Introducción al paralelismo de procesadores

Casi todos los PCs modernos pueden realizar múltiples tareas al mismo tiempo; debido a la presencia de múltiples núcleos en el procesador. Su número aumenta cada año; 2, 3, 4, 6 núcleos... Intel ha presentado recientemente un procesador funcional experimental de 80 núcleos (sí, no es una errata; ochenta núcleos; por desgracia, este ordenador no aparecerá en las tiendas, ya que este procesador fue creado con el único objetivo de estudiar las capacidades potenciales de la tecnología).

No todos los usuarios (y ni siquiera los programadores novatos) entienden cómo funciona. Por eso, seguro que alguien hará la pregunta: ¿por qué necesitamos un procesador con tantos núcleos, si antes (con un solo núcleo), los ordenadores ejecutaban varios programas al mismo tiempo y todos funcionaban? En realidad, esto no es así. Veamos el siguiente diagrama.

Figura 1. Ejecución paralela de aplicaciones

Figura 1. Ejecución paralela de aplicaciones

El caso A del diagrama muestra lo que ocurre cuando se ejecuta un solo programa con un procesador de un solo núcleo. El procesador dedica todo su tiempo a su implementación, y el programa lleva a cabo cierta cantidad de trabajo en el transcurso del tiempo T.

En el caso B; hay 2 programas en ejecución. Pero se dispone el procesador de tal modo que materialmente, en cualquier punto del tiempo, su núcleo solo puede ejecutar un comando, con lo cual tiene que estar alternando constantemente entre dos programas: ejecutará una parte del primero, luego del segundo, etc. Esto ocurre muy rápido, muchas veces por segundo, de modo que parece que el procesador ejecuta ambos programas al mismo tiempo. De hecho, su ejecución requiere el doble del tiempo requerido para la ejecución de cada programa por separado.

El caso C muestra que este problema se resuelve con eficacia si el número de núcleos en el procesador corresponde el número de programas en ejecución. Cada programa dispone de un núcleo por separado, y la velocidad de ejecución aumenta, al igual que en el caso A.

El caso D responde a la creencia errónea de muchos usuarios. Los que piensan que si un programa se ejecuta en un procesador de núcleo múltiple, su ejecución será varias veces más rápida. En general, esto no es cierto, puesto que el procesador no es capaz de dividir de forma independiente el programa en partes separadas, y ejecutarlas todas a la vez.

Por ejemplo, si el programa pide primero la contraseña, y después realiza la comprobación de la misma, sería inaceptable realizar la solicitud de la contraseña en un núcleo y la comprobación en otro, al mismo tiempo. Simplemente, la comprobación nunca podrá realizarse de manera satisfactoria, ya que en el momento de la creación de la contraseña, esta última aún no estaba introducida.

El procesador no conoce todos los diseños que ha implementado el programador, ni toda la lógica de funcionamiento del programa, y por consiguiente, no puede separar el programa entre los núcleos de forma independiente. Así que, si utilizamos un solo programa en un sistema de núcleo múltiple, solo usará un núcleo, y se ejecutará con la misma velocidad de un procesador de un solo núcleo.

En el caso E se explica lo que hay que hacer para que el programa haga uso de todos los núcleos y se ejecute más rápido. Puesto que el programador conoce la lógica del programa, y durante su desarrollo, debe marcar de algún modo las partes del programa que se puedan ejecutar al mismo tiempo. Durante su ejecución, el programa comunicará esta información al procesador, y este último asignará al programa el número requerido de núcleos.


Paralelismo en MetaTrader

En el capítulo anterior, hemos averiguado lo que hay que hacer para poder utilizar todos los núcleos de la CPU y acelerar la ejecución de los programas: tenemos que asignar el código del programa que se pueda poner en paralelo en subprocesos independientes. En muchos lenguajes de programación, existen clases u operadores especiales para este propósito. Pero el lenguaje MQL5 no incorpora este tipo de herramientas. ¿Qué podemos hacer entonces?

Se puede resolver este problema de dos maneras:

1. Usar un archivo DLL 2. Usar recursos de MQL que no se basan en lenguajes de programación
Mediante la creación de un archivo DLL en un lenguaje que integra la herramienta de paralelización, obtendremos también la paralelización en MQL5. Según la información proporcionada por los desarrolladores de MetaTrader, el cliente de terminal tiene una arquitectura con varios subprocesos. De modo que, y bajo ciertas condiciones, los datos entrantes del mercado se procesan en unos subprocesos independientes. Por tanto, si podemos encontrar el modo de separar el código de nuestro programa a un número de Expert Advisors o indicadores, entonces MetaTrader podrá utilizar un número de núcleos de la CPU para su ejecución.


No vamos a discutir el primer método en este artículo. Está claro que podemos implementar todo lo que queramos en un archivo DLL. Trataremos de encontrar una solución, que implique solamente los procedimientos estándar de MQL5 y que no requieren el uso de ningún otro lenguaje fuera de MQL5.

De modo que nos centraremos más en la segunda manera. Vamos a tener que llevar a cabo una serie de pruebas para averiguar exactamente cómo soporta MetaTrader los procesadores de núcleos múltiples. Para hacer esto, vamos a crear un indicador de prueba y un Expert Advisor de prueba, que llevarán a cabo cualquier tarea en curso que pueda cargar en gran medida a la CPU.

He escrito el siguiente 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);   
  }
//+------------------------------------------------------------------+

Y un EA e-flood análogo a él:

//+------------------------------------------------------------------+
//|                                                      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");
  }
//+------------------------------------------------------------------+

Además, abriendo distintas combinaciones de ventanas de gráficos (un gráfico, dos gráficos con el mismo símbolo, dos gráficos con distintos símbolos), y adjuntándoles una o dos copias de este indicador o EA, podemos observar de qué forma usa el terminal los núcleos de la CPU.

Estos indicadores y EA también envían mensajes al registro, y es interesante observar el orden de su aparición. No quiero proporcionar estos registros, ya que puede generarlos por sí mismo, pero en este artículo, nos interesa averiguar cuántos núcleos y con qué combinaciones de gráficos los utiliza el terminal.

Podemos medir el número de núcleos en funcionamiento mediante el "Administrador de tareas" de Windows:

Figura 2. Núcleos de la CPU

Figura 2. Núcleos de la CPU


Los resultados de todas las mediciones están incluidos en la siguiente tabla:

No
de combinación
 Los contenidos del terminal
Uso de la CPU
1
2 indicadores en un gráfico 1 núcleo
2
2 indicadores en distintos gráficos, el mismo par 1 núcleo
3
2 indicadores en distintos gráficos, pares distintos 2 núcleos
4
2 Expert Advisors en el mismo gráfico; este caso es imposible -
5
2 Expert Advisors en distintos gráficos, el mismo par 2 núcleos
6
2 Expert Advisors en distintos gráficos, pares distintos 2 núcleos
7
2 indicadores con pares distintos, creados a partir del EA 2 núcleos


La 7a combinación es una manera muy común de crear un indicador, que se usa en muchas estrategias de trading.

La única peculiaridad es que he creado dos indicadores con dos pares distintos de divisas, ya que las combinaciones 1 y 2 dejan claro que la colocación de los indicadores en el mismo par no tiene sentido. Para esta combinación he utilizado el EA e-flood-starter, que genera dos copias 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";
     }
  }
//+------------------------------------------------------------------+

Por tanto, se han llevado a cabo todos los cálculos de los núcleos, y ahora conocemos las combinaciones en las cuales MetaTader utiliza varios núcleos. A continuación, trataremos de utilizar este conocimiento para implementar el concepto de la computación paralela.


Diseñamos un sistema paralelo

En cuanto al terminal de trading para el sistema paralelo, nos referimos a un conjunto de indicadores o Expert Advisors (o una combinación de ambos) que llevan a cabo algunas tareas comunes entre sí, por ejemplo, realizar una operación o dibujar en el gráfico. Lo que significa que este conjunto funciona como un gran indicador o como un gran EA. Pero al mismo tiempo distribuye la carga computacional entre todos los núcleos del procesador.

Dicho sistema consiste en dos tipos de componentes de software:

Por ejemplo, para un MM EA y un procesador de dos núcleos, este sería el esquema de funcionamiento del sistema:

Figura 3. El esquema del sistema con una CPU de 2 núcleos.

Figura 3. El esquema del sistema con una CPU de 2 núcleos.

Debe entenderse que el sistema que hemos desarrollado no es un programa tradicional, dónde solo puedes llamar al procedimiento necesario en un momento determinado. Los MM y CM son Expert Advisors o indicadores, es decir, que son programas independientes y autónomos. No hay una conexión directa entre ellos, operan de un modo independiente, y no pueden comunicar directamente el uno con el otro.

No comienza la ejecución de cualquiera de estos programas hasta que no aparezca cualquier evento en el terminal (por ejemplo, la llegada de cotizaciones o un tick de temporizador). Y entre los eventos, se deben almacenar todos los datos que quieren intercambiar estos programas fuera, en un lugar con acceso público (lo vamos a llamar "Buffer de intercambio de datos"). De modo que se implementa el esquema anterior en el terminal de la siguiente manera:

Figura 4. Detalles de la implementación

Figura 4. Detalles de la implementación

Para la implementación de este sistema, tenemos que responder a las siguientes preguntas:

Hay más de una respuesta para cada una de estas preguntas, y se proporcionan todas a continuación. En la práctica, se deben seleccionar las opciones específicas en base a unas determinadas circunstancias. Haremos esto en el siguiente capítulo. Mientras tanto, vamos a pensar en todas las respuestas posibles.

Combinación

La Combinación 7 es la más adecuada para un uso práctico regular (todas las demás combinaciones están enumeradas en el capítulo anterior), puesto que no hay que abrir ventanas adicionales en el terminal y colocar en ellas Expert Advisors o indicadores. El sistema entero se encuentra en una sola ventana, y el EA crea todos los indicadores (CM-1 y CM-2) automáticamente. La ausencia de ventanas abiertas o acciones manuales elimina la confusión para el trader, y por tanto, lo relacionado con dichos errores de confusión.

En algunas estrategias de trading, otras combinaciones pueden resultar más útiles. Por ejemplo, podemos crear sistemas de software enteros en base a cualquiera de ellas, que funcionan sobre el principio "cliente-servidor". Dónde los mismos CM serán comunes para varios MM. Además de su papel secundario de "computadores", estos CM comunes pueden llevar a cabo el papel de "servidor" que almacena ciertos tipos de datos unificados para todas las estrategias, así como coordinador de sus tareas colectivas. Por ejemplo, un servidor CM puede controlar de forma centralizada la distribución de los procedimientos en alguna cartera de estrategias y pares de divisas, mientras se mantiene el nivel general deseado del riesgo.

Intercambio de datos

Podemos transmitir datos entre MM y CM mediante cualquiera de estos 3 métodos:

  1. variables globales del terminal;
  2. archivos;
  3. buffers de indicadores.

El primer método es óptimo cuando hay que enviar una pequeña cantidad de variables numéricas. Si hay que enviar datos de texto, habrá que codificarlos de algún modo en números, ya que las variables globales son del tipo double.

El segundo método es otra alternativa, ya que se puede escribir cualquier cosa en un archivo. Se trata de un método muy práctico (y posiblemente más rápido que el primero) para los casos en los cuales tiene que enviar grandes cantidades de datos.

El tercer método es adecuado en el caso de que MM y CM sean indicadores. Solo se pueden enviar datos del tipo double, pero es más práctico enviar grandes matrices numéricas. Pero hay un inconveniente: durante la formación de una barra nueva, se desplaza la numeración de los elementos en los buffers. Debido a que los MM y los CM corresponden a distintos pares de divisas, las nuevas barras no aparecerán al mismo tiempo. Debemos tener en cuenta estos desplazamientos.

Sincronización

Cuando el terminal recibe una cotización para el MM, y empieza a procesarla, no puede enviar el control al CM de forma inmediata. Solo puede (como se muestra en el diagrama anterior) formar una tarea (colocando esta última en las variables globales, un archivo, o un buffer de indicador), y esperar la ejecución del CM. Puesto que todos los CM corresponden a pares de divisas distintos, el tiempo de espera puede tardar algún tiempo. Esto se debe a que un par puede recibir la cotización, mientras que el otro aún no la ha recibido, y que sólo le llegará después de algunos segundos o incluso minutos (por ejemplo, esto puede ocurrir de noche con los pares no líquidos).

Por tanto, para controlar el CM, no debemos usar los eventos OnTick y OnCalculate, que dependen de las cotizaciones. En su lugar, tenemos que utilizar el evento OnTimer (innovación de MQL5), que se ejecuta a una frecuencia determinada (por ejemplo, 1 segundo). En este caso, se limitan mucho los retardos en el sistema.

Además, en lugar de OnTimer, podemos utilizar la técnica de los bucles: es decir, la colocación de un bucle infinito para el CM en OnInit o OnCalculate. Cada una de sus iteraciones es análoga al tick del temporizador.

Aviso. He realizado algunas pruebas y me he dado cuenta que mediante la combinación 7, el evento Ontimer no funciona con los indicadores (por algún motivo), aunque los temporizadores se crearon satisfactoriamente.

También debe tener cuidado con los bucles infinitos en OnInit y OnCalculate: si un solo indicador CM se encuentra en el mismo par de divisas que el EA MM, el precio deja de moverse en el gráfico, y el EA deja de funcionar (deja de generar los eventos OnTick). Los desarrolladores del terminal han explicado los motivos de este comportamiento.

Según los desarrolladores: los scripts y Expert Advisors funcionan en sus propios subprocesos independientes, mientras que todos los indicadores, con el mismo símbolo, funcionan en el mismo subproceso. Todas las demás acciones sobre el símbolo se ejecutan también de forma consecutiva en el mismo flujo que los indicadores: el procesamiento de los ticks, la sincronización del historial, y el cálculo de los indicadores. Por tanto, si el indicador lleva a cabo una acción ilimitada, no se ejecutará ningún otro evento de su símbolo.

Programa Ejecución Observación
Script En su propio subproceso, hay tantos subprocesos de ejecución como scripts Un script que está realizando un bucle no puede interrumpir el funcionamiento de otros programas.
Expert Advisor En su propio subproceso, hay tantos subprocesos de ejecución como Expert Advisors Un script que está realizando un bucle no puede interrumpir el funcionamiento de otros programas.
Indicador Un subproceso de ejecución para todos los indicadores en un símbolo. Hay tantos subropcesos de ejecución que símbolos con indicadores Un bucle infinito en un indicador interrumpirá el funcionamiento de todos los demás indicadores en este símbolo.


Crear un Expert Advisor de prueba

Vamos a elegir una estrategia de trading, cuya paralelización tendría sentido, y un algoritmo adecuado para la misma.

Por ejemplo, esta puede ser una estrategia sencilla: compilar la secuencia desde las últimas N barras, y encontrar la secuencia que más se le parece en el historial. Conociendo el movimiento del precio en el historial, abrimos la transacción pertinente.

Si la longitud de la secuencia es relativamente pequeña, esta estrategia funcionará muy rápidamente en MetaTrade 5; en unos segundos. Pero si tomamos una longitud grande, por ejemplo, todas las barras del período de tiempo M1 para las últimas 24 horas (que serían 1440 barras), y si retrocedemos hasta un año en el historial (sobre las 375.000 barras), hará falta mucho tiempo. Sin embargo, se puede poner esta búsqueda fácilmente en paralelo: basta con dividir el historial en partes iguales entre el número de núcleos disponibles, y asignar a cada núcleo la búsqueda en una parte determinada.

Los parámetros del sistema paralelo son los siguientes:

Para facilitar el desarrollo y el posterior uso, crearemos el EA de modo que, según la configuración, pueda funcionar como un EA paralelo (con cálculos en los indicadores), y como es habitual (es decir, sin el uso de indicadores). Este es el código del Expert Advisor obtenido 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");
  }
//+------------------------------------------------------------------+

El código del indicador computacional i-Thread, usado por el 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");
  }
//+------------------------------------------------------------------+

El Expert Advisor y el indicador usan la librería común ThreadCalc.mqh.

Este es su 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));
  }
//+------------------------------------------------------------------+

Nuestro sistema de trading, capaz de utilizar más de un núcleo del procesador en su funcionamiento, ¡ya está listo!

Al utilizarlo, debe recordar que en este ejemplo hemos utilizado indicadores CM con bucles infinitos.

Si está pensando ejecutar otros programas en el terminal, junto con este sistema, debe asegurarse de utilizarlos con pares de divisas distintos a los pares que utilizan los indicadores CM. Una buena manera de evitar este conflicto es modificar el sistema, de modo que en los parámetros de entrada del EA MM pueda indicar los pares de divisas de los indicadores CM directamente en los parámetros de entrada.


Medición de la velocidad de funcionamiento del Expert Advisor

Modo normal

Abrimos el gráfico EURUSD M1 e iniciamos nuestro Expert Advisor, creado en el capítulo anterior. En la configuración, indicamos la longitud de los gráficos de 24 horas (barras de 1 440 min), y la profundidad de búsqueda en el historial de un año (375 000 barras).

Figura 4. Parámetros de entrada del Expert Advisor

Figura 4. Parámetros de entrada del Expert Advisor

Se asigna el valor 1 al parámetro "Threads" (subprocesos). Esto significa que todos los cálculos del EA se van a realizar en un subproceso (en un solo núcleo). Sin embargo, no utilizará los indicadores computacionales, sino que calculará todo por sí mismo. Básicamente, siguiendo los principios de un EA normal.

El registro de su ejecución:

Figura 6. Registro del Expert Advisor

Figura 6. Registro del Expert Advisor (1 subproceso)


Modo paralelo

Ahora vamos a eliminar este EA y la posición que ha abierto. Añadimos el EA de nuevo, pero esta vez, con el parámetro "Threads" igual a 2.

Ahora, el EA tiene que crear y utilizar 2 indicadores computacionales para su funcionamiento, ocupando dos núcleos del procesador. El registro de su ejecución:

Figura 7. Registro del Expert Advisor (2 subprocesos)

Figura 7. Registro del Expert Advisor (2 subprocesos)


Comparación de velocidades

Analizando estos dos registros, hemos concluido que el tiempo de ejecución aproximado del EA es:

Por tanto, al realizar la paralelización en una CPU de 2 núcleos, hemos sido capaces de aumentar la velocidad de un EA por 1,9 veces. Se puede afirmar que la utilización de un procesador con múltiples núcleos, aumentará la velocidad de ejecución todavía más, proporcionalmente al número de núcleos.

Control de la veracidad del funcionamiento

Además del tiempo de ejecución, los registros proporcionan informaciones adicionales, que nos permiten comprobar que todas las medidas se hicieron correctamente. Las líneas EA: Beginning work ... ending work ... " y "Indicator ...: Beginning work ... ending work ..." indican que los indicadores empezaron sus cálculos en menos de un segundo desde que el EA les envió el comando.

Vamos a comprobar también que no se ha violado la estrategia del trading durante el inicio del EA en el modo paralelo. De acuerdo con los registros, está claro que el inicio del EA en el modo paralelo se hizo casi inmediatamente después de su lanzamiento en el modo normal. Es decir, que la situación del mercado era similar en ambos casos. Los registros muestran que las fechas, que se encuentran en el historial de los gráficos, eran muy parecidas en ambos casos. Así que todo está bien: el algoritmo de la estrategia funciona igual de bien en ambos casos.

Estos son los patrones, descritos en los registros de las condiciones del mercado. Esta es la situación actual del mercado (longitud 1 440 barras de minutos) en el momento de la ejecución del EA en el modo normal:

Figura 8. Situación actual del mercado

Figura 8. Situación actual del mercado

El EA ha encontrado el siguiente patrón similar en el historial:

Figura 9. Situación similar del mercado

Figura 9. Situación similar del mercado

Al ejecutar el EA en el modo paralelo, se obtiene el mismo patrón mediante el "Indicador 1". El "Indicador 2", tal y como se ha expuesto en el registro, estaba buscando patrones en la otra mitad del año del historial, por lo que encontró otro patrón similar.

Figura 10. Situación similar del mercado

Figura 10. Situación similar del mercado

Y estas son las variables globales en MetaTrader 5 durante el funcionamiento del EA en el modo paralelo:

Figura 11. Variables globales

Figura 11. Variables globales

El intercambio de datos entre el EA y los indicadores mediante las variables globales se ha implementado satisfactoriamente.


Conclusión

En este artículo, hemos averiguado que es posible poner en paralelo algoritmos potentes mediante los procedimientos estándar de MetaTrader 5. Y la solución encontrada a esta cuestión es apropiada para un uso cómodo con las estrategias de trading en situaciones reales.

En un sistema de varios núcleos, este programa funciona realmente de un modo proporcionalmente más rápido. El número de núcleos de los procesadores aumenta cada año, y es una verdadera ventaja el hecho de que los traders, que usan MetaTrader, tengan la posibilidad de utilizar estos recursos de hardware de manera eficiente. Podemos crear de manera segura unas estrategias de trading más potentes, que seguirán siendo capaces de analizar el mercado en tiempo real.