Написание биржевых индикаторов с контролем объема на примере индикатора дельты

Tapochun | 26 июля, 2018

Содержание

Введение

Как известно, терминал MetaTrader 5 транслирует два вида объемов:

В терминале реальный объем обозначается просто: "Объем". Он и будет нам интересен, ведь с появлением в терминале доступа к тиковой истории и ленте сделок также появилась возможность писать биржевые индикаторы. С помощью них можно увидеть, что творится "за кулисами", т.е. узнать, из чего состоит реальный объем: сделки какого объема и с какой частотой происходили в тот или иной момент времени, кого было больше в конкретный интервал времени — продавцов или покупателей. Значит, сейчас можно разложить объем на составляющие. Эти данные могут значительно улучшить точность ваших торговых прогнозов. Вместе с тем, корректно написать такой индикатор несколько сложнее, чем обычный. В этой статье будет подробно изложена последовательность и тонкости написания биржевых индикаторов, особенности их работы и тестирования. В качестве примера будет написан индикатор дельты (разности) объемов покупки и продажи, формирующих реальный объем. По мере создания индикатора также будут описываться правила работы с тиковым потоком.

Однако нужно помнить, что реальный объем доступен только на централизованных (биржевых) рынках. То есть он априори недоступен для рынка Форекс, т.к. это внебиржевой рынок. Мы рассмотрим реальные объемы на примере срочной секции Московской биржи (ФОРТС). Если до этого момента вы не были знакомы с ФОРТС, настоятельно рекомендую ознакомиться со статьей про  биржевое ценообразование.

Для кого эта статья

В последнее время на форуме mql5.com часто задаются вопросы по тиковым данным. Функционал этот относительно новый и постоянно улучшается. Эта статья, в первую очередь, для программистов, уже умеющих писать индикаторы и желающих повысить свой навык написания программ под MetaTrader 5. Также статья будет интересна трейдерам, которые хотят освоить биржевой рынок и заинтересованы анализом с использованием индикатора дельты и/или подобных тиковых индикаторов.

1. Подготовка. Выбор сервера

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

Понять, обновлен сервер или нет, нам поможет стакан цен. Чтобы открыть его, нужно нажать на кнопку в виде таблицы рядом с названием инструмента в левом верхнем углу экрана (если не отображается, проверьте в свойствах графика (F8) установлен ли флажок напротив параметра "Показывать кнопки быстрой торговли" вкладки "Показывать") либо использовать сочетание горячих клавиш Alt+B. Открыв стакан, нужно нажать на кнопку "Показ таблицы всех сделок". Также, кликнув правой клавишей по таблице, проверьте, чтобы не был установлен фильтр на минимальный объем.

Если сервер не обновлен, он будет транслировать в стакане так называемые сделки "неопределенного направления". Разберем подробнее, что это такое. У каждой сделки есть инициатор: покупатель или продавец. Соответственно, свойство сделки "покупка" или "продажа" должно быть явно в ней указано. Если же сделка имеет неопределенное направление (маркируется N/A в стакане), это повлияет на точность построения дельты (разности объемов покупок и продаж), которую и будет рассчитывать наш индикатор. Обновленный и не обновленный вид стакана представлены ниже (рис. 1):

  

Рис.1. Обновленный (слева) и старый (справа) вид стакана.

Правило №1. Проверить, обновлен ли сервер.

Также настоятельно рекомендую выбирать сервер с низкой задержкой (ping). Чем он ниже, тем быстрее ваш терминал сможет обмениваться информацией с сервером брокера. Если внимательно посмотреть на рис. 1, платформа MetaTrader 5 транслирует сделки с миллисекундной точностью, так что чем меньше пинг, тем быстрее вы получите информацию о сделках и тем скорее сможете ее обработать. Проверить пинг до текущего сервера (и изменить сервер при необходимости) можно в правом нижнем углу окна терминала:

Рис. 2. Задержка до выбранного сервера 30.58 миллисекунд.

Также необходимо отметить, что клиентский терминал должен быть обновлен до билда 1881 или более поздней версии. К этому билду все известные на данный момент ошибки с тиковыми данными были исправлены.

2. Методы получения тиковой истории. Формат MqlTick

Итак, мы выбрали сервер, который предоставит корректную тиковую историю. Теперь встает вопрос: как же эту историю получить? В языке MQL5 для этого введены две функции:

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

Для наших целей нам понадобится поток именно торговых тиков (COPY_TICKS_TRADE). Подробнее о типе тиков можно почитать в описании функции CopyTicks.

В структуре MqlTick мы будем анализировать значения следующих полей:

Если совсем упростить, то наш индикатор будет делать следующее: получать все торговые тики за свечу, смотреть, какой был объем покупок и какой был объем продаж, и отображать разницу этих объемов (дельту) в виде гистограммы. Если на свече было больше покупателей, столбик гистограммы будет синим. Если было больше продавцов - красным. Все просто!

3. Первый запуск. Расчет истории

Основным объектом для работы с тиками в нашем индикаторе будет CTicks _ticks (файл Ticks_article.mqh). Через него мы будем выполнять все операции с тиками.

Работа индикатора будет разделена на два основных блока: блок расчета истории и блок расчета в реальном времени

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//--- Проверка на первый запуск
   if(prev_calculated>0)                    // Если не первый запуск
     {
      // Блок №2
     }
   else                                     // Если первый запуск
     {
      // Блок №1
     }
//---
   return( rates_total );
  }

При первом запуске индикатора или нажатии кнопки "обновить" в терминале (Блок №1) мы должны рассчитать индикатор на истории. Изначально я планировал сделать универсальную расчетную функцию, которая бы использовалась как при расчете истории, так и при расчетах в реальном времени. Однако для простоты и повышения скорости расчетов в конечном итоге было решено сделать иначе. Сначала происходит расчет истории по сформированным барам (CalculateHistoryBars()), а затем считается текущий бар (CalculateCurrentBar()). Все эти действия описаны ниже:

//--- 1. Инициализируем индикаторные буферы начальными значениями
BuffersInitialize(EMPTY_VALUE);
//--- 2. Сбрасываем значения параметров повторного контроля
_repeatedControl=false;
_controlNum=WRONG_VALUE;
//--- 3. Сбрасываем время бара, в который будут записываться тики (нажатие кнопки "обновить")
_ticks.SetTime(0);
//--- 4. Устанавливаем момент начала загрузки тиков сформированных баров
_ticks.SetFrom(inpHistoryDate);
//--- 5. Проверяем момент начала загрузки
if(_ticks.GetFrom()<=0)                 // Если момент не установлен
   return(0);                           // Выходим
//--- 6. Устанавливаем момент конца загрузки истории сформированных баров
_ticks.SetTo( long( time[ rates_total-1 ]*MS_KOEF - 1 ) );
//--- 7. Загружаем историю по сформированным барам
if(!_ticks.GetTicksRange())             // Если неудачно
   return(0);                           // Выходим с ошибкой
//--- 8. Расчет истории на сформированных барах
CalculateHistoryBars( rates_total, time, volume );
//--- 9. Сбрасываем время бара, в который будут записываться тики
_ticks.SetTime(0);
//--- 10. Устанавливаем момент начала загрузки тиков последнего бара
_ticks.SetFrom( long( time[ rates_total-1 ]*MS_KOEF ) );
//--- 11. Устанавливаем момент конца загрузки тиков последнего бара
_ticks.SetTo( long( TimeCurrent()*MS_KOEF ) );
//--- 12. Загружаем историю текущего бара
if(!_ticks.GetTicksRange())             // Если неудачно
   return(0);                           // Выходим с ошибкой
//--- 13. Сбрасываем момент конца копирования
_ticks.SetTo( ULONG_MAX );
//--- 14. Запоминаем время последнего тика полученной истории
_ticks.SetFrom();
//--- 15. Расчет текущего бара
CalculateCurrentBar( true, rates_total, time, volume );
//--- 16. Устанавливаем количество тиков для последующего копирования в реальном времени
_ticks.SetCount(4000);

Код индикатора достаточно хорошо закомментирован, так что остановлюсь лишь на основных моментах.

Пункт 3. "Сбрасываем время бара, в который пишутся тики". Объект, работающий с тиками, содержит время открытия свечи, в которую записываются соответствующие ей тики. При нажатии кнопки "Обновить" в терминале происходит пересчет индикатора с самого начала. Соответственно, чтобы тики корректно записались в нужную свечу, нужно сбросить ее время.

Пункт 4. "Устанавливаем момент начала загрузки тиков сформированных баров". Получение тиковой истории может быть достаточно затратной по времени операцией. Поэтому нужно дать возможность пользователю самостоятельно указать дату начала ее загрузки. В индикаторе для этого представлен параметр inpHistoryDate. При нулевом значении происходит загрузка истории с начала текущего дня. В данном прототипе метода SetFrom(datetime) время передается в секундах. Как уже было сказано выше, сначала идет расчет сформированных баров индикатора.

Пункт 5. "Проверяем момент начала загрузки на корректность". Проверка значения, полученного в пункте 4.

