Cómo escribir una profundidad de mercado de scalping usando como base la biblioteca CGraphic

Vasiliy Sokolov | 13 septiembre, 2017


Índice

Introducción

Este artículo continúa de manera lógica la descripción de la biblioteca para el trabajo con la profundidad de mercado, publicado hace dos años. Desde entonces, en MQL5 ha aparecido el acceso a la historia de ticks. Además, con las capacidades de MetaQuotes se ha desarrollado la biblioteca CGraphic para visualizar los datos personalizados en forma de gráfico estádistico complejo. CGraphic, en cuanto a las tareas realizadas, es análoga a la función plot en el lenguaje de programación R. El trabajo con esta biblioteca se describe al detalle en otro artículo aparte

La aparición de estas posibilidades ha permitido modernizar de manera radical la profundidad de mercado propuesta anteriormente. En la nueva versión, aparte del recuadro de órdenes, ahora se representan el gráfico de ticks y las transacciones Last en él:

Fig. 1 Profundidad de mercado con gráfico de ticks.

Recordemos que la biblioteca propuesta con anterioridad constaba de dos grandes módulos: la clase CMarketBook para el trabajo con la profundidad de mercado y el panel gráfico que la representaba. Desde entonces el código ha sufrido muchos cambios y mejoras. Se han corregido errores, y la parte gráfica de la profundidad de mercado ha adquirido su propia biblioteca gráfica CPanel, sencilla y ligera.

Pero volvamos a CGraphic y sus capacidades de dibujar diagramas complejos y gráficos lineales en una ventana aparte. Podría parecer que esas capacidades tan específicas pueden resultar útiles solo para solucionar tareas estadísticas. ¡Pero no es así! En este artículo vamos a intentar mostrar cómo se pueden utilizar las posibilidades de CGraphic en los proyectos menos relacionados con la estadística, por ejemplo, al crear una profundidad de mercado de scalping.

Cambios realizados desde el lanzamiento de la anterior versión

Después de publicar el artículo "Recetas MQL5 — Escribiendo nuestra propia profundidad de mercado" he usado mucho CMarketBook en la práctica, y durante el proceso detecté una serie de errores en el código. Paulatinamente he ido mejorando la interfaz, y como resultado se han acumulado los siguientes cambios:

  1. Al principio todo el tráfico en la profundiad de mercado era mínimo. Las celdas en el recuadro de precios se representaban con la ayuda de varias clases sencillísimas. Transcurrido cierto tiempo, estas clases han adquiirido una funcionalidad adicional, y su sencillez y ligereza son muy cómodas al proyectar otros tipos de panel. Al final se ha obtenido un conjunto completo de clases, la biblioteca CPanel, que se ha separado en un proyecto independiente. Se ubica en la carpeta Include.
  2. Ha mejorado el aspecto externo de la profundidad de mercado. Por ejemplo, en lugar de un pequeño triángulo ha aparecido un gran botón cuadrado que abre y cierra la profundidad. Se ha corregido el error de superposisición de elementos, como consecuencia del mismo, al abrir de nuevo el recuadro, los elementos de la profundidad de mercado se dibujaban otra vez por encima de los ya representados.
  3. Se han añadido ajustes que posicionan el botón de apertura/cierre de la profundidad de mercado en los ejes X e Y del gráfico. Con frecuencia, debido al nombre no-estándar de un instrumento y del panle comercial adicional, el botón de apertura/cierre de la profundidad cerraba otros elementos activos del gráfico. Ahora que existe la posibilidad de ubicar manualmente el botón, se podrá evitar este solapamiento.
  4. La propia clase CMarketBook también ha cambiado significativamente. En ella se han corregido los errores de salida del rango de la matriz (array out of range); los errores surgidos al llenarse completa o parcialmente la profundidad de mercado; el error de división por cero al cambiar de símbolo. La clase CMarketBook se ha convertido en un módulo independiente y se ubica en el directorio MQL5\Include\Trade;
  5. He realizado una serie de pequeños cambios para mejorar la estabilidad general del indicador.

Precisamente vamos a comenzar a trabaja a partir de esta versión mejorada y completada, para transformarla paulatinamente en una profundidad de mercado de scalping.

Breve panorámica de la biblioteca gráfica CPanel

Se han dedicado muchos artículos a la creación de interfaces personalizadas en MQL5. Entre ellos, destaca especialmente la serie de Anatoly Kozharsky "Interfaces Gráficas", tras la cual resulta complicado añadir algo nuevo. Por eso, no vamos a profundizar detalladamente en la construcción de la interfaz gráfica. Como ya hemos dicho más arriba, la parte gráfica de la profundidad de mercado se ha transformado en una bibloteca CPanel completa. Su arquitectura básica debe ser descrita, pues sobre su base se creará un elemento gráfico especial: el gráfico de ticks. Vamos a combinarlo con el recuadro de precios, haciendo un panel completo con varios elementos.

Bien, vamos a analizar al detalle CPanel, para comprender así el principio de nuestro posterior trabajo. Los elementos gráficos en MQL5 se muestran con varias primitivas gráficas. Son:

  • la etiqueta de texto;
  • el botón;
  • el campo de edición;
  • la etiqueta rectangular;
  • la etiqueta gráfica.

Todos ellos tienen una serie de propiedades idénticas. Por ejemplo, la etiqueta rectangular, el botón y el campo de edición se pueden ajustar de tal forma que no se distingan de forma externa uno de otro.  De esta manera, prácticamente cualquier primitiva gráfica puede ejercer de base de este o aquel elemento. Por ejemplo, en lugar del botón puede representarse en realidad el campo de edición, y en lugar de la etiqueta rectangular, el botón. Esta sustitución no destacará visualmente, y el usuario que pulse sobre el campo de edición pensará que realmente pulsa sobre un botón.

Por supuesto, dicha sustitución puede parecer extraña y complicar la comprensión general de los principios de construcción de la interfaz personalizada. Debe entenderse que, aparte de las características comunes, cada elemento de base tiene su propia característica única. Por ejemplo, un campo de edición no se puede hacer transparente y una etiqueta rectangular, sí. Gracias a ello, podemos crear elementos con un aspecto único a partir de las mismas clases.

Vamos a explicar esto con un ejemplo. Digamos que queremos crear un panel gráfico con un etiqueta de texto normal:

Fig. 2. Ejemplo de forma con rótulo sin marco.

Pero si queremos rodear el texto con marco, tendremos problemas, porque la etiqueta de texto carece de la propiedad "marco". La solución es simple: ¡no usamos una etiqueta de texto, sino un botón! Este es el aspecto que la forma tendrá con él:

Fig. 3. Ejemplo de forma con rótulo en un marco.

Pueden surgir multitud de momentos sutiles de este tipo al crear una interfaz gráfica. Pero predecir de antemano lo que necesitará el usuario es imposible. Por lo tanto, lo mejor será no basar el elemento gráfico en ninguna primitiva gráfica, en su lugar, le daremos al usuario la capacidad de determinar esto por sí mismo.

