Desarrollo de indicadores bursátiles con control de volumen tomando como ejemplo el indicador delta

Tapochun | 8 noviembre, 2018

Contenido

Introducción

Como ya sabemos, el terminal MetaTrader 5 transmite dos tipos de volúmenes:

En el terminal, el volumen real se denomina simplemente como "Volumen". Es ese precisamente el que nos interesa, pues con la aparición en el terminal de acceso a la historia de ticks y la banda de transacciones, ha surgido la posibilidad de escribir indicadores bursátiles. Con su ayuda, podemos ver qué sucede "entre bambalinas", es decir, podemos saber de qué consta el volumen real: las transacciones de qué volumen y con qué frecuencia han tenido lugar en este u otro momento, y cuáles han sido más frecuentes en un intervalo concreto de tiempo, las de los vendedores o las de los compradores. Y eso significa que ahora podemos dividir el volumen en componentes. Estos datos pueden mejorar significativamente la precisión de nuestros pronósticos comerciales. Por otra parte, escribir correctamente semejante indicador resulta más complejo que escribir uno normal. En este artículo expondremos con detalle la secuencia de composición y las sutilezas del desarrollo de los indicadores bursátiles, así como las peculiaridades de su trabajo y simulación. Como ejemplo, escribiremos el indicador delta (de diferencia) de los volúmenes de compra y venta que forman el volumen real. A medida que vayamos creando el indicador, describiremos las normas de trabajo con el volumen de ticks.

Sin embargo, debemos recordar que el volumen real solo está disponible en los mercados centralizados (bursátiles). Es decir, a priori, no está disponible para el mercado fórex, ya que se trata de un mercado no bursátil. Vamos a analizar los volúmenes reales usando como ejemplo la Sección de Derivados de la Bolsa de Moscú (FORTS). Si usted no se ha familiarizado hasta este momento con FORTS, le recomendamos encarecidamente que lo haga con la ayuda del artículo sobre la formación de precios.

A quién va dirigido este artículo

Últimamente, en el foro mql5.com se formulan con frecuencia preguntas sobre los volúmenes de ticks. Esta funcionalidad es relativamente nueva y se ve mejorada continuamente. Este artículo, en primer lugar, está dedicado a los programadores que ya tienen indicadores y desean mejorar sus habilidades de escritura de programas para MetaTrader 5. Asimismo, este artículo resultará interesante a los tráders que quieran conocer bien el mercado bursátil y estén interesados en realizar análisis con ayuda del indicador delta y/o indicadores de ticks semejantes.

1. Preparación. Eligiendo el servidor

Por paradójico que sea, el desarrollo del indicador debe comenzar por la elección del servidor comercial. A la hora de trabajar con precisión con indicadores bursátiles, una de las condiciones indispensable será que los servidores del bróker estén actualizados. Por desgracia, las versiones de los servidores del bróker no se retransmiten y no siempre podemos comprender de primeras si nos llegan datos exactos.

La profundidad de mercado se encargará de ayudarnos a comprender si el servidor está o no actualizado. Para abrirla, primero debemos pulsar el botón en forma de recuadro junto al nombre del instrumento en la esquina superior izquierda de la pantalla (si no se representa, compruebe en las propiedades del gráfico (F8) si se ha establecido la casilla frente al parámetro "Mostrar los botones de comercio rápido" de la pestaña "Mostrar") o bien usar la combinación de teclas Alt+B. Tras abrir la profundidad de mercado, debemos pulsar el botón "Mostrar el recuadro de todas las transacciones". Asimismo, compruebe que esté activado el filtro de volumen mínimo clicando con el botón derecho del ratón en el recuadro.

Si el servidor no ha sido actualizado, retransmitirá en la profundidad de mercado las llamadas "transacciones de dirección no predeterminada". Más adelante veremos de qué se trata. Cada transacción tiene un iniciador: el comprador o el vendedor. Por consiguiente, la propiedad de la transacción "compra" o "venta" deberá estar indicada de forma explícita. Si la transacción tiene una dirección no predeterminada (se marca con N/A en la profundidad del mercado), esto influirá en la precisión de la construcción de delta (diferencia entre compras y ventas), que precisamente va a calcular nuestro indicador. Más abajo se muestra el aspecto de la profundidad actualizada y no acutualizada (fig. 1):

  

Fig.1. Aspecto actualizado (izquierda) y antiguo (derecha).

Regla №1. Compruebe si el servidor está actualizado.

Asimismo, recomendamos encarecidamente elegir un servidor con un retardo (ping) pequeño. Cuanto menor sea este, mayor será la velocidad a la que su terminal podrá intercambiar información con el servidor del bróker. Si miramos atentamente a la fig. 1, la plataforma MetaTrader 5 retransmite las transacciones con una precisión de hasta un milisegundo, así que cuanto menor sea el ping, mayor será la velocidad con la que usted recibirá información sobre las transacciones y con la que usted podrá procesarla. Podrá comprobar el ping hasta el servidor actual (y cambiar el servidor en caso necesario) en la ventana inferior derecha de la ventana del terminal:

Fig. 2. El retardo hasta el servidor elegido es de 30.58 milisegundos.

Asimismo, debemos destacar que el terminal de cliente debe ser actualizado hasta el build 1881 o una versión superior. Para este build ya se han sido corregido todos los errores conocidos relacionados con los datos de ticks.

