Скачать MetaTrader 5

Рецепты MQL5 - Разработка мультивалютного индикатора волатильности на MQL5

25 декабря 2013, 16:12
Anatoli Kazharski
4
4 697

Введение

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

Значения индикатора Average True Range (ATR) будем получать уже рассчитанными для каждого символа, то есть по хэндлу. Для примера всего будет шесть символов, названия которых можно установить во внешних параметрах индикатора. Корректность введенных названий будет контролироваться. Если того или иного указанного в параметрах символа не найдется в общем списке, расчеты по нему производиться не будут. Все найденные символы будут помещены в окно Обзор рынка (Market Watch), если их в нем еще нет.

В предыдущей статье "Рецепты MQL5 - Элементы управления в подокне индикатора - Полоса прокрутки" уже рассматривался холст, на котором можно выводить текст и даже рисовать. В этой статье рисовать на холсте мы не будем, но зато будем выводить сообщения о процессе выполнения программы, давая пользователю понять, что сейчас происходит.

 

Процесс разработки индикатора

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

//+------------------------------------------------------------------+
//|                                               MultiSymbolATR.mq5 |
//|                        Copyright 2010, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- Свойства индикатора
#property copyright "Copyright 2010, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window // Индикатор в отдельном подокне
#property indicator_minimum 0       // Минимальное значение индикатора
#property indicator_buffers 6       // Количество буферов для расчета индикатора
#property indicator_plots   6       // Количество графических серий
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Инициализация прошла успешно
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Деинициализация                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
  }
//+------------------------------------------------------------------+
//| 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[])       // Спред
  {
//--- Вернем размер массива данных текущего символа
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
  }
//+------------------------------------------------------------------+

Далее будем наполнять этот шаблон всем необходимым, чтобы идея превратилась в реальность. Необходимость наличия таймера будет объясняться далее в статье. В самом начале, сразу после специфических свойств индикатора, добавим константы:

//--- Константы 
#define RESET           0 // Возврат терминалу команды на пересчет индикатора
#define LEVELS_COUNT    6 // Количество уровней
#define SYMBOLS_COUNT   6 // Количество символов

Константа LEVELS_COUNT содержит значение количества уровней, которые представляют собой графические объекты "Горизонтальная линия" (OBJ_HLINE). Во внешних параметрах индикатора можно будет указать значения этих уровней.

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

//--- Подключим класс для работы с холстом
#include <Canvas\Canvas.mqh>

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

//--- Внешние параметры
input  int              IndicatorPeriod=14;       // Период усреднения
sinput string dlm01=""; //- - - - - - - - - - - - - - - - - - - - - - - - - - -
input  string           Symbol02       ="GBPUSD"; // Символ 2
input  string           Symbol03       ="AUDUSD"; // Символ 3
input  string           Symbol04       ="NZDUSD"; // Символ 4
input  string           Symbol05       ="USDCAD"; // Символ 5
input  string           Symbol06       ="USDCHF"; // Символ 6
sinput string dlm02=""; //- - - - - - - - - - - - - - - - - - - - - - - - - - -
input  int              Level01        =10;       // Уровень 1
input  int              Level02        =50;       // Уровень 2
input  int              Level03        =100;      // Уровень 3
input  int              Level04        =200;      // Уровень 4
input  int              Level05        =400;      // Уровень 5
input  int              Level06        =600;      // Уровень 6

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

//--- Глобальные переменные и массивы
CCanvas           canvas;                 // Загрузка класса
//--- Переменные/массивы для копирования данных из OnCalculate()
int               OC_rates_total     =0;  // Размер входных таймсерий
int               OC_prev_calculated =0;  // Обработано баров на предыдущем вызове
datetime          OC_time[];              // Время открытия
double            OC_open[];              // Цены открытия
double            OC_high[];              // Максимальные цены
double            OC_low[];               // Минимальные цены
double            OC_close[];             // Цены закрытия
long              OC_tick_volume[];       // Тиковые объемы
long              OC_volume[];            // Реальные объемы
int               OC_spread[];            // Спред
//--- Структура буферов для отрисовки значений индикатора
struct buffers {double data[];};
buffers           atr_buffers[SYMBOLS_COUNT];
//--- Структура массивов времени для подготовки данных
struct temp_time {datetime time[];};
temp_time         tmp_symbol_time[SYMBOLS_COUNT];
//--- Структура массивов значений индикатора ATR для подготовки данных
struct temp_atr {double value[];};
temp_atr          tmp_atr_values[SYMBOLS_COUNT];
//--- Для хранения и проверки времени первого бара в терминале
datetime          series_first_date[SYMBOLS_COUNT];
datetime          series_first_date_last[SYMBOLS_COUNT];
//--- Время бара, от которого начинать отрисовку
datetime          limit_time[SYMBOLS_COUNT];
//--- Уровни индикатора
int               indicator_levels[LEVELS_COUNT];
//--- Названия символов
string            symbol_names[SYMBOLS_COUNT];
//--- Хэндлы символов
int               symbol_handles[SYMBOLS_COUNT];
//--- Цвета линий индикатора
color             line_colors[SYMBOLS_COUNT]={clrRed,clrDodgerBlue,clrLimeGreen,clrGold,clrAqua,clrMagenta};
//--- Строка, символизирующая отсутствие символа
string            empty_symbol="EMPTY";
//--- Свойства подокна индикатора
int               subwindow_number        =WRONG_VALUE;              // Номер подокна
int               chart_width             =0;                        // Ширина графика
int               subwindow_height        =0;                        // Высота подокна
int               last_chart_width        =0;                        // Последняя в памяти ширина графика
int               last_subwindow_height   =0;                        // Последняя в памяти высота подокна
int               subwindow_center_x      =0;                        // Центр подокна по горизонтали
int               subwindow_center_y      =0;                        // Центр подокна по вертикали
string            subwindow_shortname     ="MS_ATR";                 // Короткое имя индикатора
string            prefix                  =subwindow_shortname+"_";  // Префикс для объектов
//--- Свойства холста
string            canvas_name             =prefix+"canvas";          // Название холста
color             canvas_background       =clrBlack;                 // Цвет фона канвы
uchar             canvas_opacity          =190;                      // Степень прозрачности
int               font_size               =16;                       // Размер шрифта
string            font_name               ="Calibri";                // Шрифт
ENUM_COLOR_FORMAT clr_format              =COLOR_FORMAT_ARGB_RAW;    // Компоненты цвета должны быть корректно заданы пользователем
//--- Сообщения холста
string            msg_invalid_handle      ="Невалидный хэндл индикатора! Пожалуйста, подождите...";
string            msg_prepare_data        ="Подготовка данных! Пожалуйста, подождите...";
string            msg_not_synchronized    ="Данные не синхронизированы! Пожалуйста, подождите...";
string            msg_load_data           ="";
string            msg_sync_update         ="";
string            msg_last                ="";
//--- Максимальное количество баров, установленное в настройках терминала
int               terminal_max_bars=0;

