Trabajando con las series temporales en la biblioteca DoEasy (Parte 41): Ejemplo de indicador de símbolo y periodo múltiples

25 agosto 2020, 09:57
Artyom Trishkin
0
551

Contenido


Concepto

A lo largo de los dos artículos anteriores, hemos desarrollado la capacidad de la biblioteca para trabajar con indicadores. Concretamente, hemos organizado la descarga correcta de los datos históricos y la actualización en tiempo real de los datos actuales para las series temporales de la biblioteca. En el artículo anterior, para mostrar los datos en la pantalla, ubicamos los búferes de indicador en una estructura de datos. Una sola estructura describe un búfer de indicador dibujado. Si planeamos tener en el indicador muchos búferes a dibujar, cada uno de ellos se definirá como una estructura, y cada una de estas se ubicará en una matriz.

Hoy, continuaremos detallando el concepto de trabajo con los búferes de los indicadores en las estructuras, y también crearemos un indicador de símbolo y periodo múltiples que dibuje en su subventana y en forma de velas japonesas el gráfico de precios de una de las parejas de divisas seleccionadas con el periodo del gráfico elegido. Así, entenderemos de forma gradual la necesidad de crear las clases de los búferes de indicador.

En la biblioteca hay una clase de mensajes que permite seleccionar el idioma de los mensajes mostrados por al biblioteca, y también añadir con facilidad cualquier número de idioma de usuario para mostrar los mensajes de la bilioteca en uno de ellos. Pero, hasta el momento, no existe la selección del idioma para traducir las descripciones de los parámetros de entrada: todas las descripciones de los parámetros de entrada después de realizar la compilación se muestran solo en el idioma en que el usuario ha escrito el texto para describir el parámetro de entrada en su programa.
Aquí, a la hora de crear la posibilidad de seleccionar el idioma en el que se escribirán las variables de entrada del programa, no tenemos libertad de elegir: o bien un único idioma, o bien crear el mismo conjunto de parámetros de entrada para cada uno de los idiomas necesarios para la compilación.

Vamos a elegir la segunda opción y crear una archivo aparte, en el que ubicaremos todas las enumeraciones necesarias para las dos variantes de idioma de escritura de las constantes de las enumeraciones. De esta forma, el usuario, para corregir los errores de escritura de las constantes de las enumeraciones, deberá traducir por sí mismo las descripciones de las constantes del ruso al idioma que necesite. El inglés, como idioma requerido para publicar los productos en el servicio Mercado, debe quedarse siempre en uso.

Mejorando las clases y los datos de la biblioteca

Vamos a realizar una cierta reestructuración de la ubicación de los datos en los archivos de las bibliotecas.

La estructura utilizada para transmitir desde el indicador a la biblioteca los datos de la barra actual del manejador OnCalculate()
se encuentra en el archivo \MQL5\Include\DoEasy\Defines.mqh.

//+------------------------------------------------------------------+
//| Structures                                                       |
//+------------------------------------------------------------------+
struct SDataCalculate
  {
   int         rates_total;                                 // size of input timeseries
   int         prev_calculated;                             // number of handled bars at the previous call
   int         begin;                                       // where significant data starts
   double      price;                                       // current array value for calculation
   MqlRates    rates;                                       // Price structure
  } rates_data;
//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+

Sin embargo, esta estructura no pertenece a las variables predeterminadas y las magnitudes estáticas, se adapta más a la definición de "Datos". Por eso, la eliminaremos de Defines.mqh y la definiremos en el archivo \MQL5\Include\DoEasy\Datas.mqh:

//+------------------------------------------------------------------+
//|                                                        Datas.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/es/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/es/users/artmedia70"
//+------------------------------------------------------------------+
//| Archivos de inclusión                                            |
//+------------------------------------------------------------------+
#include "InpDatas.mqh"
//+------------------------------------------------------------------+
//| Macrosustituciones                                               |
//+------------------------------------------------------------------+
#define INPUT_SEPARATOR                (",")    // Separator in the inputs string
#define TOTAL_LANG                     (2)      // Number of used languages
//+------------------------------------------------------------------+
//| Structures                                                       |
//+------------------------------------------------------------------+
struct SDataCalculate
  {
   int         rates_total;                     // size of input timeseries
   int         prev_calculated;                 // number of handled bars at the previous call
   int         begin;                           // where significant data starts
   double      price;                           // current array value for calculation
   MqlRates    rates;                           // Price structure
  } rates_data;
//+------------------------------------------------------------------+
//| Arrays                                                           |
//+------------------------------------------------------------------+
string            ArrayUsedSymbols[];           // Array of used symbols' names
ENUM_TIMEFRAMES   ArrayUsedTimeframes[];        // Array of used timeframes
//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+ 
//| Data sets                                                        |
//+------------------------------------------------------------------+

Ya mencionamos antes un archivo aparte con las enumeraciones para las variables de entrada de los programas. No lo hemos creado por el momento, pero sí que hemos escrito aquí su inclusión, para no tener que editar de nuevo el archivo Datas.mqh.

Asimismo, hemos añadido dos matrices al nuevo bloque para las matrices: dichas matrices estarán disponibles desde el programa que funcione usando como base la biblioteca, y contendrán las listas de los símbolos y marcos temporales utilizados y seleccionados en los parámetros de entrada del programa.

Ahora, vamos a crear el nuevo archivo \MQL5\Include\DoEasy\InpDatas.mqh, encargado de guardar las enumeraciones para las variables de entrada del programa:

//+------------------------------------------------------------------+
//|                                                     InpDatas.mqh |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/es/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/es/users/artmedia70"
//+------------------------------------------------------------------+
//| Macrosustituciones                                               |
//+------------------------------------------------------------------+
//#define COMPILE_EN // Comment out the string for compilation in Russian 
//+------------------------------------------------------------------+
//| Input enumerations                                               |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| English language inputs                                          |
//+------------------------------------------------------------------+
#ifdef COMPILE_EN
//+------------------------------------------------------------------+
//| Modes of working with symbols                                    |
//+------------------------------------------------------------------+
enum ENUM_SYMBOLS_MODE
  {
   SYMBOLS_MODE_CURRENT,                              // Work only with the current Symbol
   SYMBOLS_MODE_DEFINES,                              // Work with a given list of Symbols
   SYMBOLS_MODE_MARKET_WATCH,                         // Working with Symbols from the "Market Watch" window
   SYMBOLS_MODE_ALL                                   // Work with a complete list of Symbols
  };
//+------------------------------------------------------------------+
//| Mode of working with timeframes                                  |
//+------------------------------------------------------------------+
enum ENUM_TIMEFRAMES_MODE
  {
   TIMEFRAMES_MODE_CURRENT,                           // Work only with the current timeframe
   TIMEFRAMES_MODE_LIST,                              // Work with a given list of timeframes
   TIMEFRAMES_MODE_ALL                                // Work with a complete list of timeframes
  };
//+------------------------------------------------------------------+
//| Russian language inputs                                          |
//+------------------------------------------------------------------+
#else  
//+------------------------------------------------------------------+
//| Modes of working with symbols                                    |
//+------------------------------------------------------------------+
enum ENUM_SYMBOLS_MODE
  {
   SYMBOLS_MODE_CURRENT,                              // Работа только с текущим символом
   SYMBOLS_MODE_DEFINES,                              // Работа с заданным списком символов
   SYMBOLS_MODE_MARKET_WATCH,                         // Работа с символами из окна "Обзор рынка"
   SYMBOLS_MODE_ALL                                   // Работа с полным списком символов
  };
//+------------------------------------------------------------------+
//| Mode of working with timeframes                                  |
//+------------------------------------------------------------------+
enum ENUM_TIMEFRAMES_MODE
  {
   TIMEFRAMES_MODE_CURRENT,                           // Работа только с текущим таймфреймом
   TIMEFRAMES_MODE_LIST,                              // Работа с заданным списком таймфреймов
   TIMEFRAMES_MODE_ALL                                // Работа с полным списком таймфреймов
  };
#endif 
//+------------------------------------------------------------------+

Aquí, todo es muy sencillo: establecemos la macrosustitución, y si existe, la compilación se realizará con las enumeraciones cuyas constantes están anotadas en inglés. Si la macrosustitución no existe (la línea con su declaración está comentada), la compilación se realizará con las enumeraciones cuyas constantes están anotadas en ruso (o en cualquier otro idioma con el que el usuario haya corregido las descripciones en ruso de las constantes de las variables).

A este archivo añadiremos las nuevas enumeraciones, a medida que sean necesarias.

Introducimos en el archivo \MQL5\Include\DoEasy\Objects\Series\TimeSeriesDE.mqh de la clase CTimeSeries la corrección del error en el método encargado de añadir el objeto de todas las series temporales del símbolo a la lista, que a veces provocaba el error de invocación de un puntero inexistente:

//+------------------------------------------------------------------+
//| Add the specified timeseries list to the list                    |
//+------------------------------------------------------------------+
bool CTimeSeriesDE::AddSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0)
  {
   bool res=false;
   CSeriesDE *series=new CSeriesDE(this.m_symbol,timeframe,required);
   if(series==NULL)
      return res;
   this.m_list_series.Sort();
   if(this.m_list_series.Search(series)==WRONG_VALUE)
      res=this.m_list_series.Add(series);
   if(!res)
      delete series;
   series.SetAvailable(true);
   return res;
  }
//+------------------------------------------------------------------+

Tras obtener el error de adición del objeto a la lista, eliminamos el objeto "series" creado, y después intentamos obtener acceso al mismo para establecer la bandera sobre su uso. En esta situación, obtendremos error, ya que el puntero al objeto ya ha sido eliminado.

Para corregirlo, pondremos el establecimiento de la bandera en el código antes de la comprobación del resultado de la adición de un objeto a la lista:

   if(this.m_list_series.Search(series)==WRONG_VALUE)
      res=this.m_list_series.Add(series);
   series.SetAvailable(true);
   if(!res)
      delete series;
   return res;
  }
//+------------------------------------------------------------------+

En los métodos de actualización de la lista de serie temporal indicada y de todas las listas de series temporales, no siempre era posible ubicar el evento "Nueva barra" en la lista de eventos con la hora correcta del evento (hora de apertura de una nueva barra). En algunas situaciones, la hora era igual a cero.

Para corregir este punto, vamos a crear una nueva variable para guardar la hora, y si el tipo del programa es "indicador" y el trabajo se da en el símbolo y periodo actual del gráfico, registraremos la hora de la estructura de precios obtenidos OnCalculate(), de lo contrario, obtendremos la hora a partir del valor retornado por el método LastBarDate() del objeto de serie temporal. La hora obtenida la usaremos al añadir un evento a la lista con todos los eventos del objeto de todas las series temporales del símbolo:

//+------------------------------------------------------------------+
//| Update a specified timeseries list                               |
//+------------------------------------------------------------------+
void CTimeSeriesDE::Refresh(const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
  {
//--- Reset the timeseries event flag and clear the list of all timeseries events
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- Get the timeseries from the list by its timeframe
   CSeriesDE *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe));
   if(series_obj==NULL || series_obj.DataTotal()==0 || !series_obj.IsAvailable())
      return;