Así precisamente está construida la biblioteca de elementos gráficos CPanel. En esencia, CPanel es un conjunto de clases, cada una de las cuales representa este o aquel elemento de una interfaz de alto nivel. Para inicializar este elemento hay que indicar el tipo de primitiva gráfica en el que se basará. Cada clase de ese tipo tiene un padre común, la clase CNode, que ejecuta solo una función: guardar el tipo de primitivo básico. Su único constructor protegido exige indicar este tipo en el momento que se crea el elemento. 

Hay muy pocos elementos gráficos únicos de alto nivel. La "unicalidad" depende del conjunto de propiedades con el que se debe proveer el elemento básico universal, para que se convierta en único. Este elemento universal en CPanel es la clase CElChart. Como el resto de las clases, se hereda de CNode y contiene los métodos para ajustar las siguientes propiedades:

  • la anchura y altura del elemento;
  • las coordenadas X e Y del elemento con respecto al gráfico;
  • la anchura y altura del elemento;
  • el color del fondo y los marcos del elemento (si estas propiedades tienen soporte);
  • el tipo del marco del elemento (si estas propiedades tienen soporte);
  • el texto dentro del elemento, su fuente y tamaño, la alineación (si estas propiedades tienen soporte).

CElChart proporciona los métodos para establecer estas o aquellas propiedades, pero no garantiza que estas propiedades se vayan a instaurar en realidad. Ee el elemento básico el que determina plenamente si CElChart va a dar soporte a esta o aquella propiedad. Al igual que CNode, CElChart requiere que se indique el tipo de primitiva gráfica en el que se basará. De esta manera, con la ayuda de un CElChart podemos crear tanto una forma habitual, como un botón o un campo de edición, por poner un ejemplo. En la práctica esto resulta muy cómodo.

Por ejemplo: vamos a dibujar un panel como en la figura 3. Para ello, necesitaremos dos elementos: un fondo con un marco y un texto con un marco. Ambios son ejemplares de la misma clase CElChart. Pero en ellos se usan dos primitivas gráficas distintas:  OBJ_RECTANGLE_LABEL y BJ_BUTTON. Obtenemos el código siguiente:

//+------------------------------------------------------------------+
//|                                       MarketBookArticlePanel.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Panel\ElChart.mqh>

CElChart Fon(OBJ_RECTANGLE_LABEL);
CElChart Label(OBJ_BUTTON);
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
//---
   Fon.Height(100);
   Fon.Width(250);
   Fon.YCoord(200);
   Fon.XCoord(200);
   Fon.BackgroundColor(clrWhite);
   Fon.BorderType(BORDER_FLAT);
   Fon.BorderColor(clrBlack);
   Fon.Show();
   
   Label.Text("Meta Quotes Language");
   Label.BorderType(BORDER_FLAT);
   Label.BorderColor(clrBlack);
   Label.Width(150);
   Label.Show();
   Label.YCoord(240);
   Label.XCoord(250);
   ChartRedraw();
//---
   return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+

Después de que se hayan creado los elementos, queda establecer sus propiedades en la función OnInit. Ahora podemos representar los elementos en el gráfico, llamar sus métodos Show.

Gracias a las combinaciones de la primitiva básica con la clase CElChart, se pueden crear interfaces gráficas potentes, flexibles y, lo más importante, sencillas. Así está construida la representación gráfica de la profundidad de mercado, en la que el recuadro de la profundidad consta de multitud de elementos CBookCell, basados a su vez en CElChart.

El motor gráfico CPanel da soporte al anidamiento. Esto significa que dentro de un elemento se pueden ubicar otros adicionales. Gracias al anidamiento se alcanza la universalidad en el control. Por ejemplo, un comando dado para una forma global puede enviarse a todos sus elementos. Vamos a modificar el ejemplo mostrado más arriba:

//+------------------------------------------------------------------+
//|                                       MarketBookArticlePanel.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Panel\ElChart.mqh>

CElChart Fon(OBJ_RECTANGLE_LABEL);
CElChart *Label;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
//---
   Fon.Height(100);
   Fon.Width(250);
   Fon.YCoord(200);
   Fon.XCoord(200);
   Fon.BackgroundColor(clrWhite);
   Fon.BorderType(BORDER_FLAT);
   Fon.BorderColor(clrBlack);
   
   Label = new CElChart(OBJ_BUTTON);
   Label.Text("Meta Quotes Language");
   Label.BorderType(BORDER_FLAT);
   Label.BorderColor(clrBlack);
   Label.Width(150);
   Label.YCoord(240);
   Label.XCoord(250);
   //Label.Show();
   Fon.AddElement(Label);
   
   Fon.Show();
   ChartRedraw();
//---
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }

Ahora durante la ejecución del programa se crea dinámicamente CLabel, el puntero al elemento CElCahrt. Después de crear y mostrar las coordenadas correspondientes, se añade a la forma Form. Ahora no es necesario representarlo con el comando aparte Show. En lugar de ello, basta con ejecutar el comando Show solo para el elemento Fon, la forma principal de nuestra aplicación. Este comando, por sus características, se ejecutará para todos los sub-elementos anidados, incluido Label. 

CPanel no solo establece la propiedad del elemento, sino que da soporte al desarrollo del modelo de eventos. Un evento en CPanel puede ser lo que sea, y no solo un evento recibido del gráfico. Los responsables de ello son la clase CEvent y el método Event. La clase CEvent es abstracta. En ella se basan multitud de clases ya más concretas, por ejemplo: CEventChartObjClick.

Supongamos que en nuestra forma personalizada hay variaos elementos gráficos anidados. El usuario puede crear un evento, por ejemplo, clicar con el ratón sobre cualquiera de estos elementos. ¿Cómo saber qué ejemplar de la clase precisamente debe procesar el evento? Para ello, usaremos el evento CEventChartObjClick: crearemos un ejemplar de clase y lo enviaremos a nuestra forma central Form:

CElChart Fon(OBJ_RECTANGLE_LABEL);
...
...
void OnChartEvent(const int id,         // identificador de evento   
                  const long& lparam,   // parámetro del evento del tipo long 
                  const double& dparam, // parámetro del evento del tipo double 
                  const string& sparam  // parámetro del evento del tipo string 
  )
{ 
   CEvent* event = NULL;
   switch(id)
   {
      case CHARTEVENT_OBJECT_CLICK:
         event = new CEventChartObjClick(sparam);
         break;
   }
   if(event != NULL)
   {
      Fon.Event(event);
      delete event;
   }
}

Con ayuda de este método, hemos enviado el evento de gran amplitud CEventChartObjClick, que recibirán todos los elementos dentro del ejemplar Fon. Que este evento sea o no procesado, depende ya de la lógica interna de la propia forma. 

Vamos a permitir que nuestra etiqueta Meta Quotes Language procese esta pulsación, cambiando el texto a "Enjoy". Para ello, crearemos la clase CEnjoy y la proveeremos de la lógica necesaria, redefiniremos el método OnClick, el procesador del evento homónimo:

//+------------------------------------------------------------------+
//|                                       MarketBookArticlePanel.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Panel\ElChart.mqh>
#include <Panel\Node.mqh>

CElChart Fon(OBJ_RECTANGLE_LABEL);

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
class CEnjoy : public CElChart
{
protected:
   virtual void OnClick(void);
public:
                CEnjoy(void);
   
};

