Отслеживание формирования баров

Рассмотренный в предыдущем разделе индикатор IndUnityPercent.mq5 пересчитывается на последнем баре на каждом тике, так как использует цены Close. Некоторые индикаторы и эксперты специально разрабатываются в более экономичном стиле — с однократным расчетом на каждом баре. Например, мы могли бы считать формулу Unity по ценам открытия, и тогда разумно пропускать тики. Способов определения появления нового бара можно придумать несколько:

  • Запоминать время текущего 0-го бара (через параметр time функции OnCalculatetime[0] или в общем случае iTime(symbol, period, 0)) и ждать, когда оно изменится;
  • Запоминать количество баров rates_total (или iBars(symbol, period)) и реагировать на увеличение на 1 (изменение на другое количество в ту или иную сторону подозрительно и может свидетельствовать о модификации истории);
  • Ждать бара с тиковым объемом равным 1 (первый тик на баре).

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

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

В данном разделе мы представим простой класс MultiSymbolMonitor (см. файл MultiSymbolMonitor.mqh) для отслеживания формирования новых баров по заданном списку символов.

В конструктор класса можно передать требуемый таймфрейм. По умолчанию контролируется таймфрейм текущего графика, на котором запущена программа.

class MultiSymbolMonitor
{
protected:
   ENUM_TIMEFRAMES period;
   
public:
   MultiSymbolMonitor(): period(_Period) {}
   MultiSymbolMonitor(const ENUM_TIMEFRAMES p): period(p) {}
   ...

Для хранения списка контролируемых символов воспользуемся вспомогательным классом MapArray из предыдущего раздела. В этот массив будем записывать пары [имя символа;временная метка последнего бара], то есть шаблонных типов <string,datetime>. Для заполнения массива предусмотрен метод attach.

protected:
   MapArray<string,datetimelastTime;
...
public:
   void attach(const string symbol)
   {
      lastTime.put(symbolNULL);
   }

Для заданного массива класс умеет обновлять и проверять метки времени в методе check, вызывая функцию iTime в цикле по символам.

   ulong check(const bool refresh = false)
   {
      ulong flags = 0;
      for(int i = 0i < lastTime.getSize(); i++)
      {
         const string symbol = lastTime.getKey(i);
         const datetime dt = iTime(symbolperiod0);
        
         if(dt != lastTime[symbol]) // есть ли изменения?
         {
            flags |= 1 << i;
         }
         
         if(refresh// обновить метку времени
         {
            lastTime.put(symboldt);
         }
      }
      return flags;
   }

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

Оперативный мониторинг тиков с нескольких инструментов мы рассмотрим в главе про интерактивные события на графиках (см. индикатор-шпион EventTickSpy.mq5 в разделе Генерация пользовательских событий).

Сейчас же ограничимся проверками баров с доступной точностью и вернемся к рассмотрению метода check.

Каждый момент времени характеризуется собственным состоянием набора временных меток по всем символам в массиве. Например, в 12:00 может сформироваться новый бар только по самому ликвидному инструменту, а для нескольких других инструментов тики появятся через несколько миллисекунд или даже секунд. В этот промежуток в массиве обновится один элемент, а остальные будут старыми. Затем постепенно все символы получат бары 12:00.

Для всех символов, по которым время открытия последнего бара не равно сохраненному, метод взводит бит под номером символа, формируя таким образом битовую маску с изменениями. В списке должно быть не более 64 символов.

Если возвращаемое значение равно нулю — изменений не зафиксировано.

Параметр refresh задает, будет ли метод check просто фиксировать изменения (false) или произведет обновление состояния в соответствии с текущей рыночной ситуацией (true).

Метод describe позволяет по битовой маске получить список изменившихся символов.

   string describe(ulong flags = 0)
   {
      string message = "";
      if(flags == 0flags = check();
      for(int i = 0i < lastTime.getSize(); i++)
      {
         if((flags & (1 << i)) != 0)
         {
            message += lastTime.getKey(i) + "\t";
         }
      }
      return message;
   }

Наконец, метод inSync пригодится для определения того, имеют ли все символы в массиве одинаковое время последнего бара. Его имеет смысл применять только для корзины валют с одинаковыми торговыми сессиями.

   bool inSync() const
   {
      if(lastTime.getSize() == 0return false;
      const datetime first = lastTime[0];
      for(int i = 1i < lastTime.getSize(); i++)
      {
         if(first != lastTime[i]) return false;
      }
      return true;
   }

С помощью описанного класса реализуем простой мультивалютный индикатор IndMultiSymbolMonitor.mq5, единственная задача которого будет в детектировании новых баров по списку символов.

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

#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0

Список инструментов задается в соответствующей входной переменной и затем преобразуется в массив, регистрируемый в объекте monitor.

input string Instruments = "EURUSD,GBPUSD,USDCHF,USDJPY,AUDUSD,USDCAD,NZDUSD";
   
#include <MQL5Book/MultiSymbolMonitor.mqh>
   
MultiSymbolMonitor monitor;
   
void OnInit()
{
   string symbols[];
   const int n = StringSplit(Instruments, ',', symbols);
   for(int i = 0i < n; ++i)
   {
      monitor.attach(symbols[i]);
   }
}

Обрабочик OnCalculate вызывает монитор на тиках и выводит изменения состояния в журнал.

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
{
   const ulong changes = monitor.check(true);
   if(changes != 0)
   {
      Print("New bar(s) on: "monitor.describe(changes),
         ", in-sync:"monitor.inSync());
   }
   return rates_total;
}

Чтобы проверить данный индикатор нам потребовалось бы провести за терминалом много времени онлайн. Однако MetaTrader 5 позволяет сделать это намного проще — с помощью тестера. Сделаем это в следующем разделе.