2. Métodos de obtención de la histoia de ticks. Formato MqlTick

Bien, ya hemos elegido el servidor que nos proporcionará una historia de ticks correcta. Ahora nos surge la pregunta: ¿como obtener esta historia? Para ello, en el lenguaje MQL5 se han introducido dos funciones:

Para nuestro indicador, vamos a necesitar ambas funciones. Estas permiten obtener los ticks en el formato MqlTick. En esta estructura se guardan los datos sobre la hora, los precios, el volumen y sobre qué datos precisamente han cambiado con el tick actual. Asimismo, será necesario tener en cuenta que podemos obtener una historia de ticks de tres tipos. Dicho tipo se define con una bandera:

Para nuestros objetivos, vamos a necesitar precisamente el flujo de los ticks comerciales (COPY_TICKS_TRADE). Podrá leer más información sobre estos ticks en la descripción de CopyTicks.

En la estructura MqlTick vamos a analizar los valores de los siguientes campos:

Si lo simplificamos totalmente, nuestro indicador hará lo siguiente: recibir todos los ticks comerciales de una vela, ver qué volumen de compras ha habido y cuál ha sido el volumen de ventas, y después representar la diferencia de estos volúmenes (delta) como un histograma. Si en la vela ha habido más compradores que vendedores, la columna del histograma será azul. Si ha habido más vendedores, será roja. ¡Es muy sencillo!

3. Primera inicialización. Cálculo de la historia

El principal objeto para trabajar con los ticks en nuestro indicador será CTicks _ticks (archivo Ticks_article.mqh). A través del mismo, ejecutaremos todas las operaciones con ticks.

El funcionamiento del indicador está dividido en dos bloques principales: el bloque de cálculo de la historia y el bloque de cálculo en tiempo real.

//+------------------------------------------------------------------+
//| 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[])
  {
//--- Comprobando la primera inicialización
   if(prev_calculated>0)                    // Si no es la primera inicialización
     {
      // Bloque №2
     }
   else                                     // Si es la primera inicialización
     {
      // Bloque №1
     }
//---
   return( rates_total );
  }

Al inicializar el indicador o al pulsar el botón "actualizar" en el terminal (Bloque №1) debemos calcular el indicador en la historia. Inicialmente, planeábamos crear una función de cálculo universal que se usaría tanto en el cálculo de la historia como en los cálculos en tiempo real. Sin embargo, para mayor simplicidad y para aumentar la velocidad de los cálculos, finalmente hemos decidido hacerlo de otra forma. Primero se calcula la historia de las barras formadas (CalculateHistoryBars()), y después se calcula la barra actual (CalculateCurrentBar()). Todas estas acciones se describen más abajo:

//--- 1. Inicializamos los búferes de indicador con los valores iniciales
BuffersInitialize(EMPTY_VALUE);
//--- 2. Reseteamos los valores de los parámetros de control repetido
_repeatedControl=false;
_controlNum=WRONG_VALUE;
//--- 3. Reseteamos la hora de la barra en la que se anotarán los ticks (pulsar el botón "actualizar")
_ticks.SetTime(0);
//--- 4. Establecemos el momento de inicio de la carga de ticks de las barras formadas
_ticks.SetFrom(inpHistoryDate);
//--- 5. Comprobamos el momento de inicio de la carga
if(_ticks.GetFrom()<=0)                 // Si el momento no ha sido establecido
   return(0);                           // Salimos
//--- 6. Establecemos el momento de finalización de la carga de la historia de las barras formadas
_ticks.SetTo( long( time[ rates_total-1 ]*MS_KOEF - 1 ) );
//--- 7. Cargamos la historia de las barras formadas
if(!_ticks.GetTicksRange())             // Si no tenemos éxito
   return(0);                           // Salimos con error
//--- 8. Cálculo de la historia de barras formadas
CalculateHistoryBars( rates_total, time, volume );
//--- 9. Reseteamos la hora de la barra en la que se registrarán los ticks
_ticks.SetTime(0);
//--- 10. Establecemos el momento de inicio de la carga de ticks de la última barra
_ticks.SetFrom( long( time[ rates_total-1 ]*MS_KOEF ) );
//--- 11. Establecemos el momento de finalización de la carga de ticks de la última barra
_ticks.SetTo( long( TimeCurrent()*MS_KOEF ) );
//--- 12. Cargamos la historia de la barra actual
if(!_ticks.GetTicksRange())             // Si no tenemos éxito
   return(0);                           // Salimos con error
//--- 13. Reseteamos el momento de finalización del copiado
_ticks.SetTo( ULONG_MAX );
//--- 14. Recordamos la hora del último tick de la historia obtenida
_ticks.SetFrom();
//--- 15. Cálculo de la barra actual
CalculateCurrentBar( true, rates_total, time, volume );
//--- 16. Establecemos el número de ticks para el copiado posterior en tiempo real
_ticks.SetCount(4000);

El código del indicador está bien comentado, ya que solo nos detenemos en los momentos principales.

Punto 3. "Reseteamos la hora de la barra en la que se anotarán los ticks". El objeto que trabaje con los ticks contendrá la hora de apertura de la vela en la que se registran los ticks que se corresponden con ella. Al pulsar el botón "Actualizar" en el terminal tiene lugar el recálculo del indicador desde el principio. Por consiguiente, para que los ticks se registren correctamente en la vela necesaria, tendremos que resetear su hora.