CEnjoy::CEnjoy(void) : CElChart(OBJ_BUTTON)
{
}
void CEnjoy::OnClick(void)
{
   if(Text() != "Enjoy!")
      Text("Enjoy!");
   else
      Text("Meta Quotes Language");
}
CEnjoy Label;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
//---
   Fon.Height(100);
   Fon.Width(250);
   Fon.YCoord(200);
   Fon.XCoord(200);
   Fon.BackgroundColor(clrWhite);
   Fon.BorderType(BORDER_FLAT);
   Fon.BorderColor(clrBlack);
   
   Label.Text("Meta Quotes Language");
   Label.BorderType(BORDER_FLAT);
   Label.BorderColor(clrBlack);
   Label.Width(150);
   Label.YCoord(240);
   Label.XCoord(250);
   //Label.Show();
   Fon.AddElement(&Label);
   
   Fon.Show();
   ChartRedraw();
//---
   return(INIT_SUCCEEDED);
}
void OnChartEvent(const int id,         // identificador de evento   
                  const long& lparam,   // parámetro del evento del tipo long 
                  const double& dparam, // parámetro del evento del tipo double 
                  const string& sparam  // parámetro del evento del tipo string 
  )
{ 
   CEvent* event = NULL;
   switch(id)
   {
      case CHARTEVENT_OBJECT_CLICK:
         event = new CEventChartObjClick(sparam);
         break;
   }
   if(event != NULL)
   {
      Fon.Event(event);
      delete event;
   }
   ChartRedraw();
}
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+

Podría parecer extraño que enviemos el evento CEventObjClick a la forma Form a través del método Event, y que lo procesemos en el método OnClick. Ciertamente, muchos eventos estándar (por ejemplo, el click de ratón) tienen sus propios métodos-eventos especiales. Si los redefinimos, el evento correspondiente llegará a ellos. Si no hacemos esto, todos los eventos se procesarán a un nivel más alto, en el método Event. Se trata también de un método virtual, se lo puede redefinir de la misma forma que el método OnClick. En este nivel, el trabajo con el evento tiene lugar a través del análisis del ejemplar CEvent pasado. 

Por el momento, dejaremos estos detalles e indicaremos las propiedades principales de CPanel.

  • Todas las clases СPanel que implementan los elementos de la interfaz gráfica pueden basarse en cualquier primitiva gráfica elegida. Se elige e indica en el momento en que se crea el ejemplar de la clase.
  • Cada elemento derivado de CPanel puede contener una multitud ilimitada de otros elementos de CPanel. Así se implementa el anidamiento y, por lo tanto, la universalidad del control. Todos los eventos se difunden por el árbol, y de esta forma cada elemento obtiene acceso a cada evento.
  • El modelo de eventos CPanel tiene dos niveles. En la base del modelo de nivel bajo se encuentran Event y las clases del tipo CEvent. Así se puede procesar absolutamente cualquier modelo, incluso aquellos que no tienen soporte en MQL. Asimsmo, los eventos enviados a través de CEvent son siempre de amplia difusión.  A un nivel más elevado, los eventos estándar se transforman en llamadas de los métodos correspondientes. Por ejemplo, el evento CEventChartObjClick se transforma en la llamada de OnClick, y la llamada del evento Show genera la llamada recursiva de los métodos OnShow de todos los elementos derivados. En este nivel, el evento se puede llamar directamente. Así, si llamamos el método Show(), este representará el panel, y la llamada del método Refresh actualizará la representación de este panel.

La panorámica ha resultado bastante rápida y concentrada, sin embargo, la imagen general obtenida debería ser suficiente para comprender nuestras acciones posteriores al crear una profundidad de mercado de scalping.

Sincronización del flujo de ticks y el recuadro de órdenes

El recuadro de órdenes es una estructura dinámica que cambia los valores de los mercados líquidos decenas de veces por segundo. Para acceder al segmento actual del recuadro de órdenes es necesario procesar el evento especial BookEvent en el procesador homónimo del evento:la función OnBookEvent. En el momento del cambio, en el recuadro de órdenes el terminal llama el evento OnBookEvent, indicando el símbolo en el que estos cambios han sucedido. Recordemos que en la anterior versión del artículo se desarrolló la clase CMarketBook, que proporcionaba un acceso cómodo al segmento actual de la profundidad de mercado. Para obtener el segmento actual de la profundidad de mercado, en esta clase bastaría con llamar el método Regresh() en la función OnBookEvent. Esto tendría el aspecto siguiente:

//+------------------------------------------------------------------+
//|                                                   MarketBook.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots 0
#include <MarketBook.mqh>

CMarketBook MarketBook.mqh
double fake_buffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   MarketBook.SetMarketBookSymbol(Symbol());
//--- indicator buffers mapping
   SetIndexBuffer(0,fake_buffer,INDICATOR_CALCULATIONS);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| MarketBook change event                                          |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
{
   if(symbol != MarketBook.GetMarketBookSymbol())
      return;
   MarketBook.Refresh();
   ChartRedraw();
}

En la nueva versión, nuestra profundidad de mercado, además del recuadro de órdenes, representa un gráfico de ticks en tiempo real. Por eso, lo deberemos proveer con funciones adicionales para trabajar con los ticks que llegan recientemente. Podemos analizar los ticks en MQL5 con la ayuda de tres mecanismos básicos. Son:

  • La obtención del valor del último tick conocido a través de la función SymbolInfoTick;
  • El procesamiento del evento de llegada de un nuevo tick en la función OnTick para los expertos, y en OnCalculate para los indicadores;
  • La obtención de la historia de ticks con la ayuda de las funciones CopyTicks y CopyTicksRange.

Los dos primeros métodos se pueden combinar entre sí. Por ejemplo, en los eventos OnTick u OnCalculate se puede llamar la función SymbolInfoTick y obtener acceso a los parámetros del último tick. Sin embargo, estos dos métodos no nos convienen, debido a la naturaleza de la aparición del flujo de ticks.

Para comprender cómo se forman los ticks, recurriremos al artículo "Principios de formación de precios en el mercado bursátil tomando de ejemplo la Sección de Derivados de la Bolsa de Moscú" y analizaremos una profundidad de mercado del oro condicional: 

Precio, $ por onza troy de oro Número de onzas (contratos)
1280.8 17
1280.3 3
1280.1 5
1280.0 1
1279.8 2
1279.7 15
1279.3 3
1278.8 13

Fig 4. Ejemplo de la profundidad de mercado.

Imaginemos que en el momento de la actualización de la profundidad de mercado, finalizamos la historia de ticks, y con la ayuda de un algoritmo especial determinamos cuántos ticks han llegado desde el momento de la anterior actualización. En teoría, cada tick debe corresponder como mínimo a un cambio en la profundidad del mercado, lo que significa que con cada cambio de la profundidad de mercado, solo puede llegar como máximo un tick. Sin embargo, en la práctica, no se observa un patrón de este tipo, y para que la sincronización sea correcta hay que trabajar con la historia de ticks.