При загрузке индикатора на график в функции OnInit() будут выполняться следующие действия:

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

Удобнее будет разнести все это по отдельным функциям. В итоге код функции OnInit() примет очень читабельный вид, как это показано ниже:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Проверим корректность входных параметров
   if(!CheckInputParameters())
      return(INIT_PARAMETERS_INCORRECT);
//--- Включим таймер с интервалом 1 секунду
   EventSetTimer(1);
//--- Установим шрифт для отображения на холсте
   canvas.FontSet(font_name,font_size,FW_NORMAL);
//--- Инициализация массивов
   InitArrays();
//--- Инициализируем массив символов 
   InitSymbolNames();
//--- Инициализируем массив уровней
   InitLevels();
//--- Получим хэндлы индикаторов
   GetIndicatorHandles();
//--- Установим свойства индикаторов
   SetIndicatorProperties();
//--- Получим количество баров, установленное в настройках терминала
   terminal_max_bars=TerminalInfoInteger(TERMINAL_MAXBARS);
//--- Очистим комментарий
   Comment("");
//--- Обновим график
   ChartRedraw();
//--- Инициализация прошла успешно
   return(INIT_SUCCEEDED);
  }

Рассмотрим подробнее пользовательские функции в коде выше. В функции CheckInputParameters() производится проверка внешних параметров на корректность. В нашем случае проверяется только один параметр - период индикатора ATR. Я установил ограничение со значением 500. То есть, если установить период более этого значения, то индикатор завершит свою работу, выведя в журнал и в комментарий на графике сообщение о причине остановки программы. Ниже представлен код функции CheckInputParameters().

//+------------------------------------------------------------------+
//| Проверяет входные параметры на корректность                      |
//+------------------------------------------------------------------+
bool CheckInputParameters()
  {
   if(IndicatorPeriod>500)
     {
      Comment("Уменьшите период индикатора! Indicator Period: ",IndicatorPeriod,"; Limit: 500;");
      printf("Уменьшите период индикатора! Indicator Period: %d; Limit: %d;",IndicatorPeriod,500);
      return(false);
     }
//---
   return(true);
  }
Кстати, для быстрого перехода к определению той или иной функции нужно установить текстовый курсор в название функции и нажать Alt+G или вызвать контекстное меню правой кнопкой на нужной функции и выбрать опцию Перейти к определению. Если определение функции находится в другом файле, то этот файл будет открыт в редакторе. Так же можно открывать подключенные библиотеки и классы. Это очень удобно.

Далее идут три функции инициализации массивов: InitArrays(), InitSymbolNames() и InitLevels(). Ниже представлен их код:

//+------------------------------------------------------------------+
//| Первая инициализация массивов                                    |
//+------------------------------------------------------------------+
void InitArrays()
  {
   ArrayInitialize(limit_time,NULL);
   ArrayInitialize(series_first_date,NULL);
   ArrayInitialize(series_first_date_last,NULL);
   ArrayInitialize(symbol_handles,INVALID_HANDLE);
//---
   for(int s=0; s<SYMBOLS_COUNT; s++)
      ArrayInitialize(atr_buffers[s].data,EMPTY_VALUE);
  }
//+------------------------------------------------------------------+
//| Инициализирует массив символов                                   |
//+------------------------------------------------------------------+
void InitSymbolNames()
  {
   symbol_names[0]=AddSymbolToMarketWatch(_Symbol);
   symbol_names[1]=AddSymbolToMarketWatch(Symbol02);
   symbol_names[2]=AddSymbolToMarketWatch(Symbol03);
   symbol_names[3]=AddSymbolToMarketWatch(Symbol04);
   symbol_names[4]=AddSymbolToMarketWatch(Symbol05);
   symbol_names[5]=AddSymbolToMarketWatch(Symbol06);
  }
//+------------------------------------------------------------------+
//| Инициализирует массив уровней                                    |
//+------------------------------------------------------------------+
void InitLevels()
  {
   indicator_levels[0]=Level01;
   indicator_levels[1]=Level02;
   indicator_levels[2]=Level03;
   indicator_levels[3]=Level04;
   indicator_levels[4]=Level05;
   indicator_levels[5]=Level06;
  }

В функции InitSymbolNames() используется еще одна пользовательская функция AddSymbolToMarketWatch(). В нее передается название символа, и если этот символ есть в общем списке, то он будет установлен в окно Обзор рынка, а функция вернет строку с названием этого символа. Если же такого символа нет, то функция вернет строку "EMPTY", и в последствии при проверке в других функциях для этого элемента в массиве символов не будут производиться никакие действия.

//+------------------------------------------------------------------+
//| Добавляет указанный символ в окно обзор рынка                    |
//+------------------------------------------------------------------+
string AddSymbolToMarketWatch(string symbol)
  {
   int      total=0; // Количество символов
   string   name=""; // Имя символа
//--- Если передали пустую строку, вернем пустую строку
   if(symbol=="")
      return(empty_symbol);
//--- Всего символов на сервере
   total=SymbolsTotal(false);
//--- Пройтись по всему списку символов
   for(int i=0;i<total;i++)
     {
      //--- Имя символа на сервере
      name=SymbolName(i,false);
      //--- Если есть такой символ, то
      if(name==symbol)
        {
         //--- установим его в окно Обзор Рынка и
         SymbolSelect(name,true);
         //--- вернем его имя
         return(name);
        }
     }
//--- Если такого символа нет, вернем строку, символизирующую отсутствие символа
   return(empty_symbol);
  }

GetIndicatorHandles() - еще одна функция, которая вызывается при инициализации индикатора. В ней осуществляется попытка получить хэндлы индикатора ATR для каждого указанного символа. Если для какого-то символа хэндл не был получен, то функция вернет false, но в OnInit() это никак не обрабатывается, так как наличие хэндлов будет проверяться и в других частях программы.

//+------------------------------------------------------------------+
//| Получает хэндлы индикаторов                                      |
//+------------------------------------------------------------------+
bool GetIndicatorHandles()
  {
//--- Признак того, что все хэндлы валидны
   bool valid_handles=true;
//--- Пройдемся в цикле по всем символам и ...
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      //--- Если символ есть
      if(symbol_names[s]!=empty_symbol)
        {
         // И если хэндл текущего символа невалиден
         if(symbol_handles[s]==INVALID_HANDLE)
           {
            //--- Получим его
            symbol_handles[s]=iATR(symbol_names[s],Period(),IndicatorPeriod);
            //--- Если не удалось получить хэндл, попробуем в следующий раз
            if(symbol_handles[s]==INVALID_HANDLE)
               valid_handles=false;
           }
        }
     }
//--- Выведем сообщение, если хэндл для одного из символов не получен
   if(!valid_handles)
     {
      msg_last=msg_invalid_handle;
      ShowCanvasMessage(msg_invalid_handle);
     }
//---
   return(valid_handles);
  }