Punto 4. "Establecemos el momento de inicio de la carga de ticks de las barras formadas". La obtención de la historia de ticks puede ocupar bastante tiempo. Por eso, debemos dar la posibilidad al usuario de indicar por sí mismo la fecha de inicio de su carga. Para ello, en el indicador se presenta el parámetro inpHistoryDate. Si indicamos el valor cero, tendrá lugar la carga de la historia desde el comienzo del día actual. En este prototipo del método SetFrom(datetime), la hora se transmite en segundos. Como ya hemos dicho más arriba, primero va el cálculo de las barras formadas del indicador.

Punto 5. "Comprobamos que el momento de comienzo de la carga sea correcto". Comprobando el valor obtenido en el punto 4.

Punto 6. "Establecemos el momento de finalización de la carga de la historia de las barras formadas". Como momento de carga de la historia de las barras formadas servirá el milisegundo anterior a la apertura de la vela actual (rates_total-1). En este caso, el momento de finalización de la carga tendrá el tipo long. Al transmitir el parámetro al método, tendremos que indicar explícitamente que se transmite el tipo long en el caso de que en la clase también haya un método al que se transmite un parámetro con el tipo datetime. En lo que se refiere al método SetTo(), en la clase no se ha implementado su carga con un argumento del tipo datetime, sin embargo, mejor cubrirse las espaldas y transmitir el parámetro del tipo long.

Punto 7. "Cargamos la historia de las barras formadas". La obtención de la historia tiene lugar con la ayuda de la función GetTicksRange(), que es un envoltorio para la función CopyTicksRange() con adición de comprobaciones de posibles errores. Si al realizar la carga surgen errores, en el siguiente tick se volverá a pedir toda la historia. En el archivo Ticks_article.mqh de los anexos podrá familiarizarse con mayor detalle con esta y otras funciones para trabajar con ticks. 

Punto 8. "Cálculo de la historia de barras formadas". Describiremos con más detalle el cálculo en las barras formadas en el apartado correspondiente del artículo.

Puntos 9-12. El cálculo de las barras ha finalizado. Debemos calcular la barra actual. Aquí se establece el rango para el copiado, y se obtienen los tick de la vela actual.

Punto 13. "Reseteamos el momento de finalización del copiado". En lo sucesivo, seguiremos obteniendo los ticks con la ayuda del objeto _ticks, pero no los obtendremos de un momento hasta otro, como hacíamos al cargar la historia, sino desde el momento de la llegada del último tick hasta el final de toda la historia disponible. Precisamente por eso es mejor resetear el momento en el que copiado finaliza: al calcular en tiempo real, no lo vamos a necesitar más.

Punto 14. "Recordamos la hora del último tick de la historia obtenida". La hora del último tick de la historia obtenida la necesitaremos en el futuro como momento de inicio del copiado de datos en tiempo real.

Punto 15. "Cálculo de la barra actual". El cálculo de la barra actual también se describirá en una sección aparte del artículo y tendrá diferencias importantes con respecto al método de cálculo de las barras formadas.

Punto 16. "Establecemos el número de ticks para el copiado posterior en tiempo real". Si antes obteníamos los ticks con ayuda de la función CopyTicksRange(), envuelta en el método GetTicksRange(), para el tiempo real vamos a usar la función CopyTicks(), envuelta en el método GetTicks(). El método SetCount() establece el número de ticks para las solicitudes posteriores. Se ha elegido 4000 porque el terminal guarda 4096 ticks de cada símbolo para el acceso rápido, y las solicitudes para estos ticks se ejecutan con la mayor rapidez. Da igual que establezcamos el valor 100 o el valor 4000, esto no influirá de manera alguna en la velocidad de recepción de los ticks (~1 ms).

Bien, vamos a analizar con mayor detalle las funciones de cálculo.

4. Función de cálculo de la historia de barras formadas

La propia función tiene el aspecto siguiente:

//+------------------------------------------------------------------+
//| Función de cálculo de las barras formadas de la historia         |
//+------------------------------------------------------------------+
bool CalculateHistoryBars(const int rates_total,    // Número de barras calculadas
                          const datetime& time[],   // Matriz de hora de apertura de las barras 
                          const long& volume[]      // Matriz de valores del volumen real
                          )
  {
//--- Volúmenes totales
   long sumVolBuy=0;
   long sumVolSell=0;
//--- Número de la barra para almacenar en el búfer
   int bNum=WRONG_VALUE;
//--- Obtenemos el número de ticks en la matriz
   const int limit=_ticks.GetSize();
//--- Ciclo por todos los ticks
   for(int i=0; i<limit && !IsStopped(); i++)
     {
      //--- Determinamos la vela en la que se registran los ticks
      if(_ticks.IsNewCandle(i))                         // Si la próxima vela se comienza a formar
        {
         //--- Comprobamos si el número de la vela formada (completada) está registrado
         if(bNum>=0) // Si el número está registrado
           {
            //--- Comprobamos si está registrado el valor de los volúmenes
            if(sumVolBuy>0 || sumVolSell>0) // Si todos los parámetros están registrados
              {
               //--- Controlamos el volumen total de la vela
               VolumeControl(false,bNum,volume[bNum],time[bNum],sumVolBuy,sumVolSell);
              }
            //--- Introducimos los valores en los búferes
            DisplayValues(bNum,sumVolBuy,sumVolSell,__LINE__);
           }
         //--- Reseteamos los volúmenes de la vela anterior
         sumVolBuy=0;
         sumVolSell=0;
         //--- Establecemos el número de vela que se corresponde con la  hora de su apertura
         bNum=_ticks.GetNumByTime(false);
         //--- Comprobamos si el número es correcto
         if(bNum>=rates_total || bNum<0) // Si el número es incorrecto     
           {
            //--- Salimos sin calcular la historia
            return( false );
           }
        }
      //--- Añadimos el volumen en el tick al componente necesario
      AddVolToSum(_ticks.GetTick(i),sumVolBuy,sumVolSell);
     }
//--- Comprobamos si está registrado el valor de los volúmenes de la última vela formada
   if(sumVolBuy>0 || sumVolSell>0) // Si todos los parámetros están registrados
     {
      //--- Controlamos el volumen total de la vela
      VolumeControl(false,bNum,volume[bNum],time[bNum],sumVolBuy,sumVolSell);
     }
//--- Introducimos los valores en los búferes
   DisplayValues(bNum,sumVolBuy,sumVolSell,__LINE__);
//--- El cálculo ha finalizado
   return( true );
  }

