Aplicación de recursos gráficos en trading

Por supuesto, embellecer no es el objetivo principal de los recursos. Veamos cómo crear una herramienta útil basada en ellos. También eliminaremos una omisión más: hasta ahora solo hemos utilizado recursos dentro de objetos OBJ_BITMAP_LABEL, que se posicionan en coordenadas de pantalla. Sin embargo, los recursos gráficos también pueden incrustarse en objetos OBJ_BITMAP con referencia a coordenadas de cotización: precios y hora.

Anteriormente en el libro hemos visto el indicador IndDeltaVolume.mq5 que calcula el volumen delta (tick o real) para cada barra. Además de esta representación del volumen delta, existe otra no menos popular entre los usuarios: el perfil de mercado. Se trata de la distribución de volúmenes en el contexto de los niveles de precios. Dicho histograma puede construirse para toda la ventana, para una profundidad determinada (por ejemplo, dentro de un día) o para una sola barra.

Es la última opción la que aplicamos en forma de nuevo indicador DeltaVolumeProfile.mq5. Ya hemos considerado los principales detalles técnicos de la solicitud del historial de ticks en el marco del indicador anterior, por lo que ahora nos centraremos principalmente en el componente gráfico.

La bandera ShowSplittedDelta en la variable de entrada controlará cómo se muestran los volúmenes: desglosados por direcciones de compra/venta o colapsados.

input bool ShowSplittedDelta = true;

No habrá búferes en el indicador. Calculará y mostrará un histograma para una barra específica a petición del usuario, y en concreto, haciendo clic en dicha barra. Por lo tanto, utilizaremos el manejador OnChartEvent. En este manejador, obtenemos las coordenadas de la pantalla, las recalculamos en precio y tiempo, y llamamos a alguna función de ayuda RequestData, que inicia el cálculo.

void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
   if(id == CHARTEVENT_CLICK)
   {
      datetime time;
      double price;
      int window;
      ChartXYToTimePrice(0, (int)lparam, (int)dparamwindowtimeprice);
      time += PeriodSeconds() / 2;
      const int b = iBarShift(_Symbol_Periodtimetrue);
      if(b != -1 && window == 0)
      {
         RequestData(biTime(_Symbol_Periodb));
      }
   }
   ...
}

Para rellenarlo, necesitamos la clase DeltaVolumeProfile, que está construida para ser similar a la clase CalcDeltaVolume de IndDeltaVolume.mq5.

La nueva clase describe variables que tienen en cuenta el método de cálculo del volumen (tickType), el tipo de precio sobre el que se construye el gráfico (barType), el modo de la variable de entrada ShowSplittedDelta (se colocará en una variable miembro delta), así como un prefijo para los objetos generados en el gráfico.

class DeltaVolumeProfile
{
   const COPY_TICKS tickType;
   const ENUM_SYMBOL_CHART_MODE barType;
   const bool delta;
   
   static const string prefix;
   ...
public:
   DeltaVolumeProfile(const COPY_TICKS typeconst bool d) :
      tickType(type), delta(d),
      barType((ENUM_SYMBOL_CHART_MODE)SymbolInfoInteger(_SymbolSYMBOL_CHART_MODE))
   {
   }
   
   ~DeltaVolumeProfile()
   {
      ObjectsDeleteAll(0prefix0); // TODO: delete resources
   }
   ...
};
   
static const string DeltaVolumeProfile::prefix = "DVP";
   
DeltaVolumeProfile deltas(TickTypeShowSplittedDelta);

La dirección tick type puede cambiarse al valor TRADE_TICKS solo para los instrumentos de trading para los que se dispone de volúmenes reales. De manera predeterminada, está activado el modo INFO_TICKS, que funciona en todos los instrumentos.

Los ticks de una barra concreta se solicitan mediante el método createProfileBar.

   int createProfileBar(const int i)
   {
      MqlTick ticks[];
      const datetime time = iTime(_Symbol_Periodi);
      // prev and next - time limits of the bar
      const datetime prev = time;
      const datetime next = prev + PeriodSeconds();
      ResetLastError();
      const int n = CopyTicksRange(_SymbolticksCOPY_TICKS_ALL,
         prev * 1000next * 1000 - 1);
      if(n > -1 && _LastError == 0)
      {
         calcProfile(itimeticks);
      }
      else
      {
         return -_LastError;
      }
      return n;
   }