Функцию ShowCanvasMessage() мы рассмотрим чуть позже вместе с остальными функциями для работы с холстом.

Свойства индикаторов устанавливаются в функции SetIndicatorProperties(). Так как свойства для каждой графической серии однотипны, то удобнее все это делать в циклах:

//+------------------------------------------------------------------+
//| Устанавливает свойства индикаторов                               |
//+------------------------------------------------------------------+
void SetIndicatorProperties()
  {
//--- Установим короткое имя
   IndicatorSetString(INDICATOR_SHORTNAME,subwindow_shortname);
//--- Установим количество знаков после запятой
   IndicatorSetInteger(INDICATOR_DIGITS,_Digits);
//--- Определим буферы для отрисовки
   for(int s=0; s<SYMBOLS_COUNT; s++)
      SetIndexBuffer(s,atr_buffers[s].data,INDICATOR_DATA);
//--- Установим метки для текущего символа
   for(int s=0; s<SYMBOLS_COUNT; s++)
      PlotIndexSetString(s,PLOT_LABEL,"ATR ("+IntegerToString(s)+", "+symbol_names[s]+")");
//--- Установим тип графического построения: линии
   for(int s=0; s<SYMBOLS_COUNT; s++)
      PlotIndexSetInteger(s,PLOT_DRAW_TYPE,DRAW_LINE);
//--- Установим толщину линий
   for(int s=0; s<SYMBOLS_COUNT; s++)
      PlotIndexSetInteger(s,PLOT_LINE_WIDTH,1);
//--- Установим цвет линий
   for(int s=0; s<SYMBOLS_COUNT; s++)
      PlotIndexSetInteger(s,PLOT_LINE_COLOR,line_colors[s]);
//--- Пустое значение для построения, для которого нет отрисовки
   for(int s=0; s<SYMBOLS_COUNT; s++)
      PlotIndexSetDouble(s,PLOT_EMPTY_VALUE,EMPTY_VALUE);
  }

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

Не всегда может получиться произвести все расчеты корректно с первого раза, и в этом случае мы будем использовать для возврата константу RESET, которая содержит нулевое значение. При следующем вызове OnCalculate() (например, на следующем тике) параметр prev_calculated будет содержать нулевое значение, а, значит, нужно будет сделать еще одну попытку для всех необходимых расчетов перед тем, как выводить графические серии индикатора на график.

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

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

//+------------------------------------------------------------------+
//| Копирует данные из OnCalculate                                   |
//+------------------------------------------------------------------+
void CopyDataOnCalculate(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[])
  {
   OC_rates_total=rates_total;
   OC_prev_calculated=prev_calculated;
   ArrayCopy(OC_time,time);
   ArrayCopy(OC_open,open);
   ArrayCopy(OC_high,high);
   ArrayCopy(OC_low,low);
   ArrayCopy(OC_close,close);
   ArrayCopy(OC_tick_volume,tick_volume);
   ArrayCopy(OC_volume,volume);
   ArrayCopy(OC_spread,spread);
  }

Вызывать эту функцию нужно в самом начале тела функции OnCalculate(). Также в самом начале нужно расположить еще одну пользовательскую функцию ResizeCalculatedArrays(), которая будет устанавливать размер массивам для подготовки данных перед тем, как поместить их в индикаторные буферы. Размер этих массивов должен быть равен размеру входных таймсерий.

//+------------------------------------------------------------------+
//| Изменяет размер массивов под размер основного массива            |
//+------------------------------------------------------------------+
void ResizeCalculatedArrays()
  {
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      ArrayResize(tmp_symbol_time[s].time,OC_rates_total);
      ArrayResize(tmp_atr_values[s].value,OC_rates_total);
     }
  }

Также создадим функцию ZeroCalculatedArrays(), которая инициализирует нулевыми значениями массивы для подготовки данных перед выводом их на график.

//+------------------------------------------------------------------+
//| Обнуляет массивы для подготовки данных                           |
//+------------------------------------------------------------------+
void ZeroCalculatedArrays()
  {
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      ArrayInitialize(tmp_symbol_time[s].time,NULL);
      ArrayInitialize(tmp_atr_values[s].value,EMPTY_VALUE);
     }
  }

И такая же функция понадобится для предварительного обнуления индикаторных буферов. Назовем ее ZeroIndicatorBuffers().

//+------------------------------------------------------------------+
//| Обнуляет индикаторные буферы                                     |
//+------------------------------------------------------------------+
void ZeroIndicatorBuffers()
  {
   for(int s=0; s<SYMBOLS_COUNT; s++)
      ArrayInitialize(atr_buffers[s].data,EMPTY_VALUE);
  }

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

//+------------------------------------------------------------------+
//| 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[])       // Спред
  {
//--- Для определения бара, с которого необходимо производить расчет
   int limit=0;
//--- Сделаем копию параметров OnCalculate()
   CopyDataOnCalculate(rates_total,prev_calculated,
                       time,open,high,low,close,
                       tick_volume,volume,spread);
//--- Установим размер массивам для подготовки данных
   ResizeCalculatedArrays();
//--- Если это первый расчет или загружена более глубокая история или были заполнены пропуски истории
   if(prev_calculated==0)
     {
      //--- Обнулим массивы для подготовки данных
      ZeroCalculatedArrays();
      //--- Обнулим индикаторные буферы
      ZeroIndicatorBuffers();
      //--- Остальные проверки
      // ...
      //--- Если дошли до этого момента, то значит OnCalculate() вернет ненулевое значение и это нужно запомнить
      OC_prev_calculated=rates_total;
     }
//--- Если нужно пересчитать только последние значения
   else
      limit=prev_calculated-1;

//--- Подготовим данные для отрисовки
// ...
//--- Заполним массивы данными для отрисовки
// ...

//--- Вернем размер массива данных текущего символа
   return(rates_total);
  }

Код функции OnTimer() на текущий момент имеет вот такой вид:

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- Если по какой-то причине расчеты не были завершены или
//    подкачана более глубокая история или
//    были заполнены пропуски истории, 
//    то, не дожидаясь тика, сделаем еще одну попытку
   if(OC_prev_calculated==0)
     {
      OnCalculate(OC_rates_total,OC_prev_calculated,
                  OC_time,OC_open,OC_high,OC_low,OC_close,
                  OC_tick_volume,OC_volume,OC_spread);
     }
  }

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

  • загрузка и формирование необходимого количества данных (баров);
  • проверка на наличие всех хэндлов;
  • проверка готовности необходимого количества данных;
  • синхронизация данных с сервером;
  • определение баров, с которых будет производиться отрисовка графических серий.