La esencia de esta función es clasificar la obtención de ticks según las velas formadas del marco temporal actual, obtener la diferencia de volúmenes de las compras y ventas e introducir los valores obtenidos de los volúmenes y deltas en los búferes de indicador.

Como hemos recordado anteriormente, al principio planeábamos crear una función general para el cálculo de la historia y los cálculos en tiempo real. Sin embargo, al añadir la función de cálculo de la historia de las barras formadas, perseguimos varios objetivos:

Más tarde mostraremos la descripción completa del algoritmo y las peculiaridades del trabajo con la historia de ticks.

5. Función de cálculo de la vela actual

En el código del indicador hay que prestar sobre todo atención a la función CalculateCurrentBar().

//+------------------------------------------------------------------+
//| Función de cálculo de la vela actual                             |
//+------------------------------------------------------------------+
void CalculateCurrentBar(const bool firstLaunch,   // bandera de la primera inicialización de la función
                         const int rates_total,    // Número de barras calculadas
                         const datetime& time[],   // Matriz de hora de apertura de las barras 
                         const long& volume[]      // Matriz de valores del volumen real
                         )
  {
//--- Volúmenes totales
   static long sumVolBuy=0;
   static long sumVolSell=0;
//--- Número de la barra para almacenar en el búfer
   static int bNum=WRONG_VALUE;
//--- Comprobamos la bandera de la primera inicialización
   if(firstLaunch)                                 // Si es la primera inicialización
     {
      //--- Reseteamos los parámetros estáticos
      sumVolBuy=0;
      sumVolSell=0;
      bNum=WRONG_VALUE;
     }
//--- Obtenemos el número del penúltimo tick en la matriz
   const int limit=_ticks.GetSize()-1;
//--- Hora del tick "limit"
   const ulong limitTime=_ticks.GetFrom();
//--- Ciclo por todos los ticks (incluyendo el último)
   for(int i=0; i<limit && !IsStopped(); i++)
     {
      //--- 1. Comparamos la hora del tick i con la hora del tick "limit" (comprobación de finalización del ciclo)
      if( _ticks.GetTickTimeMs( i ) == limitTime ) // Si la hora del tick es igual a la hora del tick límite
         return;                                   // Salimos
      //--- 2. Comprobamos si se ha comenzado a formar la vela ausente en el gráfico
      if(_ticks.GetTickTime(i)>=time[rates_total-1]+PeriodSeconds())                // Si la vela ha comenzado a formarse
        {
         //--- Comprobamos el log
         if(inpLog)
            Print(__FUNCTION__,": ¡ATENCIÓN! Tick futuro ["+GetMsToStringTime(_ticks.GetTickTimeMs(i))+"]. Hora del tick "+TimeToString(_ticks.GetTickTime(i))+
                  ", time[ rates_total-1 ]+PerSec() = "+TimeToString(time[rates_total-1]+PeriodSeconds()));
         //--- 2.1. Establecemos (corregimos) la hora de la próxima solicitud de ticks
         _ticks.SetFrom(_ticks.GetTickTimeMs(i));
         //--- Salimos
         return;
        }
      //--- 3. Determinamos la vela en la que se registran los ticks
      if(_ticks.IsNewCandle(i))                    // Si la próxima vela se comienza a formar
        {
         //--- 3.1. Comprobamos si el número de la vela formada (completada) está registrado
         if(bNum>=0)                               // Si el número está registrado
           {
            //--- Comprobamos si está registrado el valor de los volúmenes
            if(sumVolBuy>0 || sumVolSell>0)        // Si todos los parámetros están registrados
              {
               //--- 3.1.1. Controlamos el volumen total de la vela
               VolumeControl(true,bNum,volume[bNum],time[bNum],sumVolBuy,sumVolSell);
              }
           }
         //--- 3.2. Reseteamos los volúmenes de la vela anterior
         sumVolBuy=0;
         sumVolSell=0;
         //--- 3.3. Recordamos el número de la vela actual
         bNum=rates_total-1;
        }
      //--- 4. Añadimos el volumen en el tick al componente necesario
      AddVolToSum(_ticks.GetTick(i),sumVolBuy,sumVolSell);
      //--- 5. Introducimos los valores en los búferes
      DisplayValues(bNum,sumVolBuy,sumVolSell,__LINE__);
     }
  }

