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

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

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

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

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

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

//+------------------------------------------------------------------+
//|                                                       ATR_MS.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  6 // Количество уровней
#define SYMBOLS 6 // Количество символов
//---

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

Чтобы подключить к проекту файл с классом для работы с канвой из Стандартной библиотеки, нужно написать вот такую строчку кода:

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

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

//--- Внешние параметры
input  int              IndicatorPeriod=14;       // Indicator Period
sinput string dlm01=""; //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
input  string           Symbol02       ="GBPUSD"; // Symbol 02
input  string           Symbol03       ="AUDUSD"; // Symbol 03
input  string           Symbol04       ="NZDUSD"; // Symbol 04
input  string           Symbol05       ="USDCAD"; // Symbol 05
input  string           Symbol06       ="USDCHF"; // Symbol 06
sinput string dlm02=""; //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
input  int              Level01        =10;       // Level 01
input  int              Level02        =50;       // Level 02
input  int              Level03        =100;      // Level 03
input  int              Level04        =200;      // Level 04
input  int              Level05        =400;      // Level 05
input  int              Level06        =600;      // Level 06
//---

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

//--- Глобальные переменные и массивы
CCanvas           canvas;                     // Загрузка класса
//--- Переменные/массивы для копирования данных из OnCalculate()
int               on_calc_rates_total     =0; // Размер входных таймсерий
int               on_calc_prev_calculated =0; // Обработано баров на предыдущем вызове
datetime          on_calc_time[];             // Время открытия
double            on_calc_open[];             // Цены открытия
double            on_calc_high[];             // Максимальные цены
double            on_calc_low[];              // Минимальные цены
double            on_calc_close[];            // Цены закрытия
long              on_calc_tick_volume[];      // Тиковые объёмы
long              on_calc_volume[];           // Реальные объёмы
int               on_calc_spread[];           // Спред
//--- Структура массивов буферов
//    для отрисовки значений индикатора
struct buffers { double data[]; };
buffers           buffer_atr[SYMBOLS];
//--- Структуры массивов для подготовки данных
struct temp_time { datetime time[]; };
temp_time         temp_symbol_time[SYMBOLS];
struct temp_atr { double value[]; };
temp_atr          temp_atr_values[SYMBOLS];
//--- Для хранения и проверки времени первого бара в терминале
datetime          series_first_date[SYMBOLS];
datetime          series_first_date_last[SYMBOLS];
//--- Массив времени бара, от которого начинать отрисовку
datetime          limit_time[SYMBOLS];
//--- Массив уровней индикатора
int               levels[LEVELS];
//--- Массив названий символов
string            symbols_names[SYMBOLS];
//--- Хэндлы символов
int               handle_symbol[SYMBOLS];
//--- Цвета линий индикатора
color             line_color[SYMBOLS]=
  {clrRed,clrDodgerBlue,clrLimeGreen,clrGold,clrAqua,clrMagenta};
//--- Строка символизирующая отсутствие символа
string            empty_symbol            ="EMPTY";
//--- Свойства подокна индикатора
int               number_subwindow        =WRONG_VALUE;             // Номер подокна
int               chart_width             =0;                       // Ширина графика
int               subwindow_height        =0;                       // Высота подокна
int               last_chart_width        =0;                       // Последняя в памяти ширина графика
int               last_subwindow_height   =0;                       // Последняя в памяти высота подокна
int               subwindow_center        =0;                       // Центр подокна по горизонтали
int               subwindow_vcenter       =0;                       // Центр подокна по вертикали
string            shortname_subwindow     ="ATR_MS";                // Короткое имя индикатора
string            prefix                  =shortname_subwindow+"_"; // Префикс для объектов
//--- Свойства канвы
string            canvas_name             =prefix+"canvas";         // Название канвы
color             canvas_bg_color         =clrBlack;                // Цвет фона канвы
uchar             canvas_opacity          =190;                     // Степень прозрачности
int               font_size               =16;                      // Размер шрифта
string            font_name               ="Calibri";               // Шрифт
ENUM_COLOR_FORMAT clr_format              =COLOR_FORMAT_ARGB_RAW;   // Компоненты цвета не обрабатываются
//--- Сообщения канвы
string            message_invalid_handle  ="Невалидный хэндл индикатора! Подождите пожалуйста...";
string            message_data_available  ="Подготовка данных! Подождите пожалуйста...";
string            message_number_data     ="Данных меньше, чем период индикатора! Попробуйте уменьшить значение.";
string            message_is_synchronized ="Данные не синхронизированы! Подождите пожалуйста...";
string            message_formation_data  ="";
string            message_synchro_update  ="";
string            message_last            ="";
//--- Максимальное количество баров установленное в настройках терминала
int               terminal_max_bars       =0;
//---

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

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Проверим корректность входных параметров
   if(!CheckInputParameters())
     return(INIT_PARAMETERS_INCORRECT);