Также для каждого символа будет определяться первый "истинный" бар. Я придумал такое краткое определение, чтобы было удобнее в дальнейшем. Означает это следующее. Все таймфреймы в MetaTrader 5 строятся из минутных данных. Но если, например, на сервере дневные данные наличествуют с 1993 года, а минутные - только с 2000 года, то, если на графике включен, например часовой таймфрейм, бары будут построены от даты начала минутных данные, то есть от 2000 года. Все, что до 2000 года, будет представлено в виде дневных данных, либо ближайших к текущему таймфрейму. Поэтому для тех данных, которые не относятся к текущему таймфрейму, не нужно отображать данные индикатора, чтобы не вносить путаницу. Именно для этого мы будем определять первый "истинный" бар текущего таймфрейма, а также отмечать его вертикальной линией того же цвета, который имеет индикаторный буфер символа.

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

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

//+------------------------------------------------------------------+
//| Получает геометрию подокна индикатора                            |
//+------------------------------------------------------------------+
void GetSubwindowGeometry()
  {
//--- Получим номер подокна индикатора
   subwindow_number=ChartWindowFind(0,subwindow_shortname);
//--- Получим ширину и высоту подокна
   chart_width=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS);
   subwindow_height=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,subwindow_number);
//--- Рассчитаем центр подокна
   subwindow_center_x=chart_width/2;
   subwindow_center_y=subwindow_height/2;
  }

Когда получены свойства подокна, можно добавлять холст. Цвет его фона будет прозрачен на 100% (значение 0), а видимым он станет только, когда начнется процесс загрузки и формирования данных, чтобы дать пользователю понять, что происходит в текущий момент. В момент видимости фон холста будет иметь прозрачность со значением 190. Степень прозрачности можно установить от 0 до 255. Для более подробной информации обратитесь к описанию функции ColorToARGB() в справке.

Для установки холста напишем функцию SetCanvas():

//+------------------------------------------------------------------+
//| Устанавливает холст                                              |
//+------------------------------------------------------------------+
void SetCanvas()
  {
//--- Если холста нет, установим его
   if(ObjectFind(0,canvas_name)<0)
     {
      //--- Создадим холст
      canvas.CreateBitmapLabel(0,subwindow_number,canvas_name,0,0,chart_width,subwindow_height,clr_format);
      //--- Сделаем холст полностью прозрачным
      canvas.Erase(ColorToARGB(canvas_background,0));
      //--- Перерисуем холст
      canvas.Update();
     }
  }

Еще нам понадобится функция, которая будет проверять, изменился ли размер подокна индикатора. Если это так, то размеры холста будут автоматически подстраиваться под новый размер подокна. Назовем эту функцию OnSubwindowChange():

//+------------------------------------------------------------------+
//| Следит за размерами подокн                                       |
//+------------------------------------------------------------------+
void OnSubwindowChange()
  {
//--- Получим свойства подокна
   GetSubwindowGeometry();
//--- Если размеры подокна не изменились, выйдем
   if(!SubwindowSizeChanged())
      return;
//--- Если высота подокна меньше 1 пиксела или центр рассчитан некорректно, выйдем
   if(subwindow_height<1 || subwindow_center_y<1)
      return;
//--- Установим новый размер холста
   ResizeCanvas();
//--- Покажем последнее сообщение
   ShowCanvasMessage(msg_last);
  }

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

Код функции SubwindowSizeChanged():

//+------------------------------------------------------------------+
//| Проверяет, изменились ли размеры подокна                         |
//+------------------------------------------------------------------+
bool SubwindowSizeChanged()
  {
//--- Если размеры подокна не изменились, выйдем
   if(last_chart_width==chart_width && last_subwindow_height==subwindow_height)
      return(false);
//--- Если же изменились, то запомним их
   else
     {
      last_chart_width=chart_width;
      last_subwindow_height=subwindow_height;
     }
//---
   return(true);
  }

Код функции ResizeCanvas():

//+------------------------------------------------------------------+
//| Изменяет размер холста                                           |
//+------------------------------------------------------------------+
void ResizeCanvas()
  {
//--- Если холст уже добавлен в подокно индикатора, установим новый размер
   if(ObjectFind(0,canvas_name)==subwindow_number)
      canvas.Resize(chart_width,subwindow_height);
  }

И, наконец-то, код функции ShowCanvasMessage(), которую до этого мы также использовали при получении хэндлов индикаторов:

//+------------------------------------------------------------------+
//| Отображает сообщение на холсте                                   |
//+------------------------------------------------------------------+
void ShowCanvasMessage(string message_text)
  {
   GetSubwindowGeometry();
//--- Если холст уже добавлен в подокно индикатора
   if(ObjectFind(0,canvas_name)==subwindow_number)
     {
      //--- Если передана непустая строка и получены корректные координаты, отобразим сообщение
      if(message_text!="" && subwindow_center_x>0 && subwindow_center_y>0)
        {
         canvas.Erase(ColorToARGB(canvas_background,canvas_opacity));
         canvas.TextOut(subwindow_center_x,subwindow_center_y,message_text,ColorToARGB(clrRed),TA_CENTER|TA_VCENTER);
         canvas.Update();
        }
     }
  }

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

Код функции DeleteCanvas():

//+------------------------------------------------------------------+
//| Удаляет холст                                                    |
//+------------------------------------------------------------------+
void DeleteCanvas()
  {
//--- Удалим холст, если он есть
   if(ObjectFind(0,canvas_name)>0)
     {
      //--- Перед удалением произведем эффект затухания
      for(int i=canvas_opacity; i>0; i-=5)
        {
         canvas.Erase(ColorToARGB(canvas_background,(uchar)i));
         canvas.Update();
        }
      //--- Удалим графический ресурс
      canvas.Destroy();
     }
  }

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

//+------------------------------------------------------------------+
//| Загружает и формирует необходимое/имеющееся кол-во данных        |
//+------------------------------------------------------------------+
void LoadAndFormData()
  {
   int bars_count=100; // Количество подгружаемых баров
//---
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      int      attempts          =0;    // Счетчик попыток копирования данных
      int      array_size        =0;    // Размер массива
      datetime firstdate_server  =NULL; // Время первого бара на сервере
      datetime firstdate_terminal=NULL; // Время первого бара в базе терминала
      //--- Получим первую дату по символу/периоду в базе терминала
      SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE,firstdate_terminal);
      //--- Получим первую дату символа-периода на сервере
      SeriesInfoInteger(symbol_names[s],Period(),SERIES_SERVER_FIRSTDATE,firstdate_server);
      //--- Выведем сообщение
      msg_last=msg_load_data="Процесс загрузки и формирования данных: "+
               symbol_names[s]+"("+(string)(s+1)+"/"+(string)SYMBOLS_COUNT+") ... ";
      ShowCanvasMessage(msg_load_data);
      //--- Загрузим/сформируем данные.
      //    Если размер массива меньше, чем максимальное количество баров в терминале, а также если
      //    между первой датой серии в терминале и первой датой серии на сервере больше указанного количества баров
      while(array_size<OC_rates_total && 
            firstdate_terminal-firstdate_server>PeriodSeconds()*bars_count)
        {
         datetime copied_time[];
         //--- Получим первую дату по символу/периоду в базе терминала
         SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE,firstdate_terminal);
         //--- Загрузим/скопируем еще указанное количество баров
         if(CopyTime(symbol_names[s],Period(),0,array_size+bars_count,copied_time)!=-1)
           {
            //--- Если время первого бара из массива за вычетом кол-ва подгружаемых баров раньше, 
            //    чем время первого бара на графике, остановим цикл
            if(copied_time[0]-PeriodSeconds()*bars_count<OC_time[0])
               break;
            //--- Если размер массива не увеличился, увеличим счетчик
            if(ArraySize(copied_time)==array_size)
               attempts++;
            //--- Иначе получим текущий размер массива
            else
               array_size=ArraySize(copied_time);
            //--- Если размер массива не увеличивается в течение 100 попыток, остановим цикл
            if(attempts==100)
              {
               attempts=0;
               break;
              }
           }
         //--- Каждые 2000 баров проверяем размеры подокна 
         //    и, если размер изменился, подстроим под него размер канвы
         if(!(array_size%2000))
            OnSubwindowChange();
        }
     }
  }

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

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