//--- Update the timeseries list
   series_obj.Refresh(data_calculate);
   datetime time=
     (
      this.m_program==PROGRAM_INDICATOR && series_obj.Symbol()==::Symbol() && series_obj.Timeframe()==(ENUM_TIMEFRAMES)::Period() ? 
      data_calculate.rates.time : 
      series_obj.LastBarDate()
     );
//--- If the timeseries object features the New bar event
   if(series_obj.IsNewBar(time))
     {
      //--- send the "New bar" event to the control program chart
      series_obj.SendEvent();
      //--- set the values of the first date in history on the server and in the terminal
      this.SetTerminalServerDate();
      //--- add the "New bar" event to the list of timeseries events
      //--- in case of successful addition, set the event flag for the timeseries
      if(this.EventAdd(SERIES_EVENTS_NEW_BAR,time,series_obj.Timeframe(),series_obj.Symbol()))
         this.m_is_event=true;
     }
  }
//+------------------------------------------------------------------+
//| Update all timeseries lists                                      |
//+------------------------------------------------------------------+
void CTimeSeriesDE::RefreshAll(SDataCalculate &data_calculate)
  {
//--- Reset the flags indicating the necessity to set the first date in history on the server and in the terminal
//--- and the timeseries event flag, and clear the list of all timeseries events
   bool upd=false;
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- In the loop by the list of all used timeseries,
   int total=this.m_list_series.Total();
   for(int i=0;i<total;i++) 
     {
      //--- get the next timeseries object by the loop index
      CSeriesDE *series_obj=this.m_list_series.At(i);
      if(series_obj==NULL || !series_obj.IsAvailable() || series_obj.DataTotal()==0)
         continue;
      //--- update the timeseries list
      series_obj.Refresh(data_calculate);
      datetime time=
        (
         this.m_program==PROGRAM_INDICATOR && series_obj.Symbol()==::Symbol() && series_obj.Timeframe()==(ENUM_TIMEFRAMES)::Period() ? 
         data_calculate.rates.time : 
         series_obj.LastBarDate()
        );
      //--- If the timeseries object features the New bar event
      if(series_obj.IsNewBar(time))
        {
         //--- send the "New bar" event to the control program chart,
         series_obj.SendEvent();
         //--- set the flag indicating the necessity to set the first date in history on the server and in the terminal
         upd=true;
         //--- add the "New bar" event to the list of timeseries events
         //--- in case of successful addition, set the event flag for the timeseries
         if(this.EventAdd(SERIES_EVENTS_NEW_BAR,time,series_obj.Timeframe(),series_obj.Symbol()))
            this.m_is_event=true;
        }
     }
//--- if the flag indicating the necessity to set the first date in history on the server and in the terminal is enabled,
//--- set the values of the first date in history on the server and in the terminal
   if(upd)
      this.SetTerminalServerDate();
  }
//+------------------------------------------------------------------+

Para actualizar todas las series temporales, debemos separar el lugar desde donde se llama la actualización de la serie temporal para el símbolo actual y los demás. Cualquier otra serie temporal será actualizada en el temporizador, mientras que la serie temporal del símbolo actual la actualizaremos en OnCalculate(). Lo hemos implementado así para no influir en el temporizador de la serie temporal del símbolo actual buscando un nuevo tick, ya que en OnCalculate(), en cualquier caso, se llama la actualización del símbolo actual al llegar un nuevo tick.

Para trabajar en el temporizador, declararemos otro método más en el archivo de la clase de colección de series temporales \MQL5\Include\DoEasy\Collections\TimeSeriesCollection.mqh:

//--- Update (1) the specified timeseries of the specified symbol, (2) all timeseries of a specified symbol,
//--- (3) all timeseries of all symbols, (4) all timeseries except the current one
   void                    Refresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate);
   void                    Refresh(const string symbol,SDataCalculate &data_calculate);
   void                    Refresh(SDataCalculate &data_calculate);
   void                    RefreshAllExceptCurrent(SDataCalculate &data_calculate);

//--- Get events from the timeseries object and add them to the list
   bool                    SetEvents(CTimeSeriesDE *timeseries);

El método llamará a los métodos de actualización de todas las series temporales, exceptuando el símbolo actual (implementación del método):

//+------------------------------------------------------------------+
//| Update all timeseries except the current one                     |
//+------------------------------------------------------------------+
void CTimeSeriesCollection::RefreshAllExceptCurrent(SDataCalculate &data_calculate)
  {
//--- Reset the flag of an event in the timeseries collection and clear the event list
   this.m_is_event=false;
   this.m_list_events.Clear();
//--- In the loop by all symbol timeseries objects in the collection,
   int total=this.m_list.Total();
   for(int i=0;i<total;i++)
     {
      //--- get the next symbol timeseries object
      CTimeSeriesDE *timeseries=this.m_list.At(i);
      if(timeseries==NULL)
         continue;
      //--- if the timeseries symbol is equal to the current chart symbol or
      //--- if there is no new tick on a timeseries symbol, move to the next object in the list
      if(timeseries.Symbol()==::Symbol() || !timeseries.IsNewTick())
         continue;
      //--- Update all symbol timeseries
      timeseries.RefreshAll(data_calculate);
      //--- If the event flag enabled for the symbol timeseries object,
      //--- get events from symbol timeseries, write them to the collection event list
      //--- and set the event flag in the collection
      if(timeseries.IsEvent())
         this.m_is_event=this.SetEvents(timeseries);
     }
  }
//+------------------------------------------------------------------+

En el archivo de funciones de servicio de la biblioteca \MQL5\Include\DoEasy\Services\DELib.mqh, añadimos la función que retorna el número de barras del segundo periodo indicado dentro de una barra del primer periodo indicado del gráfico:

//+-------------------------------------------------------------------------+
//| Return the number of bars of one period in a single bar of another one  |
//+-------------------------------------------------------------------------+
int NumberBarsInTimeframe(ENUM_TIMEFRAMES timeframe,ENUM_TIMEFRAMES period=PERIOD_CURRENT)
  {
   return PeriodSeconds(timeframe)/PeriodSeconds(period==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)Period() : period);
  }
//+------------------------------------------------------------------+

Como la función PeriodSeconds() retorna el número de segundos en el periodo, para definir el número de barras de un periodo (menor) en una barra de otro periodo (mayor), bastará con dividir el número de segundos del periodo mayor por el número de segundos del periodo menor. Precisamente eso hemos hecho aquí.

En nuestros programas, podemos establecer una lista con los símbolos utilizados. Para la biblioteca, dicha lista se establece en el método SetUsedSymbols() de la clase de colección de los símbolos, en el archivo \MQL5\Include\DoEasy\Collections\SymbolsCollection.mqh. Si establecemos en nuestro programa una lista de símbolos utilizados en la que no se encuentre el símbolo actual, la biblioteca creará en la colección de series temporales las series temporales de todos los símbolos indicados en los ajustes, pero el símbolo actual no se encontrará allí. Este es constantemente invocado para calcular el posicionamiento de los datos en la pantalla. Por consiguiente, debemos corregir la falta de indicación del símbolo.

Para ello, añadimos a la lista el símbolo actual en el método SetUsedSymbols() de la clase de colección de los símbolos. El símbolo será añadido, con la condición de que el nombre del símbolo actual no se encuentre en la lista de símbolos de trabajo indicados por el usuario en los ajustes del programa. Si se encuentra allí, no se añadirá un nuevo símbolo con el mismo nombre:
//+------------------------------------------------------------------+
//| Set the list of used symbols                                     |
//+------------------------------------------------------------------+
bool CSymbolsCollection::SetUsedSymbols(const string &symbol_used_array[])
  {
   ::ArrayResize(this.m_array_symbols,0,1000);
   ::ArrayCopy(this.m_array_symbols,symbol_used_array);
   this.m_mode_list=this.TypeSymbolsList(this.m_array_symbols);
   this.m_list_all_symbols.Clear();
   this.m_list_all_symbols.Sort(SORT_BY_SYMBOL_INDEX_MW);
   //--- Use only the current symbol
   if(this.m_mode_list==SYMBOLS_MODE_CURRENT)
     {
      string name=::Symbol();
      ENUM_SYMBOL_STATUS status=this.SymbolStatus(name);
      return this.CreateNewSymbol(status,name,this.SymbolIndexInMW(name));
     }
   else
     {
      bool res=true;
      //--- Use the pre-defined symbol list
      if(this.m_mode_list==SYMBOLS_MODE_DEFINES)
        {
         int total=::ArraySize(this.m_array_symbols);
         for(int i=0;i<total;i++)
           {
            string name=this.m_array_symbols[i];
            ENUM_SYMBOL_STATUS status=this.SymbolStatus(name);
            bool add=this.CreateNewSymbol(status,name,this.SymbolIndexInMW(name));
            res &=add;
            if(!add) 
               continue;
           }
         //--- Create the new current symbol (if it is already in the list, it is not re-created)
         res &=this.CreateNewSymbol(this.SymbolStatus(NULL),NULL,this.SymbolIndexInMW(NULL));
         return res;
        }
      //--- Use the full list of the server symbols
      else if(this.m_mode_list==SYMBOLS_MODE_ALL)
        {
         return this.CreateSymbolsList(false);
        }
      //--- Use the symbol list from the Market Watch window
      else if(this.m_mode_list==SYMBOLS_MODE_MARKET_WATCH)
        {
         this.MarketWatchEventsControl(false);
         return true;
        }
     }
   return false;
  }
//+------------------------------------------------------------------+

En el archivo \MQL5\Include\DoEasy\Engine.mqh del objeto principal de la biblioteca CEngine, declaramos tres métodos privados:

//--- Set the list of used symbols in the symbol collection and create the collection of symbol timeseries
   bool                 SetUsedSymbols(const string &array_symbols[]);
private:
//--- Write all used symbols and timeframes to the ArrayUsedSymbols and ArrayUsedTimeframes arrays
   void                 WriteSymbolsPeriodsToArrays(void);
//--- Check the presence of a (1) symbol in the ArrayUsedSymbols array, (2) the presence of a timeframe in the ArrayUsedTimeframes array
   bool                 IsExistSymbol(const string symbol);
   bool                 IsExistTimeframe(const ENUM_TIMEFRAMES timeframe);
public:
//--- Create a resource file

Los métodos son necesarios para registrar la lista de símbolos y marcos temporales en las matrices declaradas anteriormente en el archivo Datas.mqh, y también para retornar la bandera de existencia de un símbolo en la matriz de nombres de los símbolos utilizados y la bandera de existencia de un marco temporal en la matriz marcos temporales utilizados.

Implementación de los métodos que retornan la presencia de la bandera de un símbolo y marco temporal en las matrices existentes:

//+------------------------------------------------------------------+
//| Check if a symbol is present in the array                        |
//+------------------------------------------------------------------+
bool CEngine::IsExistSymbol(const string symbol)
  {
   int total=::ArraySize(ArrayUsedSymbols);
   for(int i=0;i<total;i++)
      if(ArrayUsedSymbols[i]==symbol)
         return true;
   return false;
  }
//+------------------------------------------------------------------+
//| Check if a timeframe is present in the array                     |
//+------------------------------------------------------------------+
bool CEngine::IsExistTimeframe(const ENUM_TIMEFRAMES timeframe)
  {
   int total=::ArraySize(ArrayUsedTimeframes);
   for(int i=0;i<total;i++)
      if(ArrayUsedTimeframes[i]==timeframe)
         return true;
   return false;
  }