//--- Включим таймер с интервалом 1 секунда
   EventSetMillisecondTimer(1000);
//--- Установим шрифт для отображения в канве
   canvas.FontSet(font_name,font_size,FW_NORMAL);
//--- Инициализация массивов
   InitArrays();
//--- Инициализируем массив символов 
   InitSymbolsNames();
//--- Инициализируем массив уровней
   InitLevels();
//--- Получим хэндлы индикаторов
   GetHandles();
//--- Установим свойства индикатора
   SetPropertiesIndicator();
//--- Получим количество баров установленное в настройках терминала
   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(), InitSymbolsNames() и InitLevels(). Ниже представлен их код:

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

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

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

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

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

//+------------------------------------------------------------------+
//| Получает хэндлы                                                  |
//+------------------------------------------------------------------+
bool GetHandles()
  {
//--- Признак того, что все хэндлы валидны
   bool invalid=true;
//--- Пройдёмся в цикле по всем символам и ...
   for(int s=0; s<SYMBOLS; s++)
     {
      //--- Если символ есть и ...
      if(symbols_names[s]!=empty_symbol)
        {
         // ...хэндл текущего таймфрейма невалиден, получим его
         if(handle_symbol[s]==INVALID_HANDLE)
           {
            handle_symbol[s]=iATR(symbols_names[s],Period(),IndicatorPeriod);
            //--- Если не удалось получить хэндл
            if(handle_symbol[s]==INVALID_HANDLE)
               invalid=false; // Попробуем в следующий раз
           }
        }
     }
//--- Выведем сообщение, если хэндл
//    для одного из символов не получен
   if(!invalid)
     {
      message_last=message_invalid_handle;
      ShowCanvasMessage(message_invalid_handle);
     }
//---
   return(invalid);
  }
//---

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

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

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

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

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

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

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

Ниже представлен код функции CopyDataOnCalculate():

//+------------------------------------------------------------------+
//| Копирует данные из 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[])
  {
   on_calc_rates_total=rates_total;
   on_calc_prev_calculated=prev_calculated;
   ArrayCopy(on_calc_time,time);
   ArrayCopy(on_calc_open,open);
   ArrayCopy(on_calc_high,high);
   ArrayCopy(on_calc_low,low);
   ArrayCopy(on_calc_close,close);
   ArrayCopy(on_calc_tick_volume,tick_volume);
   ArrayCopy(on_calc_volume,volume);
   ArrayCopy(on_calc_spread,spread);
  }
//---

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

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

//+------------------------------------------------------------------+
//| Изменяет размер массивов под размер основного массива            |
//+------------------------------------------------------------------+
void ResizeCalculateArrays()
  {
   for(int s=0; s<SYMBOLS; s++)
     {
      ArrayResize(temp_symbol_time[s].time,on_calc_rates_total);
      ArrayResize(temp_atr_values[s].value,on_calc_rates_total);
     }
  }
//---

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

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

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