Supongamos que hemos encontrado un comprador que quiere adquirir 9 contratos de oro. Tras realizar la compra, efectúa un mínimo de tres transacciones. Si en el nivel 1280.1 o 1280.3 hay varios vendedores, se darán todavía más transacciones. Realizando una acción (compra), creará varias transacciones que tendrán lugar simultáneamente. De esta forma, los ticks también llegarán al terminal MetaTrader 5 como un "paquete". Por eso, si en OnCalculate usamos la función SymbolInfoTick, esta retornará solo el último tick de esta serie, y los anteriores se perderán.

Por eso, necesitaremos otro mecanismo más fiable para obtener los ticks con la ayuda de la función CopyTicks. Esta, como sucede con CopyTicksRange, y a diferencia de SymbolInfoTick, permite recibir una serie de ticks. Gracias a ello, la historia de ticks se representará de forma adecuada y no se perderá nada.

Pero la función CopyTiks no permite solicitar los últimos N ticks. En lugar de ello, proporcionará todos los ticks que hayan llegado desde el momento temporal indicado. Esto complica la tarea. Deberemos realizar la solicitud, recibir la matriz de ticks y compararla con la matriz de ticks obtenida en la anterior actualización. En este caso, además, aclararemos qué ticks entre los que han llegado recientemente no han entrado en la "anterior partida", es decir, son nuevos. Pero comparar los ticks entre sí directamente es imposible, ya que podrían no existir diferencias visibles entre ellos. Por ejemplo, vamos a recurrir al recuadro de transacciones mostrado más abajo:

Fig. 5. Recuadro de todas las transacciones con un ejemplo de transacciones idénticas.

Podemos ver de inmediato dos grupos de ticks absolutamente idénticos. Están designados con marcos rojos, tienen la misma hora, volumen, dirección y precio. Así nos aseguramos de que no sea posible comparar los ticks por separado unos con otros.

Pero sí que podemos comparar un grupo de ticks. Si dos grupos de ticks son iguales entre sí, podemos sacar la conclusión de que estos ticks consecutivos ya han sido analizados en una actualización de precios anterior.

Sincronizamos la clase CMarketBook con el flujo de ticks: añadimos a su matriz MqlTiks, que contiene los ticks nuevos que han llegado desde el momento de la anterior actualización. Los ticks más nuevos se calcularán con el método interior CompareTiks:

//+------------------------------------------------------------------+
//| Compare two tiks collections and find new tiks                   |
//+------------------------------------------------------------------+
void CMarketBook::CompareTiks(void)
{
   MqlTick n_tiks[];
   ulong t_begin = (TimeCurrent()-(1*20))*1000; // from 20 sec ago
   int total = CopyTicks(m_symbol, n_tiks, COPY_TICKS_ALL, t_begin, 1000);
   if(total<1)
   {
      printf("No ha sido posible recibir los ticks");
      return;
   }
   if(ArraySize(m_ticks) == 0)
   {
      ArrayCopy(m_ticks, n_tiks, 0, 0, WHOLE_ARRAY);
      return;
   }
   int k = ArraySize(m_ticks)-1;
   int n_t = 0;
   int limit_comp = 20;
   int comp_sucess = 0;
   //Revisamos las nuevas transacciones comerciales recibidas, comenzando por la última
   for(int i = ArraySize(n_tiks)-1; i >= 0 && k >= 0; i--)
   {
      if(!CompareTiks(n_tiks[i], m_ticks[k]))
      {
         n_t = ArraySize(n_tiks) - i;
         k = ArraySize(m_ticks)-1;
         comp_sucess = 0;
      }
      else
      {
         comp_sucess += 1;
         if(comp_sucess >= limit_comp)
            break;
         k--;
      }
   }
   //Guardamos los ticks recibidos
   ArrayResize(m_ticks, total);
   ArrayCopy(m_ticks, n_tiks, 0, 0, WHOLE_ARRAY);
   //Calculamos el índice del comienzo de los nuevos ticks y los copiamos en el búfer para el acceso
   ArrayResize(LastTicks, n_t);
   if(n_t > 0)
   {
      int index = ArraySize(n_tiks)-n_t;
      ArrayCopy(LastTicks, m_ticks, 0, index, n_t);
   }
}

El algoritmo presentado no es trivial. CompareTicks solicita todos los ticks de los últimos 20 segundos y los compara con la matriz de ticks guardada anteriormente, empezando por el final. Si los 20 ticks seguidos de la matriz actual son iguales a los 20 ticks de la matriz anterior, se considerará que todos los ticks que vayan tras estos 20, son nuevos.

Vamos a aclarar este algoritmo con un sencillo esquema. Supongamos que llamamos la función CopyTiks dos veces después de un pequeño intervalo de tiempo. Pero en lugar de los ticks, esta retorna una matriz de ceros y unos. Tras obtener estas dos matrices, averiguamos cuántos elementos finales únicos hay en la segunda matriz que no coincidan con los elementos de la primera matriz. Este será el número de nuevos ticks que ha llegado desde el momento de la anterior actualización. En el esquema esto puede tener el aspecto siguiente:

Fig. 6. Esquema de sincronización de series repetidas.

Al comparar, vemos que los números del 6 al 14 de la primera matriz son iguales a los números del 1 al 8 de la segunda matriz. Por consiguiente, en la matriz Array2 hay cinco nuevos valores, se trata de los elementos del 9 al 14. El algoritmo funciona en diferentes combinaciones: las matrices pueden tener diferente longitud, no disponer de elementos comunes o ser totalmente idénticas unas a otras. En todos estos casos, el número de nuevos valores se definirá correctamente.

Después de que el número de nuevos ticks sea definido, los copiamos a la matriz LastTiks. Esta matriz ha sido definida como campo público dentro de la clase CMarketBook.

Hemos obtenido una nueva versión de la clase CMarketBook, que aparte del recuadro de órdenes, ahora contiene la matriz de los ticks que llegan entre la actualización anterior y la actual. Por ejemplo, para conocer los nuevos ticks, podemos escribir este código:

//+------------------------------------------------------------------+
//| MarketBook change event                                          |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
{
   
   if(symbol != MarketBook.GetMarketBookSymbol())
      return;
   MarketBook.Refresh();
   string new_tiks = (string)ArraySize(MarketBook.LastTicks);
   printf("Han llegado " + new_tiks + " nuevos ticks");
}

La colocación de los ticks en la clase de la profundidad de mercado permite sincronizar correctamente las órdenes con el flujo de ticks. En cada momento de la actualización de la profundidad de mercado tenemos la lista de ticks que precede a esta actualización. De esta forma, el flujo de ticks y la profundidad de mercado están completamente sincronizados entre sí. Esta importante propiedad la usaremos posteriormente, entre tanto, pasaremos a la biblioteca gráfica CGraphic.

Bases de CGraphic

En el arsenal de la biblioteca CGraphic se incluyen líneas, histogramas, puntos, figuras geométricas complejas. Para nuestros objetivos será necesaria una parte mínima de sus posibilidades. Necesitaremos dos líneas para representar los niveles Ask y Bid, y puntos especiales para representar las transacciones Last. En esencia, CGraphic es un contenedor que incluye dentro de sí objetos CCurve. Cada objeto de este tipo, como podrá adivinar por el nombre, constituye una cierta curva que consta de puntos con las coordenadas X e Y. Dependiendo del tipo de representación, pueden unirse con líneas, pueden ser cimas de las columnas del histograma, o bien se representan como son: en forma de puntos. 