//+------------------------------------------------------------------+

En un ciclo por la matriz existente, obtenemos el siguiente elemento de la matriz y lo comparamos con el valor transmitido al método. Si el valor del elemento de la matriz coincide con el transmitido al método, retornamos true. Al finalizar el ciclo completo, retornamos false: no se han encontrado coincidencias entre los valores de los elementos de la matriz y el valor transmitido al método.

Implementación del método para registrar en las matrices los símbolos y marcos temporales utilizados:

//+------------------------------------------------------------------+
//| Write all used symbols and timeframes                            |
//| to the ArrayUsedSymbols and ArrayUsedTimeframes arrays           |
//+------------------------------------------------------------------+
void CEngine::WriteSymbolsPeriodsToArrays(void)
  {
//--- Get the list of all created timeseries (created by the number of used symbols)
   CArrayObj *list_timeseries=this.GetListTimeSeries();
   if(list_timeseries==NULL)
      return;
//--- Get the total number of created timeseries
   int total_timeseries=list_timeseries.Total();
   if(total_timeseries==0)
      return;
//--- Set the size of the array of used symbols equal to the number of created timeseries, while
//--- the size of the array of used timeframes is set equal to the maximum possible number of timeframes in the terminal
   if(::ArrayResize(ArrayUsedSymbols,total_timeseries,1000)!=total_timeseries || ::ArrayResize(ArrayUsedTimeframes,21,21)!=21)
      return;
//--- Set both arrays to zero
   ::ZeroMemory(ArrayUsedSymbols);
   ::ZeroMemory(ArrayUsedTimeframes);
//--- Reset the number of added symbols and timeframes to zero and,
//--- in a loop by the total number of timeseries,
   int num_symbols=0,num_periods=0;
   for(int i=0;i<total_timeseries;i++)
     {
      //--- get the next object of all timeseries of a single symbol
      CTimeSeriesDE *timeseries=list_timeseries.At(i);
      if(timeseries==NULL || this.IsExistSymbol(timeseries.Symbol()))
         continue;
      //--- increase the number of used symbols and (num_symbols variable), and
      //--- write the timeseries symbol name to the array of used symbols by the num_symbols-1 index
      num_symbols++;
      ArrayUsedSymbols[num_symbols-1]=timeseries.Symbol();
      //--- Get the list of all its timeseries from the object of all symbol timeseries
      CArrayObj *list_series=timeseries.GetListSeries();
      if(list_series==NULL)
         continue;
      //--- In the loop by the total number of symbol timeseries,
      int total_series=list_series.Total();
      for(int j=0;j<total_series;j++)
        {
         //--- get the next timeseries object
         CSeriesDE *series=list_series.At(j);
         if(series==NULL || this.IsExistTimeframe(series.Timeframe()))
            continue;
         //--- increase the number of used timeframes and (num_periods variable), and
         //--- write the timeseries timeframe value to the array of used timeframes by num_periods-1 index
         num_periods++;
         ArrayUsedTimeframes[num_periods-1]=series.Timeframe();
        }
     }
//--- Upon the loop completion, change the size of both arrays to match the exact number of added symbols and timeframes
   ::ArrayResize(ArrayUsedSymbols,num_symbols,1000);
   ::ArrayResize(ArrayUsedTimeframes,num_periods,21);
  }
//+------------------------------------------------------------------+

El método analiza todas las series temporales creadas para cada símbolo utilizado en el programa, y luego rellena las matrices de los símbolos y marcos temporales utilizados con los datos de la colección de series temporales. Todas las líneas del listado del método han sido comentadas con detalle, por lo que esperamos que no requieran de explicaciones adicionales. En cualquier caso, podrán escribir cualquier duda en los comentarios al artículo.

En el bloque de métodos para actualizar las series temporales, añadimos un método para actualizar todas las series temporales, excepto las series temporales del símbolo actual:

//--- Update (1) the specified timeseries of the specified symbol, (2) all timeseries of a specified symbol,
//--- (3) all timeseries of all symbols, (4) all timeseries except the current one
   void                 SeriesRefresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(symbol,timeframe,data_calculate);                          }
   void                 SeriesRefresh(const string symbol,SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(symbol,data_calculate);                                    }
   void                 SeriesRefresh(SDataCalculate &data_calculate)
                          { this.m_time_series.Refresh(data_calculate);                                           }
protected:
   void                 SeriesRefreshAllExceptCurrent(SDataCalculate &data_calculate)
                          { this.m_time_series.GetObject().RefreshAllExceptCurrent(data_calculate);               }
public  
//--- Return (1) the timeseries object of the specified symbol and (2) the timeseries object of the specified symbol/period

Ya hemos comprobado antes que este método es necesario. Aquí, este método simplemente llama al método homónimo de la clase de colección de las series temporales, que ya analizamos anteriormente.

En el temporizador de la clase, en el bloque de procesamiento de la colección de series temporales, llamaremos precisamente a este método para actualizar todas las series temporales excepto la actual:

//+------------------------------------------------------------------+
//| CEngine timer                                                    |
//+------------------------------------------------------------------+
void CEngine::OnTimer(SDataCalculate &data_calculate)
  {
//
// here I have removed some code not needed for the current example
//
   //--- Timeseries collection timer
      index=this.CounterIndex(COLLECTION_TS_COUNTER_ID);
      CTimerCounter* cnt6=this.m_list_counters.At(index);
      if(cnt6!=NULL)
        {
         //--- If the pause is over, work with the timeseries list (update all except the current one)
         if(cnt6.IsTimeDone())
            this.SeriesRefreshAllExceptCurrent(data_calculate);
        }
     }
//--- If this is a tester, work with collection events by tick
   else
     {
//
// here I have removed some code not needed for the current example
//
     }
  }

En el manejador del evento Calculate —en el método OnCalculate() del objeto principal de la biblioteca CEngine—, retornaremos cero en el caso de que no todas las series temporales hayan sido creadas, y rates_total, si se han creado plenamente todas las series temporales utilizadas:

//+------------------------------------------------------------------+
//| Calculate event handler                                          |
//+------------------------------------------------------------------+
int CEngine::OnCalculate(SDataCalculate &data_calculate,const uint required=0)
  {
//--- If this is not an indicator, exit
   if(this.m_program!=PROGRAM_INDICATOR)
      return 0;
//--- Re-create empty timeseries
//--- If at least one of the timeseries is not synchronized, return zero
   if(!this.SeriesSync(data_calculate,required))
      return 0;
//--- Update the timeseries of the current symbol (not in the tester) and
//--- return either 0 (in case there are empty timeseries), or rates_total
   if(!this.IsTester())
      this.SeriesRefresh(NULL,data_calculate);
   return(this.SeriesGetSeriesEmpty()==NULL ? data_calculate.rates_total : 0);
  }
//+------------------------------------------------------------------+

Antes, retornábamos rates_total, transmitido al método mediante la estructura de precios de la barra actual. Pero, para procesar correctamente la sincronización de las series temporales, tenemos que controlar la magnitud retornada del método. Retornamos cero para iniciar el recálculo de toda la historia, y rates_total, solo para calcular los datos no computados (normalmente, o bien 0, el cálculo de la barra actual, o bien 1, el cálculo de las barras anterior y actual en el momento de apertura de una nueva barra).

En el método de creación de todas las series temporales para todos los símbolos utilizados, añadimos el registro en las matrices de todos los símbolos y marcos temporales utilizados:

//+------------------------------------------------------------------+
//| Create all applied timeseries of all used symbols                |
//+------------------------------------------------------------------+
bool CEngine::SeriesCreateAll(const string &array_periods[],const int rates_total=0,const uint required=0)
  {
//--- Set the flag of successful creation of all timeseries of all symbols
   bool res=true;
//--- Get the list of all used symbols
   CArrayObj* list_symbols=this.GetListAllUsedSymbols();
   if(list_symbols==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_GET_SYMBOLS_ARRAY));
      return false;
     }
   //--- In the loop by the total number of symbols
   for(int i=0;i<list_symbols.Total();i++)
     {
      //--- get the next symbol object
      CSymbol *symbol=list_symbols.At(i);
      if(symbol==NULL)
        {
         ::Print(DFUN,"index ",i,": ",CMessage::Text(MSG_LIB_SYS_ERROR_FAILED_GET_SYM_OBJ));
         continue;
        }
      //--- In the loop by the total number of used timeframes,
      int total_periods=::ArraySize(array_periods);
      for(int j=0;j<total_periods;j++)
        {
         //--- create the timeseries object of the next symbol.
         //--- Add the timeseries creation result to the res variable
         ENUM_TIMEFRAMES timeframe=TimeframeByDescription(array_periods[j]);
         res &=this.SeriesCreate(symbol.Name(),timeframe,rates_total,required);
        }
     }
//--- Write all used symbols and timeframes to the ArrayUsedSymbols and ArrayUsedTimeframes arrays
   this.WriteSymbolsPeriodsToArrays();
//--- Return the result of creating all timeseries for all symbols
   return res;
  }
//+------------------------------------------------------------------+

Después de llamar a este método desde la función de inicialización de la biblioteca en el programa, la llamada de este método prepara dos matrices (en caso necesario) para su uso en los programas: la matriz de todos los símbolos utilizados y la matriz de todos los marcos temporales utilizados. Ya hemos analizado el método más arriba.

Con esto, damos por finalizada la mejora de las clases de la biblioteca.

Hoy, vamos crear un indicador de prueba en el que comprobaremos el funcionamiento de la biblioteca en los indicadores con los modos de símbolo y periodo múltiples.
En dicho indicador, tendremos la posibilidad de establecer cuatro símbolos a utilizar, además de todas las series temporales posibles. La selección del símbolo y el marco temporal con el que trabaja el indicador en un momento, la implementaremos con la ayuda de botones. En el gráfico se destacarán hasta cuatro botones con las denominaciones de los símbolos establecidos en los ajustes, y enfrente del símbolo, un botón con el que, en posición pulsada, mostraremos la lista de botones con los marcos temporales disponibles.
Solo podremos tener al mismo tiempo en estado pulsado un botón del símbolo y un botón del marco temporal de dicho símbolo.

De esta forma, podremos seleccionar el símbolo con el que trabajará el indicador, así como el marco temporal cuyos datos se representarán en el gráfico, en la subventana del indicador. Adelantándonos un poco, diremos que ha resultado un tanto incómodo escribir el proceso de trabajo con los botones en el estilo de procedimental. Y el código de control de los estados de los botones no ha resultado óptimo. Pero, a la hora de poner a prueba el funcionamiento de las series temporales en un indicador de símbolo y periodo múltiples, el código usado para trabajar con los botones ocupará un segundo plano, sobre todo teniendo en cuenta que se trata de un indicador de prueba.

Creando el indicador de prueba

El motivo por el que creamos el anterior indicador de prueba y por el que vamos a hacer lo que nos proponemos a continuación, no solo es poner a prueba y mostrar el funcionamiento en los indicadores con las series temporales de la biblioteca, sino también darle algo de rodaje a la estructura del búfer de indicador, ya que, usando como base los valores obtenidos del uso de esta estructura, formaremos el conjunto de clases de búfer de los indicadores. Hoy, completaremos la estructura del búfer, convirtiéndola en un búfer de dibujado al estilo de las "Velas japonesas".

