Прикладное применение графических ресурсов в трейдинге

Чтобы у вас не создалось впечатление, что ресурсы подходят только для украшений, покажем, как на их основе можно разработать инструмент, полезный для трейдеров. Заодно ликвидируем еще одно упущение: до сих пор мы использовали ресурсы только внутри объектов OBJ_BITMAP_LABEL, которые позиционируются в экранных координатах. Однако графические ресурсы могут быть встроены и в объекты OBJ_BITMAP с привязкой к координатам котировок: ценам и времени.

Ранее в книге был описан индикатор IndDeltaVolume.mq5, рассчитывающий дельту объемов (тиковых или реальных) для каждого бара. Кроме такого представления дельты объемов существует и еще одно, не менее популярное у пользователей, — так называемый профиль рынка. Это распределение объемов в разрезе ценовых уровней. Подобную гистограмму можно строить для всего окна целиком, на заданную глубину (например, внутри дня) или для отдельно взятого бара.

Именно последний вариант мы и реализуем в виде нового индикатора DeltaVolumeProfile.mq5. Основные технические тонкости запроса истории тиков мы уже рассмотрели в рамках вышеупомянутого индикатора, поэтому сконцентрируемся теперь, в основном, на графической составляющей.

Флаг ShowSplittedDelta во входной переменной будет управлять тем, как отображать объемы: в разбивке на покупки/продажи или в свернутом виде.

input bool ShowSplittedDelta = true;

В индикаторе не будет буферов. Рассчитывать и показывать гистограмму для конкретного бара он будет по запросу пользователя, а конкретно - по щелчку мыши на этом баре. Таким образом, на первый план выходит обработчик OnChartEvent. В нем мы получаем экранные координаты, пересчитываем их в цену и время, и вызываем некую вспомогательную функцию RequestData, запускающую расчет.

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));
      }
   }
   ...
}

Для её наполнения нам потребуется класс DeltaVolumeProfile, который построен по подобию класса CalcDeltaVolume из IndDeltaVolume.mq5.

В новом классе описаны переменные, учитывающие способ подсчета объемов (tickType), тип цены, по которой строится график (barType), режим из входной переменной ShowSplittedDelta (будет помещен в переменную-член delta), а также префикс для генерируемых объектов на графике.

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: удалить ресурсы
   }
   ...
};
   
static const string DeltaVolumeProfile::prefix = "DVP";
   
DeltaVolumeProfile deltas(TickTypeShowSplittedDelta);

Напомним, что TickType можно менять на значение TRADE_TICKS только для торговых инструментов, по которым доступны реальные объемы. По умолчанию включен режим INFO_TICKS, работоспособный на всех инструментах.

Запрос тиков для конкретного бара делает метод createProfileBar.

   int createProfileBar(const int i)
   {
      MqlTick ticks[];
      const datetime time = iTime(_Symbol_Periodi);
      // prev и next - временные границы бара
      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;
   }

Непосредственный анализ тиков и подсчет объемов выполняется в защищенном методе calcProfile. В нем мы прежде всего узнаем диапазон цен бара и его размер в пикселях.

   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));
      ...

Руководствуясь этой информацией, создаем объект OBJ_BITMAP, выделяем массив для изображения и создаем ресурс. Фон всей картинки — пустой (прозрачный). Каждый объект привязывается верхней средней точкой к цене High своего бара и имеет ширину одного бара.

      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);
      ...

Далее следует подсчет объемов в тиках переданного массива. Количество ценовых уровней равно высоте бара в пикселях (h). Обычно оно меньше, чем диапазон цены в пунктах, и потому пиксели выступают, своего рода, корзинами для подсчета статистики. Если на мелком таймфрейме диапазон пунктов окажется меньше размера в пикселях, гистограмма получится визуально разреженной. Объемы покупок и продаж аккумулируются отдельно в массивах plus и 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]); // вернет Bid или Last
         const int index = (int)((high - p1) / range * (h - 1));
         if(tickType == TRADE_TICKS)
         {
            // если доступны реальные объемы, можем учитывать их
            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((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;
            }
         }
         ...

Для нормирования гистограммы попутно находим максимальное значение.

         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]);
         }
      }
      ...

Наконец, полученная статистика выводится в графический буфер data и отправляется в ресурс. Объемы покупки выводятся синим цветом, продажи — красным, а если включен нетто-режим, то сумма — зеленым.

      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);
   }

Теперь мы можем вернуться к функции RequestData: её задача — вызвать метод createProfileBar и обработать ошибки (если они возникли).

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); // запрос на обновление графика
      EventChartCustom(0TRY_AGAINbcount + 1NULL);
   }
   Comment("");
}

Единственная стратегия обработки ошибок заключается в том, чтобы попробовать запросить тики еще раз, потому что они могли не успеть загрузиться. Для этой цели функция посылает графику пользовательское сообщение TRY_AGAIN и сама же обрабатывает его.

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");
      }
   }
}

Мы повторяем этот процесс не более 5 раз, потому что история тиков может иметь ограниченную глубину, и нагружать компьютер зря не имеет смысла.

Дополнительно класс DeltaVolumeProfile снабжен механизмом для обработки сообщения CHARTEVENT_CHART_CHANGE, чтобы перерисовать уже имеющиеся объекты в случае изменения размеров или масштаба графика. Подробности можно выяснить в исходном коде.

Результат работы индикатора показан на следующем изображении.

Отображение побаровых гистограмм раздельных объемов в графических ресурсах

Отображение побаровых гистограмм раздельных объемов в графических ресурсах

Учтите, что сразу после нанесения индикатора гистограммы не отображаются: вам следует щелкнуть на баре, чтобы для него рассчиталась гистограмма.