Debido a ciertas peculiaridades de representación de las transacciones Last, trabajaremos con gráficos de dos dimensiones. Vamos a intentar crear un sencillo gráfico de dos dimensiones en forma de línea:

//+------------------------------------------------------------------+
//|                                                   TestCanvas.mq5 |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Graphics\Graphic.mqh>

CGraphic Graph;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   double x[] = {1,2,3,4,5,6,7,8,9,10};
   double y[] = {1,2,3,2,4,3,5,6,4,3};
   CCurve* cur = Graph.CurveAdd(x, y, CURVE_LINES, "Line");   
   Graph.CurvePlotAll();
   Graph.Create(ChartID(), "Ticks", 0, (int)50, (int)60, 510, 300); 
   Graph.Redraw(true);
   Graph.Update();
   return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+

Si lo iniciamos en forma de experto, en el gráfico aparecerá esta imagen:

Fig. 7. Ejemplo de un gráfico lineal de dos dimensiones creado con la ayuda de CGraphic

Ahora vamos a intentar cambiar la presentación, para ello, modificaremos el tipo de representación de nuestra curva de dos dimensiones CCurve en el punto:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   double x[] = {1,2,3,4,5,6,7,8,9,10};
   double y[] = {1,2,3,2,4,3,5,6,4,3};
   CCurve* cur = Graph.CurveAdd(x, y, CURVE_POINTS, "Points");   
   cur.PointsType(POINT_CIRCLE);
   Graph.CurvePlotAll();
   Graph.Create(ChartID(), "Ticks", 0, (int)50, (int)60, 510, 300); 
   Graph.Redraw(true);
   Graph.Update();
   return(INIT_SUCCEEDED);
}

El mismo gráfico en forma de puntos ahora tiene el aspecto siguiente:

Fig. 8. Ejemplo de gráfico punteado de dos dimensionaes creado con la ayuda de CGraphic

Como podemos ver, las principales acciones consisten en crear un objeto de curva, cuyos valores deberán contenerse preliminarmente en las matrices x e y: 

double x[] = {1,2,3,4,5,6,7,8,9,10};
double y[] = {1,2,3,2,4,3,5,6,4,3};
CCurve* cur = Graph.CurveAdd(x, y, CURVE_POINTS, "Points"); 

El objeto creado se ubica dentro de CGraphic, y el método CurveAdd retorna un enlace a él. Esto se ha hecho así para que podamos crear las propiedades necesarias de esta curva (lo que hemos hecho precisamente en el primer ejemplo), estableciendo el tipo de curva como CURVE_POINTS e indicando el tipo de signo en forma de círculo:

cur.PointsType(POINT_CIRCLE);

Después de que las líneas se hayan añadido, hay que representar el gráfico en el gráfico ejecutando los comandos Create y Redraw.

La misma secuencia de acciones ejecutaremos en nuestro proyecto de la profundidad de mercado, pero los datos para las curvas los prepararemos de forma especial, y todos los comandos los ubicaremos dentro de la clase especial CElTickGraph, el elemento hijo de CElChart.

Integración de CGraphic con la biblioteca CPanel

Ya hemos aclarado los momentos esenciales del trabajo con CGraphic. Ahora ha llegado el momento de implementar esta clase en la biblioteca CPanel. Como ya se ha dicho, CPanel proporciona acceso a los eventos necesarios, ubica correctamente los elementos gráficos y gestiona sus propiedades. Todo esto es necesario para hacer del gráfico de ticks una parte limitada de un panel único de la profundidad de mercado. Por eso, en primer lugar, escribiremos el elemento especial CElTickGraph, la parte de CPanel que integra CGraphic en el panel. Además, CElTickGraph recibirá el flujo de ticks de precios actualizado y redibujará el gráfico de ticks. La última tarea es la más complicada. Brevemente, enumeraremos qué debe saber hacer CElTickGraph.

  • CElTickGraph marca una zona rectangular dentro del panel común de la profundidad de mercado. La zona se destaca con un marco negro. 
  • Dentro de la zona CElTickGraph se ubica el gráfico CGraphic, que representa el flujo ticks de precios.
  • El flujo de ticks representa los últimos N ticks. El número N se puede cambiar en los ajustes.
  • CElTickGraph actualiza los valores de las curvas CCurve, incluidas en CGraphic, de tal forma que los ticks antiguos se eliminan del gráfico, y los nuevos se añaden al mismo. Gracias a ello, CElTickGraph crea el efecto de un gráfico de ticks que cambia suavemente.

Para simplificar la tarea de CElTickGraph, usaremos una solución auxiliar: un búfer circular cuyo principio de acción ya se ha descrito con detalle en un artículo aparte.

Vamos a crear cuatro búferes circulares para representar las siguientes magnitudes:

  • el nivel Ask (se representa como una línea roja);
  • el nivel Bid (se representa como una línea azul);
  • la última transacción del lado de la compra (se representa como un triángulo azul orientado hacia abajo);
  • la última transacción del lado de la venta (se representa como un triángulo rojo orientado hacia arriba);

Como se puede ver, vamos a diferenciar las transacciones Last entre sí, por eso necesitaremos también dos tipos de búferes circulares.

La segunda diferencia es que el número de puntos entre Ask/Bid y los precios Last no coincide. Cuando dibujamos una línea continua, para cada punto en el eje X existe un valor en el eje Y. Pero si en lugar de una línea se usan puntos, un punto en el momento X puede encontrarse en el gráfico, o podría no hacerlo. Hay que tener en cuenta eata propiedad y usar un gráfico de dos dimensiones.  Supongamos que tenemos un punto con las siguientes coordenadas X-Y: 1000-57034. Entonces, en el momento de llegada de un nuevo tick, este mismo punto tendrá las coordendas 999-57034. Después de cinco ticks más, se desplazará a la posición 994-57034. Su última posición será 0-57034. Después desaparecerá del gráfico. El punto que lo siga podrá retrasarse con respecto a él en un número diferente de saltos. Cuando el punto 1 tenga las coordenadas 994-57034, el punto 2 se encontrará en 995:57035 o en 998:57035. Combinando las líneas en un gráfico de dos dimensiones, podremos representar estos huecos de manera fiable, sin volcar el flujo de ticks en una sucesión continua.

Vamos a imaginar un recuadro de ticks hipotético que represente el flujo de transacciones teniendo en cuenta los índices:

Index Ask Bid Buy Sell
999 57034 57032 57034
998 57034 57032
57032
997 57034 57031 57034
996 57035 57033 57035
995 57036 57035

994 57036 57035

993 57036 57035
57035
992 57036 57034
57034
991 57036 57035 57035
...



Recuadro 1. Esquema de Sincronización del flujo de ticks en un recuadro (gráfico) bidimensional.

En el mismo, Ask y Bid están completamente rellenos, pero a veces no hay transacciones de compra (Buy) y venta (Sell). La colocación de las lecturas según los índices sincroniza correctamente series de longitud diferente. Sea cuantas sean las transacciones Last, siempre se corresponderán con los niveles Ask y Bid necesarios.