Para crear el indicador de prueba, tomaremos el indicador del artículo anterior y lo guardaremos en la nueva carpeta \MQL5\Indicators\TestDoEasy\Part41\ con el nuevo nombre TestDoEasyPart41.mq5.

Para comenzar, estableceremos el dibujado del indicador en una ventana aparte, describiremos todos los búferes de indicador necesarios y añadiremos otra macrosustitución, que indicará el número máximo de símbolos utilizados (y, por consiguiente, el número de búferes de indicador a dibujar):

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart41.mq5 |
//|                        Copyright 2020, MetaQuotes Software Corp. |
//|                             https://mql5.com/es/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2020, MetaQuotes Software Corp."
#property link      "https://mql5.com/es/users/artmedia70"
#property version   "1.00"
//--- includes
#include <DoEasy\Engine.mqh>
//--- properties
#property indicator_separate_window
#property indicator_buffers 21      // 5 arrays (Open[] High[] Low[] Close[] Color[]) * 4 drawn buffers + 1 BufferTime[] calculated buffer
#property indicator_plots   4       // 1 candlesticks buffer consisting of 5 arrays (Open[] High[] Low[] Close[] Color[]) * 4 symbols
//--- plot Pair1
#property indicator_label1  "Pair 1"
#property indicator_type1   DRAW_COLOR_CANDLES
#property indicator_color1  clrLimeGreen,clrRed,clrDarkGray
//--- plot Pair2
#property indicator_label2  "Pair 2"
#property indicator_type2   DRAW_COLOR_CANDLES
#property indicator_color2  clrDeepSkyBlue,clrFireBrick,clrDarkGray
//--- plot Pair3
#property indicator_label3  "Pair 3"
#property indicator_type3   DRAW_COLOR_CANDLES
#property indicator_color3  clrMediumPurple,clrDarkSalmon,clrGainsboro
//--- plot Pair4
#property indicator_label4  "Pair 4"
#property indicator_type4   DRAW_COLOR_CANDLES
#property indicator_color4  clrMediumAquamarine,clrMediumVioletRed,clrGainsboro

//--- classes

//--- enums

//--- defines
#define PERIODS_TOTAL   (21)              // Total amount of available chart periods
#define SYMBOLS_TOTAL   (4)               // Maximum number of drawn symbol buffers
//--- structures

Bien, ¿por qué el número de búferes de indicador es igual a 21?
La respuesta es sencilla: el estilo de dibujado de DRAW_COLOR_CANDLES presupone la presencia de cinco matrices relacionadas con él:

  1. la matriz de precios Open
  2. la matriz de precios High
  3. la matriz de precios Low
  4. la matriz de precios Close
  5. la matriz de color (Color)

Nosotros utilizaremos en el indicador un número máximo de símbolos igual a 4. Por consiguiente: cuatro búferes a dibujar por cinco matrices relacionadas con cada uno de ellos, igual a veinte; y un búfer adicional para guardar las horas de las barras, que transmitiremos a las funciones. En total, 21 búferes de indicador, de los cuales, cuatro son de dibujado.

Vamos a escribir la estructura del búfer "Velas japonesas":

//--- structures
struct SDataBuffer                        // Candlesticks buffer structure
  {
private:
   ENUM_TIMEFRAMES   m_buff_timeframe;    // Buffer timeframe
   string            m_buff_symbol;       // Buffer symbol
   int               m_buff_open_index;   // The index of the indicator buffer related to the Open[] array
   int               m_buff_high_index;   // The index of the indicator buffer related to the High[] array
   int               m_buff_low_index;    // The index of the indicator buffer related to the Low[] array
   int               m_buff_close_index;  // The index of the indicator buffer related to the Close[] array
   int               m_buff_color_index;  // The index of the color buffer related to the Color[] array
   int               m_buff_next_index;   // The index of the next free indicator buffer
   bool              m_used;              // The flag of using the buffer in the indicator
   bool              m_show_data;         // The flag of displaying the buffer on the chart before enabling/disabling its display
public:
   double            Open[];              // The array assigned as INDICATOR_DATA by the Open indicator buffer
   double            High[];              // The array assigned as INDICATOR_DATA by the High indicator buffer
   double            Low[];               // The array assigned as INDICATOR_DATA by the Low indicator buffer
   double            Close[];             // The array assigned as INDICATOR_DATA by the Close indicator buffer
   double            Color[];             // The array assigned as INDICATOR_COLOR_INDEX by the Color indicator buffer
//--- Set indices for the drawn OHLC and Color buffers
   void              SetIndexes(const int index_first)
                       {
                        this.m_buff_open_index=index_first;
                        this.m_buff_high_index=index_first+1;
                        this.m_buff_low_index=index_first+2;
                        this.m_buff_close_index=index_first+3;
                        this.m_buff_color_index=index_first+4;
                        this.m_buff_next_index=index_first+5;
                       }
//--- Methods of setting and returning values of the private structure members
   void              SetTimeframe(const ENUM_TIMEFRAMES timeframe)   { this.m_buff_timeframe=(timeframe==PERIOD_CURRENT ? ::Period() : timeframe); }
   void              SetSymbol(const string symbol)                  { this.m_buff_symbol=symbol;        }
   void              SetUsed(const bool flag)                        { this.m_used=flag;                 }
   void              SetShowDataFlag(const bool flag)                { this.m_show_data=flag;            }
   int               IndexOpenBuffer(void)                     const { return this.m_buff_open_index;    }
   int               IndexHighBuffer(void)                     const { return this.m_buff_high_index;    }
   int               IndexLowBuffer(void)                      const { return this.m_buff_low_index;     }
   int               IndexCloseBuffer(void)                    const { return this.m_buff_close_index;   }
   int               IndexColorBuffer(void)                    const { return this.m_buff_color_index;   }
   int               IndexNextBuffer(void)                     const { return this.m_buff_next_index;    }
   ENUM_TIMEFRAMES   Timeframe(void)                           const { return this.m_buff_timeframe;     }
   string            Symbol(void)                              const { return this.m_buff_symbol;        }
   bool              IsUsed(void)                              const { return this.m_used;               }
   bool              GetShowDataFlag(void)                     const { return this.m_show_data;          }
   void              Print(void);
  };
//--- Display structure data to the journal
void SDataBuffer::Print(void)
  {
   string array[8];
   array[0]="Buffer "+this.Symbol()+" "+TimeframeDescription(this.Timeframe())+":";
   array[1]=" Open buffer index: "+(string)this.IndexOpenBuffer();
   array[2]=" High buffer index: "+(string)this.IndexHighBuffer();
   array[3]=" Low buffer index: "+(string)this.IndexLowBuffer();
   array[4]=" Close buffer index: "+(string)this.IndexCloseBuffer();
   array[5]=" Color buffer index: "+(string)this.IndexColorBuffer();
   array[6]=" Next buffer index: "+(string)this.IndexNextBuffer();
   array[7]=" Used: "+(string)(bool)this.IsUsed();
   for(int i=0;i<ArraySize(array);i++)
      ::Print(array[i]);
  }
//--- input variables

La estructura tiene variables para guardar los valores de los índices de los búferes relacionados con las matrices OHLC y Color correspondientes: según su índice, siempre podremos recurrir al búfer necesario. El siguiente índice libre para vincular un nuevo búfer de indicador a las matrices de la estructura siempre se podrá obtener de la variable m_buff_next_index, con la ayuda del método IndexNextBuffer(), que retorna el índice que va después del búfer de color en la estructura actual.

Por el listado, podemos ver que en la estructura se encuentran todos los métodos para establecer y retornar todos los valores determinados en la sección privada de la estructura, además de un método para mostrar en el diario todos los datos de la estructura: en una matriz, se colocan los datos de los índices de los búferes OHLC y los colores, el próximo índice libre para vincular la nueva matriz y la bandera de uso de este búfer. A continuación, todos estos datos de la matriz se muestran en un ciclo en el diario.

Como ejemplo, así se mostrarán en el diario los datos de los cuatro búferes a dibujar establecidos en los ajustes del indicador (el búfer AUDUSD se muestra en el gráfico):

2020.04.08 21:55:21.528 Buffer EURUSD H1:
2020.04.08 21:55:21.528  Open buffer index: 0
2020.04.08 21:55:21.528  High buffer index: 1
2020.04.08 21:55:21.528  Low buffer index: 2
2020.04.08 21:55:21.528  Close buffer index: 3
2020.04.08 21:55:21.528  Color buffer index: 4
2020.04.08 21:55:21.528  Next buffer index: 5
2020.04.08 21:55:21.528  Used: false
2020.04.08 21:55:21.530 Buffer AUDUSD H1:
2020.04.08 21:55:21.530  Open buffer index: 5
2020.04.08 21:55:21.530  High buffer index: 6
2020.04.08 21:55:21.530  Low buffer index: 7
2020.04.08 21:55:21.530  Close buffer index: 8
2020.04.08 21:55:21.530  Color buffer index: 9
2020.04.08 21:55:21.530  Next buffer index: 10
2020.04.08 21:55:21.530  Used: true
2020.04.08 21:55:21.532 Buffer EURAUD H1:
2020.04.08 21:55:21.532  Open buffer index: 10
2020.04.08 21:55:21.532  High buffer index: 11
2020.04.08 21:55:21.532  Low buffer index: 12
2020.04.08 21:55:21.532  Close buffer index: 13
2020.04.08 21:55:21.532  Color buffer index: 14
2020.04.08 21:55:21.532  Next buffer index: 15
2020.04.08 21:55:21.532  Used: false
2020.04.08 21:55:21.533 Buffer EURGBP H1:
2020.04.08 21:55:21.533  Open buffer index: 15
2020.04.08 21:55:21.533  High buffer index: 16
2020.04.08 21:55:21.533  Low buffer index: 17
2020.04.08 21:55:21.533  Close buffer index: 18
2020.04.08 21:55:21.533  Color buffer index: 19
2020.04.08 21:55:21.533  Next buffer index: 20
2020.04.08 21:55:21.533  Used: false

Podemos ver que el índice del búfer Open de cada búfer "Velas japonesas" siguiente coincide con el índice "Next buffer index" del búfer "Velas japonesas" anterior. Así, gracias al diario, podemos comprender que el próximo búfer libre tiene el índice 20. Asimismo, podemos designar este índice como próximo búfer de cálculo del indicador (por ejemplo), lo cual, a propósito, vamos a hacer para el búfer de cálculo que guarda la hora de las barras del gráfico actual.

Vamos a añadir el bloque de parámetros de entrada del indicador:

//--- input variables
/*sinput*/ ENUM_SYMBOLS_MODE  InpModeUsedSymbols=  SYMBOLS_MODE_DEFINES;            // Mode of used symbols list
sinput   string               InpUsedSymbols    =  "EURUSD,AUDUSD,EURAUD,EURGBP,EURCAD,EURJPY,EURUSD,GBPUSD,NZDUSD,USDCAD,USDJPY";  // List of used symbols (comma - separator)
sinput   ENUM_TIMEFRAMES_MODE InpModeUsedTFs    =  TIMEFRAMES_MODE_LIST;            // Mode of used timeframes list
sinput   string               InpUsedTFs        =  "M1,M5,M15,M30,H1,H4,D1,W1,MN1"; // List of used timeframes (comma - separator)
sinput   uint                 InpButtShiftX     =  0;    // Buttons X shift 
sinput   uint                 InpButtShiftY     =  10;   // Buttons Y shift 
sinput   bool                 InpUseSounds      =  true; // Use sounds
//--- indicator buffers