Ella es parecida a la función CalculateHistoryBars() anterior, pero tiene sus peculiaridades. Vamos a analizarlas en mayor profundidad. Más abajo mostramos el prototipo de la función:

//+------------------------------------------------------------------+
//| Función de cálculo de la vela actual                             |
//+------------------------------------------------------------------+
void CalculateCurrentBar(const bool firstLaunch,   // bandera de la primera inicialización de la función
                         const int rates_total,    // Número de barras calculadas
                         const datetime& time[],   // Matriz de hora de apertura de las barras 
                         const long& volume[]      // Matriz de valores del volumen real
                         )

Destacaremos de inmediato, que CalculateCurrentBar() se usará en dos casos: al calcular la historia de la vela actual en el primer inicio y al realizar los cálculos en tiempo real. La bandera firstLaunch será la encargada de elegir el modo de cálculo. La diferencia entre los modos solo residirá en que en el primer inicio, las variables estáticas que contienen las sumas de las ventas y compras se resetearán a cero, así como el número de la vela en los búferes en los que se registrarán estas sumas y su diferencia: delta. ¡Debemos de nuevo recordar que en el indicador solo se usan volúmenes reales!

//--- Volúmenes totales
   static long sumVolBuy=0;
   static long sumVolSell=0;
//--- Número de la barra para almacenar en el búfer
   static int bNum=WRONG_VALUE;
//--- Comprobamos la bandera de la primera inicialización
   if(firstLaunch)                                 // Si es la primera inicialización
     {
      //--- Reseteamos los valores de los volúmenes
      sumVolBuy=0;
      sumVolSell=0;
      //--- Reseteamos el número de vela
      bNum=WRONG_VALUE;
     }

Después de declarar las variables estáticas, se obtienen el número y la hora del último tick en la matriz:

//--- Obtenemos el número del último tick en la matriz
   const int limit=_ticks.GetSize()-1;
//--- Hora del tick "limit"
   const ulong limitTime=_ticks.GetFrom();

El número será el limitador del ciclo de iteración de ticks. Establecemos la condición de que el último tick y los ticks con una hora como en el último, no participarán en los cálculos. ¿Y por qué? Porque los ticks comerciales pueden llegar en paquetes, en caso de que se implemente una orden de mercado única con varias órdenes límite de diferentes contrapartes. Un paquete de ticks (transacciones) representa las transacciones realizadas al mismo tiempo (con una precisión de milisegundos) y que tienen el mismo tipo (compra o venta) (fig. 3). Para que conste, debemos destacar que varios paquetes de ticks pueden representarse en el terminal como si llegasen en el mismo milisegundo, puesto que la bolsa retransmite las transacciones con una precisión de hasta un milisegundo. Podemos comprobarlo iniciando el script test_getTicksRange, de los anexos al artículo.

Fig. 3. Paquete de ticks (la orden de mercado de compra ha iniciado 4 transacciones de 26 lotes).

Para tener en cuenta correctamente el volumen, debemos calcular un paquete de ticks cada vez y solo cuando este haya sido transmitido por completo al terminal, es decir, cuando esté disponible una transacción ejecutada en un momento posterior (fig.4).


Fig. 4. Transacción en el momento .373, momento de cálculo del paquete .334.

Sí, precisamente así. No podemos contar con que el paquete llegue completo al terminal hasta que no esté disponible la transacción después del paquete, puesto que el paquete en el terminal puede ser transmitido por partes. Ahora no vamos a detenernos con detalle en este momento, el lector deberá simplemente confiar en el autor. A partir de ello, podemos formular la regla №2:

Regla №2. El cálculo de los ticks se debe realizar solo después de recibir el tick que siga a este paquete.

En el punto 13 del algoritmo de primera inicialización hemos guardado la hora del último tick recibido. Ahora la usaremos, registrándola en la variable limitTime.

A continuación, pasaremos directamente al ciclo de cálculo de los ticks:

//--- 1. Comparamos la hora del tick i con la hora del tick "limit" (comprobación de finalización del ciclo)
      if( _ticks.GetTickTimeMs( i ) == limitTime ) // Si la hora del tick es igual a la hora del tick límite
         return;                                   // Salimos

Punto 1. "Comparamos la hora del tick con la hora del último tick". Como hemos dicho antes, el último tick no se tiene en cuenta en los cálculos, puesto que el cálculo solo se da según los paquetes de ticks formados. Pero también sabemos que el último paquete de ticks podría no ser copiado por completo. Así que deberemos excluir del cálculo no solo el último tick recibido, sino también todos los ticks de su paquete (si los ha habido).

//--- 2. Comprobamos si se ha comenzado a formar la vela ausente en el gráfico
      if(_ticks.GetTickTime(i)>=time[rates_total-1]+PeriodSeconds())

Punto 2. "Comprobamos si se ha comenzado a formar la vela ausente en el gráfico". Suena un poco extraño, sí. Cómo puede comenzar a formarse una vela ausente en el gráfico, se preguntará usted. Para responder a esta pregunta, debemos primero entender una peculiaridad del procesamiento/obtención de los datos de ticks en el terminal. Estas particularidades fueron aclaradas como resultado de una extensa comunicación con los desarrolladores de la mesa de servicio. Vamos a mostrar aquí una reproducción libre de la misma: 