Ya hemos descrito los principios generales del funcionamiento de CElTickGraph. Ahora vamos a mostrar su código fuente completo, y después analizaremos los momentos más complejos del mismo.

//+------------------------------------------------------------------+
//|                                                         Graf.mqh |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#include <Panel\ElChart.mqh>
#include <RingBuffer\RiBuffDbl.mqh>
#include <RingBuffer\RiBuffInt.mqh>
#include <RingBuffer\RiMaxMin.mqh>
#include "GlobalMarketBook.mqh"
#include "GraphicMain.mqh"
#include "EventNewTick.mqh"

input int TicksHistoryTotal = 200;
//+------------------------------------------------------------------+
//| Determina el número de la curva en el objeto gráfico CGraphic           |
//+------------------------------------------------------------------+
enum ENUM_TICK_LINES
{
   ASK_LINE,
   BID_LINE,
   LAST_BUY,
   LAST_SELL,
   LAST_LINE,
   VOL_LINE
};
//+------------------------------------------------------------------+
//| Elemento gráfico que representa el gráfico de ticks              |
//+------------------------------------------------------------------+
class CElTickGraph : public CElChart
{
private:
   
   CGraphicMain m_graf;
   /* Búferes circulares para el trabajo rápido con el flujo de ticks*/
   CRiMaxMin    m_ask;
   CRiMaxMin    m_bid;
   CRiMaxMin    m_last;
   CRiBuffDbl   m_last_buy;
   CRiMaxMin    m_last_sell;
   CRiBuffInt   m_vol;
   CRiBuffInt   m_flags;
   
   double       m_xpoints[];  // Matriz de índices
   void         RefreshCurves();
   void         SetMaxMin(void);
public:
                CElTickGraph(void);
   virtual void Event(CEvent* event);
   void         SetTiksTotal(int tiks);
   int          GetTiksTotal(void);
   void         Redraw(void);
   virtual void Show(void);
   virtual void OnHide(void);
   virtual void OnRefresh(CEventRefresh* refresh);
   void         AddLastTick();
};
//+------------------------------------------------------------------+
//| Inicialización del gráfico                                       |
//+------------------------------------------------------------------+
CElTickGraph::CElTickGraph(void) : CElChart(OBJ_RECTANGLE_LABEL)
{
   double y[] = {0};
   y[0] = MarketBook.InfoGetDouble(MBOOK_BEST_ASK_PRICE);
   double x[] = {0};
   
   CCurve* cur = m_graf.CurveAdd(x, y, CURVE_LINES, "Ask");   
   cur.Color(ColorToARGB(clrLightCoral, 255));
   cur.LinesEndStyle(LINE_END_ROUND);
   
   cur = m_graf.CurveAdd(x, y, CURVE_LINES, "Bid");
   cur.Color(ColorToARGB(clrCornflowerBlue, 255));
   
   cur = m_graf.CurveAdd(x, y, CURVE_POINTS, "Buy");
   cur.PointsType(POINT_TRIANGLE_DOWN);
   cur.PointsColor(ColorToARGB(clrCornflowerBlue, 255));
   cur.Color(ColorToARGB(clrCornflowerBlue, 255));
   cur.PointsFill(true);
   cur.PointsSize(5);
   
   
   cur = m_graf.CurveAdd(x, y, CURVE_POINTS, "Sell");
   cur.PointsType(POINT_TRIANGLE);
   cur.PointsColor(ColorToARGB(clrLightCoral, 255));
   cur.Color(ColorToARGB(clrLightCoral, 255));
   cur.PointsFill(true);
   cur.PointsSize(5);
   
   m_graf.CurvePlotAll();
   m_graf.IndentRight(1);
   m_graf.GapSize(1);
   SetTiksTotal(TicksHistoryTotal);
}
//+------------------------------------------------------------------+
//| Establece el número de ticks en la ventana del gráfico           |
//+------------------------------------------------------------------+
void CElTickGraph::SetTiksTotal(int tiks)
{
   m_last.SetMaxTotal(tiks);
   m_last_buy.SetMaxTotal(tiks);
   m_last_sell.SetMaxTotal(tiks);
   m_ask.SetMaxTotal(tiks);
   m_bid.SetMaxTotal(tiks);
   m_vol.SetMaxTotal(tiks);
   ArrayResize(m_xpoints, tiks);
   for(int i = 0; i < ArraySize(m_xpoints); i++)
      m_xpoints[i] = i;
}