Como en la enumeración de la selección del modo de uso de los símbolos ENUM_SYMBOLS_MODE, ubicado en el archivo Defines.mqh, tenemos dos modos que no necesitamos ("Trabajo con los símbolos de la ventana "Observación de mercado" y "Trabajo con la lista completa de símbolos"):

//+------------------------------------------------------------------+
//| Modes of working with symbols                                    |
//+------------------------------------------------------------------+
enum ENUM_SYMBOLS_MODE
  {
   SYMBOLS_MODE_CURRENT,                              // Work with the current symbol only
   SYMBOLS_MODE_DEFINES,                              // Work with the specified symbol list
   SYMBOLS_MODE_MARKET_WATCH,                         // Work with the Market Watch window symbols
   SYMBOLS_MODE_ALL                                   // Work with the full symbol list
  };
//+------------------------------------------------------------------+

... para evitar seleccionar estos dos modos en los ajustes, haremos que la variable InpModeUsedSymbols sea no externa, comentando su modificador sinput. De esta forma, el modo de trabajo con los símbolos en el indicador siempre será "Trabajo con la lista de símbolos establecida", y se usarán los primeros cuatro símbolos de la lista indicada con la variable de entrada InpUsedSymbols.

Vamos a añadir la definición de los búferes de indicador y el bloque de variables globales:

//--- indicator buffers
SDataBuffer    Buffers[];                       // Array of the indicator buffer data structures assigned to the timeseries
double         BufferTime[];                    // The calculated buffer for storing and passing data from the time[] array
//--- global variables
CEngine        engine;                          // CEngine library main object
string         prefix;                          // Prefix of graphical object names
int            min_bars;                        // The minimum number of bars for the indicator calculation
int            used_symbols_mode;               // Mode of working with symbols
string         array_used_symbols[];            // The array for passing used symbols to the library
string         array_used_periods[];            // The array for passing used timeframes to the library
//+------------------------------------------------------------------+

Como búferes dibujados del indicador, hemos declarado la matriz de estructuras del búfer "Velas japonesas", eso resulta bastante más cómodo que definir cuatro búferes idénticos, y también resulta más cómodo recurrir a cada búfer según el índice de su ubicación en la matriz, correspondiente a su botón: si debemos seleccionar el búfer con el primer botón, elegimos el búfer que se encuentra en la matriz en primer lugar; si debemos seleccionar el búfer designado con el último botón, elegimos el búfer que se encuentra en la matriz en último lugar, etcétera.

Asimismo, necesitaremos un búfer de cálculo —el búfer de tiempo— para transmitir en la función del indicador la hora de las barras de la matriz predeterminada time[] del manejador OnCalculate() del indicador.

Ya conocemos el bloque con las variables globales por los asesores de prueba y los indicadores de casi todos los artículos de la biblioteca: todas las variables han sido descritas, y no nos detendremos en ellas con detalle.
El número mínimo de barras para el cálculo del indicador lo necesitaremos para determinar si hay barras suficientes para calcular la serie temporal, y así representar correctamente los datos del indicador del marco temporal mayor en el marco temporal menor.

Por ejemplo, si nos encontramos en un periodo del gráfico М15, y tomamos los datos para la representación del gráfico Н1, para representar correctamente todas las barras, deberemos tener un mínimo de 4 barras, ya que en una barra de 1 hora caben 4 barras de 15 minutos.

La función NumberBarsInTimeframe() en el archivo de funciones de servicio de la biblioteca DELib.mqh (que ya hemos escrito y analizado anteriormente), se ocupará de calcular el número necesario de barras del gráfico actual, dependiendo del periodo utilizado para los cálculos.

Ya hemos escrito más arriba que nos hemos encontrado con dificultades a la hora de escribir el indicador en el estilo procedimental. Debido a ello, nos hemos visto obligados a crear funciones auxiliares adicionales para buscar, establecer y controlar los estados de los botones y búferes. Si los botones y los búferes fuesen escritos por objetos, estos simplificaría un tanto el acceso a sus propiedades y el establecimiento de sus modos. Pero, por el momento, lo hecho, hecho está, y se diría que, al contrario de lo que parece, resulta más rápido escribir la prueba en el estilo procedimental. Además, en cuanto al indicador de prueba, no es necesario crear objetos temporales que luego no necesitaremos.

Echemos un vistazo a las funciones auxiliares escritas.

Función para establecer los estados de los búferes de indicador dibujados:

//+------------------------------------------------------------------+
//| Set the state for drawn buffers                                  |
//+------------------------------------------------------------------+
void SetPlotBufferState(const int buffer_index,const bool state)
  {
//--- Depending on a passed status, define whether data should be displayed in the data window (state==true) or not (state==false)
   PlotIndexSetInteger(buffer_index,PLOT_SHOW_DATA,state);
//--- Create the buffer description consisting of a symbol and timeframe and set the buffer description by its buffer_index index
   string params=Buffers[buffer_index].Symbol()+" "+TimeframeDescription(Buffers[buffer_index].Timeframe());
   string label=params+" Open;"+params+" High;"+params+" Low;"+params+" Close";
   PlotIndexSetString(buffer_index,PLOT_LABEL,(state ? label : NULL));
//--- If the buffer is active (drawn), set a short name for the indicator with the displayed symbol and timeframe
   if(state)
      IndicatorSetString(INDICATOR_SHORTNAME,engine.Name()+" "+Buffers[buffer_index].Symbol()+" "+TimeframeDescription(Buffers[buffer_index].Timeframe()));
  }  
//+------------------------------------------------------------------+

Transmitimos a la función el índice del búfer para el que debemos establecer el estado, transmitido igualmente como parámetro de entrada.

Debemos tener en cuenta una peculiaridad a considerar cuando trabajemos con los búferes de los indicadores que requieren varias matrices relacionadas para la representación.

Por ejemplo, si el búfer de indicador requiere 2 matrices, los índices relacionados con estos búferes de las matrices tendrán los valores 0 y 1. Estos valores se establecen con la ayuda de la función SetIndexBuffer(). Al utilizar un búfer de dibujado que usa dos matrices de datos, no se observan problemas de comprensión especiales en cuanto al acceso al búfer dibujado: solo tenemos que indicar el búfer con el índice 0 para acceder a sus propiedades.

No obstante, si tenemos que usar dos o más búferes de dibujado que utilicen dos matrices, aquí podrían surgir malentendidos sobre el índice con el que recurrir a los búferes de dibujado segundo, tercero o último.

Veamos un ejemplo de tres búferes de dibujado con dos matrices cada uno, y los números de los índices de los búferes dibujados y sus matrices:
  • Búfer de dibujado №1 — índice de búfer de dibujado 0
    • Matriz №1 — índice de búfer 0
    • Matriz №2 — índice de búfer 1
  • Búfer de dibujado №2 — índice de búfer de dibujado 1
    • Matriz №1 — índice de búfer 2
    • Matriz №2 — índice de búfer 3
  • Búfer de dibujado №3 — índice de búfer de dibujado 2
    • Matriz №1 — índice de búfer 4
    • Matriz №2 — índice de búfer 5

Podría parecer que aquí tenemos seis matrices para tres búferes de dibujado, y que, para obtener acceso a la segunda matriz dibujada, debemos recurrir al índice 2 (ya que el 0 y el 1 están ocupados por las matrices del primer búfer). Pero no. Para recurrir al segundo búfer de dibujado, deberemos recurrir precisamente a los índices de los búferes de dibujado, y no de todas las matrices designadas como búferes para cada búfer de dibujado, es decir, según el índice 1.

De esta forma, para vincular una matriz con un búfer mediante la función SetIndexBuffer(), deberemos indicar el número ordinal de todas las matrices designadas para su uso como búferes del indicador; pero, para obtener los datos de un búfer dibujado mediante la función PlotIndexGetInteger() o establecer los datos de un búfer de dibujado mediante las funciones PlotIndexSetDouble(), PlotIndexSetInteger(), PlotIndexSetString(), deberemos indicar el índice del búfer de dibujado necesario, y no el número de la matriz. En este ejemplo, para el primer búfer de dibujado, el índice será 0, para el segundo 1, y para el tercero, 2. Esto es algo que debemos saber y tener en cuenta.

Función que retorna la bandera de uso del símbolo establecido en los ajustes:

//+------------------------------------------------------------------+
//| Return the flag of using a symbol specified in the settings      |
//+------------------------------------------------------------------+
bool IsUsedSymbolByInput(const string symbol)
  {
   int total=ArraySize(array_used_symbols);
   for(int i=0;i<total;i++)
      if(array_used_symbols[i]==symbol)
         return true;
   return false;
  }
//+------------------------------------------------------------------+

Si el símbolo está presente en la matriz de símbolos utilizados, la función retornará true, de lo contrario, false. En ocasiones, podemos no indicar el símbolo actual en la lista de símbolos utilizados, pero este siempre estará presente: sus datos son necesarios para ejecutar los cálculos internos de la biblioteca. Esta función retorna la bandera que indica que el símbolo actual no ha sido establecido en los ajustes, y que este debe ser omitido.

Función que retorna el índice del búfer de dibujado según el símbolo:

//+------------------------------------------------------------------+
//| Return the structure drawn buffer index by symbol                |
//+------------------------------------------------------------------+
int IndexBuffer(const string symbol)
  {
   int total=ArraySize(Buffers);
   for(int i=0;i<total;i++)
      if(Buffers[i].Symbol()==symbol)
         return i;
   return WRONG_VALUE;
  }
//+------------------------------------------------------------------+

A la función se transmiten el nombre del símbolo y el índice del búfer que debemos retornar. En un ciclo por todos los búferes, buscamos el búfer con este símbolo y retornamos el índice del ciclo en caso de coincidencia. Si no hay un búfer con este símbolo, retornamos -1.

Función que retorna el número del primer índice libre que puede ser designado como próximo búfer de dibujado del indicador:

//+------------------------------------------------------------------+
//| Return the first free index of the drawn buffer                  |
//+------------------------------------------------------------------+
int FirstFreePlotBufferIndex(void)
  {
   int num=WRONG_VALUE,total=ArraySize(Buffers);
   for(int i=0;i<total;i++)
      if(Buffers[i].IndexNextBuffer()>num)
         num=Buffers[i].IndexNextBuffer();
   return num;
  }
//+------------------------------------------------------------------+

En un ciclo por todos los búferes de dibujado de la matriz de estructuras de los búferes, comprobamos el valor del siguiente búfer libre.
Si es superior al anterior, registramos el nuevo valor. Una vez finalizado el ciclo, retornamos el valor registrado de la variable "num".

Funcioes escritas para ejecutar las funciones auxiliares de establecimiento y búsqueda de los estados de los botones y búferes:

//+------------------------------------------------------------------+
//| Return the button status                                         |
//+------------------------------------------------------------------+
bool ButtonState(const string name)
  {
   return (bool)ObjectGetInteger(0,name,OBJPROP_STATE);
  }
//+------------------------------------------------------------------+
//| Set the button status                                            |
//+------------------------------------------------------------------+
void SetButtonState(const string button_name,const bool state)
  {
//--- Set the button status and its color depending on the status
   ObjectSetInteger(0,button_name,OBJPROP_STATE,state);
   if(state)
      ObjectSetInteger(0,button_name,OBJPROP_BGCOLOR,C'220,255,240');
   else
      ObjectSetInteger(0,button_name,OBJPROP_BGCOLOR,C'240,240,240');
//--- If not in the tester, 
//--- set the status to the terminal global variable
   if(!engine.IsTester())
      GlobalVariableSet((string)ChartID()+"_"+button_name,state);
  }
//+------------------------------------------------------------------+
//| Set the symbol button status                                     |
//+------------------------------------------------------------------+
void SetButtonSymbolState(const string button_symbol_name,const bool state)
  {
//--- Set the symbol button status
   SetButtonState(button_symbol_name,state);
//--- Detect wrong names if the timeframe button status is not specified
//--- Write the button status to the global variable only if its name contains no "PERIOD_CURRENT" substring
   if(StringFind(button_symbol_name,"PERIOD_CURRENT")==WRONG_VALUE)
      GlobalVariableSet((string)ChartID()+"_"+button_symbol_name,state);
//--- Set the visibility for all period buttons corresponding to the symbol button
   SetButtonPeriodVisible(button_symbol_name,state);
  }
//+------------------------------------------------------------------+
//| Set the period button status                                     |
//+------------------------------------------------------------------+
void SetButtonPeriodState(const string button_period_name,const bool state)
  {
//--- Set the button status and write it to the terminal global variable
   SetButtonState(button_period_name,state);
   GlobalVariableSet((string)ChartID()+"_"+button_period_name,state);
  }
//+------------------------------------------------------------------+
//| Set the "visibility" of period buttons for the symbol button     |
//+------------------------------------------------------------------+
void SetButtonPeriodVisible(const string button_symbol_name,const bool state_symbol)
  {
//--- In the loop by the amount of used timeframes
   int total=ArraySize(array_used_periods);
   for(int j=0;j<total;j++)
     {
      //--- create the name of the next period button
      string butt_name_period=button_symbol_name+"_"+EnumToString(ArrayUsedTimeframes[j]);
      //--- Set the status and visibility for the period button depending on the symbol button status
      ObjectSetInteger(0,butt_name_period,OBJPROP_TIMEFRAMES,(engine.IsTester() ? OBJ_ALL_PERIODS : (state_symbol ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS)));
     }   
  }
//+------------------------------------------------------------------+
//| Reset the states of the remaining symbol buttons                 |
//+------------------------------------------------------------------+
void ResetButtonSymbolState(const string button_symbol_name)
  {
//--- In the loop by all chart objects,
   for(int i=ObjectsTotal(0,0)-1;i>WRONG_VALUE;i--)
     {
      //--- get the name of the next object
      string name=ObjectName(0,i,0);
      //--- If this is a pressed button, or the object does not belong to the indicator, or this is a period button, move on to the next one
      if(name==button_symbol_name || StringFind(name,prefix)==WRONG_VALUE || StringFind(name,"_PERIOD_")>0)
         continue;
      //--- Reset the symbol button status by object name
      SetButtonSymbolState(name,false);
     }
  }
//+------------------------------------------------------------------+
//| Reset the states of the remaining symbol period buttons          |
//+------------------------------------------------------------------+
void ResetButtonPeriodState(const string button_period_name,const string symbol)
  {
//--- In the loop by all chart objects,
   for(int i=ObjectsTotal(0,0)-1;i>WRONG_VALUE;i--)
     {
      //--- get the name of the next object
      string name=ObjectName(0,i,0);
      //--- If this is a pressed button, or the object does not belong to the indicator, or this is not a period button, or the button does not belong to the symbol, move on to the next one
      if(name==button_period_name || StringFind(name,prefix)==WRONG_VALUE || StringFind(name,"_PERIOD_")==WRONG_VALUE || StringFind(name,symbol)==WRONG_VALUE)
         continue;
      //--- Reset the period button status by object name
      SetButtonPeriodState(name,false);
     }
  }
//+---------------------------------------------------------------------------+
//| Return the name of the pressed period button corresponding to the symbol  |
//+---------------------------------------------------------------------------+
string GetNamePressedTimeframe(const string button_symbol_name,const string symbol)
  {
//--- In the loop by all chart objects,
   for(int i=ObjectsTotal(0,0)-1;i>WRONG_VALUE;i--)
     {
      //--- get the name of the next object
      string name=ObjectName(0,i,0);
      //--- If this is a pressed button, or the object does not belong to the indicator, or this is not a period button, or the button does not belong to the symbol, move on to the next one
      if(name==button_symbol_name || StringFind(name,prefix)==WRONG_VALUE || StringFind(name,"_PERIOD_")==WRONG_VALUE || StringFind(name,symbol)==WRONG_VALUE)
         continue;
      //--- If the button is pressed, return the name of the pressed button graphic object
      if(ButtonState(name))
         return name;
     }
//--- Return NULL if no symbol period buttons are pressed
   return NULL;
  }
//+------------------------------------------------------------------+
//| Set the buffer states, 'true' - only for the specified one       |
//+------------------------------------------------------------------+
void SetAllBuffersState(const string symbol)
  {
   int total=ArraySize(Buffers);
//--- Get the specified buffer index
   int index=IndexBuffer(symbol);
//--- In a loop by the number of drawn buffers
   for(int i=0;i<total;i++)
     {
      //--- if the loop index is equal to the specified buffer index,
      //--- set the flag of its usage to 'true', otherwise - to 'false'
      //--- forcibly set the flag indicating that pressing the button for the buffer has already been handled
      Buffers[i].SetUsed(i!=index ? false : true);
      Buffers[i].SetShowDataFlag(false);
     }
  }
//+------------------------------------------------------------------+

La función de procesamiento de la pulsación de los botones ha sido ligeramente rediseñada, dado que solo podemos tener pulsados un botón de símbolo y un botón de periodo que se corresponda con el botón del símbolo:

//+------------------------------------------------------------------+
//| Handle pressing the buttons                                      |
//+------------------------------------------------------------------+
void PressButtonEvents(const string button_name)
  {
//--- Convert button name into its string ID
   string button=StringSubstr(button_name,StringLen(prefix));
      //--- Get the index of the drawn buffer by timeframe, its symbol and index
   int index=StringFind(button,"_PERIOD_");
   string symbol=StringSubstr(button,5,index-5);
   int buffer_index=IndexBuffer(symbol);
//--- Create the button name for the terminal's global variable
   string name_gv=(string)ChartID()+"_"+prefix+button;
//--- Get the button status (pressed/released). If not in the tester,
//--- write the status to the button global variable (1 or 0)
   bool state=ButtonState(button_name);
   if(!engine.IsTester())
      GlobalVariableSet(name_gv,state);
//--- Set the button color depending on its status, 
//--- write its status to the buffer structure depending on the button status (used/not used)
//--- initialize the buffer corresponding to the button timeframe by the buffer index received earlier
   if(StringFind(button_name,"_PERIOD_")==WRONG_VALUE)
     {
      SetButtonSymbolState(button_name,state);
      ResetButtonSymbolState(button_name);
     }
   else
     {
      SetButtonPeriodState(button_name,state);
      ResetButtonPeriodState(button_name,symbol);
     }
//--- Get the timeframe from the pressed symbol timeframe button
   string pressed_period=GetNamePressedTimeframe(button_name,symbol);
   ENUM_TIMEFRAMES timeframe=
     (
      StringFind(button,"_PERIOD_")==WRONG_VALUE ? 
      TimeframeByDescription(StringSubstr(pressed_period,StringFind(pressed_period,"_PERIOD_")+8)) :
      TimeframeByDescription(StringSubstr(button,StringFind(button,"_PERIOD_")+8))
     );
//--- Set the states of all buffers, 'true' - for the pressed button symbol buffer, the rest are 'false'
   SetAllBuffersState(symbol);
//--- Set the displayed timeframe for the buffer
   Buffers[buffer_index].SetTimeframe(timeframe);
//--- If the button pressing is not handled yet
   if(Buffers[buffer_index].GetShowDataFlag()!=state)
     {
      //--- Initialize all indicator buffers
      InitBuffersAll();
      //--- If the buffer is active, fill it with historical data
      if(state)
         BufferFill(buffer_index);
      //--- Set the flag indicating that pressing the button has already been handled
      Buffers[buffer_index].SetShowDataFlag(state);
     }

//--- Here you can add additional handling of button pressing:
//--- If the button is pressed
   if(state)
     {
      //--- If M1 button is pressed
      if(button=="BUTT_M1")
        {
         
        }
      //--- If button M2 is pressed
      else if(button=="BUTT_M2")
        {
         
        }
      //---
      // Remaining buttons ...
      //---
     }
   //--- Not pressed
   else 
     {
      //--- M1 button
      if(button=="BUTT_M1")
        {
         
        }
      //--- M2 button
      if(button=="BUTT_M2")
        {
         
        }
      //---
      // Remaining buttons ...
      //---
     }
//--- re-draw the chart
   ChartRedraw();
  }
//+------------------------------------------------------------------+

Función para crear el panel de botones:

//+------------------------------------------------------------------+
//| Create the buttons panel                                         |
//+------------------------------------------------------------------+
bool CreateButtons(const int shift_x=20,const int shift_y=0)
  {
   int total_symbols=ArraySize(array_used_symbols);
   int total_periods=ArraySize(ArrayUsedTimeframes);
   uint ws=48,hs=18,w=26,h=16,shift_h=2,x=InpButtShiftX+1, y=InpButtShiftY+h+1;
   //--- In the loop by the number of used symbols,
   for(int i=0;i<SYMBOLS_TOTAL;i++)
     {
      //--- create the name of the next symbol button
      string butt_name_symbol=prefix+"BUTT_"+array_used_symbols[i];
      //--- create the next symbol button with a shift calculated as
      //--- ((button height + 2) * loop index)
      uint ys=y+(hs+shift_h)*i;
      if(ButtonCreate(butt_name_symbol,x,ys,ws,hs,array_used_symbols[i],clrGray))
        {
         bool state_symbol=(engine.IsTester() && i==0 ? true : false);
         //--- If not in the tester,
         if(!engine.IsTester())
           {
            //--- set the name of the terminal global variable for storing the symbol button status
            string name_gv_symbol=(string)ChartID()+"_"+butt_name_symbol;
            //--- if there is no global variable with the symbol name, create it set to 'false',
            if(!GlobalVariableCheck(name_gv_symbol))
               GlobalVariableSet(name_gv_symbol,false);
            //--- get the symbol button status from the terminal global variable
            state_symbol=GlobalVariableGet(name_gv_symbol);
           }
         //--- Set the status for the symbol button
         SetButtonState(butt_name_symbol,state_symbol);
         
         //--- In the loop by the amount of used timeframes
         for(int j=0;j<total_periods;j++)
           {
            //--- create the name of the next period button
            string butt_name_period=butt_name_symbol+"_"+EnumToString(ArrayUsedTimeframes[j]);
            uint yp=ys-(hs-h)/2;
            //--- create the next period button with a shift calculated as
            //--- (symbol button width + (period button width + 1) * loop index)
            if(ButtonCreate(butt_name_period,x+ws+2+(w+1)*j,yp,w,h,TimeframeDescription(ArrayUsedTimeframes[j]),clrGray))
               ObjectSetInteger(0,butt_name_period,OBJPROP_TIMEFRAMES,(engine.IsTester() ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS));
            else
              {
               Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_name_period,"\"");
               return false;
              }
            bool state_period=(engine.IsTester() && ArrayUsedTimeframes[j]==Period() ? true : false);
            //--- If not in the tester,
            if(!engine.IsTester())
              {
               //--- set the name of the terminal global variable for storing the period button status
               string name_gv_period=(string)ChartID()+"_"+butt_name_period;
               //--- if there is no global variable with the period name, create it set to 'false',
               if(!GlobalVariableCheck(name_gv_period))
                  GlobalVariableSet(name_gv_period,false);
               //--- get the period button status from the terminal global variable
               state_period=GlobalVariableGet(name_gv_period);
              }
            //--- Set the status and visibility for the period button depending on the symbol button status
            SetButtonState(butt_name_period,state_period);
            ObjectSetInteger(0,butt_name_period,OBJPROP_TIMEFRAMES,(engine.IsTester() ? OBJ_ALL_PERIODS : (state_symbol ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS)));
           }   
        }
      else
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_name_symbol,"\"");
         return false;
        }
     }
   ChartRedraw(0);
   return true;
  }