//+------------------------------------------------------------------+
//| Проверяет количество доступных данных у всех символов            |
//+------------------------------------------------------------------+
bool CheckAvailableData()
  {
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      //--- Если такой символ есть
      if(symbol_names[s]!=empty_symbol)
        {
         double   data[]; // Массив для проверки количества данных индикатора
         datetime time[]; // Массив для проверки количества баров
         int      calculated_values =0;    // Количество данных индикатора
         int      available_bars    =0;    // Количество баров текущего периода
         datetime firstdate_terminal=NULL; // Первая дата имеющихся данных текущего периода в терминале
         //--- Получим кол-во рассчитанных значений индикатора
         calculated_values=BarsCalculated(symbol_handles[s]);
         //--- Получим первую дату данных текущего периода в терминале
         firstdate_terminal=(datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_TERMINAL_FIRSTDATE);
         //--- Получим количество доступных баров от указанной даты
         available_bars=Bars(symbol_names[s],Period(),firstdate_terminal,TimeCurrent());
         //--- Проверим готовность данных баров: даем 5 попыток получить значения
         for(int i=0; i<5; i++)
           {
            //--- Скопируем указанное количество данных
            if(CopyTime(symbol_names[s],Period(),0,available_bars,time)!=-1)
              {
               //--- Если скопировалось нужное количество, остановим цикл
               if(ArraySize(time)>=available_bars)
                  break;
              }
           }
         //--- Проверим готовность данных индикатора: даем 5 попыток получить значения
         for(int i=0; i<5; i++)
           {
            //--- Скопируем указанное количество данных
            if(CopyBuffer(symbol_handles[s],0,0,calculated_values,data)!=-1)
              {
               //--- Если скопировалось нужное количество, остановим цикл
               if(ArraySize(data)>=calculated_values)
                  break;
              }
           }
         //--- Если скопировано меньше данных, значит нужно совершить еще одну попытку
         if(ArraySize(time)<available_bars || ArraySize(data)<calculated_values)
           {
            msg_last=msg_prepare_data;
            ShowCanvasMessage(msg_prepare_data);
            OC_prev_calculated=0;
            return(false);
           }
        }
     }
//---
   return(true);
  }

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

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

//+------------------------------------------------------------------+
//| Проверяет событие загрузки более глубокой истории                |
//+------------------------------------------------------------------+
bool CheckLoadedHistory()
  {
   bool loaded=false;
//---
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      //--- Если такой символ есть
      if(symbol_names[s]!=empty_symbol)
        {
         //--- Если нужно обновить серии
         if(OC_prev_calculated==0)
           {
            //--- Получим первую дату по символу/периоду
            series_first_date[s]=(datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE);
            //--- Если здесь в первый раз (отсутствует значение), то
            if(series_first_date_last[s]==NULL)
               //--- Запомним первую дату по символу/периоду для последующих сравнений 
               //    с целью определения загрузки более глубокой истории
               series_first_date_last[s]=series_first_date[s];
           }
         else
           {
            //--- Получим первую дату по символу/периоду
            series_first_date[s]=(datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE);
            //--- Если даты отличаются, то есть дата в памяти более поздняя, чем та, которую получили сейчас,
            //     то значит была загрузка более глубокой истории
            if(series_first_date_last[s]>series_first_date[s])
              {
               //--- Выведем сообщение в журнал
               Print("(",symbol_names[s],",",TimeframeToString(Period()),
                     ") > Была загружена/сформирована более глубокая история: ",
                     series_first_date_last[s]," > ",series_first_date[s]);
               //--- Запомним дату
               series_first_date_last[s]=series_first_date[s];
               loaded=true;
              }
           }
        }
     }
//--- Если была загружена/сформирована более глубокая история, то
//    отправим команду на обновление графических серий индикатора
   if(loaded)
      return(false);
//---
   return(true);
  }

Напишем еще одну функцию для проверки синхронизированности данных в терминале и на сервере. Эта проверка будет осуществляться только в случае, если есть соединение с сервером. Ниже представлен код функции CheckSymbolIsSynchronized():

//+------------------------------------------------------------------+
//| Проверяет синхронизированность по символу/периоду                |
//+------------------------------------------------------------------+
bool CheckSymbolIsSynchronized()
  {
//--- Если есть соединение с сервером, проверим синхронизированность данных
   if(TerminalInfoInteger(TERMINAL_CONNECTED))
     {
      for(int s=0; s<SYMBOLS_COUNT; s++)
        {
         //--- Если символ есть
         if(symbol_names[s]!=empty_symbol)
           {
            //--- Если данные не синхронизированы, сообщим об этом и попробуем снова
            if(!SeriesInfoInteger(symbol_names[s],Period(),SERIES_SYNCHRONIZED))
              {
               msg_last=msg_not_synchronized;
               ShowCanvasMessage(msg_not_synchronized);
               return(false);
              }
           }
        }
     }
//---
   return(true);
  }

Утилитарную функцию преобразования таймфрейма в строку возьмем из предыдущих статей серии "Рецепты MQL5":

//+------------------------------------------------------------------+
//| Преобразует таймфрейм в строку                                   |
//+------------------------------------------------------------------+
string TimeframeToString(ENUM_TIMEFRAMES timeframe)
  {
   string str="";
//--- Если переданное значение некорректно, берем таймфрейм текущего графика
   if(timeframe==WRONG_VALUE || timeframe== NULL)
      timeframe= Period();
   switch(timeframe)
     {
      case PERIOD_M1  : str="M1";  break;
      case PERIOD_M2  : str="M2";  break;
      case PERIOD_M3  : str="M3";  break;
      case PERIOD_M4  : str="M4";  break;
      case PERIOD_M5  : str="M5";  break;
      case PERIOD_M6  : str="M6";  break;
      case PERIOD_M10 : str="M10"; break;
      case PERIOD_M12 : str="M12"; break;
      case PERIOD_M15 : str="M15"; break;
      case PERIOD_M20 : str="M20"; break;
      case PERIOD_M30 : str="M30"; break;
      case PERIOD_H1  : str="H1";  break;
      case PERIOD_H2  : str="H2";  break;
      case PERIOD_H3  : str="H3";  break;
      case PERIOD_H4  : str="H4";  break;
      case PERIOD_H6  : str="H6";  break;
      case PERIOD_H8  : str="H8";  break;
      case PERIOD_H12 : str="H12"; break;
      case PERIOD_D1  : str="D1";  break;
      case PERIOD_W1  : str="W1";  break;
      case PERIOD_MN1 : str="MN1"; break;
     }
//---
   return(str);
  }

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