//+------------------------------------------------------------------+
//| Actualiza la línea de ticks                                      |
//+------------------------------------------------------------------+
void CElTickGraph::RefreshCurves(void) 
{
   int total_last = m_last.GetTotal();
   int total_ask = m_ask.GetTotal();
   int total_bid = m_bid.GetTotal();
   int total = 10;
   for(int i = 0; i < m_graf.CurvesTotal(); i++)
   {
      CCurve* curve = m_graf.CurveGetByIndex(i);
      double y_points[];
      double x_points[];
      switch(i)
      {
         case LAST_LINE:
         {
            m_last.ToArray(y_points);
            if(ArraySize(x_points) < ArraySize(y_points))
               ArrayCopy(x_points, m_xpoints, 0, 0, ArraySize(y_points));
            curve.Update(x_points, y_points);
            break;
         }
         case ASK_LINE:
            m_ask.ToArray(y_points);
            if(ArraySize(x_points) < ArraySize(y_points))
               ArrayCopy(x_points, m_xpoints, 0, 0, ArraySize(y_points));
            curve.Update(x_points, y_points);
            break;
         case BID_LINE:
            m_bid.ToArray(y_points);
            if(ArraySize(x_points) < ArraySize(y_points))
               ArrayCopy(x_points, m_xpoints, 0, 0, ArraySize(y_points));
            curve.Update(x_points, y_points);
            break;
         case LAST_BUY:
         {
            m_last_buy.ToArray(y_points);
            CPoint2D points[];
            ArrayResize(points, ArraySize(y_points));
            int k = 0;
            for(int p = 0; p < ArraySize(y_points);p++)
            {
               if(y_points[p] == -1)
                  continue;
               points[k].x = p;
               points[k].y = y_points[p];
               k++;
            }
            if(k > 0)
            {
               ArrayResize(points, k);
               curve.Update(points);
            }
            break;
         }
         case LAST_SELL:
         {
            m_last_sell.ToArray(y_points);
            CPoint2D points[];
            ArrayResize(points, ArraySize(y_points));
            int k = 0;
            for(int p = 0; p < ArraySize(y_points);p++)
            {
               if(y_points[p] == -1)
                  continue;
               points[k].x = p;
               points[k].y = y_points[p];
               k++;
            }
            if(k > 0)
            {
               ArrayResize(points, k);
               curve.Update(points);
            }
            break;
         }
      }
   }
   
}
//+------------------------------------------------------------------+
//| Retorna el número ticks en la ventana del gráfico                |
//+------------------------------------------------------------------+
int CElTickGraph::GetTiksTotal(void)
{
   return m_ask.GetMaxTotal();
}
//+---------------------------------------------------------------------------------------------------------+
//| Actualiza el gráfico en el momento de la actualización de la profundidad de mercado                     |
//+---------------------------------------------------------------------------------------------------------+
void CElTickGraph::OnRefresh(CEventRefresh* refresh)
{
   //Trazamos en el gráfico los últimos ticks recibidos
   int dbg = 5;
   int total = ArraySize(MarketBook.LastTicks);
   for(int i = 0; i < ArraySize(MarketBook.LastTicks); i++)
   {
      MqlTick tick = MarketBook.LastTicks[i];
      if((tick.flags & TICK_FLAG_BUY)==TICK_FLAG_BUY)
      {
         m_last_buy.AddValue(tick.last);
         m_last_sell.AddValue(-1);
         m_ask.AddValue(tick.last);
         m_bid.AddValue(tick.bid);
      }
      if((tick.flags & TICK_FLAG_SELL)==TICK_FLAG_SELL)
      {
         m_last_sell.AddValue(tick.last);
         m_last_buy.AddValue(-1);
         m_bid.AddValue(tick.last);
         m_ask.AddValue(tick.ask);
      }
      if((tick.flags & TICK_FLAG_ASK)==TICK_FLAG_ASK ||
         (tick.flags & TICK_FLAG_BID)==TICK_FLAG_BID)
      {
         m_last_sell.AddValue(-1);
         m_last_buy.AddValue(-1);
         m_bid.AddValue(tick.bid);
         m_ask.AddValue(tick.ask);
      }
   }
   MqlTick tick;
   if(!SymbolInfoTick(Symbol(), tick))
       return;
   if(ArraySize(MarketBook.LastTicks)>0)
   {
      RefreshCurves();
      m_graf.Redraw(true);
      m_graf.Update();
   }
}
void CElTickGraph::Event(CEvent *event)
{
   CElChart::Event(event);
   if(event.EventType() != EVENT_CHART_CUSTOM)
      return;
   CEventNewTick* ent = dynamic_cast<CEventNewTick*>(event);
   if(ent == NULL)
      return;
   MqlTick tick;
   ent.GetNewTick(tick);
   if((tick.flags & TICK_FLAG_BUY) == TICK_FLAG_BUY)
   {
      int last = m_last_buy.GetTotal()-1;
      if(last >= 0)
         m_last_buy.ChangeValue(last, tick.last);
   }
}
//+-------------------------------------------------------------------------------------+
//| Calcula la escala según los ejes, de tal forma que el precio actual siempre esté    |
//| en la parte media del gráfico de precios                                            |
//+-------------------------------------------------------------------------------------+
void CElTickGraph::SetMaxMin(void)
{
   double max = m_last.MaxValue();
   double min = m_last.MinValue();
   double curr = m_last.GetValue(m_last.GetTotal()-1);
   double max_delta = max - curr;
   double min_delta = curr - min;
   if(max_delta > min_delta)
      m_graf.SetMaxMinValues(0, m_last.GetTotal(), (max-max_delta*2.0), max);
   else
      m_graf.SetMaxMinValues(0, m_last.GetTotal(), min, (min+min_delta*2.0));
}
//+----------------------------------------------------------------------+
//| Actualiza el gráfico                                                 |
//+----------------------------------------------------------------------+
void CElTickGraph::Redraw(void)
{
   m_graf.Redraw(true);
   m_graf.Update();
}
//+--------------------------------------------------------------------------------------+
//| Intercepta la representación del gráfico, cambiando la prioridad de representación   |
//+--------------------------------------------------------------------------------------+
void CElTickGraph::Show(void)
{
   BackgroundColor(clrNONE);
   BorderColor(clrBlack);
   Text("Ticks:");
   //m_graf.BackgroundColor(clrWhiteSmoke);
   m_graf.Create(ChartID(), "Ticks", 0, (int)XCoord()+20, (int)YCoord()+30, 610, 600); 
   m_graf.Redraw(true);
   m_graf.Update();
   CElChart::Show();
}

//+-----------------------------------------------------------------------------------+
//| En el momento de la representación mostramos el gráfico                           |
//+-----------------------------------------------------------------------------------+
void CElTickGraph::OnHide(void)
{
   m_graf.Destroy();
   CNode::OnHide();
}

Vamos a analizar este código con más detalle. Comenzaremos con el constructor de la clase CElTickGraph::CElTickGraph. Por su declaración, queda claro que la propia clase se basa en la primitiva gráfica OBJ_RECTANGLE_LABEL, es decir, en una etiqueta rectangular normal. En el constructor se crean varias curvas del tipo CCurve, cada una de las cuales es responsable de su tipo de datos. Para cada una de ellas se establecen sus propiedades: el nombre de la línea, su tipo y color. En el momento de creación de la curva, los valores que va a representar aún no se conocen, por eso usamos las matrices falsas double x e y, que contienen las coordenadas del primer punto. Después de crear las curvas y ubicarlas en el objeto CGraphic, se configuran los búferes circulares en el método SetTiksTotal. La configuración se reduce a establecer el número límite de ticks a guardar, que se indica con el parámetro TicksHistoryTotal.

Cuando se han añadido a CGraphic todas las curvas necesarias, y los búferes circulares se han configurado como es debido, la profundidad de mercado está lista para funcionar. Durante su funcionamiento, se llaman dos métodos principales: se trata de CElTickGraph::OnRefresh y CElTickGraph::RefreshCurves. Vamos a verlos.

El método OnRefresh se llama después del cambio de la profundidad. Es posible realizar un seguimiento de estos cambios con la ayuda de la función OnBookEvent:

//+------------------------------------------------------------------+
//| MarketBook change event                                          |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
{
   
   if(symbol != MarketBook.GetMarketBookSymbol())
      return;
   MarketBook.Refresh();
   MButton.Refresh();
   ChartRedraw();
}

Primero se actualiza la profundidad de mercado (MarketBook.Refresh()), después, el panel encargado de representarla: MButton.Refresh(). Puesto que el panel se representa en forma de botón y es posible expandirlo/ocultarlo, ese mismo botón será el elemento padre de todo el panel. Por eso, todos los eventos, incluido la muestra de actualizaciones, llegan a través de este botón (MButton). La orden de actualización atraviesa todos los elementos incluidos en el botón y al final llega hasta CElTickGraph, en el que se ubica el algoritmo que actualiza el propio gráfico. El algoritmo está implementado en el método OnRefresh.

Al principio, el algoritmo recibe el número de ticks que han tenido tiempo de surgir desde el momento de la anterior actualización. Después los valores de cada uno de los ticks se añaden al búfer circular correspondiente. El precio Ask del tick se añade al búfer circular m_ask, el precio Bid, al búfer m_bid, etcétera. Si el tipo del último tick es la transacción Last, los precios Ask y Bid son forzosamente sincronizados con el precio Last. Esto se hace porque el propio terminal no realiza la sincronización, sino que muestra los valores Ask y Bid de los ticks anteriores. De esta forma, las transacciones Last siempre se encuentran de forma garantizada al nivel Ask, o bien al nivel Bid. Conviene notar que la profundidad de mercado estándar no hace esta sincronización, y Last puede encontrarse visualmente en ella entre estas dos líneas.