Los ticks en el terminal se colectan en un flujo aparte, independientemente del funcionamiento de los indicadores o expertos. Las velas se construyen en otro flujo: en el flujo de ejecución de los indicadores. Estos flujos no están sincronizados entre sí de ninguna forma. Después de que el tick se haya aplicado a la vela, tiene lugar el cálculo del indicador. En este caso, además, no se omite un solo tick. De esto podemos deducir que llamando la función CopyTicks() podemos obtener datos de ticks más recientes que los que ya se han aplicado a las barras.

En la práctica, esto significará lo siguiente. Al calcular las velas rates_total-1, el indicador puede obtener los ticks de la siguiente vela que aún no se ha formado (el tick aún no se ha aplicado a ella). Para evitar esta situación (y evitar errores de salida fuera de la matriz), debemos añadir esta comprobación.

Regla 3. Hay que tener en cuenta que pueden obtenerse ticks que aún no han aparecido en el gráfico de la vela.

En el caso de que el tick(s) "futuro/s" (la vela según los ticks todavía no se ha formado) hayan sido detectados, deberemos reescribir el momento temporal a partir del cual tendrá lugar la siguiente solicitud de ticks (punto 2.1.). Y, por supuesto, salir de inmediato del ciclo y de la función, esperando la llegada de un nuevo tick y la formación de una nueva vela en el gráfico:

//--- 2. Comprobamos si se ha comenzado a formar la vela ausente en el gráfico
      if(_ticks.GetTickTime(i)>=time[rates_total-1]+PeriodSeconds())                // Si la vela ha comenzado a formarse
        {
         //--- 2.1. Establecemos (corregimos) la hora de la próxima solicitud de ticks
         _ticks.SetFrom(_ticks.GetTickTimeMs(i));
         //--- Salimos
         return;
        }

El algoritmo posterior coincide prácticamente en su totalidad con la función CalculateHistoryBars(). Vamos a analizarlo con mayor detalle.

//--- 3. Determinamos la vela en la que se registran los ticks
      if(_ticks.IsNewCandle(i))

Punto 3. Determinamos la vela en la que se registran los ticks. Aquí se compara la hora del tick i y la hora de apertura de la vela en la que se registran los ticks. Si la hora del tick i sale fuera de los límites de la vela, la hora de apertura de la vela cambia y se activa el algoritmo de preparación para el análisis de la próxima vela:

//--- 3. Determinamos la vela en la que se registran los ticks
      if(_ticks.IsNewCandle(i))                    // Si la próxima vela se comienza a formar
        {
         //--- 3.1. Comprobamos si el número de la vela formada (completada) está registrado
         if(bNum>=0)                               // Si el número está registrado
           {
            //--- Comprobamos si está registrado el valor de los volúmenes
            if(sumVolBuy>0 || sumVolSell>0)        // Si todos los parámetros están registrados
              {
               //--- 3.1.1. Controlamos el volumen total de la vela
               VolumeControl(true,bNum,volume[bNum],time[bNum],sumVolBuy,sumVolSell);
              }
           }
         //--- 3.2. Reseteamos los volúmenes de la vela anterior
         sumVolBuy=0;
         sumVolSell=0;
         //--- 3.3. Recordamos el número de la vela actual
         bNum=rates_total-1;
        }

Punto 3.1. Comprobamos si el número de la vela formada está registrado. En el modo de cálculo de la historia (primera inicialización) esta comprobación prevendrá contra el acceso a las matrices de tiempo y volumen con un índice incorrecto (-1). A continuación, se comprueba si se han realizado transacciones en la vela. Si no ha habido transacciones, no será necesario el control de volumen.

Punto 3.1.1. Controlamos el volumen total. En el procesimiento VolumeControl() se suman los volúmenes de Buy y Sell acumulados por nuestro indicador en una vela, y se comparan con el "volumen de control", es decir, con el volumen real transferido directamente desde la bolsa (el valor de la matriz Volume[] de la vela formada). Si el volumen de la bolsa coincide con el volumen acumulado, pasamos a los cálculos posteriores. Y si los volúmenes no coinciden...un segundo. Se preguntará, ¿cómo pueden no coincidir los volúmenes? Se trata del mismo volumen total. Lo que ocurre es que un volumen lo hemos calculado nosotros con nuestro indicador, mientras que el otro ha llegado de la bolsa. ¡Los volúmenes deben coincidir! 

Exacto, los volúmenes deben coincidir. Y esta regla se debe cumplir para todas las velas. Y es que, nuestro indicador:

No es nada complicado. Pero en tiempo real (para la vela rates_total-1) debemos recordar aquellos mismos ticks del "futuro" (regla 3), cuando llega el tick de la nueva vela, y en el gráfico la vela aún no se ha formado. ¡Esta peculiaridad también deja su huella en el control del volumen! En este momento del procesamiento del tick, el indicador contendrá un valor aún no actualizado del volumen en la matriz volume[] (el valor todavía cambiará). Por consiguiente, no podremos realizar correctamente la comparación del volumen recopilado por el indicador y el volumen de la matriz volume[]. En la práctica, el volumen de control volume[rates_total-1] a veces no coincidirá con la suma de volúmenes (sumVolBuy+sumVolSell) recopilados por el indicador. En este caso, en el procedimiento VolumeControl() se muestran dos variantes de solución a este problema:

  1. Recalcular el volumen de la vela y compararlo con el valor de control obtenido a través de la función CopyRealVolume();
  2. En el caso de que la primera opción no haya solucionado el problema, se establecerá la bandera de control del volumen en el momento de formación de una nueva vela.