//+------------------------------------------------------------------+
//| Определяет время первого истинного бара для отрисовки            |
//+------------------------------------------------------------------+
bool DetermineFirstTrueBar()
  {
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      datetime time[];           // Массив времени баров
      int      available_bars=0; // Количество баров
      //--- Если такого символа нет, перейти к следующему
      if(symbol_names[s]==empty_symbol)
         continue;
      //--- Получим общее количество баров символа
      available_bars=Bars(symbol_names[s],Period());
      //--- Скопируем массив времени баров. Если не получилось, попробуем еще раз.
      if(CopyTime(symbol_names[s],Period(),0,available_bars,time)<available_bars)
         return(false);
      //--- Получим время первого истинного бара, который соответствует текущему таймфрейму
      limit_time[s]=GetFirstTrueBarTime(time);
      //--- Установим вертикальную линию на истинном баре
      CreateVerticalLine(0,0,limit_time[s],prefix+symbol_names[s]+": begin time series",
                          2,STYLE_SOLID,line_colors[s],false,TimeToString(limit_time[s]),"\n");
     }
//---
   return(true);
  }
//+------------------------------------------------------------------+
//| Возвращает время первого истинного бара текущего периода         |
//+------------------------------------------------------------------+
datetime GetFirstTrueBarTime(datetime &time[])
  {
   datetime true_period =NULL; // Время первого истинного бара
   int      array_size  =0;    // Размер массива
//--- Получим размер массива
   array_size=ArraySize(time);
   ArraySetAsSeries(time,false);
//--- Поочередно проверяем каждый бар
   for(int i=1; i<array_size; i++)
     {
      //--- Если бар соответствует текущему таймфрейму
      if(time[i]-time[i-1]==PeriodSeconds())
        {
         //--- Запомним и остановим цикл
         true_period=time[i];
         break;
        }
     }
//--- Вернем время первого истинного бара
   return(true_period);
  }

Время первого истинного бара отметим на графике вертикальной линией с помощью функции CreateVerticalLine():

//+------------------------------------------------------------------+
//| Создает вертикальную линию в указанной временнОй точке           |
//+------------------------------------------------------------------+
void CreateVerticalLine(long            chart_id,           // id графика
                        int             window_number,      // номер окна
                        datetime        time,               // время
                        string          object_name,        // имя объекта
                        int             line_width,         // толщина линии
                        ENUM_LINE_STYLE line_style,         // стиль линии
                        color           line_color,         // цвет линии
                        bool            selectable,         // нельзя выделить объект, если FALSE
                        string          description_text,   // текст описания
                        string          tooltip)            // нет всплывающей подсказки, если "\n"
  {
//--- Если объект успешно создан
   if(ObjectCreate(chart_id,object_name,OBJ_VLINE,window_number,time,0))
     {
      //--- установим ему свойства
      ObjectSetInteger(chart_id,object_name,OBJPROP_TIME,time);
      ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTABLE,selectable);
      ObjectSetInteger(chart_id,object_name,OBJPROP_STYLE,line_style);
      ObjectSetInteger(chart_id,object_name,OBJPROP_WIDTH,line_width);
      ObjectSetInteger(chart_id,object_name,OBJPROP_COLOR,line_color);
      ObjectSetString(chart_id,object_name,OBJPROP_TEXT,description_text);
      ObjectSetString(chart_id,object_name,OBJPROP_TOOLTIP,tooltip);
     }
  }

Функции с проверками готовы. В итоге, часть кода функции OnCalculate(), когда переменная prev_calculated равна нулю, теперь будет выглядеть так, как показано ниже:

//--- Если это первый расчет или загружена более глубокая история или были заполнены пропуски истории
   if(prev_calculated==0)
     {
      //--- Обнулим массивы для подготовки данных
      ZeroCalculatedArrays();
      //--- Обнулим индикаторные буферы
      ZeroIndicatorBuffers();
      //--- Получим свойства подокна
      GetSubwindowGeometry();
      //--- Добавим холст
      SetCanvas();
      //--- Загрузим и сформируем необходимое/имеющееся количество данных
      LoadAndFormData();
      //--- Если есть невалидный хэндл, попробуем получить его снова
      if(!GetIndicatorHandles())
         return(RESET);
      //--- Проверим количество доступных данных по всем символам
      if(!CheckAvailableData())
         return(RESET);
      //--- Проверим, если загружена более глубокая история
      if(!CheckLoadedHistory())
         return(RESET);
      //--- Проверим синхронизированность данных по символу/периоду на данный момент
      if(!CheckSymbolIsSynchronized())
         return(RESET);
      //--- Определим для каждого символа, с какого бара начинать отрисовку
      if(!DetermineFirstTrueBar())
         return(RESET);
      //--- Если дошли до этого момента, то значит OnCalculate() вернет ненулевое значение и это нужно запомнить
      OC_prev_calculated=rates_total;
     }

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

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- Если была загружена более глубокая история
   if(!CheckLoadedHistory())
      OC_prev_calculated=0;
//--- Если по какой-то причине расчеты не были завершены или
//    подкачана более глубокая история или
//    были заполнены пропуски истории, 
//    то, не дожидаясь тика, сделаем еще одну попытку
   if(OC_prev_calculated==0)
     {
      OnCalculate(OC_rates_total,OC_prev_calculated,
                  OC_time,OC_open,OC_high,OC_low,OC_close,
                  OC_tick_volume,OC_volume,OC_spread);
     }
  }

Осталось написать два основных цикла для размещения их в функции OnCalculate():

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

Код первого цикла представлен ниже:

//--- Подготовим данные для отрисовки
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      //--- Если символ существует
      if(symbol_names[s]!=empty_symbol)
        {
         double percent=0.0; // Для расчета процента прогресса
         msg_last=msg_sync_update="Подготовка данных ("+IntegerToString(rates_total)+" баров) : "+
                      symbol_names[s]+"("+(string)(s+1)+"/"+(string)(SYMBOLS_COUNT)+") - 00% ... ";
         //--- Выведем сообщение
         ShowCanvasMessage(msg_sync_update);
         //--- Проконтролируем каждое значение массива
         for(int i=limit; i<rates_total; i++)
           {
            PrepareData(i,s,time);
            //--- Каждые 1000 баров обновляем сообщение
            if(i%1000==0)
              {
               //--- Процент прогресса
               ProgressPercentage(i,s,percent);
               //--- Выведем сообщение
               ShowCanvasMessage(msg_sync_update);
              }
            //--- Каждые 2000 баров проверяем размеры подокна
            //    и, если размер изменился, подстроим под него размер холста
            if(i%2000==0)
               OnSubwindowChange();
           }
        }
     }

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