Пункт 6. "Устанавливаем момент конца загрузки истории сформированных баров". Моментом конца загрузки истории сформированных баров служит предыдущая миллисекунда до открытия текущей  (rates_total-1) свечи. В данном случае момент конца загрузки имеет тип long. При передаче параметра в метод нужно явно указывать, что передается тип long в случае, если в классе также есть метод, в который параметр передается с типом datetime. В случае с методом SetTo() в классе не реализована его перегрузка с аргументом типа datetime, однако лучше подстраховаться и явно передать параметр типа long.

Пункт 7. "Загружаем историю по сформированным барам". Получение истории происходит с помощью функции GetTicksRange(), которая является оберткой для функции CopyTicksRange() с добавлением проверок на возможные ошибки. И если при загрузке ошибки возникают, на следующем тике повторно запрашивается вся история. Подробнее с этой и другими функциями для работы с тиками вы можете ознакомиться в файле Ticks_article.mqh в приложении. 

Пункт 8. "Расчет истории на сформированных барах". Подробнее расчет на сформированных барах будет описан в соответствующем параграфе статьи.

Пункты 9-12. Расчет по барам завершен. Нужно рассчитать текущую свечу. Здесь устанавливается диапазон для копирования и происходит получение тиков текущей свечи.

Пункт 13. "Сбрасываем момент конца копирования". В дальнейшем мы продолжим получать тики с помощью объекта _ticks, но получать мы их будем не с одного момента до другого, как делали это при загрузке истории, а с момента прихода последнего тика до конца всей доступной истории. Именно поэтому лучше скинуть момент конца копирования — при расчете в реальном времени он нам больше не понадобится.

Пункт 14. "Запоминаем время последнего тика полученной истории". Время последнего тика полученной истории нам понадобится в будущем как момент начала копирования данных в реальном времени.

Пункт 15. "Расчет текущего бара". Расчет текущего бара также будет описан в отдельной части статьи и будет иметь важные отличия от метода расчета сформированных баров.

Пункт 16. "Устанавливаем количество тиков для последующего копирования в реальном времени". Если раньше мы получали тики с помощью функции CopyTicksRange(), обернутой в метод GetTicksRange(), то в реальном времени мы будем использовать функцию CopyTicks(), обернутую в метод GetTicks(). Метод SetCount() устанавливает количество тиков для последующих запросов. 4000 выбрано потому, что терминал хранит 4096 тиков по каждому символу для быстрого доступа и запросы к этим тикам выполняются быстрее всего. А установка значения что 100, что 4000 никак не влияет на скорость получения тиков (~1 мс).

Итак, остановимся подробнее на расчетных функциях.

4. Функция расчета истории по сформированным барам

Сама функция выглядит следующим образом:

//+------------------------------------------------------------------+
//| Функция расчета сформированных баров истории                     |
//+------------------------------------------------------------------+
bool CalculateHistoryBars(const int rates_total,    // Количество просчитанных баров
                          const datetime& time[],   // Массив времен открытия баров 
                          const long& volume[]      // Массив значений реального объема
                          )
  {
//--- Суммарные объемы
   long sumVolBuy=0;
   long sumVolSell=0;
//--- Номер бара для записи в буфер
   int bNum=WRONG_VALUE;
//--- Получаем количество тиков в массиве
   const int limit=_ticks.GetSize();
//--- Цикл по всем тикам
   for(int i=0; i<limit && !IsStopped(); i++)
     {
      //--- Определяем свечу, в которую пишутся тики
      if(_ticks.IsNewCandle(i))                         // Если начало формирования следующей свечи
        {
         //--- Проверяем, записан ли номер сформированной (завершенной) свечи
         if(bNum>=0) // Если номер записан
           {
            //--- Проверяем, записаны ли значения объемов
            if(sumVolBuy>0 || sumVolSell>0) // Если все параметры записаны
              {
               //--- Проводим контроль суммарного объема свечи
               VolumeControl(false,bNum,volume[bNum],time[bNum],sumVolBuy,sumVolSell);
              }
            //--- Заносим значения в буферы
            DisplayValues(bNum,sumVolBuy,sumVolSell,__LINE__);
           }
         //--- Сбрасываем объемы предыдущей свечи
         sumVolBuy=0;
         sumVolSell=0;
         //--- Устанавливаем номер свечи, соотв. времени ее открытия
         bNum=_ticks.GetNumByTime(false);
         //--- Проверяем номер на корректность
         if(bNum>=rates_total || bNum<0) // Если номер некорректный     
           {
            //--- Выходим без расчета истории
            return( false );
           }
        }
      //--- Добавляем объем на тике к нужному компоненту
      AddVolToSum(_ticks.GetTick(i),sumVolBuy,sumVolSell);
     }
//--- Проверяем, записаны ли значения объемов последней сформированной свечи
   if(sumVolBuy>0 || sumVolSell>0) // Если все параметры записаны
     {
      //--- Проводим контроль суммарного объема свечи
      VolumeControl(false,bNum,volume[bNum],time[bNum],sumVolBuy,sumVolSell);
     }
//--- Заносим значения в буферы
   DisplayValues(bNum,sumVolBuy,sumVolSell,__LINE__);
//--- Расчет завершен
   return( true );
  }