//+------------------------------------------------------------------+
//| Обнуление индикаторных буферов                                   |
//+------------------------------------------------------------------+
void ZeroIndicatorBuffers()
  {
   for(int s=0; s<SYMBOLS; s++)
      ArrayInitialize(buffer_atr[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);
//--- Установим размер массивам для подготовки данных
   ResizeCalculateArrays();
//--- Если это первый расчёт или
//    загружена более глубокая история или
//    были заполнены пропуски истории
   if(prev_calculated==0)
     {
      //--- Обнулим массивы для подготовки данных
      ZeroCalculateArrays();
      //--- Обнулим индикаторные буферы
      ZeroIndicatorBuffers();
      //--- Остальные проверки
      // ...
      //--- Если дошли до этого момента, то
      //    значит OnCalculate() вернёт ненулевое значение и
      //    это нужно запомнить
      on_calc_prev_calculated=rates_total;
     }
//--- Если нужно пересчитать только последние значения
   else
      limit=prev_calculated-1;
      
//--- Подготовим данные для отрисовки
// ...
//--- Заполним массивы данными для отрисовки
// ...

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

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

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- Если по какой-то причине расчёты не были завершены или
//    подкачана более глубокая история или
//    были заполнены пропуски истории, то
//    не дожидаясь тика сделаем ещё одну попытку
   if(on_calc_prev_calculated==0)
     {
      OnCalculate(on_calc_rates_total,on_calc_prev_calculated,
                  on_calc_time,on_calc_open,on_calc_high,on_calc_low,on_calc_close,
                  on_calc_tick_volume,on_calc_volume,on_calc_spread);
     }
  }
//---

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

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

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

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

//+------------------------------------------------------------------+
//| Установим свойства подокна                                       |
//+------------------------------------------------------------------+
void SetSubwindowProperties()
  {
//--- Получим номер подокна индикатора
   number_subwindow=ChartWindowFind(0,shortname_subwindow);
//--- Получим ширину и высоту подокна
   chart_width=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS);
   subwindow_height=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,number_subwindow);
//--- Получим центр подокна
   subwindow_center=chart_width/2;
   subwindow_vcenter=subwindow_height/2;
  }
//---

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

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

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

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

//+------------------------------------------------------------------+
//| Следит за размерами канвы                                        |
//+------------------------------------------------------------------+
void EventChartChange()
  {
//--- Получим свойства подокна
   SetSubwindowProperties();
//--- Если размеры подокна не изменились, выйдем
   if(!CheckSubwindowSize())
      return;
//--- Если размер подокна меньше одного пикселя или
//    центр рассчитан некорректно, выйдем
   if(subwindow_height<1 || subwindow_vcenter<1)
      return;
//--- Установим новый размер канве
   ResizeCanvas();
//--- Покажем последнее сообщение
   ShowCanvasMessage(message_last);
  }
//---

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

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

//+------------------------------------------------------------------+
//| Проверим размеры подокна                                         |
//+------------------------------------------------------------------+
bool CheckSubwindowSize()
  {
//--- Если размеры подокна не изменились, выйдем
   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);
  }
//---

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

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

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

//+------------------------------------------------------------------+
//| Выводит сообщение в канве                                        |
//+------------------------------------------------------------------+
void ShowCanvasMessage(string message_text)
  {
   SetSubwindowProperties();
//--- Если канва есть в подокне индикатора
   if(ObjectFind(0,canvas_name)==number_subwindow)
     {
      //--- Отобразим сообщение,
      //    если передана не пустая строка и координаты получены
      if(message_text!="" && subwindow_center>0 && subwindow_vcenter>0)
        {
         canvas.Erase(ColorToARGB(canvas_bg_color,canvas_opacity));
         canvas.TextOut(subwindow_center,subwindow_vcenter,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_bg_color,(uchar)i));
         canvas.Update();
        }
      //--- Удаление канвы
      canvas.Destroy();
     }
  }
//---

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

