Utilizar datos de Profundidad de Mercado en algoritmos aplicados

La Profundidad de Mercado se considera una tecnología muy útil para desarrollar sistemas avanzados de trading. En concreto, el análisis de la distribución de los volúmenes de Profundidad de Mercado en los niveles próximos al mercado permite conocer de antemano el precio medio de ejecución de la orden de un volumen específico: basta con sumar los volúmenes de los niveles (en sentido opuesto) que asegurarán su llenado. En un mercado poco activo, con volúmenes insuficientes, el algoritmo puede abstenerse de abrir una operación para evitar un deslizamiento significativo del precio.

A partir de los datos de Profundidad de Mercado pueden construirse también otras estrategias. Por ejemplo, puede ser importante conocer los niveles de precios en los que se encuentran los grandes volúmenes.

MarketBookVolumeAlert.mq5

En el siguiente indicador de prueba MarketBookVolumeAlert.mq5 implementamos un algoritmo sencillo para rastrear los volúmenes o los cambios en los mismos que superan un valor determinado.

#property indicator_chart_window
#property indicator_plots 0
   
input string WorkSymbol = ""// WorkSymbol (if empty, use current chart symbol)
input bool CountVolumeInLots = false;
input double VolumeLimit = 0;
   
const string _WorkSymbol = StringLen(WorkSymbol) == 0 ? _Symbol : WorkSymbol;

No hay gráficos en el indicador. El símbolo controlado se introduce en el parámetro WorkSymbol (si se deja en blanco quiere decirse que se trata del símbolo de trabajo del gráfico). El umbral mínimo de objetos rastreados, es decir, la sensibilidad del algoritmo, se especifica en el parámetro VolumeLimit. En función del parámetro CountVolumeInLots, los volúmenes se analizan y se muestran al usuario en lotes (true) o en unidades (false). Esto también afecta a la forma de introducir el valor VolumeLimit. La conversión de unidades a fracciones de lotes la proporciona la macro VOL: el tamaño del contrato utilizado en ella contract se inicializa en OnInit (véase más abajo).

#define VOL(V) (CountVolumeInLots ? V / contract : V)

Si se encuentran grandes volúmenes por encima del umbral, el programa mostrará un mensaje sobre el nivel correspondiente en el comentario. Para guardar el historial de avisos más cercano, utilizamos la clase de comentarios multilínea que ya conocemos (Comments.mqh).

#define N_LINES 25                // number of lines in the comment buffer
#include <MQL5Book/Comments.mqh>

En el manejador OnInit vamos a preparar los ajustes necesarios y a suscribirnos a los eventos de DOM.

double contract;
int digits;
   
void OnInit()
{
   MarketBookAdd(_WorkSymbol);
   contract = SymbolInfoDouble(_WorkSymbolSYMBOL_TRADE_CONTRACT_SIZE);
   digits = (int)MathRound(MathLog10(contract));
   Print(SymbolInfoDouble(_WorkSymbolSYMBOL_SESSION_BUY_ORDERS_VOLUME));
   Print(SymbolInfoDouble(_WorkSymbolSYMBOL_SESSION_SELL_ORDERS_VOLUME));
}

Las propiedades SYMBOL_SESSION_BUY_ORDERS_VOLUME y SYMBOL_SESSION_SELL_ORDERS_VOLUME, si están rellenadas por su bróker para el símbolo seleccionado, le ayudarán a averiguar qué umbral tiene sentido elegir. Por defecto, VolumeLimit es 0, por lo que absolutamente todos los cambios en el libro de órdenes generarán avisos. Para filtrar las fluctuaciones insignificantes se recomienda fijar VolumeLimit en un valor que supere el tamaño medio de los volúmenes en todos los niveles (busque de antemano en el libro de órdenes integrado o en el indicador MarketBookDisplay.mq5).

De la forma habitual, implementamos la finalización.

void OnDeinit(const int)
{
   MarketBookRelease(_WorkSymbol);
   Comment("");
}

El trabajo principal lo realiza el procesador OnBookEvent. Describe un array estático MqlBookInfo mbp para almacenar la versión anterior del libro de órdenes (desde la última llamada a la función).