Por consiguiente, el primer método intenta resolver el problema del control antes de que se forme la nueva vela, y el segundo debe resolver con total garantía el problema después de la formación de una nueva vela.

Punto 3.2. "Reseteamos los volúmenes de la vela anterior". Después de que se forme una nueva vela, reseteamos los contadores de los volúmenes a cero.

Punto 3.3. "Recordamos el número de la vela actual". Esta es otra ventaja más de la separación de las funciones de cálculo en dos: la función de cálculo de barras formadas y la función de cálculo de la vela actual. El número de la vela actual es siempre = rates_total-1.

//--- 4. Añadimos el volumen en el tick al componente necesario
      AddVolToSum(_ticks.GetTick(i),sumVolBuy,sumVolSell);

Punto 4. Añadimos el volumen del tick al volumen total. Primero debemos aclarar qué datos han cambiado según la bandera del tick analizado:

//+------------------------------------------------------------------+
//| Añadimos el volumen del tick al volumen total                    |
//+------------------------------------------------------------------+
void AddVolToSum(const MqlTick &tick,        // Parámetros del tick comprobado
                 long& sumVolBuy,            // Volumen total de compras (out)
                 long& sumVolSell            // Volumen total de ventas (out)
                )
  {
//--- Comprobamos la dirección del tick
   if(( tick.flags&TICK_FLAG_BUY)==TICK_FLAG_BUY && ( tick.flags&TICK_FLAG_SELL)==TICK_FLAG_SELL) // Si el tick es de ambos sentidos
        Print(__FUNCTION__,": ОШИБКА! ¡Tick '"+GetMsToStringTime(tick.time_msc)+"' de sentido desconocido!");
   else if(( tick.flags&TICK_FLAG_BUY)==TICK_FLAG_BUY)   // Si es un tick de compra
        sumVolBuy+=(long)tick.volume;
   else if(( tick.flags&TICK_FLAG_SELL)==TICK_FLAG_SELL) // Si es un tick de venta
        sumVolSell+=(long)tick.volume;
   else                                                  // Si no es un tick comercial
        Print(__FUNCTION__,": ОШИБКА! ¡El tick '"+GetMsToStringTime(tick.time_msc)+"' no es comercial!");
  }

Aquí querríamos de nuevo centrar la atención sobra la Regla 1. Si el trabajo tiene lugar en un servidor que retransmite las transacciones de un sentido desconocido, será imposible determinar quién ha sido el iniciador de la transacción, el comprador o el vendedor. Y en el log mostrará los errores correspondientes. Si se ha determinado el iniciador, se añadirá el volumen al volumen total de las compras o las ventas. Si la bandera no contiene noticias sobre el iniciador de la transacción, también obtendremos error.

//--- 5. Introducimos los valores en los búferes
      DisplayValues(bNum,sumVolBuy,sumVolSell,__LINE__);

Punto 5. Introducimos los valores en los búferes. En el procedimiento DisplayValues() tiene lugar el control del índice de los búferes de indicador (para ello, transmitimos a la función el número de línea de la llamada), el cálculo de delta y el registro en los búferes de delta y los volúmenes de compras y ventas.

6. Cálculo en tiempo real

Vamos a describir el algoritmo de los cálculos en tiempo real

//--- 1. Comprobamos la formación de una nueva barra
if(rates_total>prev_calculated) // Si la barra es nueva
  {
   //--- Inicializamos los índices de los búferes rates_total-1 con valores vacíos
   BuffersIndexInitialize(rates_total-1,EMPTY_VALUE);
   //--- 2. Comprobamos si hay que controlar el volumen en la barra rates_total-2
   if(_repeatedControl && _controlNum==rates_total-2)
     {
      //--- 3. Realizamos una nueva comprobación de control
      RepeatedControl(false,_controlNum,time[_controlNum]);
     }
   //--- 4. Reseteamos los valores de la nueva comprobación de control
   _repeatedControl=false;
   _controlNum=WRONG_VALUE;
  }
//--- 5. Cargamos los nuevos ticks
if(!_ticks.GetTicks() )               // Si no ha habido éxito
   return( prev_calculated );         // Salimos con error
//--- 6. Recordamos la hora del último tick de la historia obtenida
_ticks.SetFrom();
//--- 7. Cálculo en tiempo real
CalculateCurrentBar(false,rates_total,time,volume);

Punto 1. Comprobamos la formación de una nueva barra. Esta comprobación es muy importante, ya que en el punto 2.1.1. hemos descubierto que si no hemos pasado este control en la función principal de cálculo en el procedimiento de control de los volúmenes (cálculo en tiempo real), deberemos pasarlo en el momento en que se forma una nueva barra. ¡Y ahora es precisamente ese momento!

Punto 2. Comprobamos si hay que controlar el volumen en la barra rates_total-2. Si se ha establecido la bandera de nuevo control y se debe realizar el nuevo control en la vela rates_total-2 (que justo acaba de finalizar), realizamos una nueva comprobación de control (p. 3).