//+------------------------------------------------------------------+
//| Загружает и формируем необходимое/имеющееся кол-во данных        |
//+------------------------------------------------------------------+
void LoadFormationData()
  {
   int number_bars=100; // Количество подгружаемых баров
//---
   for(int s=0; s<SYMBOLS; s++)
     {
      int      count_try        =0;    // Счётчик попыток копирования данных
      int      array_size       =0;    // Размер массива
      datetime server_firstdate =NULL; // Время первого бара на сервере
      datetime series_firstdate =NULL; // Время первого бара в базе терминала
      //--- Получим первую дату символа-периода в базе терминала
      SeriesInfoInteger(symbols_names[s],Period(),SERIES_FIRSTDATE,series_firstdate);
      //--- Получим первую дату символа-периода на сервере
      SeriesInfoInteger(symbols_names[s],Period(),SERIES_SERVER_FIRSTDATE,server_firstdate);
      //--- Выведем сообщение
      message_last=message_formation_data="Процесс загрузки и формирования данных: "+
                   symbols_names[s]+"("+IntegerToString(s+1)+"/"+IntegerToString(SYMBOLS)+") ... ";
      ShowCanvasMessage(message_formation_data);
      //--- Загрузим/сформируем данные,
      //    если размер массива меньше, чем максимальное количество баров в терминале, а также
      //    между первой датой серии в терминале и первой датой серии на сервере
      //    больше указанного количества баров
      while(array_size<on_calc_rates_total && 
            series_firstdate-server_firstdate>PeriodSeconds()*number_bars)
        {
         datetime copied_time[];
         //--- Получим первую дату символа-периода в базе терминала
         SeriesInfoInteger(symbols_names[s],Period(),SERIES_FIRSTDATE,series_firstdate);
         //--- Загрузим/скопируем ещё указанное количество баров
         if(CopyTime(symbols_names[s],Period(),0,array_size+number_bars,copied_time))
           {
            //--- Если время первого бара массива с вычетом кол-ва подгружаемых баров раньше, 
            //    чем время первого бара на графике, остановим цикл
            if(copied_time[0]-PeriodSeconds()*number_bars<on_calc_time[0])
               break;
            //--- Если размер массива не увеличился,
            //    увеличим счётчик
            if(ArraySize(copied_time)==array_size)
               count_try++;
            //--- Иначе получим текущий размер массива
            else
               array_size=ArraySize(copied_time);
            //--- Если размер массива не увеличивается в течении
            //    100 попыток, остановим цикл
            if(count_try==100)
              {
               count_try=0;
               break;
              }
           }
         //--- Каждые 2000 баров проверяем размеры подокна
         //    и если размер изменился подгоним под него размер канвы
         if(!(array_size%2000))
            EventChartChange();
        }
     }
  }
//---

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

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

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

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

//+------------------------------------------------------------------+
//| Проверка события загрузки более глубокой истории                 |
//+------------------------------------------------------------------+
bool CheckEventLoadHistory()
  {
   bool load=false;
//---
   for(int s=0; s<SYMBOLS; s++)
     {
      //--- Если такой символ есть
      if(symbols_names[s]!=empty_symbol)
        {
         //--- Если нужно обновить серии
         if(on_calc_prev_calculated==0)
           {
            //--- Получим первую дату символа-периода
            series_first_date[s]=(datetime)SeriesInfoInteger(symbols_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(symbols_names[s],Period(),SERIES_FIRSTDATE);
            //--- Если даты отличаются, то есть дата в памяти более поздняя,
            //    чем та, которую получили сейчас, то
            //    значит была загрузка более глубокой истории
            if(series_first_date_last[s]>series_first_date[s])
              {
               //--- Выведем сообщение в журнал
               Print("(",symbols_names[s],",",TimeframeToString(Period()),
                     ") > Была загружена/сформирована более глубокая история: ",
                     series_first_date_last[s]," > ",series_first_date[s]);
               //--- Запомним дату
               series_first_date_last[s]=series_first_date[s];
               load=true;
              }
           }
        }
     }
//--- Если была загружена/сформирована более глубокая история, то
//    отправим команду на обновление графических серий индикатора
   if(load)
      return(false);
//---
   return(true);
  }
//---

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

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

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

//+------------------------------------------------------------------+
//| Определение времени первого истинного бара для отрисовки         |
//+------------------------------------------------------------------+
bool DetermineBeginForCalculate()
  {
   for(int s=0; s<SYMBOLS; s++)
     {
      datetime time[];              // Массив времени баров
      int      total_period_bars=0; // Количество баров
      //--- Если такого символа нет, перейти к следующему
      if(symbols_names[s]==empty_symbol)
         continue;
      //--- Получим общее количество баров символа
      total_period_bars=Bars(symbols_names[s],Period());
      //--- Скопируем массив времени баров.
      //    Если не получилось, попробуем ещё раз
      if(CopyTime(symbols_names[s],Period(),0,total_period_bars,time)<total_period_bars)
         return(false);
      //--- Получим время первого истинного бара,
      //    который соответствует текущему таймфрейму
      limit_time[s]=GetFirstTruePeriodBarTime(time);
      //--- Установим вертикальную линию на истинном баре
      CreateVerticalLines(0,0,limit_time[s],prefix+symbols_names[s]+": begin time series",
                          2,STYLE_SOLID,line_color[s],false,TimeToString(limit_time[s]),"\n");
     }
//---
   return(true);
  }
//+------------------------------------------------------------------+
//| Возвращает время первого истинного бара текущего периода         |
//+------------------------------------------------------------------+
datetime GetFirstTruePeriodBarTime(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);
  }