Суть этой функции — рассортировать полученные тики по сформированным свечам текущего таймфрейма, получить разность объемов покупок и продаж каждой свечи и занести полученные значения объемов и дельт в буферы индикатора.

Как было упомянуто ранее, изначально планировалось сделать одну общую функцию для расчета истории и для расчетов в реальном времени. Однако добавив функцию расчета истории по сформированным барам, мы преследуем несколько целей:

Полное описание алгоритма расчета и особенностей работы с тиковой историей будут приведены далее.

5. Функция расчета текущей свечи

В коде индикатора на функцию CalculateCurrentBar() нужно обратить наибольшее внимание.

//+------------------------------------------------------------------+
//| Функция расчета текущей свечи                                    |
//+------------------------------------------------------------------+
void CalculateCurrentBar(const bool firstLaunch,   // Флаг первого запуска функции
                         const int rates_total,    // Количество просчитанных баров
                         const datetime& time[],   // Массив времен открытия баров 
                         const long& volume[]      // Массив значений реального объема
                         )
  {
//--- Суммарные объемы
   static long sumVolBuy=0;
   static long sumVolSell=0;
//--- Номер бара для записи в буфер
   static int bNum=WRONG_VALUE;
//--- Проверяем флаг первого запуска
   if(firstLaunch)                                 // Если первый запуск
     {
      //--- Сбрасываем статические параметры
      sumVolBuy=0;
      sumVolSell=0;
      bNum=WRONG_VALUE;
     }
//--- Получаем номер предпоследнего тика в массиве
   const int limit=_ticks.GetSize()-1;
//--- Время тика limit
   const ulong limitTime=_ticks.GetFrom();
//--- Цикл по всем тикам (исключая последний)
   for(int i=0; i<limit && !IsStopped(); i++)
     {
      //--- 1. Сравниваем время i тика со временем limit-тика (проверка завершения цикла)
      if( _ticks.GetTickTimeMs( i ) == limitTime ) // Если время тика равно времени предельного тика
         return;                                   // Выходим
      //--- 2. Проверяем, начала ли формироваться свеча, отсутствующая на графике
      if(_ticks.GetTickTime(i)>=time[rates_total-1]+PeriodSeconds())                // Если свеча начала формироваться
        {
         //--- Проверяем ведение лога
         if(inpLog)
            Print(__FUNCTION__,": ВНИМАНИЕ! Тик будущего ["+GetMsToStringTime(_ticks.GetTickTimeMs(i))+"]. Время тика "+TimeToString(_ticks.GetTickTime(i))+
                  ", time[ rates_total-1 ]+PerSec() = "+TimeToString(time[rates_total-1]+PeriodSeconds()));
         //--- 2.1. Устанавливаем (корректируем) время следующего запроса тиков
         _ticks.SetFrom(_ticks.GetTickTimeMs(i));
         //--- Выходим
         return;
        }
      //--- 3. Определяем свечу, в которую пишутся тики
      if(_ticks.IsNewCandle(i))                    // Если начало формирования следующей свечи
        {
         //--- 3.1. Проверяем, записан ли номер сформированной (завершенной) свечи
         if(bNum>=0)                               // Если номер записан
           {
            //--- Проверяем, записаны ли значения объемов
            if(sumVolBuy>0 || sumVolSell>0)        // Если все параметры записаны
              {
               //--- 3.1.1. Проводим контроль суммарного объема свечи
               VolumeControl(true,bNum,volume[bNum],time[bNum],sumVolBuy,sumVolSell);
              }
           }
         //--- 3.2. Сбрасываем объемы предыдущей свечи
         sumVolBuy=0;
         sumVolSell=0;
         //--- 3.3. Запоминаем номер текущей свечи
         bNum=rates_total-1;
        }
      //--- 4. Добавляем объем на тике к нужному компоненту
      AddVolToSum(_ticks.GetTick(i),sumVolBuy,sumVolSell);
      //--- 5. Заносим значения в буферы
      DisplayValues(bNum,sumVolBuy,sumVolSell,__LINE__);
     }
  }

Она похожа на предыдущую функцию CalculateHistoryBars(), но у нее есть свои особенности. Разберем их подробнее. Ниже представлен прототип функции:

//+------------------------------------------------------------------+
//| Функция расчета текущей свечи                                    |
//+------------------------------------------------------------------+
void CalculateCurrentBar(const bool firstLaunch,   // Флаг первого запуска функции
                         const int rates_total,    // Количество просчитанных баров
                         const datetime& time[],   // Массив времен открытия баров 
                         const long& volume[]      // Массив значений реального объема
                         )

Сразу отметим, что CalculateCurrentBar() будет использоваться в двух случаях: при расчете истории текущей свечи на первом запуске и при расчетах в реальном времени. За выбор режима расчета будет отвечать флаг firstLaunch. Разница между режимами будет лишь в том, что при первом запуске будут сбрасываться к нулевому значению статические переменные, содержащие суммы объемов покупок и продаж, а также номер свечи в буферах, в которые будут записаны эти суммы и их разность - дельта. И еще раз сделаю акцент на том, что в индикаторе используются только реальные объемы!

//--- Суммарные объемы
   static long sumVolBuy=0;
   static long sumVolSell=0;
//--- Номер бара для записи в буфер
   static int bNum=WRONG_VALUE;
//--- Проверяем флаг первого запуска
   if(firstLaunch)                                 // Если первый запуск
     {
      //--- Обнуляем значения сумм объемов
      sumVolBuy=0;
      sumVolSell=0;
      //--- Сбрасываем номер свечи
      bNum=WRONG_VALUE;
     }

После объявления статических переменных идет получение номера и времени последнего тика в массиве:

//--- Получаем номер последнего тика в массиве
   const int limit=_ticks.GetSize()-1;
//--- Время тика limit
   const ulong limitTime=_ticks.GetFrom();

Номер будет являться ограничителем цикла перебора тиков. Установим условие, что последний тик и тики с временем, как у последнего, не будут участвовать в расчетах. Но почему? Все дело в том, что торговые тики могут приходить пачками в случае, если один рыночный ордер был залит несколькими лимитными ордерами разных контрагентов. Пачка тиков (сделок) - это сделки, совершенные в один момент времени (с точностью до миллисекунд) и имеющие один и тот же тип (покупка или продажа) (рис. 3). Для справки нужно отметить, что несколько пачек тиков могут быть отображены в терминале как пришедшие в одну миллисекунду, т.к. биржа транслирует сделки с точностью до наносекунд. В этом можно убедиться, запустив скрипт test_getTicksRange из приложения к статье.

Рис. 3. Пачка тиков (рыночный ордер на покупку инициировал 4 сделки на 26 лотов).

Чтобы корректно учесть объем, нужно одну пачку тиков просчитать за один раз и только тогда, когда она полностью будет передана в терминал, т.е. тогда, когда будет доступна сделка, заключенная в последующий момент времени (рис.4).


Рис. 4. Сделка в момент .373, момент расчета пачки .334.

Да, именно так. Мы не можем рассчитывать на то, что пачка пришла в терминал полностью до того момента, пока не доступна следующая за пачкой сделка, т.к. пачка в терминал может быть передана по частям. Сейчас подробнее не буду на этом останавливаться, просто поверьте мне на слово. Из этого можно сформулировать правило №2:

Правило №2. Расчет пачки тиков следует производить только после получения следующего за этой пачкой тика.

В п. 13 алгоритма первого запуска мы сохранили время последнего полученного тика. Теперь воспользуемся им, записав в переменную limitTime.

Далее перейдем непосредственно к циклу расчета тиков:

//--- 1. Сравниваем время i тика со временем limit-тика (проверка завершения цикла)
      if( _ticks.GetTickTimeMs( i ) == limitTime ) // Если время тика равно времени предельного тика
         return;                                   // Выходим

Пункт 1. "Сравниваем время тика со временем последнего тика". Как было сказано выше, последний тик не учитывается в расчетах, т.к. расчет идет только по сформированным пачкам тиков. Но также мы знаем, что последняя пачка тиков могла быть скопирована не полностью. Так что мы должны исключить из расчета не только последний полученный тик, но и все тики его пачки (если такие были).

//--- 2. Проверяем, начала ли формироваться свеча, отсутствующая на графике
      if(_ticks.GetTickTime(i)>=time[rates_total-1]+PeriodSeconds())

Пункт 2. "Проверяем, начала ли формироваться свеча, отсутствующая на графике". Понимаю, звучит немного странно. Как может начать формироваться свеча, отсутствующая на графике, спросите вы. Чтобы ответить на этот вопрос, нужно разобраться с особенностью обработки/получения тиковых данных в терминале. Они были выяснены при продолжительном общении с разработчиками в сервисдеске. Приведу их вольный пересказ: 

