Синхронизация нескольких графиков по одному инструменту на разных таймфреймах

Dmitriy Gizlyk | 15 марта, 2018

Введение

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

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


1. Постановка задачи

Основная задача нашего индикатора — синхронизация открытых в MetaTrader 5 графиков. При этом программа должна определить нужные графики по инструменту. В то же время, программа должна постоянно отслеживать состояние всех графических объектов на всех интересующих нас графиках. Каждое изменение объекта на одном из графиков программа должна повторить на остальных графиках.

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

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

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

Казалось бы, второй вариант нам идеально подходит. Но и здесь есть небольшая ложка дегтя: функцию OnChartEvent вызывают только события того графика, на котором запущена программа. И это бы нас не останавливало, если б мы определились с Мастер-графиком, на котором проводили бы все изменения. Тогда хватило бы одного экземпляра индикатора. Но мы не хотим быть ограничены одним графиком для изменения объектов. Нам нужно запускать экземпляры индикатора на каждом графике. Эту работу можно выполнить самостоятельно, а можно и автоматизировать, благодаря функции ChartIndicatorAdd.

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

  1. При запуске индикатора открытые графики фильтруются по символу. Проверяется наличие индикатора на открытых графиках соответствующего инструмента. Все объекты текущего графика клонируются на  отобранные графики.
  2. Блок-схема процесса инициализации.

  3. Обработка событий графика. Когда появляется событие создания или изменения графического объекта, программа считывает с графика информацию об измененном объекте и передает эти данные на все графики из ранее созданного списка.

Процесс обработки событий.

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


2. Организация работы с графиками

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

class CCloneIndy
  {
private:
   string            s_Symbol;
   string            s_IndyName;
   string            s_IndyPath;

public:
                     CCloneIndy();
                    ~CCloneIndy();
   bool              SearchCharts(long chart,long &charts[]);

protected:
   bool              AddChartToArray(const long chart,long &charts[]);
   bool              AddIndicator(const long master_chart,const long slave_chart);
  };

При инициализации класса сохраним исходные данные в переменные и зададим короткое имя индикатора. Оно понадобится нам, чтобы получать параметры, необходимые при запуске копий индикатора.

CCloneIndy::CCloneIndy()
  {
   s_Symbol=_Symbol;
   s_IndyName=MQLInfoString(MQL_PROGRAM_NAME);
   s_IndyPath=MQLInfoString(MQL_PROGRAM_PATH);
   int pos=StringFind(s_IndyPath,"\\Indicators\\",0);
   if(pos>=0)
     {
      pos+=12;
      s_IndyPath=StringSubstr(s_IndyPath,pos);
     }
   IndicatorSetString(INDICATOR_SHORTNAME,s_IndyName);
  }

2.1. Функция отбора графиков по инструменту

Рассмотрим детально, как работает функция отбора нужных графиков. Сначала проверим идентификатор мастер-графика, переданного в параметрах функции. Если он недействителен, то функция сразу возвращает false. Если идентификатор не задан, присвоим параметру идентификатор текущего графика. Затем проверим, соответствует ли имя сохраненного инструмента символу мастер-графика. При несоответствии перезапишем наименование инструмента для фильтрации графиков.

bool CCloneIndy::SearchCharts(long chart,long &charts[])
  {
   switch((int)chart)
     {
      case -1:
        return false;
        break;
      case 0:
        chart=ChartID();
        break;
      default:
        if(s_Symbol!=ChartSymbol(chart))
           s_Symbol=ChartSymbol(chart);
        break;
     }

На следующем шаге организовываем перебор всех открытых графиков. Если идентификатор проверяемого графика равен идентификатору мастер-графика, то переходим к следующему.

   long check_chart=ChartFirst();
   while(check_chart!=-1)
     {
      if(check_chart==chart)
        {
         check_chart=ChartNext(check_chart);
         continue;
        }

Затем проверяем, соответствует ли символ анализируемого графика искомому. Если инструмент не удовлетворяет условию поиска, переходим к следующему графику.

      if(ChartSymbol(check_chart)!=s_Symbol)
        {
         check_chart=ChartNext(check_chart);
         continue;
        }

После этого проверяем, есть ли на проверяемом графике наш индикатор. Если он уже прикреплен, то сохраняем идентификатор графика в массив и переходим к следующему.

      int handl=ChartIndicatorGet(check_chart,0,s_IndyName);
      if(handl!=INVALID_HANDLE)
        {
         AddChartToArray(check_chart,charts);
         check_chart=ChartNext(check_chart);
         continue;
        }

Если индикатора еще нет на проверяемом графике, то запускаем функцию его вызова, указав идентификаторы мастер-графика и проверяемого графика. При неудачной операции переходим к следующему графику, а к этому графику попытаемся прикрепить индикатор при следующем вызове функции.

      if(!AddIndicator(chart,check_chart))
        {
         check_chart=ChartNext(check_chart);
         continue;
        }

Если индикатор успешно прикрепился на график, добавляем идентификатор этого графика в массив. При этом будет запущена функция клонирования всех объектов с мастер-графика на проверяемый.

      AddChartToArray(check_chart, charts);
      check_chart=ChartNext(check_chart);
     }
//---
   return true;
  }