void OnBookEvent(const string &symbol)
{
   if(symbol != _WorkSymbolreturn// process only the requested symbol 
   
   static MqlBookInfo mbp[];      // previous table/book
   MqlBookInfo mbi[];
   if(MarketBookGet(symbolmbi)) // read the current book
   {
      if(ArraySize(mbp) == 0// first time we just save, because nothing to compare
      {
         ArrayCopy(mbpmbi);
         return;
      }
      ...

Si hay un libro de órdenes antiguo y otro nuevo, comparamos los volúmenes de sus niveles entre sí en bucles anidados mediante i y j. Recordemos que un aumento del índice significa una disminución del precio.

      int j = 0;
      for(int i = 0i < ArraySize(mbi); ++i)
      {
         bool found = false;
         for( ; j < ArraySize(mbp); ++j)
         {
            if(MathAbs(mbp[j].price - mbi[i].price) < DBL_EPSILON * mbi[i].price)
            {       // mbp[j].price == mbi[i].price
               if(VOL(mbi[i].volume_real - mbp[j].volume_real) >= VolumeLimit)
               {
                  NotifyVolumeChange("Enlarged"mbp[j].price,
                     VOL(mbp[j].volume_real), VOL(mbi[i].volume_real));
               }
               else
               if(VOL(mbp[j].volume_real - mbi[i].volume_real) >= VolumeLimit)
               {
                  NotifyVolumeChange("Reduced"mbp[j].price,
                     VOL(mbp[j].volume_real), VOL(mbi[i].volume_real));
               }
               found = true;
               ++j;
               break;
            }
            else if(mbp[j].price > mbi[i].price)
            {
               if(VOL(mbp[j].volume_real) >= VolumeLimit)
               {
                  NotifyVolumeChange("Removed"mbp[j].price,
                     VOL(mbp[j].volume_real), 0.0);
               }
               // continue the loop increasing ++j to lower prices
            }
            else // mbp[j].price < mbi[i].price
            {
               break;
            }
         }
         if(!found// unique (new) price
         {
            if(VOL(mbi[i].volume_real) >= VolumeLimit)
            {
               NotifyVolumeChange("Added"mbi[i].price0.0VOL(mbi[i].volume_real));
            }
         }
      }
      ...

Aquí no se hace hincapié en el tipo de nivel, sino sólo en el valor del volumen. Sin embargo, si lo desea, puede añadir fácilmente la designación de compras o ventas a las notificaciones, dependiendo del campo type del nivel en el que se haya producido el cambio importante.

Por último, guardamos una nueva copia de mbi en un array estático mbp para compararla con ella en la siguiente llamada a la función.

      if(ArrayCopy(mbpmbi) <= 0)
      {
         Print("ArrayCopy failed:"_LastError);
      }
      if(ArrayResize(mbpArraySize(mbi)) <= 0// shrink if needed
      {
         Print("ArrayResize failed:"_LastError);
      }
   }
}

ArrayCopy no encoge automáticamente un array de destino dinámico si resulta ser mayor que el array de origen, por lo que establecemos explícitamente su tamaño exacto con ArrayResize.

Una función auxiliar NotifyVolumeChange simplemente añade información sobre el cambio encontrado al comentario.

void NotifyVolumeChange(const string actionconst double price,
   const double previousconst double volume)
{
   const string message = StringFormat("%s: %s %s -> %s",
      action,
      DoubleToString(price, (int)SymbolInfoInteger(_WorkSymbolSYMBOL_DIGITS)),
      DoubleToString(previousdigits),
      DoubleToString(volumedigits));
   ChronoComment(message);
}

En la siguiente imagen se muestra el resultado del indicador para los ajustes CountVolumeInLots=false, VolumeLimit=20.

Notificaciones sobre cambios de volumen en el libro de órdenes
Notificaciones sobre cambios de volumen en el libro de órdenes

MarketBookQuasiTicks.mq5

Como segundo ejemplo del posible uso del libro de órdenes, echemos un vistazo al problema de la obtención de ticks multidivisa. Ya hemos hablado de ello en la sección Generación de eventos personalizados, donde hemos visto una de las posibles soluciones y el indicador EventTickSpy.mq5. Ahora, después de familiarizarnos con la API de Profundidad de Mercado, podemos implementar una alternativa.

Vamos a crear un indicador MarketBookQuasiTicks.mq5, que se suscribirá a los libros de órdenes de una lista dada de instrumentos y encontrará los precios de la mejor oferta y demanda en ellos, es decir, pares de precios en torno al diferencial, que no son más que los precios Ask y Bid.

Por supuesto, esta información no es un equivalente completo de los ticks estándar (recuérdese que los flujos de operaciones/ticks y del libro de órdenes pueden proceder de proveedores completamente distintos), pero proporciona una visión adecuada y oportuna del mercado.

Los nuevos valores de los precios por símbolos se mostrarán en un comentario de varias líneas.

La lista de símbolos de trabajo se especifica en el parámetro de entrada SymbolList como una lista separada por comas. La activación y desactivación de suscripciones a eventos de Profundidad de Mercado se realiza en los manejadores OnInit y OnDeinit.

#define N_LINES 25                // number of lines in the comment buffer
#include <MQL5Book/Comments.mqh>
   
input string SymbolList = "EURUSD,GBPUSD,XAUUSD,USDJPY"// SymbolList (comma,separated,list)
   
const string WorkSymbols = StringLen(SymbolList) == 0 ? _Symbol : SymbolList;
string symbols[];
   
void OnInit()
{
   const int n = StringSplit(WorkSymbols, ',', symbols);
   for(int i = 0i < n; ++i)
   {
      if(!MarketBookAdd(symbols[i]))
      {
         PrintFormat("MarketBookAdd(%s) failed with code %d"symbols[i], _LastError);
      }
   }
}
   
void OnDeinit(const int)
{
   for(int i = 0i < ArraySize(symbols); ++i)
   {
      if(!MarketBookRelease(symbols[i]))
      {
         PrintFormat("MarketBookRelease(%s) failed with code %d"symbols[i], _LastError);
      }
   }
   Comment("");
}

El análisis de cada nuevo libro de órdenes se realiza en OnBookEvent.

void OnBookEvent(const string &symbol)
{
   MqlBookInfo mbi[];
   if(MarketBookGet(symbolmbi)) // getting the current order book
   {
      int half = ArraySize(mbi) / 2// estimate the middle of the order book
      bool correct = true;
      for(int i = 0i < ArraySize(mbi); ++i)
      {
         if(i > 0)
         {
            if(mbi[i - 1].type == BOOK_TYPE_SELL
               && mbi[i].type == BOOK_TYPE_BUY)
            {
               half = i// specify the middle of the order book
            }
            
            if(mbi[i - 1].price <= mbi[i].price)
            {
               correct = false;
            }
         }
      }
      
      if(correct// retrieve the best Bid/Ask prices from the correct order book 
      {
         // mbi[half - 1].price // Ask
         // mbi[half].price     // Bid
         OnSymbolTick(symbolmbi[half].price);
      }
   }
}

Los precios Ask/Bid encontrados en el mercado se pasan a la función de ayuda OnSymbolTick para que se muestren en un comentario.

void OnSymbolTick(const string &symbolconst double price)
{
   const string message = StringFormat("%s %s",
      symbolDoubleToString(price, (int)SymbolInfoInteger(symbolSYMBOL_DIGITS)));
   ChronoComment(message);
}

Si lo desea, puede asegurarse de que nuestros ticks sintetizados no difieren mucho de los ticks estándar.

Así es como se ve en el gráfico la información sobre los cuasi-ticks entrantes.

Cuasi-ticks multisímbolo basados en eventos del libro de órdenes
Cuasi-ticks multisímbolo basados en eventos del libro de órdenes

Al mismo tiempo, hay que señalar una vez más que los eventos del libro de órdenes sólo están disponibles en la plataforma en línea, pero no en el probador. Si el sistema de trading se construye exclusivamente a partir de cuasi-ticks del libro de órdenes, su simulación requerirá el uso de soluciones de terceros que garanticen la recopilación y reproducción de los libros de órdenes en el probador.