Тики в терминале собираются отдельным потоком независимо от работы индикаторов или экспертов. А свечи строятся в другом потоке — в потоке выполнения индикаторов. Эти потоки между собой никак не синхронизированы. После того как тик применен к свече, происходит расчет индикатора. При этом ни один тик не пропускается. Из этого следует, что вызывая функцию CopyTicks(), можно получить более свежие тиковые данные, чем те, что уже применены к барам.

На практике это будет означать следующее. При расчете свечи rates_total-1 индикатор может получить тики следующей свечи, которая еще не сформирована (тик к ней еще не был применен). Чтобы избежать такой ситуации (и избежать ошибки выхода за пределы массива), нужно добавить эту проверку.

Правило 3. Нужно учесть, что могут быть получены тики, еще не появившейся на графике свечи.

В случае если тик(-и) "будущего" (свеча под тики еще не сформирована) были обнаружены , мы должны переписать момент времени, с которого будет происходить следующий запрос тиков (пункт 2.1.). И, конечно же, сразу выйти из цикла и функции, ожидая прихода нового тика и формирования новой свечи на графике:

//--- 2. Проверяем, начала ли формироваться свеча, отсутствующая на графике
      if(_ticks.GetTickTime(i)>=time[rates_total-1]+PeriodSeconds())                // Если свеча начала формироваться
        {
         //--- 2.1. Устанавливаем (корректируем) время следующего запроса тиков
         _ticks.SetFrom(_ticks.GetTickTimeMs(i));
         //--- Выходим
         return;
        }

Дальнейший алгоритм практически полностью совпадает с функцией CalculateHistoryBars(). Рассмотрим его подробнее.

//--- 3. Определяем свечу, в которую пишутся тики
      if(_ticks.IsNewCandle(i))

Пункт 3. Определяем свечу, в которую пишутся тики. Здесь сравнивается время i тика и время открытия свечи, в которую пишутся тики. Если время i тика выходит за пределы свечи, время открытия свечи изменяется и срабатывает алгоритм подготовки к анализу следующей свечи:

//--- 3. Определяем свечу, в которую пишутся тики
      if(_ticks.IsNewCandle(i))                    // Если начало формирования следующей свечи
        {
         //--- 3.1. Проверяем, записан ли номер сформированной (завершенной) свечи
         if(bNum>=0)                               // Если номер записан
           {
            //--- Проверяем, записаны ли значения объемов
            if(sumVolBuy>0 || sumVolSell>0)        // Если все параметры записаны
              {
               //--- 3.1.1. Проводим контроль суммарного объема свечи
               VolumeControl(true,bNum,volume[bNum],time[bNum],sumVolBuy,sumVolSell);
              }
           }
         //--- 3.2. Сбрасываем объемы предыдущей свечи
         sumVolBuy=0;
         sumVolSell=0;
         //--- 3.3. Запоминаем номер текущей свечи
         bNum=rates_total-1;
        }

Пункт 3.1. Проверяем, записан ли номер сформированной свечи. В режиме расчета истории (первый запуск) эта проверка предотвратит доступ к массивам времени и объема под некорректным индексом (-1). Далее идет проверка того, были ли на свече заключены сделки. Если сделок не было, то и контроль объема не требуется.

Пункт 3.1.1. Проводим контроль суммарного объема. В процедуре VolumeControl() происходит сложение объемов Buy и Sell, накопленных нашим индикатором за свечу, и их сравнение с "контрольным" объемом, т.е. реальным объемом, непосредственно переданным с биржи (значением из массива Volume[] сформированной свечи). Если объем с биржи совпадает с накопленным объемом, переходим к дальнейшим расчетам. Если же объемы не совпадают... стоп. Как могут не совпасть объемы, спросите вы? Это же один и тот же суммарный объем. Просто один посчитали мы в нашем индикаторе, а другой пришел с биржи. Объемы должны совпасть! 

Все верно, объемы должны совпасть. И это правило должно выполняться для всех свечей. Ведь что делает наш индикатор:

Вроде бы ничего сложного. Но в реальном времени (для свечи rates_total-1) нужно помнить о тех самых тиках "будущего" (правило 3), когда пришел тик новой свечи, а на графике свеча еще не сформировалась. Эта особенность накладывает свой отпечаток и на контроль объема! В этот момент обработки тика индикатор будет содержать еще не обновленное значение объема в массиве volume[] (значение еще изменится). Соответственно, мы не сможем корректно провести сравнение собранного индикатором объема и объема из массива volume[]. На практике контрольный объем volume[rates_total-1] иногда не будет совпадать с суммой объемов (sumVolBuy+sumVolSell), собранных индикатором. В этом случае в процедуре VolumeControl() приведены два варианта решения этой проблемы:

  1. Пересчет объема свечи и сравнение его с контрольным значением, полученным через функцию CopyRealVolume();
  2. В случае если первый вариант не решил проблему, производится установка флага контроля объема в момент образования новой свечи.