//+------------------------------------------------------------------+

Funciones de inicialización de los búferes de dibujado del indicador:

//+------------------------------------------------------------------+
//| Initialize the timeseries and the appropriate buffers by index   |
//+------------------------------------------------------------------+
bool InitBuffer(const int buffer_index)
  {
//--- Leave if the wrong index is passed
   if(buffer_index==WRONG_VALUE)
      return false;
//--- Initialize drawn OHLC buffers using the "empty" value, while Color is initialized using zero
   ArrayInitialize(Buffers[buffer_index].Open,EMPTY_VALUE);
   ArrayInitialize(Buffers[buffer_index].High,EMPTY_VALUE);
   ArrayInitialize(Buffers[buffer_index].Low,EMPTY_VALUE);
   ArrayInitialize(Buffers[buffer_index].Close,EMPTY_VALUE);
   ArrayInitialize(Buffers[buffer_index].Color,0);
//--- Set the flag of the buffer display in the data window by index
   SetPlotBufferState(buffer_index,Buffers[buffer_index].IsUsed());
   return true;
  }
//+------------------------------------------------------------------+
//| Initialize all timeseries and the appropriate buffers            |
//+------------------------------------------------------------------+
void InitBuffersAll(void)
  {
//--- Initialize the next buffer in the loop by the total number of chart periods
   int total=ArraySize(Buffers);
   for(int i=0;i<total;i++)
      InitBuffer(i);
  }