Después de que la última serie de ticks se ha haya ubicado en los búferes circulares, se llama el método OnRefreshCurves, responsable del dibujado de los ticks en el gráfico. En el método se ubica un ciclo en el que se revisan todas las curvas CCurve disponibles. Para cada curva se realiza una actualización completa de los puntos, con la ayuda del método curve.Update. Los puntos para los ejes Y los obtendremos copiando todos los valores del búfer circular a una matriz double normal. Los puntos para los ejes X se obtienen de una forma más refinada. Utilizando la revisión completa, la coordinada x de cada punto se cambia a x-1. Es decir, si el elemento x tenía el valor 1000, entonces después de la revisión tendrá el valor 999. De esta forma, se consigue un efecto de movimiento en el que el gráfico dibuja nuevos valores, mientras que los antiguos desaparecen de él sin dejar rastro.

Después de que todos los valores estén ubicados en los índices necesarios y las curvas CCurve hayan sido actualizadas, queda actualizar la propia profundidad de mercado. Para ello, en el método OnRefresh se llaman los métodos de actualización del gráfico:  m_graf.Redraw y m_graf.Update.

El algoritmo de representación del gráfico de ticks permite elegir dos modos:

  • El gráfico de ticks se representa sin vincular los últimos precios a la parte media de la profundidad. Los máximos y mínimos del gráfico se calculan de forma automática, dentro e CGraphic.
  • El gráfico de ticks se representa teniendo en cuenta la vinculación de los últimos precios a la parte media de la profundidad. Se encuentren donde se encuentren los máximos y los mínimos de los precios, el precio actual (último) siempre estará en la parte media del gráfico.

En el primer caso se llama el escalado automático, realizado por el propio CGraphic. En el segundo caso, SetMaxMin se encarga del escalado.

Instalación. Trabajando en la dinámica. Características comparativas de la profundidad del mercado

Todos los archivos necesarios para la aplicación que hemos desarrollado se pueden dividir de forma condicional en cuatro grupos:

  • Archivos de la biblioteca gráfica CPanel. Ubicados en MQL5\Include\Panel;
  • Archivos de la clase de la profundidad de mercado MarketBook. Ubicados en MQL5\Include\Trade;
  • Archivos de las clases de los búferes circulares. Ubicados en MQL5\Include\RingBuffer;
  • Propiamente, los archivos de la profundidad de mercado de scalping. Ubicados en MQL5\Indicators\MarketBookArticle.

El fichero adjunto contiene todos estos archivos en los directorios correspondientes. Para instalar el programa basta con descomprimir el fichero en la carpeta MQL5. No es necesario crear ninguna subcarpeta. Después de descomprimir, compile el archivo MQL5\Indicators\MarketBookArticle\MarketBook.mq5. Tras la compilación, aparecerá el indicador correspondiente, que surgirá en la ventana del Navegador MetaTrader 5. 

El mejor método para valorar el algoritmo obtenido es representar los cambios del gráfico de ticks en la dinámica. El vídeo que vemos a continuación muestra cómo el gráfico de ticks cambia con el tiempo, desplazando suavemente la ventana del gráfico hacia la derecha:


Debemos notar que el gráfico de ticks obtenido de nuestra profundidad de meercado se diferencia del gráfico análogo de la profundidad de mercado en MetaTrader 5. En el recuadro comparativo de más abajo se muestran estas diferencias:

Profundidad de mercado estándar de MetaTrader 5 Profundidad de mercado desarrollada
Los precios Last, Ask y Bid no están interrelacionados. El precio Last puede encontrarse en niveles distintos de Ask y Bid. Los precio Last, Ask, Bid están sincronizados entre sí. El precio Last siempre se encuentra o bien al nivel Ask, o bien al nivel Bid.
Los precios Last se representan en forma de círculos de diferente diámetro, directamente proprocional al volumen de las transacciones. El círculo con el máximo diámetro se corresponde con la transacción con el máximo volumen realizada en los últimos N ticks, donde N es el periodo de la ventana móvil del gráfico de ticks. Las transacciones de compra se representan en forma de triángulo azul orientado hacia abajo, las tranasacciones de venta, con un triángulo rojo orientado hacia arriba. No se destacan las transacciones en función del volumen.
La escala del gráfico de ticks está sincronizada con la altura del recuadro de órdenes pendientes. De esta forma, cualquier nivel en el recuadro de transacciones corresponde a este mismo nivel en el gráfico de ticks. La desventaja de este tipo de solución es la imposibilidad de representar el gráfico de ticks en su escala, mayor en tamaño. La ventaja es que los precios se ven mejor y los niveles de la profundidad se corresponden totalmente con el gráfico de ticks. Las dimensiones del gráfico de ticks y del recuadro de órdenes pendientes no se corresponden unas con otras. El precio actual del gráfico de ticks puede corresponder más o menos a la parte media del recuadro de órdenes. La desventaja de esta solución es la ausencia de correspondencia visual entre los niveles del recuadro de órdenes y el gráfico de ticks. La ventaja, es la posibilidad de establecer prácticamente cualquier escala del gráfico de ticks.
El gráfico de ticks está equipado con un histograma de volúmenes adicional, ubicado debajo de él.  El gráfico de ticks no contiene ningún complemento.

Recuadro 2. Características comparativas de la profundidad de mercado estándar y la desarrollada.

Conclusión

Hemos analizado los momentos esenciales del desarrollo de la profundidad de mercado de scalping.

  • Hemos mejorado el aspecto del recuadro de órdenes
  • Hemos añadido al panel un gráfico de ticks basado en CGraphic, modernizando sustancialmente el propio motor gráfico
  • Hemos perfeccionado la clase de la profundidad de mercado, añadiéndole un algoritmo de sincronización de los ticks con el actual recuadro de órdenes.

Sin embargo, incluso ahora, nuestra profundidad de mercado aún está muy lejos de la versión completa de scalping. Por supesto, es posible que muchos usuarios queden decepcionados al leer hasta este punto, y no ver al final un análogo completo de la profundidad de mercado estándar o incluso programas especializados como la plataforma de Bondar o QScalp. Pero debemos entender que cualquier producto programático complejo debe superar una serie de niveles evolutivos durante su desarrollo. Esto es lo que considero conveniente añadir a la profundidad de mercado en las siguientes versiones:

  • Posibilidad de colocar órdenes límite directamente en el panel de la profundidad
  • Posibilidad de realizar un seguimiento de la orden en el gráfico de ticks
  • Diferenciar las transacciones Last por volumen, representándolas con distintos métodos en el gráfico
  • Representar indicadores adicionales de forma paralela al gráfico de ticks. Por ejemplo, representar bajo el gráfico de ticks un histograma de la relación de todas las órdenes Buy Limit con las órdenes Sell Limit.
  • Y, al fin, lo más importante: cargar y guardar la historia de la profundidad, para que sea posible construir estrategias comerciales en el modo de simulación offline.

Todo lo enumerado se puede implementar, y quizá estas posibilidades sean publicadas. Si los usuarios muestran interés por este tema, continuaremos la serie de artículos.