Соответственно, первый способ пытается решить проблему контроля до формирования новой свечи, а второй гарантированно должен решить проблему после формирования новой свечи.

Пункт 3.2. "Сбрасываем объемы предыдущей свечи". После того как образовалась новая свеча, сбрасываем счетчики объемов в ноль.

Пункт 3.3. "Запоминаем номер текущей свечи". Еще один плюс разделения расчетных функций на функцию расчета по сформированным барам и функцию расчета текущей свечи. Номер текущей свечи всегда = rates_total-1.

//--- 4. Добавляем объем на тике к нужному компоненту
      AddVolToSum(_ticks.GetTick(i),sumVolBuy,sumVolSell);

Пункт 4. Добавляем объем тика к суммарному объему. Сначала по флагу анализируемого тика выясняем, какие данные изменились:

//+------------------------------------------------------------------+
//| Добавляем объем тика к суммарному объему                         |
//+------------------------------------------------------------------+
void AddVolToSum(const MqlTick &tick,        // Параметры проверяемого тика
                 long& sumVolBuy,            // Суммарный объем покупок (out)
                 long& sumVolSell            // Суммарный объем продаж (out)
                )
  {
//--- Проверяем направление тика
   if(( tick.flags&TICK_FLAG_BUY)==TICK_FLAG_BUY && ( tick.flags&TICK_FLAG_SELL)==TICK_FLAG_SELL) // Если тик обоих направлений
        Print(__FUNCTION__,": ОШИБКА! Тик '"+GetMsToStringTime(tick.time_msc)+"' неизвестного направления!");
   else if(( tick.flags&TICK_FLAG_BUY)==TICK_FLAG_BUY)   // Если тик на покупку
        sumVolBuy+=(long)tick.volume;
   else if(( tick.flags&TICK_FLAG_SELL)==TICK_FLAG_SELL) // Если тик на продажу
        sumVolSell+=(long)tick.volume;
   else                                                  // Если тик не торговый
        Print(__FUNCTION__,": ОШИБКА! Тик '"+GetMsToStringTime(tick.time_msc)+"' не торговый!");
  }

Здесь еще раз хочу акцентировать внимание на Правило 1. Если работа происходит на сервере, который транслирует сделки неизвестного направления, невозможно будет определить, кто стал инициатором сделки — покупатель или продавец. И в журнале будут соответствующие ошибки. Если инициатор определен, происходит добавление объема к суммарному объему покупок или продаж. Если же флаг не содержит данных об инициаторе сделки, также будет получена ошибка.

//--- 5. Заносим значения в буферы
      DisplayValues(bNum,sumVolBuy,sumVolSell,__LINE__);

Пункт 5. Заносим значения в буферы. В процедуре DisplayValues() происходит контроль индекса индикаторных буферов (для этого мы передаем в функцию номер строки вызова), расчет дельты и запись в буферы дельты, объемов покупок и продаж.

6. Расчет в реальном времени

Опишем алгоритм блока расчетов в реальном времени:

//--- 1. Проверяем образование нового бара
if(rates_total>prev_calculated) // Если новый бар
  {
   //--- Инициализируем индексы буферов rates_total-1 пустыми значениями
   BuffersIndexInitialize(rates_total-1,EMPTY_VALUE);
   //--- 2. Проверяем, нужно ли контролировать объем на баре rates_total-2
   if(_repeatedControl && _controlNum==rates_total-2)
     {
      //--- 3. Проводим перепроверку
      RepeatedControl(false,_controlNum,time[_controlNum]);
     }
   //--- 4. Сбрасываем значения перепроверки
   _repeatedControl=false;
   _controlNum=WRONG_VALUE;
  }
//--- 5. Загружаем новые тики
if(!_ticks.GetTicks() )               // Если неудачно
   return( prev_calculated );         // Выходим с ошибкой
//--- 6. Запоминаем время последнего тика полученной истории
_ticks.SetFrom();
//--- 7. Расчет в реальном времени
CalculateCurrentBar(false,rates_total,time,volume);

Пункт 1. Проверяем образование нового бара. Эта проверка очень важна, так как в пункте 2.1.1. мы выяснили, что если в основной расчетной функции в процедуре контроля объемов (расчет в реальном времени) мы этот контроль не прошли, его нужно пройти в момент образования нового бара. И это как раз тот момент!

Пункт 2. Проверяем, нужно ли контролировать объем на баре rates_total-2. Если флаг повторного контроля был установлен и повторный контроль должен быть произведен на свече rates_total-2 (только что завершенной), производим перепроверку (п. 3).