Код функции PrepareData():

//+------------------------------------------------------------------+
//| Подготавливает данные перед отрисовкой                           |
//+------------------------------------------------------------------+
void PrepareData(int bar_index,int symbol_number,datetime const &time[])
  {
   int attempts=100; // Количество попыток копирования
//--- Время бара указанного символа и таймфрейма
   datetime symbol_time[];
//--- Массив для копирования значения индикатора
   double atr_values[];
//--- Если в зоне баров текущего таймфрейма
   if(time[bar_index]>=limit_time[symbol_number])
     {
      //--- Скопируем время
      for(int i=0; i<attempts; i++)
        {
         if(CopyTime(symbol_names[symbol_number],0,time[bar_index],1,symbol_time)==1)
           {
            tmp_symbol_time[symbol_number].time[bar_index]=symbol_time[0];
            break;
           }
        }
      //--- Скопируем значение индикатора
      for(int i=0; i<attempts; i++)
        {
         if(CopyBuffer(symbol_handles[symbol_number],0,time[bar_index],1,atr_values)==1)
           {
            tmp_atr_values[symbol_number].value[bar_index]=atr_values[0];
            break;
           }
        }
     }
//--- Если вне зоны баров текущего таймфрейма, установим пустое значение
   else
      tmp_atr_values[symbol_number].value[bar_index]=EMPTY_VALUE;
  }

Код функции ProgressPercentage():

//+------------------------------------------------------------------+
//| Вычисляет процент прогресса                                      |
//+------------------------------------------------------------------+
void ProgressPercentage(int bar_index,int symbol_number,double &percent)
  {
   string message_text="";
   percent=(double(bar_index)/OC_rates_total)*100;
//---
   if(percent<=9.99)
      message_text="0"+DoubleToString(percent,0);
   else if(percent<99)
      message_text=DoubleToString(percent,0);
   else
      message_text="100";
//---
   msg_last=msg_sync_update="Подготовка данных ("+(string)OC_rates_total+" баров) : "+
            symbol_names[symbol_number]+
            "("+(string)(symbol_number+1)+"/"+(string)SYMBOLS_COUNT+") - "+message_text+"% ... ";
  }

Во втором основном цикле в функции OnCalculate() заполняются индикаторные буферы:

//--- Заполним индикаторные буферы
   for(int s=0; s<SYMBOLS_COUNT; s++)
     {
      //--- Если указанного символа не существует, обнулим буфер
      if(symbol_names[s]==empty_symbol)
         ArrayInitialize(atr_buffers[s].data,EMPTY_VALUE);
      else
        {
         //--- Сформируем сообщение
         msg_last=msg_sync_update="Обновление данных индикатора: "+
                      symbol_names[s]+"("+(string)(s+1)+"/"+(string)SYMBOLS_COUNT+") ... ";
         //--- Выведем сообщение
         ShowCanvasMessage(msg_sync_update);
         //--- Заполним индикаторные буферы значениями
         for(int i=limit; i<rates_total; i++)
           {
            FillIndicatorBuffers(i,s,time);
            //--- Каждые 2000 баров проверяем размеры подокна
            //    и, если размер изменился, подстроим под него размер холста
            if(i%2000==0)
               OnSubwindowChange();
           }
        }
     }

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

//+------------------------------------------------------------------+
//| Заполняет индикаторные буферы                                    |
//+------------------------------------------------------------------+
void FillIndicatorBuffers(int bar_index,int symbol_number,datetime const &time[])
  {
//--- Для проверки полученного значения индикатора
   bool check_value=false;
//--- Счетчик баров текущего таймфрейма
   static int bars_count=0;
//--- Обнулим счетчик баров текущего таймфрейма в начале таймсерии символа
   if(bar_index==0)
      bars_count=0;
//--- Если в зоне баров текущего таймфрейма, и счетчик меньше 
//    указанного периода индикатора, увеличим счетчик
   if(bars_count<IndicatorPeriod && time[bar_index]>=limit_time[symbol_number])
      bars_count++;
//--- Если в зоне индикатора, и времена текущего символа и указанного символа совпадают
   if(bars_count>=IndicatorPeriod && 
      time[bar_index]==tmp_symbol_time[symbol_number].time[bar_index])
     {
      //--- Если полученное значение не пустое
      if(tmp_atr_values[symbol_number].value[bar_index]!=EMPTY_VALUE)
        {
         check_value=true;
         atr_buffers[symbol_number].data[bar_index]=tmp_atr_values[symbol_number].value[bar_index];
        }
     }
//--- Установим пустое значение, если не получилось установить выше
   if(!check_value)
      atr_buffers[symbol_number].data[bar_index]=EMPTY_VALUE;
  }

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

Вот такие строчки кода нужно добавить после второго основного цикла до возвращаемого функцией значения:

//--- Удалим холст
   DeleteCanvas();
//--- Установим уровни индикаторов
   SetIndicatorLevels();
//--- Обнулим переменные
   msg_last="";
   msg_sync_update="";
//--- Обновим график
   ChartRedraw();

Код функции SetIndicatorLevels() для установки горизонтальных уровней:

//+------------------------------------------------------------------+
//| Устанавливает уровни индикаторов                                 |
//+------------------------------------------------------------------+
void SetIndicatorLevels()
  {
//--- Получим номер подокна индикатора
   subwindow_number=ChartWindowFind(0,subwindow_shortname);
//--- Установим уровни
   for(int i=0; i<LEVELS_COUNT; i++)
      CreateHorizontalLine(0,subwindow_number,
                            prefix+"level_0"+(string)(i+1)+"",
                            CorrectValueBySymbolDigits(indicator_levels[i]*_Point),
                            1,STYLE_DOT,clrLightSteelBlue,false,false,false,"\n");
  }
//+------------------------------------------------------------------+
//| Коррекция значения по количеству знаков в цене (double)          |
//+------------------------------------------------------------------+
double CorrectValueBySymbolDigits(double value)
  {
   return(_Digits==3 || _Digits==5) ? value*=10 : value;
  }

Код функции CreateHorizontalLine() для установки горизонтального уровня с указанными свойствами:

//+------------------------------------------------------------------+
//| Создает горизонтальную линию на указанном уровне цены            |
//+------------------------------------------------------------------+
void CreateHorizontalLine(long            chart_id,      // id графика
                          int             window_number, // номер окна
                          string          object_name,   // имя объекта
                          double          price,         // уровень цены
                          int             line_width,    // толщина линии
                          ENUM_LINE_STYLE line_style,    // стиль линии
                          color           line_color,    // цвет линии
                          bool            selectable,    // нельзя выделить объект, если FALSE
                          bool            selected,      // линия выбрана
                          bool            back,          // фоновое расположение
                          string          tooltip)       // нет всплывающей подсказки, если "\n"
  {
//--- Если объект успешно создан
   if(ObjectCreate(chart_id,object_name,OBJ_HLINE,window_number,0,price))
     {
      //--- установим ему свойства
      ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTABLE,selectable);
      ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTED,selected);
      ObjectSetInteger(chart_id,object_name,OBJPROP_BACK,back);
      ObjectSetInteger(chart_id,object_name,OBJPROP_STYLE,line_style);
      ObjectSetInteger(chart_id,object_name,OBJPROP_WIDTH,line_width);
      ObjectSetInteger(chart_id,object_name,OBJPROP_COLOR,line_color);
      ObjectSetString(chart_id,object_name,OBJPROP_TOOLTIP,tooltip);
     }
  }

Функции для удаления графических объектов:

//+------------------------------------------------------------------+
//| Удаляет уровни                                                   |
//+------------------------------------------------------------------+
void DeleteLevels()
  {
   for(int i=0; i<LEVELS_COUNT; i++)
      DeleteObjectByName(prefix+"level_0"+(string)(i+1)+"");
  }
//+------------------------------------------------------------------+
//| Удаляет вертикальные линии начала серий                          |
//+------------------------------------------------------------------+
void DeleteVerticalLines()
  {
   for(int s=0; s<SYMBOLS_COUNT; s++)
      DeleteObjectByName(prefix+symbol_names[s]+": begin time series");
  }
//+------------------------------------------------------------------+
//| Удаляет объект по имени                                          |
//+------------------------------------------------------------------+
void DeleteObjectByName(string object_name)
  {
//--- Если есть такой объект
   if(ObjectFind(0,object_name)>=0)
     {
      //--- Если была ошибка при удалении, сообщим об этом
      if(!ObjectDelete(0,object_name))
         Print("Ошибка ("+IntegerToString(GetLastError())+") при удалении объекта!");
     }
  }

В функции OnDeinit() нужно добавить вот такой код:

//+------------------------------------------------------------------+
//| Деинициализация                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(reason==REASON_REMOVE      || // Если индикатор удален с графика или
      reason==REASON_CHARTCHANGE || // символ или период был изменен или
      reason==REASON_RECOMPILE   || // программа была перекомпилирована или
      reason==REASON_CHARTCLOSE  || // график был закрыт или
      reason==REASON_CLOSE       || // терминал был закрыт или
      reason==REASON_PARAMETERS)    // параметры были изменены
     {
      //--- Отключим таймер
      EventKillTimer();
      //--- Удалим уровни
      DeleteLevels();
      //--- Удалим вертикальные линии
      DeleteVerticalLines();
      //--- Удалим холст
      DeleteCanvas();
      //--- Освободим расчетную часть индикатора
      for(int s=0; s<SYMBOLS_COUNT; s++)
         IndicatorRelease(symbol_handles[s]);
      //--- Очистим комментарий
      Comment("");
     }
//--- Обновим график
   ChartRedraw();
  }

Теперь все готово и можно провести тщательные тесты. Максимальное количество баров в окне можно установить в настройках терминала на вкладке Графики. От количества баров в окне зависит насколько быстро индикатор будет готов к работе.

Рис. 1 - Установка максимального количество баров в настройках терминала

Рис. 1 - Установка максимального количество баров в настройках терминала

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

При загрузке индикатора на график можно увидеть, как поочередно подготавливаются данные для всех символов с процентом прогресса:

Рис. 2 - Сообщение на холсте во время подготовки данных

Рис. 2 - Сообщение на холсте во время подготовки данных

Ниже показан скриншот, как выглядит индикатор на 20-ти минутном таймфрейме:

Рис. 3 - Мультивалютный индикатор ATR на 20-ти минутном таймфрейме

Рис. 3 - Мультивалютный индикатор ATR на 20-ти минутном таймфрейме

Начало "истинных" баров отмечается на графике вертикальными линиями. На скриншоте ниже видно, что для валютной пары NZDUSD (желтая линия) "истинные" бары начинаются с 2000 года (сервер MetaQuotes-Demo), для всех остальных - с начала 1999 года, поэтому видна только одна линия (все на одной дате). Видно также, что разделители периодов до 1999 года имеют меньший интервал, и если проанализировать время баров, то можно убедиться, что это дневные бары.

Рис. 4 - Вертикальными линиями отмечено начало истинных баров для каждого символа

 

Заключение

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

Прикрепленные файлы |
multisymbolatr.mq5 (46.98 KB)
Dennis Kirichenko
Dennis Kirichenko | 25 дек 2013 в 16:15
Анатолий, спасибо за статью. Очень интересный подход к волатильности. Почерпнул новые идеи :-)
Иван Корнилов
Иван Корнилов | 17 май 2014 в 04:22
Огромнейшие спасибо за статью, нашел в ней именно то что искал, и спасибо MQ, за то, что освещаете такие тонкие темы, как мультивалютный анализ.
M1k3
M1k3 | 22 июл 2014 в 07:23

Замечательно, но у меня такая картина в тестере и реале даже после того как shiftbars был задан вручную и индикатор поменян на расчеты по PRICE_CLOSE, снизу Ваш:

 scr

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

Anatoli Kazharski
Anatoli Kazharski | 22 июл 2014 в 07:43
M1k3:

Замечательно, но у меня такая картина в тестере даже после того как shiftbars был задан вручную, снизу Ваш:

... 

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

Именно в таком виде я этот индикатор в тестере даже и не тестировал. В нём много лишнего для тестера. В статье про это также ничего не сказано.

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

Разбираться нужно. У меня пока нет ответа. 

Основы программирования на MQL5 - Списки Основы программирования на MQL5 - Списки

Новая версия языка программирования торговых стратегий - MQL [MQL5] - имеет более эффективный и мощный инструментарий по сравнению с предыдущей [MQL4]. И это преимущество прежде всего относится к средствам объектно-ориентированного программирования. В данной статье рассматривается возможность использования такого пользовательского типа данных, относящегося к сложному, как узлы и списки. Приводится пример использования списков при программировании практических задач в MQL5.

Работа с GSM-модемом из эксперта на MQL5 Работа с GSM-модемом из эксперта на MQL5

На текущий момент существует достаточно средств для комфортного удалённого мониторинга торгового счёта: мобильные терминалы, push-уведомления, работа с ICQ. Но всё это требует обязательного наличия интернета. Данная статья описывает создание эксперта, который позволит вам находиться на связи с торговым терминалом даже в той ситуации, когда мобильный интернет будет недоступен, а именно - при помощи звонков и SMS-сообщений.

Создание цифровых фильтров, незапаздывающих по времени Создание цифровых фильтров, незапаздывающих по времени

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

SQL и MQL5: Работаем с базой данных SQLite SQL и MQL5: Работаем с базой данных SQLite

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