По завершении цикла выходим из функции и возвращаем результат true.

2.2. Функция вызова индикатора

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

bool CCloneIndy::AddIndicator(const long master_chart,const long slave_chart)
  {
   if(master_chart<0 || slave_chart<=0 || master_chart==slave_chart)
      return false;

Затем получаем хэндл индикатора на мастер-графике. Если он будет недействительным, то выходим из функции с результатом false.

   int master_handle=ChartIndicatorGet(master_chart,0,s_IndyName);
   if(master_handle==INVALID_HANDLE)
      return false;

При действительном хэндле мы получим параметры для вызова аналогичного индикатора на новый график. При ошибке получения параметров выходим из функции с результатом false.

   MqlParam params[];
   ENUM_INDICATOR type;
   if(IndicatorParameters(master_handle,type,params)<0)
      return false;

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

   params[0].string_value=s_IndyPath;
   params[1].integer_value=slave_chart;
   ENUM_TIMEFRAMES Timeframe=ChartPeriod(slave_chart);
   int slave_handle=IndicatorCreate(s_Symbol,Timeframe,type,ArraySize(params),params);
   if(slave_handle<0)
      return false;

В заключение функции добавим к графику-приемнику индикатор по полученному хэндлу.

   return ChartIndicatorAdd(slave_chart,0,slave_handle);
  }

Возможно, у читателя возникнет вопрос о том, почему к графику-приемнику нельзя сразу прикрепить индикатор по хэндлу с мастер-графика. Ответ на этот вопрос прост: для этого индикатор и график должны соответствовать по инструменту и таймфрейму. А исходя из наших задач, таймфрейм графиков будет отличаться.

Более подробно с исходным кодом класса можно ознакомиться во вложении.

3. Классы для работы с графическими объектами

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

Средства MQL5 позволяют программам одного графика создавать и изменять объекты на другом через указание идентификатора графика в функциях работы с графическими объектами. Это подходит для работы с небольшим количеством графиков и графических объектов.

Но есть и другой вариант. Ранее мы решили использовать события для отслеживания изменения объектов на графике. Мы даже уже написали код для добавления копий индикатора на все интересующие нас графики. Так почему бы нам не воспользоваться событийной моделью и для того, чтобы передавать данные об изменяемых объектах между индикаторами на разных графиках? Работу с объектами мы отдадим индикатору, находящемуся на графике. Такой подход позволит нам распределить работу с объектами между всеми индикаторами и создать некую асинхронную модель.

Звучит красиво, но, как известно, функция OnChartEvent получает только 4 параметра:

Как же уместить всю информацию об объекте в эти 4 параметра? Мы просто будем передавать идентификатор события, а всю информацию об объекте запишем в параметр типа string. Для сбора информации об объекте в одну переменную типа string воспользуемся наработками из статьи "Использование облачных хранилищ для обмена данными между терминалами".

Создадим класс CCloneObjects, который будет собирать данные о графических объектах и затем выводить их на график.

class CCloneObjects
  {
private:
   string            HLineToString(long chart, string name, int part);
   string            VLineToString(long chart, string name, int part);
   string            TrendToString(long chart, string name, int part);
   string            RectangleToString(long chart, string name, int part);
   bool              CopySettingsToObject(long chart,string name,string &settings[]);

public:
                     CCloneObjects();
                    ~CCloneObjects();
//---
   string            CreateMessage(long chart, string name, int part);
   bool              DrawObjects(long chart, string message);
  };

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

string CCloneObjects::TrendToString(long chart,string name, int part)
  {
   string result = NULL;
   if(ObjectFind(chart,name)!=0)
      return result;
   
   switch(part)
     {
      case 0:
        result+=IntegerToString(ENUM_SET_TYPE_DOUBLE)+"="+IntegerToString(OBJPROP_PRICE)+"=0="+DoubleToString(ObjectGetDouble(chart,name,OBJPROP_PRICE,0),5)+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_TIME)+"=0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_TIME,0))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_DOUBLE)+"="+IntegerToString(OBJPROP_PRICE)+"=1="+DoubleToString(ObjectGetDouble(chart,name,OBJPROP_PRICE,1),5)+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_TIME)+"=1="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_TIME,1))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_RAY_LEFT)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_RAY_LEFT))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_RAY_RIGHT)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_RAY_RIGHT))+"|";
        break;
      default:
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_COLOR)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_COLOR))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_WIDTH)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_WIDTH))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_STYLE)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_STYLE))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_BACK)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_BACK))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_STRING)+"="+IntegerToString(OBJPROP_TEXT)+"="+ObjectGetString(chart,name,OBJPROP_TEXT)+"|";
        result+=IntegerToString(ENUM_SET_TYPE_STRING)+"="+IntegerToString(OBJPROP_TOOLTIP)+"="+ObjectGetString(chart,name,OBJPROP_TOOLTIP);
        break;
     }
   return result;
  }