Пункт 3. Производим перепроверку. В процедуре перепроверки, как ранее уже говорилось, производится получение всех тиков за свечу, определение объемов покупок и продаж, вычисление дельты и сравнение сумм объемов с контрольным значением.

Пункт 5. Загружаем новые тики. Получаем тики с момента прихода последнего тика на предыдущем запуске индикатора. При расчете в реальном времени тики получаем при помощи функции GetTicks(), которая использует функцию CopyTicks().

Пункт 6. Запоминаем время последнего тика. Это время последнего тика, полученного в п. 5. или после расчета истории. На следующем запуске индикатора тиковая история будет запрашиваться с этого момента.

Пункт 7. Расчет в реальном времени. Как было ранее сказано, процедура CalculateCurrentBar() используется и при расчете истории, и при расчете в реальном времени. За это отвечает флаг firstLaunch, который в данном случае установлен в значение false.

7. Особенности работы в тестере стратегий

При использовании тестера стратегий всегда нужно помнить, что это отдельная программа со своим функционалом. Даже если тестер может сделать то же самое, что и терминал, это не значит, что он сделает это так же, как терминал. Подобная ситуация (на данном этапе развития тестера) происходит и с программами, использующими тиковые данные. Несмотря на то, что индикатор рассчитывается корректно (контроль объемов проходит успешно), индикатор в тестере рассчитывается несколько иначе. Особенность опять кроется в обработке пачек тиков.

Если в терминале за один тик может произойти несколько сделок (т.е. можно получить пачку тиков), то в тестере каждый тик будет получен отдельно, даже если подряд идет множество тиков одной пачки. В этом можно убедиться, если запустить тестовый индикатор test_tickPack из приложения. Картина будет примерно такая:

2018.07.13 10:00:00   OnCalculate: Получено тиков 4. [0] = 2018.07.13 10:00:00.564, [3] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Получено тиков 2. [0] = 2018.07.13 10:00:00.571, [1] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Получено тиков 3. [0] = 2018.07.13 10:00:00.571, [2] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Получено тиков 4. [0] = 2018.07.13 10:00:00.571, [3] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Получено тиков 5. [0] = 2018.07.13 10:00:00.571, [4] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Получено тиков 6. [0] = 2018.07.13 10:00:00.571, [5] = 2018.07.13 10:00:00.571 FLAG_BUY
2018.07.13 10:00:00   OnCalculate: Получено тиков 7. [0] = 2018.07.13 10:00:00.571, [6] = 2018.07.13 10:00:00.572 FLAG_BUY

Вы можете проделать этот эксперимент сами, обязательно в режиме "Каждый тик на основе реальных тиков", последовательно нажимая F12. Тики будут добавляться строго по одному! Хотя, конечно, в реальности эта пачка могла попасть в терминал как частями, так и целиком, но крайне маловероятно, что по одному. Это и не плохо, и не хорошо, просто надо принять эту особенность как факт и помнить про нее.

Заключение

На этом я заканчиваю описание алгоритма построения индикатора. В нем были описаны те нюансы и сложности, с которыми мне пришлось столкнуться во время написания своих тиковых индикаторов и с которыми часто сталкиваются другие пользователи. Надеюсь, мой опыт будет полезен сообществу, и на его основе будет появляться все больше и больше новых программ, использующих тиковые данные, что даст толчок к дальнейшему развитию платформы MetaTrader 5. Если вы, уважаемые читатели, знаете еще какие-то особенности при работе с торговыми тиками, или если я где-то не прав — пишите, буду рад обсудить эту тему.

Вот такая картина в итоге получилась. Синий столбец — преобладание покупателей на свече, красный — преобладание продавцов.

Если Вас заинтересовал данный индикатор, возможно, Вы захотите ознакомиться с его улучшенной версией.


Рис. 5. Индикатор дельты на инструменте RTS-6.18.

Оценка реальных объемов открывает большие горизонты для анализа биржевого рынка и позволяет лучше понять движения цены. Данный индикатор — лишь малая часть того, что можно создать на основе анализа тиковых данных. Создание биржевых индикаторов на основе реальных объемов — задача вполне выполнимая. И я надеюсь, что эта статья поможет вам в создании подобных индикаторов и улучшит вашу торговлю.

Файлы, используемые в статье

Имя файла Тип Описание
 1. Delta_article.mq5  Файл индикатора  Реализация индикатора дельты
 2. Ticks_article.mqh  Файл класса  Вспомогательный класс для работы с тиковыми данными
 3. test_getTicksRange.mq5  Файл скрипта  Тестовый скрипт для проверки возможности получения нескольких пачек тиков в одну миллисекунду
 4. test_tickPack.mq5  Файл индикатора  Тестовый индикатор для проверки получения тиков в тестере