El análisis directo de los ticks y el cálculo de los volúmenes se realiza en el método protegido calcProfile. En él, en primer lugar, averiguamos el rango de precios de la barra y su tamaño en píxeles.

   void calcProfile(const int bconst datetime timeconst MqlTick &ticks[])
   {
      const string name = prefix + (string)(ulong)time;
      const double high = iHigh(_Symbol_Periodb);
      const double low = iLow(_Symbol_Periodb);
      const double range = high - low;
      
      ObjectCreate(0nameOBJ_BITMAP0timehigh);
      
      int x1y1x2y2;
      ChartTimePriceToXY(00timehighx1y1);
      ChartTimePriceToXY(00timelowx2y2);
      
      const int h = y2 - y1 + 1;
      const int w = (int)(ChartGetInteger(0CHART_WIDTH_IN_PIXELS)
         / ChartGetInteger(0CHART_WIDTH_IN_BARS));
      ...

Basándonos en esta información, creamos un objeto OBJ_BITMAP, asignamos un array para la imagen y creamos un recurso. El fondo de toda la imagen está vacío (transparente). Cada objeto está anclado por el punto medio superior al precio High de su barra y tiene una anchura de una barra.

      uint data[];
      ArrayResize(dataw * h);
      ArrayInitialize(data0);
      ResourceCreate(name + (string)ChartID(), datawh00wCOLOR_FORMAT_ARGB_NORMALIZE);
         
      ObjectSetString(0nameOBJPROP_BMPFILE"::" + name + (string)ChartID());
      ObjectSetInteger(0nameOBJPROP_XSIZEw);
      ObjectSetInteger(0nameOBJPROP_YSIZEh);
      ObjectSetInteger(0nameOBJPROP_ANCHORANCHOR_UPPER);
      ...

A continuación se calculan los volúmenes en ticks del array pasado. El número de niveles de precios es igual a la altura de la barra en píxeles (h). Suele ser inferior al rango de precios en puntos, por lo que los píxeles actúan como una especie de cesta para calcular las estadísticas. Si en un marco temporal pequeño, el rango de puntos es menor que el tamaño en píxeles, el histograma será visualmente escaso. Los volúmenes de compras y ventas se acumulan por separado en los arrays plus y minus.

      long plus[], minus[], max = 0;
      ArrayResize(plush);
      ArrayResize(minush);
      ArrayInitialize(plus0);
      ArrayInitialize(minus0);
      
      const int n = ArraySize(ticks);
      for(int j = 0j < n; ++j)
      {
         const double p1 = price(ticks[j]); // returns Bid or Last
         const int index = (int)((high - p1) / range * (h - 1));
         if(tickType == TRADE_TICKS)
         {
            // if real volumes are available, we can take them into account
            if((ticks[j].flags & TICK_FLAG_BUY) != 0)
            {
               plus[index] += (long)ticks[j].volume;
            }
            if((ticks[j].flags & TICK_FLAG_SELL) != 0)
            {
               minus[index] += (long)ticks[j].volume;
            }
         }
         else // tickType == INFO_TICKS or tickType == ALL_TICKS
         if(j > 0)
         {
           // if there are no real volumes,
           // price movement up/down is an estimate of the volume type
            if((ticks[j].flags & (TICK_FLAG_ASK | TICK_FLAG_BID)) != 0)
            {
               const double d = (((ticks[j].ask + ticks[j].bid)
                              - (ticks[j - 1].ask + ticks[j - 1].bid)) / _Point);
               if(d > 0plus[index] += (long)d;
               else minus[index] -= (long)d;
            }
         }
         ...

Para normalizar el histograma, buscamos el valor máximo.

         if(delta)
         {
            if(plus[index] > maxmax = plus[index];
            if(minus[index] > maxmax = minus[index];
         }
         else
         {
            if(fabs(plus[index] - minus[index]) > max)
               max = fabs(plus[index] - minus[index]);
         }
      }
      ...

Por último, las estadísticas resultantes se envían al búfer gráfico data y se envían al recurso. Los volúmenes de compra aparecen en azul y los de venta, en rojo. Si el modo neto está activado, el importe aparece en verde.

      for(int i = 0i < hi++)
      {
         if(delta)
         {
            const int dp = (int)(plus[i] * w / 2 / max);
            const int dm = (int)(minus[i] * w / 2 / max);
            for(int j = 0j < dpj++)
            {
               data[i * w + w / 2 + j] = ColorToARGB(clrBlue);
            }
            for(int j = 0j < dmj++)
            {
               data[i * w + w / 2 - j] = ColorToARGB(clrRed);
            }
         }
         else
         {
            const int d = (int)((plus[i] - minus[i]) * w / 2 / max);
            const int sign = d > 0 ? +1 : -1;
            for(int j = 0j < fabs(d); j++)
            {
               data[i * w + w / 2 + j * sign] = ColorToARGB(clrGreen);
            }
         }
      }
      ResourceCreate(name + (string)ChartID(), datawh00wCOLOR_FORMAT_ARGB_NORMALIZE);
   }

Ahora podemos volver a la función RequestData: su tarea es llamar al método createProfileBar y gestionar los errores (si los hay).

void RequestData(const int bconst datetime timeconst int count = 0)
{
   Comment("Requesting ticks for "time);
   if(deltas.createProfileBar(b) <= 0)
   {
      Print("No data on bar "b", at "TimeToString(time),
         ". Sending event for refresh...");
      ChartSetSymbolPeriod(0_Symbol_Period); // request to update the chart
      EventChartCustom(0TRY_AGAINbcount + 1NULL);
   }
   Comment("");
}

La única estrategia de gestión de errores es intentar solicitar de nuevo los ticks porque puede que no hayan tenido tiempo de cargarse. Para ello, la función envía un mensaje TRY_AGAIN personalizado al gráfico y lo procesa ella misma.

void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
   ...
   else if(id == CHARTEVENT_CUSTOM + TRY_AGAIN)
   {
      Print("Refreshing... ", (int)dparam);
      const int b = (int)lparam;
      if((int)dparam < 5)
      {
         RequestData(biTime(_Symbol_Periodb), (int)dparam);
      }
      else
      {
         Print("Give up. Check tick history manually, please, then click the bar again");
      }
   }
}

Repetimos este proceso no más de 5 veces, porque el historial de ticks puede tener una profundidad limitada, y no tiene sentido cargar el ordenador sin motivo.

La clase DeltaVolumeProfile también dispone del mecanismo para procesar el mensaje CHARTEVENT_CHART_CHANGE con el fin de redibujar los objetos existentes en caso de cambio de tamaño o escala del gráfico. Los detalles se pueden encontrar en el código fuente.

El resultado de este indicador se muestra en la siguiente imagen:

Visualización de histogramas por barras de volúmenes separados en recursos gráficos

Visualización de histogramas por barras de volúmenes separados en recursos gráficos

Tenga en cuenta que los histogramas no se muestran inmediatamente después de dibujar el indicador: tiene que hacer clic en la barra para calcular su histograma.