//+------------------------------------------------------------------+
//| Создание вертикальной линии                                      |
//+------------------------------------------------------------------+
void CreateVerticalLines(long            chart_id,         // id графика
                         int             number_window,    // номер окна
                         datetime        time,             // время
                         string          name_line,        // имя объекта
                         int             width_line,       // толщина линии
                         ENUM_LINE_STYLE style_line,       // стиль линии
                         color           color_line,       // цвет линии
                         bool            selectable,       // нельзя выделить объект, если FALSE
                         string          description_text, // текст описания
                         string          tooltip)          // нет всплывающей подсказки, если "\n"
  {
//--- Если объект успешно создан...
   if(ObjectCreate(chart_id,name_line,OBJ_VLINE,number_window,time,0))
     {
      // ...установим ему свойства
      ObjectSetInteger(chart_id,name_line,OBJPROP_TIME,time);
      ObjectSetInteger(chart_id,name_line,OBJPROP_SELECTABLE,selectable);
      ObjectSetInteger(chart_id,name_line,OBJPROP_STYLE,style_line);
      ObjectSetInteger(chart_id,name_line,OBJPROP_WIDTH,width_line);
      ObjectSetInteger(chart_id,name_line,OBJPROP_COLOR,color_line);
      ObjectSetString(chart_id,name_line,OBJPROP_TEXT,description_text);
      ObjectSetString(chart_id,name_line,OBJPROP_TOOLTIP,tooltip);
     }
  }
//---

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

//--- Если это первый расчёт или
//    загружена более глубокая история или
//    были заполнены пропуски истории
   if(prev_calculated==0)
     {
      //--- Обнулим массивы для подготовки данных
      ZeroCalculateArrays();
      //--- Обнулим индикаторные буферы
      ZeroIndicatorBuffers();
      //--- Получим свойства подокна
      SetSubwindowProperties();
      //--- Установим канву
      SetCanvas();
      //--- Загрузим и сформируем необходимое/имеющееся количество данных
      LoadFormationData();
      //--- Если есть невалидный хэндл,
      //    попробуем получить его снова
      if(!GetHandles())
         return(RESET);
      //--- Проверяет количество доступных данных у всех символов
      if(!CheckAvailableData())
         return(RESET);
      //--- Если загружена более глубокая история
      if(!CheckEventLoadHistory())
         return(RESET);
      //--- Проверим синхронизированность данных по символу/периоду на данный момент
      if(!CheckSymbolIsSynchronized())
         return(RESET);
      //--- Определим для каждого символа,
      //    с какого бара начинать отрисовку
      if(!DetermineBeginForCalculate())
         return(RESET);
      //--- Если дошли до этого момента, то
      //    значит OnCalculate() вернёт ненулевое значение и
      //    это нужно запомнить
      on_calc_prev_calculated=rates_total;
     }
//---

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

//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- Если была загружена более глубокая история
   if(!CheckEventLoadHistory())
      on_calc_prev_calculated=0;