//+------------------------------------------------------------------+

Función para calcular una barra de todos los búferes de indicador activos:

//+------------------------------------------------------------------+
//| Calculating a single bar of all active buffers                   |
//+------------------------------------------------------------------+
void CalculateSeries(const int index,const datetime time)
  {
//--- In the loop by the total number of buffers, get the next buffer
   int total=ArraySize(Buffers);
   for(int i=0;i<total;i++)
     {
      //--- if the buffer is not used (the symbol button is released), move on to the next one
      if(!Buffers[i].IsUsed())
        {
         SetBufferData(i,index,NULL);
         continue;
        }
      //--- get the timeseries object by the buffer timeframe
      CSeriesDE *series=engine.SeriesGetSeries(Buffers[i].Symbol(),(ENUM_TIMEFRAMES)Buffers[i].Timeframe());   // Here we should use the timeframe from the pressed button next to the pressed symbol button
      //--- if the timeseries is not received
      //--- or the bar index passed to the function is beyond the total number of bars in the timeseries, move on to the next buffer
      if(series==NULL || index>series.GetList().Total()-1)
         continue;
      //--- get the bar object from the timeseries corresponding to the one passed to the bar time function on the current chart
      CBar *bar=engine.SeriesGetBarSeriesFirstFromSeriesSecond(NULL,PERIOD_CURRENT,time,Buffers[i].Symbol(),Buffers[i].Timeframe());
      if(bar==NULL)
         continue;
      //--- get the specified property from the obtained bar and
      //--- call the function of writing the value to the buffer by i index
      SetBufferData(i,index,bar);
     }
  }
//+------------------------------------------------------------------+

Función que registra en el búfer de dibujado indicado los valores del objeto de barra transmitido:

//+------------------------------------------------------------------+
//| Write data on a single bar to the specified buffer               |
//+------------------------------------------------------------------+
void SetBufferData(const int buffer_index,const int index,const CBar *bar)
  {
//--- Get the bar index by its time falling within the time limits on the current chart
   int n=(bar!=NULL ? iBarShift(NULL,PERIOD_CURRENT,bar.Time()) : index);
//--- If the passed index on the current chart (index) is less than the calculated time of bar start on another timeframe
   if(index<n)
      //--- in the loop from the n bar on the current chart to zero
      while(n>WRONG_VALUE && !IsStopped())
        {
         //--- fill in the buffer by the n index with the passed values of the bar passed to the function (0 - EMPTY_VALUE)
         //--- and decrease the n value
         Buffers[buffer_index].Open[n]=(bar.Open()>0 ? bar.Open() : EMPTY_VALUE);
         Buffers[buffer_index].High[n]=(bar.High()>0 ? bar.High() : EMPTY_VALUE);
         Buffers[buffer_index].Low[n]=(bar.Low()>0 ? bar.Low() : EMPTY_VALUE);
         Buffers[buffer_index].Close[n]=(bar.Close()>0 ? bar.Close() : EMPTY_VALUE);
         Buffers[buffer_index].Color[n]=(bar.TypeBody()==BAR_BODY_TYPE_BULLISH ? 0 : bar.TypeBody()==BAR_BODY_TYPE_BEARISH ? 1 : 2);
         n--;
        }
//--- If the passed index on the current chart (index) is not less than the calculated time of bar start on another timeframe
//--- Set 'value' for the buffer by the 'index' passed to the function (0 - EMPTY_VALUE)
   else
     {
      //--- If the bar object is passed to the function, fill in the indicator buffers with its data
      if(bar!=NULL)
        {
         Buffers[buffer_index].Open[index]=(bar.Open()>0 ? bar.Open() : EMPTY_VALUE);
         Buffers[buffer_index].High[index]=(bar.High()>0 ? bar.High() : EMPTY_VALUE);
         Buffers[buffer_index].Low[index]=(bar.Low()>0 ? bar.Low() : EMPTY_VALUE);
         Buffers[buffer_index].Close[index]=(bar.Close()>0 ? bar.Close() : EMPTY_VALUE);
         Buffers[buffer_index].Color[index]=(bar.TypeBody()==BAR_BODY_TYPE_BULLISH ? 0 : bar.TypeBody()==BAR_BODY_TYPE_BEARISH ? 1 : 2);
        }
      //--- If NULL, instead of the bar object, is passed to the function, fill in the indicator buffers with the empty value
      else
        {
         Buffers[buffer_index].Open[index]=Buffers[buffer_index].High[index]=Buffers[buffer_index].Low[index]=Buffers[buffer_index].Close[index]=EMPTY_VALUE;
         Buffers[buffer_index].Color[index]=2;
        }
     }
  }
//+------------------------------------------------------------------+


Ahora, echemos un vistazo al manejador OnInit() del indicador, en el que se crean los botones y se preparan todos los búferes de indicador:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Initialize DoEasy library
   OnInitDoEasy();

//--- Set indicator global variables
   prefix=engine.Name()+"_";
   //--- Get the index of the maximum used timeframe in the array,
   //--- calculate the number of bars of the current period fitting in the maximum used period
   //--- Use the obtained value if it exceeds 2, otherwise use 2
   int index=ArrayMaximum(ArrayUsedTimeframes);
   int num_bars=NumberBarsInTimeframe(ArrayUsedTimeframes[index]);
   min_bars=(index>WRONG_VALUE ? (num_bars>2 ? num_bars : 2) : 2);
//--- Check and remove remaining indicator graphical objects
   if(IsPresentObectByPrefix(prefix))
      ObjectsDeleteAll(0,prefix);

//--- Create the button panel
   if(!CreateButtons(InpButtShiftX,InpButtShiftY))
      return INIT_FAILED;

//--- Check playing a standard sound using macro substitutions
   engine.PlaySoundByDescription(SND_OK);
//--- Wait for 600 milliseconds
   engine.Pause(600);
   engine.PlaySoundByDescription(SND_NEWS);