Детально с кодом всех функций можно ознакомиться во вложении к статье.


4. Собираем индикатор

Мы всё подготовили. Теперь соберем наш индикатор для отслеживания и клонирования графических объектов. Параметр у нашего индикатора будет только один — идентификатор графика.

sinput long    Chart =  0;

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

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

Дело в том, что функция ChartID возвращает идентификатор того графика, с которого был вызван индикатор, а не того, к которому он прикреплен. Это связано с особенностями обработки индикаторов в MetaTrader 5. Если по одному инструменту и таймфрейму несколько раз вызывается один и тот же индикатор, то запускается он только  один раз — при первом вызове. Последующие обращения к нему, даже с других графиков, вызывают уже запущенный индикатор. В свою очередь, индикатор работает на своем графике и возвращает информацию о нем. Таким образом, при вызове экземпляров индикатора в классе CCloneIndy новые копии индикатора будут работать и возвращать информацию о графике, с которого был запущен первый его экземпляр. Чтобы этого избежать, мы должны конкретно указать каждому экземпляру индикатора график, который им будет обслуживаться.

Рассмотрим подробнее код индикатора. В блоке глобальных переменных объявим:

CCloneIndy    *CloneIndy;
CCloneObjects *CloneObjects;
long           l_Chart;
long           ar_Charts[];

В функции OnInit инициализируем экземпляры классов для работы с графиками и объектами.

int OnInit()
  {
//--- indicator Create classes
   CloneIndy   =  new   CCloneIndy();
   if(CheckPointer(CloneIndy)==POINTER_INVALID)
      return INIT_FAILED;
   CloneObjects=  new CCloneObjects();
   if(CheckPointer(CloneObjects)==POINTER_INVALID)
      return INIT_FAILED;

Инициализируем идентификатор рабочего графика.

   l_Chart=(Chart>0 ? Chart : ChartID());

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

   CloneIndy.SearchCharts(l_Chart,ar_Charts);

В конце функции инициализируем таймер с интервалом 10 секунд. Единственной задачей таймера будет актуализация списка графиков для клонирования объектов.

   EventSetTimer(10);
//---
   return(INIT_SUCCEEDED);
  }

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

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   string message1=NULL;
   string message2=NULL;
   int total=0;

Далее на операторе switch построим разветвление операций в зависимости от входящего события.

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

   switch(id)
     {
      case CHARTEVENT_OBJECT_CHANGE:
      case CHARTEVENT_OBJECT_CREATE:
      case CHARTEVENT_OBJECT_DRAG:
        message1=CloneObjects.CreateMessage(l_Chart,sparam,0);
        message2=CloneObjects.CreateMessage(l_Chart,sparam,1);
        total=ArraySize(ar_Charts);
        for(int i=0;i<total;i++)
          {
           EventChartCustom(ar_Charts[i],(ushort)id,0,0,message1);
           EventChartCustom(ar_Charts[i],(ushort)id,0,0,message2);
          }
        break;

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

      case CHARTEVENT_OBJECT_DELETE:
        total=ArraySize(ar_Charts);
        for(int i=0;i<total;i++)
           EventChartCustom(ar_Charts[i],(ushort)id,0,0,sparam);
        break;

Следующие два блока предназначены для обработки сообщений, полученных с других графиков. Когда поступает информация о создании или изменении объекта, мы вызываем функцию, выводящую объект на график. В параметрах функции передаем идентификатор рабочего графика и полученное сообщение.

      case CHARTEVENT_CUSTOM + CHARTEVENT_OBJECT_CHANGE:
      case CHARTEVENT_CUSTOM + CHARTEVENT_OBJECT_CREATE:
      case CHARTEVENT_CUSTOM + CHARTEVENT_OBJECT_DRAG:
        CloneObjects.DrawObjects(l_Chart,sparam);
        ChartRedraw(l_Chart);
        break;

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

      case CHARTEVENT_CUSTOM + CHARTEVENT_OBJECT_DELETE:
        ObjectDelete(l_Chart,sparam);
        ChartRedraw(l_Chart);
        break;
     }
  }

Подробно с кодом индикатора и используемых классов можно ознакомиться во вложении.


Заключение

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



Ссылки

  1. Использование облачных хранилищ для обмена данными между терминалами

Программы, используемые в статье:

#
 Имя
Тип 
Описание 
1 ChartObjectsClone.mq5  Индикатор  Индикатор обмена данными между графиками
2 CloneIndy.mqh  Библиотека класса  Класс для отбора графиков по инструменту
3 CloneObjects.mqh  Библиотека класса  Класс для работы с графическими объектами
4 ChartObjectsClone.mqproj    Файл описания проекта