//--- Если по какой-то причине расчёты не были завершены или
//    подкачана более глубокая история или
//    были заполнены пропуски истории, то
//    не дожидаясь тика сделаем ещё одну попытку
   if(on_calc_prev_calculated==0)
     {
      OnCalculate(on_calc_rates_total,on_calc_prev_calculated,
                  on_calc_time,on_calc_open,on_calc_high,on_calc_low,on_calc_close,
                  on_calc_tick_volume,on_calc_volume,on_calc_spread);
     }
  }
//---

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

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

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

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

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

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

//+------------------------------------------------------------------+
//| Процент прогресса                                                |
//+------------------------------------------------------------------+
void PercentProgress(int i,int symbols_number,double &percent)
  {
   string message_text="";
   percent=(double(i)/on_calc_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="99";
//---
   message_last=message_synchro_update="Подготовка данных ("+IntegerToString(on_calc_rates_total)+" баров) : "+
                symbols_names[symbols_number]+
                "("+IntegerToString(symbols_number+1)+"/"+IntegerToString(SYMBOLS)+") - "+message_text+"% ... ";
  }
//---

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

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

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

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

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

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

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

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

//+------------------------------------------------------------------+
//| Установка уровней                                                |
//+------------------------------------------------------------------+
void SetLevels()
  {
//--- Получим номер подокна индикатора
   number_subwindow=ChartWindowFind(0,shortname_subwindow);
//--- Установим уровни
   for(int s=0; s<LEVELS; s++)
      CreateHorizontalLines(0,number_subwindow,
                            prefix+"level_0"+IntegerToString(s+1)+"",
                            CorrectValueBySymbolDigits(levels[s]*_Point),
                            1,STYLE_DOT,clrLightSteelBlue,false,false,false,"\n");
  }
//+------------------------------------------------------------------+
//| Коррекция значения по количеству знаков в цене (double)          |
//+------------------------------------------------------------------+
double CorrectValueBySymbolDigits(double value)
  {
   return(_Digits==3 || _Digits==5) ? value*=10 : value;
  }
//---

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

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

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

//+------------------------------------------------------------------+
//| Удаляет уровни                                                   |
//+------------------------------------------------------------------+
void DeleteLevels()
  {
   for(int i=0; i<LEVELS; i++)
      DeleteObjectByName(prefix+"level_0"+IntegerToString(i+1)+"");
  }
//+------------------------------------------------------------------+
//| Удаляет вертикальные линии начала серий                          |
//+------------------------------------------------------------------+
void DeleteVerticalLines()
  {
   for(int s=0; s<SYMBOLS; s++)
      DeleteObjectByName(prefix+symbols_names[s]+": begin time series");
  }
//+------------------------------------------------------------------+
//| Удаляет объект по имени                                          |
//+------------------------------------------------------------------+
void DeleteObjectByName(string Name)
  {
//--- Если есть такой объект
   if(ObjectFind(0,Name)>=0)
     {
      //--- Если была ошибка при удалении, сообщим об этом
      if(!ObjectDelete(0,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; s++)
         IndicatorRelease(handle_symbol[s]);
      //--- Очистим комментарий
      Comment("");
     }
//--- Обновим график
   ChartRedraw();
  }
//---

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

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

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

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

Сообщение на канве во время подготовки данных

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

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

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

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

Кстати, быстро найти тот или иной объект на графике можно открыв Список объектов нажав одновременно клавиши (Ctrl+B), затем нужно открыть список объектов, которые были созданы программно (кнопка Все) и выделив интересующий объект нажать кнопку Показать. Это удобно.

На этом статью можно закончить. Скачать код, который описывался в статье можно ниже. В одной из будущих статей попробуем реализовать торговую систему, в которой анализируется волатильность и посмотрим, что из этого получится. Посмотрите также ещё одну версию подобного индикатора в статье Мультисимвольный индикатор волатильности ATR MULTI SYMBOLS. Там показан пример того, какие ещё можно реализовать возможности в индикаторе, а в конце статьи показано видео.




Скачать исходный код индикатора ATR_MS.mq5


Комментариев нет :

Отправить комментарий