//--- indicator buffers mapping
   //--- In the loop by the total number of all symbols
   int total_symbols=ArraySize(array_used_symbols);
   for(int i=0;i<SYMBOLS_TOTAL;i++)
     {
      //--- get the next symbol
      //--- if the loop index is less than the size of used symbols array, the symbol name is taken from the array,
      //--- otherwise, this is an empty (unused) buffer, and the buffer symbol name is "EMPTY "+loop index
      string symbol=(i<total_symbols ? array_used_symbols[i] : "EMPTY "+string(i+1));
      //--- Increase the size of the buffer structures array, set the buffer symbol,
      ArrayResize(Buffers,ArraySize(Buffers)+1,SYMBOLS_TOTAL);
      Buffers[i].SetSymbol(symbol);
      //--- set the values of all indices for binding the indicator buffers with the structure arrays and
      //--- specify the next buffer index
      int index_first=(i==0 ? i : Buffers[i-1].IndexNextBuffer());
      Buffers[i].SetIndexes(index_first);
      
      //--- Setting the drawn buffer according to the button status
      //--- Set the symbol button status. The first button is active in the tester
      bool state_symbol=(engine.IsTester() && i==0 ? true : false);
      //--- Set the name of the symbol button corresponding to the buffer with the loop index and its timeframe
      string name_butt_symbol=prefix+"BUTT_"+Buffers[i].Symbol();
      string name_butt_period=name_butt_symbol+"_PERIOD_"+TimeframeDescription(Buffers[i].Timeframe());
      //--- If not in the tester, while the chart features the button with the specified name,
      if(!engine.IsTester() && ObjectFind(ChartID(),name_butt_symbol)==0)
        {
         //--- set the name of the terminal global variable for storing the button status
         string name_gv_symbol=(string)ChartID()+"_"+name_butt_symbol;
         string name_gv_period=(string)ChartID()+"_"+name_butt_period;
         //--- get the symbol button status from the terminal global variable
         state_symbol=GlobalVariableGet(name_gv_symbol);
        }
      
      //--- Get the timeframe from pressed symbol timeframe buttons
      string pressed_period=GetNamePressedTimeframe(name_butt_symbol,symbol);
      //--- Convert button name into its string ID
      string button=StringSubstr(name_butt_symbol,StringLen(prefix));
      ENUM_TIMEFRAMES timeframe=
        (
         StringFind(button,"_PERIOD_")==WRONG_VALUE ? 
         TimeframeByDescription(StringSubstr(pressed_period,StringFind(pressed_period,"_PERIOD_")+8)) :
         TimeframeByDescription(StringSubstr(button,StringFind(button,"_PERIOD_")+8))
        );
      
      //--- Set the values for all structure fields
      Buffers[i].SetTimeframe(timeframe);
      Buffers[i].SetUsed(state_symbol);
      Buffers[i].SetShowDataFlag(state_symbol);
      
      //--- Bind drawn indicator buffers by the buffer index equal to the loop index with the structure bar price arrays (OHLC)
      SetIndexBuffer(Buffers[i].IndexOpenBuffer(),Buffers[i].Open,INDICATOR_DATA);
      SetIndexBuffer(Buffers[i].IndexHighBuffer(),Buffers[i].High,INDICATOR_DATA);
      SetIndexBuffer(Buffers[i].IndexLowBuffer(),Buffers[i].Low,INDICATOR_DATA);
      SetIndexBuffer(Buffers[i].IndexCloseBuffer(),Buffers[i].Close,INDICATOR_DATA);
      //--- Bind the color buffer by the buffer index equal to the loop index with the corresponding structure arrays
      SetIndexBuffer(Buffers[i].IndexColorBuffer(),Buffers[i].Color,INDICATOR_COLOR_INDEX);
      //--- set the "empty value" for all structure buffers, 
      PlotIndexSetDouble(Buffers[i].IndexOpenBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
      PlotIndexSetDouble(Buffers[i].IndexHighBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
      PlotIndexSetDouble(Buffers[i].IndexLowBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
      PlotIndexSetDouble(Buffers[i].IndexCloseBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
      PlotIndexSetDouble(Buffers[i].IndexColorBuffer(),PLOT_EMPTY_VALUE,0);
      //--- set the drawing type
      PlotIndexSetInteger(i,PLOT_DRAW_TYPE,DRAW_COLOR_CANDLES);
      //--- Depending on the button status, set the graphical series name
      //--- and specify whether the buffer data in the data window is displayed or not
      SetPlotBufferState(i,state_symbol);
      //--- set the direction of indexing of all structure buffers as in the timeseries
      ArraySetAsSeries(Buffers[i].Open,true);
      ArraySetAsSeries(Buffers[i].High,true);
      ArraySetAsSeries(Buffers[i].Low,true);
      ArraySetAsSeries(Buffers[i].Close,true);
      ArraySetAsSeries(Buffers[i].Color,true);
      
      //--- Print data on the next buffer in the journal
      //Buffers[i].Print();
     }
   //--- Bind the calculated indicator buffer by the FirstFreePlotBufferIndex() buffer index with the BufferTime[] array of the indicator
   //--- set the direction of indexing the BufferTime[] calculated buffer as in the timeseries
   int buffer_temp_index=FirstFreePlotBufferIndex();
   SetIndexBuffer(buffer_temp_index,BufferTime,INDICATOR_CALCULATIONS);
   ArraySetAsSeries(BufferTime,true);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+


Vamos a echar un vistazo al manejador OnCalculate() del indicador:

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//+------------------------------------------------------------------+
//| OnCalculate code block for working with the library:             |
//+------------------------------------------------------------------+
   
//--- Pass the current symbol data from OnCalculate() to the price structure
   CopyData(rates_data,rates_total,prev_calculated,time,open,high,low,close,tick_volume,volume,spread);

//--- Check for the minimum number of bars for calculation
   if(rates_total<min_bars || Point()==0) return 0;
   
//--- Handle the Calculate event in the library
//--- If the OnCalculate() method of the library returns zero, not all timeseries are ready - leave till the next tick
   if(engine.0)
      return 0;
   
//--- If working in the tester
   if(MQLInfoInteger(MQL_TESTER)) 
     {
      engine.OnTimer(rates_data);   // Working in the timer
      PressButtonsControl();        // Button pressing control
      EventsHandling();             // Working with events
     }
//+------------------------------------------------------------------+
//| OnCalculate code block for working with the indicator:           |
//+------------------------------------------------------------------+
//--- Set OnCalculate arrays as timeseries
   ArraySetAsSeries(open,true);
   ArraySetAsSeries(high,true);
   ArraySetAsSeries(low,true);
   ArraySetAsSeries(close,true);
   ArraySetAsSeries(time,true);
   ArraySetAsSeries(tick_volume,true);
   ArraySetAsSeries(volume,true);
   ArraySetAsSeries(spread,true);

//--- Check and calculate the number of calculated bars
//--- If limit = 0, there are no new bars - calculate the current one
//--- If limit = 1, a new bar has appeared - calculate the first and the current ones
//--- If limit > 1, there are changes in history - the full recalculation of all data
   int limit=rates_total-prev_calculated;
   
//--- Recalculate the entire history
   if(limit>1)
     {
      limit=rates_total-1;
      InitBuffersAll();
     }
//--- Prepare data

//--- Calculate the indicator
   for(int i=limit; i>WRONG_VALUE && !IsStopped(); i--)
     {
      BufferTime[i]=(double)time[i];
      CalculateSeries(i,time[i]);
     }
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

En general, hemos intentado describir todo lo que hemos hecho con estas u otras funciones en los comentarios al listado de todas las funciones presentadas.
Esperamos que no surjan dudas al respecto. En cualquier caso, podrá plantear cualquier duda en los comentarios al final del artículo: las resolveremos con mucho gusto.

Las demás funciones que el indicador ha heredado de su anterior versión permanecen sin cambios destacables.
En los archivos adjuntos al final del artículo, el lector podrá ver el código completo del indicador.

Compilamos el indicador y lo iniciamos en el gráfico EURUSD M15.


Como podemos ver, se han representado los cuatro botones con los primeros cuatro símbolos. Hasta que no se pulse alguno de los botones, para ellos no se representarán los botones de selección de periodo para la representación. Basta con pulsar el botón de un símbolo, y se abrirá para él una lista con los botones de selección del periodo. Seleccionamos el periodo y vemos cómo se representan en el gráfico las velas del símbolo y el periodo elegidos. Ahora, el estado de los botones seleccionados ha sido registrado en las variables globales del terminal, y después de reiniciar el indicador o pulsar un botón de otro símbolo y retornar posteriormente al anterior, para él ya se representarán los botones de los periodos con el botón seleccionado del periodo anteriormente utilizado.

Bien. Ya hemos comprobado el concepto de construcción de los búferes de indicador utilizando el guardado en sus estructuras. No obstante, el trabajo con estos desde el indicador sigue siendo bastante incómodo. Por eso, a partir del próximo artículo, comenzaremos a crear algunas clases de búferes de indicador que nos permitan crear nuestros propios indicadores de forma más cómoda y sencilla.

Bueno, y también cabe destacar que, durante los dos últimos artículos, hemos podido familiarizarnos con algunos métodos para crear de forma sencilla indicadores de símbolo y periodo múltiples con la ayuda de las series temporales de la biblioteca.

¿Qué es lo próximo?

En el próximo artículo, abordaremos el desarrollo de las clases de los búferes de indicador.

Más abajo se adjuntan todos los archivos de la versión actual de la biblioteca y los archivos del asesor de prueba. Puede descargarlo todo y ponerlo a prueba por sí mismo.
Si tiene preguntas, observaciones o sugerencias, podrá concretarlas en los comentarios al artículo.
Querríamos recordar al lector que en este artículo hemos creado un indicador de prueba en MQL5 para MetaTrader 5.
Los archivos adjuntos han sido diseñados solo para MetaTrader 5, y en MetaTrader 4, la biblioteca en su versión actual no ha sido puesta a prueba.
En la cuarta versión de la plataforma, el tipo de dibujado de búferes utilizado hoy (DRAW_COLOR_CANDLES) no tiene soporte, pero, a la hora de crear las clases de los búferes de los indicadores, intentaremos también implementar algunas cosas de MQL5 en MetaTrader 4.

Volver al contenido

Artículos de esta serie:

Trabajando con las series temporales en la biblioteca DoEasy (Parte 35): El objeto "Barra" y la lista de serie temporal del símbolo
Trabajando con las series temporales en la biblioteca DoEasy (Parte 36): El objeto de series temporales de todos los periodos utilizados del símbolo
Trabajando con las series temporales en la biblioteca DoEasy (Parte 37): Colección de series temporales - Base de datos de series temporales según el símbolo y el periodo
Trabajando con las series temporales en la biblioteca DoEasy (Parte 38): Colección de series temporales - Actualización en tiempo real y acceso a los datos desde el programa
Trabajando con las series temporales en la biblioteca DoEasy (Parte 39): Indicadores basados en la biblioteca - Preparación de datos y eventos de la series temporales
Trabajando con las series temporales en la biblioteca DoEasy (Parte 40): Indicadores basados en la biblioteca - actualización de datos en tiempo real

Traducción del ruso hecha por MetaQuotes Software Corp.
Artículo original: https://www.mql5.com/ru/articles/7804

Archivos adjuntos |
MQL5.zip (3705.42 KB)
Sobre los métodos de búsqueda de las zonas de sobrecompra/sobreventa. Parte I Sobre los métodos de búsqueda de las zonas de sobrecompra/sobreventa. Parte I

Las zonas de sobrecompra/sobreventa caracterizan un determinado estado del mercado que se distingue por el debilitamiento de la dinámica de los precios de los instrumentos financieros. En este caso, además, dicha dinámica negativa se manifiesta en mayor medida en el estadio final del desarrollo de una tendencia de cualquier escala. Y dado que la magnitud del beneficio en el trading depende directamente de la posibilidad de abarcar la máxima amplitud en la tendencia, la precisión a la hora de detectar estas zonas supone una tarea de capital importancia al comerciar con cualquier instrumento financiero.

Optimización móvil continua (Parte 7): Encajando la parte lógica del optimizador automático con la parte gráfica y el control de la misma desde el programa Optimización móvil continua (Parte 7): Encajando la parte lógica del optimizador automático con la parte gráfica y el control de la misma desde el programa

Este artículo es el penúltimo de la serie, y describe cómo encajar la parte gráfica del programa del optimizador automático con su parte lógica. En él, analizaremos el proceso de inicio y optimización, comenzando por la pulsación del botón y terminando el redireccionamiento al gestor de optimizaciones.

Trabajando con las series temporales en la biblioteca DoEasy (Parte 42): La clase del objeto de búfer de indicador abstracto Trabajando con las series temporales en la biblioteca DoEasy (Parte 42): La clase del objeto de búfer de indicador abstracto

En este artículo, comenzamos a construir las clases de los búferes de indicador para la biblioteca DoEasy. En esta parte, crearemos la clase básica de búfer abstracto, que será la principal para crear los diferentes tipos de clases de los búferes de indicador.

Monitoreo multidivisas de las señales comerciales (Parte 5): Señales compuestas Monitoreo multidivisas de las señales comerciales (Parte 5): Señales compuestas

En la parte 5 del desarrollo de la aplicación para monitorear las señales comerciales, introduciremos el concepto de la señal compuesta en nuestro sistema e implementaremos la funcionalidad necesaria para ello. Antes usábamos las señales simples en nuestra aplicación (RSI, WPR, CCI), también podíamos usar nuestro propio indicador personalizado.