Punto 3. Realizamos la comprobación de control. Como ya hemos mencionado, durante la comprobación de control, se obtienen todos los ticks de la vela, además, se determinan los volúmenes de compra y venta, se calcula el delta y se comparan las sumas de los volúmenes con el valor de control.

Punto 5. Cargamos los nuevos ticks. Obtenemos los ticks desde el momento de llegada del último tick en el anterior inicio del indicador. Al realizar el cálculo en tiempo real, obtenemos los ticks con la ayuda de la función GetTicks(), que usa la función CopyTicks().

Punto 6. Recordamos la hora del último tick. Se trata de la hora del último tick obtenido en el p. 5. o después de calcular la historia. En la siguiente inicialización del indicador, la historia de ticks se solicitará a partir de este momento.

Punto 7. Cálculo en tiempo real. Como hemos dicho anteriormente, el procedimiento CalculateCurrentBar() se usa tanto al calcular la historia, como al realizar cálculos en tiempo real. La responsable de ello es la bandera firstLaunch, que en este caso se ha establecido en el valor false.

7. Peculiaridades del trabajo en el simulador de estrategias

Al usar el simulador de estrategias, siempre debemos recordar que se trata de un programa aparte, con su propia funcionalidad. Incluso si el simulador puede hacer lo mismo que el terminal, esto no significa que lo vaya a hacer de la misma forma que el terminal. Una situación semejante (en esta etapa de desarrollo del simulador de estrategias) tiene lugar con los programas que usan los datos de ticks. A pesar de que el indicador se calcula correctamente (el control de los volúmenes tiene éxito), el indicador en el simulador de estrategias se calcula de una forma un tanto distinta. Una vez más, el motivo se oculta en el procesamiento de los paquetes de ticks.

A diferencia del terminal, donde varias transacciones pueden tener lugar en un solo tick (es decir, podemos obtener un paquete de ticks), en el simulador de estrategias cada tick se obtendrá por separado, incluso si llegan uno tras otro multitud de ticks de un mismo paquete. Podemos comprobar esto iniciando el indicador de prueba test_tickPack, de los anexos. El resultado aproximado será el siguiente:

2018.07.13 10:00:00   OnCalculate: Ticks recibidos 4. [0] = 2018.07.13 10:00:00.564, [3] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Ticks recibidos 2. [0] = 2018.07.13 10:00:00.571, [1] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Ticks recibidos 3. [0] = 2018.07.13 10:00:00.571, [2] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Ticks recibidos 4. [0] = 2018.07.13 10:00:00.571, [3] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Ticks recibidos 5. [0] = 2018.07.13 10:00:00.571, [4] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Ticks recibidos 6. [0] = 2018.07.13 10:00:00.571, [5] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Ticks recibidos 7. [0] = 2018.07.13 10:00:00.571, [6] = 2018.07.13 10:00:00.572 FLAG_BUY

Usted puede reaizar este experimento por sí mismo, siempre en el modo "Cada tick basado en ticks reales", pulsando F12 sucesivamente. ¡Los ticks se añadirán estrictamente de uno en uno! Aunque, claro, en realidad este paquete podría haber llegado al terminal tanto por partes como en una sola pieza, pero es muy poco probable que lo haga de uno en uno. Esto no es ni bueno, ni malo, simplemente tenemos que aceptar esta peculiaridad como un hecho, y recordarla.

Conclusión

Y con esto finalizamos la descripción del algoritmo de construcción del indicador. En este artículo, hemos descrito las sutiliezas y dificultades más comunes que muchos usuarios, incluido el autor, encuentran al desarrollar indicadores de ticks. Esperamos que nuestra experiencia resulte de utilidad, y que la comunidad pueda usarla como base a la hora de desarrollar más aplicaciones utilizando datos de ticks, dando impulso a la futura evolución de la plataforma MetaTrader 5. Si usted conoce otras peculiaridades a la hora de trabajar con ticks comerciales, o si ha detectado alguna inexactitud, no dude en ponerse en contacto conmigo. Estaré encantado de discutir este tema.

Como resultado final, tenemos la imagen que sigue. La columna azul indica la predominancia de compradores en la vela, la roja, la predominancia de vendedores.


Fig. 5. Indicador de delta en el instrumento RTS-6.18.

La valoración de volúmenes reales abre nuevos horizontes para el análisis del mercado de valores, permitiendo una mejor comprensión del movimiento de los precios. Este indicador es solo una pequeña parte de lo que podemos desarrollar en función del análisis de datos de ticks. Crear indicadores bursátiles basados ​​en volúmenes reales es una tarea factible. Espero que este artículo ayude al lector a crear dichos indicadores y mejorar su comercio.

Si está interesado en el indicador, próximamente podrá encontrar su versión mejorada en apartado "Productos" de mi perfil. ¡Les deseo éxito en el comercio!

Archivos usados en el artículo

Nombre del archivoTipoDescripción
 1. Delta_article.mq5 Archivo del indicador Implementación del indicador de delta
 2. Ticks_article.mqh Archivo de la clase Clase auxiliar para trabajar ocn los datos de ticks
 3. test_getTicksRange.mq5 Archivo del script Script de prueba para comprobar la posibilidad de obtener varios paquetes de ticks en un milisegundo
 4. test_tickPack.mq5 Archivo del indicador Indicador de prueba para comprobar la obtención de ticks en el simulador de estrategias