Разработка мультивалютного индикатора для анализа расхождения цен

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

В дополнение рассмотрим такие вопросы, как: изменение свойств графика, обработка событий CHARTEVENT_OBJECT_DRAG (перетаскивание графического объекта) и CHARTEVENT_CHART_CHANGE (изменение размеров графика или изменение свойств графика через диалог свойств); тип данных для отрисовки индикаторных буферов более, чем одним цветом; определение максимумов и минимумов в индикаторных буферах в зоне видимости для установки максимума/минимума графика; инверсия ряда.

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

- Checks.mqh - функции для различных проверок. В основном для проверки и загрузки доступных данных.
- SettingChart.mqh - функции для управления графическими объектами.
- SetDeleteObjects.mqh - функции для управления свойствами графика.

Функции, которые не подходят  к перечисленным выше категориям оставим в главном файле.

Далее приступим к программированию индикатора. Сначала нужно создать новый проект. Для этого создайте в директории Metatrader 5\MQL5\Indicators папку с именем индикатора, а в ней папку Include, в которой будут размещаться подключаемые файлы. Далее создайте главный файл в папке индикатора. Это можно сделать вручную, создав текстовый файл с расширением *.mq5 или с помощью Мастера MQL5 с уже готовым шаблоном. Кроме основных функций программы OnInit(), OnDeinit() и OnCalculate(), будут ещё использоваться OnChartEvent() и OnTimer().

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

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

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

Ниже показан код с перечислением специфических параметров:

//---
#property indicator_chart_window // Выводить индикатор в окно графика
#property indicator_buffers 25   // Количество буферов для расчёта индикатора
#property indicator_plots   5    // Количество графических серий
//--- Цвета цветовых буферов
#property indicator_color1  clrDodgerBlue,C'0,50,100'
#property indicator_color2  clrLimeGreen,C'20,80,20'
#property indicator_color3  clrGold,C'160,140,0'
#property indicator_color4  clrAqua,C'0,140,140'
#property indicator_color5  clrMagenta,C'130,0,130'
//---

С помощью директивы #define объявим константы, а с помощью командной строки #include подключим файлы с функциями, о которых уже было написано выше и класс для работы с канвой из Стандартной библиотеки:

//--- Константа для возврата терминалу команды на пересчёт индикатора
#define RESET 0
//--- Количество символов
#define SYMBOLS 5
//--- Имя программы
#define PROGRAM_NAME MQL5InfoString(MQL5_PROGRAM_NAME)

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

Для создания выпадающих списков во внешних параметрах для выбора типа отрисовки и режима начальной точки расхождения цен, создадим перечисления ENUM_DRAWTYPE и ENUM_START_POINT:

//--- Тип рисования данных
enum ENUM_DRAWTYPE
  {
   LINES   =0, // Lines
   BARS    =1, // Bars
   CANDLES =2  // Candles
  };
//--- Режим начальной точки расхождения цен
enum ENUM_START_POINT
  {
   VERT_LINE =0, // Vertical Line
   MONTH     =1, // Month
   WEEK      =2, // Week
   DAY       =3, // Day
   HOUR      =4  // Hour
  };
//---

О типах отображения данных уже рассказывалось выше, теперь немного подробнее о том, что означает начальная точка расхождения цен. Всего создадим пять режимов: Vertical Line (вертикальная линия), Month (месяц), Week (неделя), Day (день) и Hour (час). При выборе режима Vertical Line при загрузке индикатора на график, будет установлена вертикальная линия, которую можно будет перемещать, и которая будет указывать программе бар, на котором цены всех символов будут смыкаться в одну точку. Этой точкой опоры будет уровень цены открытия указанного бара текущего символа. Любой другой режим будет указывать программе, что цены должны смыкаться каждый раз в начале указанного периода. То есть, в начале каждого месяца, в начале каждой недели, в начале каждого дня или в начале каждого часа.

Ниже можно ознакомиться со списком внешних параметров индикатора:

//--- Внешние параметры
input  ENUM_DRAWTYPE    DrawType             =CANDLES;   // Draw Type
input  ENUM_START_POINT StartPriceDivergence =VERT_LINE; // Start Price Divergence
input  bool             TwoColor             =false;     // Two Colored Bars/Candles
sinput string dlm01=""; //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
input  string           Symbol02             ="GBPUSD";  // Symbol 02
input  bool             Inverse02            =false;     // Inverse Symbol 02
input  string           Symbol03             ="AUDUSD";  // Symbol 03
input  bool             Inverse03            =false;     // Inverse Symbol 03
input  string           Symbol04             ="NZDUSD";  // Symbol 04
input  bool             Inverse04            =false;     // Inverse Symbol 04
input  string           Symbol05             ="USDCAD";  // Symbol 05
input  bool             Inverse05            =false;     // Inverse Symbol 05
input  string           Symbol06             ="USDCHF";  // Symbol 06
input  bool             Inverse06            =false;     // Inverse Symbol 06
//---

Нумерация символов начинается с двух, так как первый это текущий символ на графике. Для каждого подключаемого символа можно включить инверсию. То есть, при включенной инверсии, данные символа для которого включена инверсия, будут перевёрнутыми. Это может быть полезным, когда в списке используемых для анализа символов присутствуют такие, когда допустим USD (доллар США), может быть как базовой валютой, так и котируемой. Например, в валютной паре EURUSD, доллар является котируемой валютой, а в валютной паре USDCHF - базовой. В таком случае, если на графике текущий символ EURUSD, то для USDCHF можно включить инверсию, получив таким образом более удобное представление данных для анализа.

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

//--- Структура массивов индикаторных буферов
struct buffers
  {
   double            open[];   // Буфер для цен открытия
   double            high[];   // Буфер для цен максимумов
   double            low[];    // Буфер для цен минимумов
   double            close[];  // Буфер для цен закрытия
   double            icolor[]; // Буфер для определения цвета элемента
  };
buffers           buffer_data[SYMBOLS];
//--- Загрузка класса
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[];           // Спред

//--- Для хранения и проверки времени первого бара в терминале
datetime          series_first_date[SYMBOLS];
datetime          series_first_date_last[SYMBOLS];
//--- Массив времени бара, от которого начинать отрисовку
datetime          limit_time[SYMBOLS];
//--- Массив названий символов
string            symbols_names[SYMBOLS];
//--- Массив названий символов
bool              inverse[SYMBOLS];
//--- Цвета линий индикатора
color             line_color[SYMBOLS]=
  {clrDodgerBlue,clrLimeGreen,clrGold,clrAqua,clrMagenta};
//--- Строка символизирующая отсутствие символа
string            empty_symbol="EMPTY";
//--- Свойства графика
int               number_window              =WRONG_VALUE;               // Номер окна индикатора
int               chart_width                =0;                         // Ширина графика
int               chart_height               =0;                         // Высота графика
int               last_chart_width           =0;                         // Последняя в памяти ширина графика
int               last_chart_height          =0;                         // Последняя в памяти высота графика
int               chart_wcenter              =0;                         // Центр графика по горизонтали
int               chart_vcenter              =0;                         // Центр графика по вертикали
color             color_bar_up               =clrRed;                    // Цвет бара вверх
color             color_bar_down             =C'100,0,0';                // Цвет бара вниз
string            shortname_indicator        ="MS_PriceDivergence";      // Короткое имя индикатора
string            prefix                     =shortname_indicator+"_";   // Префикс для объектов
//--- Имя вертикальной линии начальной точки расхождения цен
string            start_price_divergence=prefix+"start_price_divergence";
//--- Свойства канвы
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_data_available     ="Подготовка данных! Подождите пожалуйста...";
string            message_is_synchronized    ="Данные не синхронизированы! Подождите пожалуйста...";
string            message_formation_data     ="";
string            message_data_update        ="";
string            message_last               ="";
//---
ENUM_TIMEFRAMES   timeframe_start_point      =Period();    // Таймфрейм для точки расхождения цен
datetime          first_period_time          =NULL;        // Время первого указанного периода данных на графике
double            level_divergence           =0.0;         // Цена начальной точки расхождения цен
datetime          time_divergence            =NULL;        // Время начальной точки расхождения цен
double            symbol_difference[SYMBOLS];              // Разница в цене относительно текущего символа
double            inverse_difference[SYMBOLS];             // Разница, которая образуется при расчёте инверсии
//---

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

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

//+------------------------------------------------------------------+
//| Проверка на использование индикатора в тестере                   |
//+------------------------------------------------------------------+
bool CheckTesterMode()
  {
//--- Если индикатор был запущен в тестере, то сообщим, что
//    он не предназначен для использования в тестере
   if(MQL5InfoInteger(MQL5_TESTER) || 
      MQL5InfoInteger(MQL5_VISUAL_MODE) || 
      MQL5InfoInteger(MQL5_OPTIMIZATION))
     {
      Comment("На данный момент индикатор <- "+PROGRAM_NAME+" -> не предназначен для использования в тестере!");
      return(false);
     }
//---
   return(true);
  }
//---

Ещё одна новая функция предназначенная для установки цвета барам/свечам текущего символа - SetBarColors(). Учитывается, включен ли во внешних параметрах двухцветный режим. Ниже можно ознакомиться с кодом функции SetBarColors(). Располагается она в файле SettingChart.mqh.

//+------------------------------------------------------------------+
//| Установим цвета баров текущего символа                           |
//+------------------------------------------------------------------+
void SetBarColors()
  {
//--- Цвет бара вверх, тени и окантовки тела бычьей свечи
   ChartSetInteger(0,CHART_COLOR_CHART_UP,color_bar_up);
//--- Цвет тела бычьей свечи
   ChartSetInteger(0,CHART_COLOR_CANDLE_BULL,color_bar_up);
//--- Цвет линии графика и японских свечей "Доджи"
   ChartSetInteger(0,CHART_COLOR_CHART_LINE,color_bar_up);
//--- Если включен двухцветный режим
   if(TwoColor)
     {
      //--- Цвет бара вниз, тени и окантовки тела медвежьей свечи
      ChartSetInteger(0,CHART_COLOR_CHART_DOWN,color_bar_down);
      //--- Цвет тела медвежьей свечи
      ChartSetInteger(0,CHART_COLOR_CANDLE_BEAR,color_bar_down);
     }
//--- Если двухцветный режим отключен
   else
     {
      //--- Цвет бара вниз, тени и окантовки тела медвежьей свечи
      ChartSetInteger(0,CHART_COLOR_CHART_DOWN,color_bar_up);
      //--- Цвет тела медвежьей свечи
      ChartSetInteger(0,CHART_COLOR_CANDLE_BEAR,color_bar_up);
     }
  }
//---

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

//+------------------------------------------------------------------+
//| Определяет таймфрейм для режима начальной точки цен              |
//+------------------------------------------------------------------+
void InitModeStartPriceDivergence()
  {
//--- Если режим вертикальной линии, выйдем
   if(StartPriceDivergence==VERT_LINE)
      return;
//--- Иначе определим период
   switch(StartPriceDivergence)
     {
      case MONTH : timeframe_start_point=PERIOD_MN1; break;
      case WEEK  : timeframe_start_point=PERIOD_W1;  break;
      case DAY   : timeframe_start_point=PERIOD_D1;  break;
      case HOUR  : timeframe_start_point=PERIOD_H1;  break;
     }
  }
//---

Функция CheckInputParameters() будет отличаться от того, что было в предыдущей статье и теперь выглядит так:

//+------------------------------------------------------------------+
//| Проверяет входные параметры на корректность                      |
//+------------------------------------------------------------------+
bool CheckInputParameters()
  {
//--- Если сейчас режим не вертикальной линии
   if(StartPriceDivergence!=VERT_LINE)
     {
      //--- Если текущий период больше либо равен
      //    указанному для начальной точки расхождения цен, сообщим об этом и выйдем
      if(PeriodSeconds()>=PeriodSeconds(timeframe_start_point))
        {
         Print("Текущий таймфрейм должен быть меньше, чем в параметре Start Price Divergence!");
         Comment("Текущий таймфрейм должен быть меньше, чем в параметре Start Price Divergence!");
         return(false);
        }
     }
//---
   return(true);
  }
//---

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

//+------------------------------------------------------------------+
//| Первая инициализация массивов                                    |
//+------------------------------------------------------------------+
void InitArrays()
  {
   ArrayInitialize(limit_time,NULL);
   ArrayInitialize(symbol_difference,0.0);
   ArrayInitialize(inverse_difference,0.0);
   ArrayInitialize(series_first_date,NULL);
   ArrayInitialize(series_first_date_last,NULL);
//---
   for(int s=0; s<SYMBOLS; s++)
     {
      ArrayInitialize(buffer_data[s].open,EMPTY_VALUE);
      ArrayInitialize(buffer_data[s].high,EMPTY_VALUE);
      ArrayInitialize(buffer_data[s].low,EMPTY_VALUE);
      ArrayInitialize(buffer_data[s].close,EMPTY_VALUE);
      ArrayInitialize(buffer_data[s].icolor,EMPTY_VALUE);
     }
  }
//+------------------------------------------------------------------+
//| Инициализирует массив символов                                   |
//+------------------------------------------------------------------+
void InitSymbolsNames()
  {
   symbols_names[0]=CheckGetSymbol(Symbol02);
   symbols_names[1]=CheckGetSymbol(Symbol03);
   symbols_names[2]=CheckGetSymbol(Symbol04);
   symbols_names[3]=CheckGetSymbol(Symbol05);
   symbols_names[4]=CheckGetSymbol(Symbol06);
  }
//+------------------------------------------------------------------+
//| Инициализирует массив инверсий                                   |
//+------------------------------------------------------------------+
void InitInverse()
  {
   inverse[0]=Inverse02;
   inverse[1]=Inverse03;
   inverse[2]=Inverse04;
   inverse[3]=Inverse05;
   inverse[4]=Inverse06;
  }
//---

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

//+------------------------------------------------------------------+
//| Устанавливает свойства индикатора                                |
//+------------------------------------------------------------------+
void SetPropertiesIndicator()
  {
//--- Установим короткое имя
   IndicatorSetString(INDICATOR_SHORTNAME,shortname_indicator);
//--- Установим количество знаков
   IndicatorSetInteger(INDICATOR_DIGITS,_Digits);
//--- Определим буферы для отрисовки
//    В режиме линия нужен только один буфер отображающий цену открытия
   if(DrawType==LINES)
     {
      for(int s=0; s<SYMBOLS; s++)
         SetIndexBuffer(s,buffer_data[s].close,INDICATOR_DATA);
     }
//--- В других режимах используем все цены для построения 
//    баров/свеч и дополнительный буфер для двухцветного режима
   else if(DrawType==BARS || DrawType==CANDLES)
     {
      for(int s=0; s<SYMBOLS; s++)
        {
         static int number_buffer=0;
         SetIndexBuffer(number_buffer,buffer_data[s].open,INDICATOR_DATA);
         number_buffer++;
         SetIndexBuffer(number_buffer,buffer_data[s].high,INDICATOR_DATA);
         number_buffer++;
         SetIndexBuffer(number_buffer,buffer_data[s].low,INDICATOR_DATA);
         number_buffer++;
         SetIndexBuffer(number_buffer,buffer_data[s].close,INDICATOR_DATA);
         number_buffer++;
         SetIndexBuffer(number_buffer,buffer_data[s].icolor,INDICATOR_COLOR_INDEX);
         number_buffer++;
        }
     }
//--- Установим метки для текущего таймфрейма
//    В режиме линия только цена открытия
   if(DrawType==LINES)
     {
      for(int s=0; s<SYMBOLS; s++)
         PlotIndexSetString(s,PLOT_LABEL,symbols_names[s]+",Close");
     }
//--- В других режимах все цены баров/свеч
//    В качестве разделителя используется ";"
   else if(DrawType==BARS || DrawType==CANDLES)
     {
      for(int s=0; s<SYMBOLS; s++)
        {
         PlotIndexSetString(s,PLOT_LABEL,
                            symbols_names[s]+",Open;"+
                            symbols_names[s]+",High;"+
                            symbols_names[s]+",Low;"+
                            symbols_names[s]+",Close");
        }
     }
//--- Установим тип линий для индикаторных буферов
//    Линия
   if(DrawType==LINES)
      for(int s=0; s<SYMBOLS; s++)
         PlotIndexSetInteger(s,PLOT_DRAW_TYPE,DRAW_LINE);
//--- Бары
   if(DrawType==BARS)
      for(int s=0; s<SYMBOLS; s++)
         PlotIndexSetInteger(s,PLOT_DRAW_TYPE,DRAW_COLOR_BARS);
//--- Свечи
   if(DrawType==CANDLES)
      for(int s=0; s<SYMBOLS; s++)
         PlotIndexSetInteger(s,PLOT_DRAW_TYPE,DRAW_COLOR_CANDLES);

//--- Установим тип линий для данных текущего символа
//    Линия
   if(DrawType==LINES)
      ChartSetInteger(0,CHART_MODE,CHART_LINE);
//--- Бары
   if(DrawType==BARS)
      ChartSetInteger(0,CHART_MODE,CHART_BARS);
//--- Свечи
   if(DrawType==CANDLES)
      ChartSetInteger(0,CHART_MODE,CHART_CANDLES);

//--- Установим толщину линий
   for(int s=0; s<SYMBOLS; s++)
      PlotIndexSetInteger(s,PLOT_LINE_WIDTH,1);
//--- Установим цвет линий для режима Линия
   if(DrawType==LINES)
      for(int s=0; s<SYMBOLS; s++)
         PlotIndexSetInteger(s,PLOT_LINE_COLOR,line_color[s]);
//--- Установим показ данных в Окне Данных только существующих символов
   for(int s=0; s<SYMBOLS; s++)
     {
      if(symbols_names[s]!=empty_symbol)
         PlotIndexSetInteger(s,PLOT_SHOW_DATA,true);
      else
         PlotIndexSetInteger(s,PLOT_SHOW_DATA,false);
     }
//--- Пустое значение для построения, для которого нет отрисовки
   for(int s=0; s<SYMBOLS; s++)
      PlotIndexSetDouble(s,PLOT_EMPTY_VALUE,EMPTY_VALUE);
  }
//---

И наконец ещё одна новая функция для использования в OnInit() это SetLineStartPricesDivergence(). Она устанавливает вертикальную зелёную линию для управления началом расхождения цен в режиме VERT_LINE (см. код ниже).

//+------------------------------------------------------------------+
//| Устанавливает вертикальную линию для начальной точки цен         |
//+------------------------------------------------------------------+
void SetLineStartPricesDivergence()
  {
//--- Если вертикальной линии нет, установим её
   if(StartPriceDivergence==VERT_LINE && 
      ObjectFind(0,start_price_divergence)<0)
      //--- Установим вертикальную линию на истинном баре
      CreateVerticalLines(0,0,TimeCurrent()+PeriodSeconds(),start_price_divergence,
                          2,STYLE_SOLID,clrGreenYellow,true,true,false,"","\n");
//--- Если не в режиме вертикальной линии
   if(StartPriceDivergence!=VERT_LINE)
      DeleteObjectByName(start_price_divergence);
  }
//---

А ниже представлено то, как выглядит всё выше описанное в функции OnInit(). Когда всё распределено по отдельным функциям и файлам, читать программу становится очень просто.

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Проверим, не используется ли сейчас индикатор в тестере
   if(!CheckTesterMode())
      return(INIT_FAILED);
//--- Установим цвет барам/свечам
   SetBarColors();
//--- Определим период для начальной точки расхождения цен
   InitModeStartPriceDivergence();
//--- Проверим корректность входных параметров
   if(!CheckInputParameters())
      return(INIT_PARAMETERS_INCORRECT);
//--- Включим таймер с интервалом 1 секунда
   EventSetMillisecondTimer(1000);
//--- Установим шрифт для отображения в канве
   canvas.FontSet(font_name,font_size,FW_NORMAL);
//--- Инициализация массивов
   InitArrays();
//--- Инициализируем массив символов 
   InitSymbolsNames();
//--- Инициализируем массив инверсий
   InitInverse();
//--- Установим свойства индикатора
   SetPropertiesIndicator();
//--- Установим вертикальную линию начала расхождения цен
   SetLineStartPricesDivergence();
//--- Очистим комментарий
   Comment("");
//--- Обновим график
   ChartRedraw();
//--- Инициализация прошла успешно
   return(INIT_SUCCEEDED);
  }
//---

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

Функции для проверки и загрузки данных я сделал более жёсткими. То есть, каждое значение, которое нужно получить, проходит через указанное количество попыток с остановкой цикла, если значение получено. И так как теперь есть режимы, где понадобится определять начало того или иного периода (месяц, неделя, день, час), то получать время начала периода мы будем через старший таймфрейм. Поэтому была написана дополнительная функция подобная LoadFormationData(), которая имеет похожее название - LoadFormationDataHighTF(). Её код очень похож на аналог, поэтому я не буду его приводить здесь.

А контрольную проверку на доступность данных текущего и старшего таймфрейма я реализовал в одной функции CheckAvailableData():

//+------------------------------------------------------------------+
//| Проверяет количество доступных данных у всех символов            |
//+------------------------------------------------------------------+
bool CheckAvailableData()
  {
   int try=100;
//---
   for(int s=0; s<SYMBOLS; s++)
     {
      //--- Если такой символ есть
      if(symbols_names[s]!=empty_symbol)
        {
         datetime time[];                    // Массив для проверки количества баров
         int      total_period_bars   =0;    // Количество баров текущего периода
         datetime terminal_first_date =NULL; // Первая дата имеющихся данных текущего периода в терминале
         //--- Получим первую дату данных текущего периода в терминале
         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<try; i++)
           {
            //--- Скопируем указанное количество данных
            if(CopyTime(symbols_names[s],Period(),0,total_period_bars,time))
              {
               //--- Если скопировалось нужное количество, остановим цикл
               if(ArraySize(time)>=total_period_bars)
                  break;
              }
           }
         //--- Если скопировано меньше данных
         //    значит нужно совершить ещё одну попытку
         if(ArraySize(time)==0 || ArraySize(time)<total_period_bars)
           {
            message_last=message_data_available;
            ShowCanvasMessage(message_data_available);
            on_calc_prev_calculated=0;
            return(false);
           }
        }
     }
//--- Если в режиме вертикальной линии для начальной точки расхождения цен, выходим
   if(StartPriceDivergence==VERT_LINE)
      return(true);
   else
     {
      datetime time[];                   // Массив для проверки количества баров
      int      total_period_bars  =0;    // Количество баров текущего периода
      datetime terminal_firstdate =NULL; // Первая дата имеющихся данных текущего периода в терминале
      //--- Получим первую дату данных текущего периода в терминале
      for(int i=0; i<try; i++)
         if((terminal_firstdate=(datetime)SeriesInfoInteger(Symbol(),Period(),SERIES_FIRSTDATE))>0)
            break;
      //--- Получим количество доступных баров от указанной даты
      for(int i=0; i<try; i++)
         if((total_period_bars=(int)SeriesInfoInteger(Symbol(),timeframe_start_point,SERIES_BARS_COUNT))>0)
            break;
      //--- Проверим готовность данных баров
      //    Скопируем указанное количество данных
      for(int i=0; i<try; i++)
         if(CopyTime(Symbol(),timeframe_start_point,
                     terminal_firstdate+PeriodSeconds(timeframe_start_point),TimeCurrent(),time)>0)
            break;
      //--- Если скопировано меньше данных
      //    значит нужно совершить ещё одну попытку
      if(ArraySize(time)<=0 || total_period_bars<=0)
        {
         message_last=message_data_available;
         ShowCanvasMessage(message_data_available);
         on_calc_prev_calculated=0;
         return(false);
        }
     }
//---
   return(true);
  }
//---

Функция FillIndicatorBuffers() существенно усложнилась для текущей задачи. Это связано с тем, что теперь есть несколько режимов и для каждого нужно произвести свои действия. В основном всё можно разделить на четыре этапа.

- Получение данных указанного символа.
- Получение данных старшего таймфрейма, и определение времени и ценового уровня, где цены всех символов смыкаются.
- Расчёт значений и заполнение индикаторного буфера.
- Контрольная проверка.

Код функции подробно прокомментирован для изучения:

//+------------------------------------------------------------------+
//| Заполняет индикаторные буферы                                    |
//+------------------------------------------------------------------+
void FillIndicatorBuffers(int i,int s,datetime const &time[])
  {
   MqlRates    rates[];               // Структура данных
   double      period_open[];         // Цена открытия бара в начале периода расхождения цен
   datetime    period_time[];         // Время начала периода расхождения цен
   int         try            =100;   // Количество попыток копирования
   datetime    high_tf_time   =NULL;  // Время бара старшего таймфрейма
   
//--- Если находимся вне зоны "истинных" баров символа, выйдем
   if(time[i]<limit_time[s])
      return;
//--- Сбросим последнюю ошибку
   ResetLastError();
//--- Получим данные текущего бара указанного символа
   for(int j=0; j<try; j++)
      if(CopyRates(symbols_names[s],Period(),time[i],1,rates)==1)
        { ResetLastError(); break; }
//--- Если неудалось получить данные, выйдем
   if(ArraySize(rates)<1 || GetLastError()!=0)
      return;
//--- Если время бара текущего символа на графике
//    неравно времени бара указанного символа, выйдем
   if(time[i]!=rates[0].time)
      return;
//--- Если в режиме вертикальной линии для начальной точки расхождения цен
   if(StartPriceDivergence==VERT_LINE)
     {
      //--- Получим время линии
      time_divergence=(datetime)ObjectGetInteger(0,start_price_divergence,OBJPROP_TIME);
      //--- Получим время первого бара
      first_period_time=time[0];
     }
//--- Если в других режимах, то будем отслеживать начало периода
   else
     {
      //--- Если зашли сюда в первый раз, то
      //    запомним данные первого бара старшего таймфрейма
      if(time_divergence==NULL)
        {
         ResetLastError();
         //--- Получим время открытия первого бара старшего таймфрейма
         for(int j=0; j<try; j++)
            if(CopyTime(Symbol(),timeframe_start_point,time[0]+PeriodSeconds(timeframe_start_point),1,period_time)==1)
              { ResetLastError(); break; }
         //--- Если не удалось получить цену/время, выйдем
         if(ArraySize(period_time)<1 || GetLastError()!=0)
            return;
         //--- Иначе запомним время первого бара старшего таймфрейма
         else
            first_period_time=period_time[0];
        }
      //--- Если время текущего бара на текущем таймфрейме
      //    раньше, чем время первого бара старшего таймфрейма
      if(time[i]<first_period_time)
         high_tf_time=first_period_time;
      //--- Иначе будем получать данные последнего бара старшего таймфрейма
      //    относительно текущего бара текущего таймфрейма
      else
         high_tf_time=time[i];
      //--- Обнулим последнюю ошибку
      ResetLastError();
      //--- Получим цену открытия первого бара старшего таймфрейма
      for(int j=0; j<try; j++)
         if(CopyOpen(Symbol(),timeframe_start_point,high_tf_time,1,period_open)==1)
           { ResetLastError(); break; }
      //--- Получим время открытия первого бара старшего таймфрейма
      for(int j=0; j<try; j++)
         if(CopyTime(Symbol(),timeframe_start_point,high_tf_time,1,period_time)==1)
           { ResetLastError(); break; }
      //--- Если не удалось получить цену/время, выйдем
      if(ArraySize(period_open)<1 || ArraySize(period_time)<1 || GetLastError()!=0)
         return;
      //--- Если время текущего таймфрейма раньше, чем время начала первого периода или
      //    время указанного периода неравно тому, что в памяти
      if(time[i]<first_period_time || time_divergence!=period_time[0])
        {
         symbol_difference[s]  =0.0; // Обнулим разницу цен символов
         inverse_difference[s] =0.0; // Обнулим разницу инверсии
         //--- Запомним время начала расхождения цен
         time_divergence=period_time[0];
         //--- Запомним уровень начала расхождения цен
         level_divergence=period_open[0];
         //--- Установим вертикальную линию в начале периода расхождения цен
         CreateVerticalLines(0,0,period_time[0],start_price_divergence+"_"+TimeToString(time_divergence),
                             2,STYLE_SOLID,clrWhite,false,false,true,TimeToString(time_divergence),"\n");
        }
     }
//--- Если в режиме вертикальной линии и
//    время бара раньше, чем время линии
   if(StartPriceDivergence==VERT_LINE && time[i]<time_divergence)
     {
      //--- Держим нулевые значения разницы
      symbol_difference[s]  =0.0;
      inverse_difference[s] =0.0;
      //--- Используем реальные значения символа
      buffer_data[s].low[i]   =rates[0].low;
      buffer_data[s].open[i]  =rates[0].open;
      buffer_data[s].high[i]  =rates[0].high;
      buffer_data[s].close[i] =rates[0].close;
      //--- Установим цвет текущему элементу индикаторного буфера
      SetColorsIndicatorSeries(i,s,rates[0].close,rates[0].open);
     }
//--- Если в других режимах
   else
     {
      //--- Если нужна инверсия данных символа
      if(inverse[s])
        {
         //--- Если начался новый период, пересчитаем переменные для расчёта
         if(symbol_difference[s]==0.0)
           {
            //--- Если режим вертикальной линии
            if(StartPriceDivergence==VERT_LINE)
              {
               //--- Рассчитаем разницу
               symbol_difference[s]  =rates[0].open-on_calc_open[i];
               inverse_difference[s] =on_calc_open[i]-(-on_calc_open[i]);
              }
            //--- Для других режимов
            else
              {
               //--- Рассчитаем разницу
               symbol_difference[s]  =rates[0].open-level_divergence;
               inverse_difference[s] =level_divergence-(-level_divergence);
              }
           }
         //--- Для режима Линия только цена закрытия
         if(DrawType==LINES)
            buffer_data[s].close[i]=-(rates[0].close-symbol_difference[s])+inverse_difference[s];
         //--- Для других режимов все цены
         else
           {
            buffer_data[s].low[i]   =-(rates[0].low-symbol_difference[s])+inverse_difference[s];
            buffer_data[s].open[i]  =-(rates[0].open-symbol_difference[s])+inverse_difference[s];
            buffer_data[s].high[i]  =-(rates[0].high-symbol_difference[s])+inverse_difference[s];
            buffer_data[s].close[i] =-(rates[0].close-symbol_difference[s])+inverse_difference[s];
            //--- Установим цвет текущему элементу индикаторного буфера
            SetColorsIndicatorSeries(i,s,rates[0].close,rates[0].open);
           }
        }
      //--- Если без инверсии, то в начале периода
      //    нужно вычитать только разницу между ценами символов
      else
        {
         //--- Если начался новый период
         if(symbol_difference[s]==0.0)
           {
            //--- Если режим вертикальной линии
            if(StartPriceDivergence==VERT_LINE)
               symbol_difference[s]=rates[0].open-on_calc_open[i];
            //--- Для других режимов
            else
               symbol_difference[s]=rates[0].open-level_divergence;
           }
         //--- Для режима отрисовки Линия только цена открытия
         if(DrawType==LINES)
            buffer_data[s].close[i]=rates[0].close-symbol_difference[s];
         //--- Для других режимов отрисовки все цены
         else
           {
            buffer_data[s].low[i]   =rates[0].low-symbol_difference[s];
            buffer_data[s].open[i]  =rates[0].open-symbol_difference[s];
            buffer_data[s].high[i]  =rates[0].high-symbol_difference[s];
            buffer_data[s].close[i] =rates[0].close-symbol_difference[s];
            //--- Установим цвет текущему элементу индикаторного буфера
            SetColorsIndicatorSeries(i,s,rates[0].close,rates[0].open);
           }
        }
     }
//--- Контрольная проверка рассчитанных значений
//    Для режима Линия только цена открытия
   if(DrawType==LINES)
     {
      //--- Если текущее время раньше, чем время первого периода или
      //    время бара неравно времени бара текущего символа, запишем пустое значение
      if(time[i]!=rates[0].time || time[i]<first_period_time)
         buffer_data[s].close[i]=EMPTY_VALUE;
     }
//--- Для других режимов все цены
   else
     {
      //--- Если текущее время раньше, чем время первого периода или
      //    время бара неравно времени бара текущего символа
      if(time[i]!=rates[0].time || time[i]<first_period_time)
        {
         //--- Запишем пустое значение
         buffer_data[s].low[i]   =EMPTY_VALUE;
         buffer_data[s].open[i]  =EMPTY_VALUE;
         buffer_data[s].high[i]  =EMPTY_VALUE;
         buffer_data[s].close[i] =EMPTY_VALUE;
        }
     }
  }
//---

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

//+------------------------------------------------------------------+
//| Устанавливает цвет элементу буфера по условию                    |
//+------------------------------------------------------------------+
void SetColorsIndicatorSeries(int i,int symbol_number,double close,double open)
  {
//--- Если включен двухцветный режим, проверим условие
   if(TwoColor)
     {
      //--- Если цена закрытия больше цены открытия, то
      //    это бар вверх и используем первый цвет
      if(close>open)
         buffer_data[symbol_number].icolor[i]=0;
      //--- иначе это бар вниз и используем второй цвет
      else
         buffer_data[symbol_number].icolor[i]=1;
     }
//--- Если одноцветный режим, то используем для всех баров/свечей первый цвет
   else
      buffer_data[symbol_number].icolor[i]=0;
  }
//---

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

- Определение номера первого и последнего видимых баров.
- Определение максимума и минимума видимых баров текущего символа.
- Определение максимума и минимума среди всех массивов символов.
- Установка максимума и минимума в свойствах графика.

Ниже представлен код функции CorrectMaximumMinimumChart(). Она находится в файле SettingChart.mqh.

//+------------------------------------------------------------------+
//| Корректирует максимум/минимум графика относительно всех буферов  |
//+------------------------------------------------------------------+
void CorrectMaximumMinimumChart()
  {
   double low[];                  // Массив минимумов
   double high[];                 // Массив максимумов
   int    try               =10;  // Кол-во попыток
   int    size_array        =0;   // Размер массива для рисования
   int    visible_bars      =0;   // Количество видимых баров
   int    first_visible_bar =0;   // Номер первого видимого бара
   int    last_visible_bar  =0;   // Номер последнего видимого бара
   double max_price         =0.0; // Максимальная цена
   double min_price         =0.0; // Минимальная цена
   double offset_max_min    =0.0; // Отступ от максимума/минимума графика
//--- Обнулим последнюю ошибку
   ResetLastError();
//--- Количество видимых баров
   visible_bars=(int)ChartGetInteger(0,CHART_VISIBLE_BARS);
//--- Номер первого видимого бара
   first_visible_bar=(int)ChartGetInteger(0,CHART_FIRST_VISIBLE_BAR);
//--- Номер последнего видимого бара
   last_visible_bar=first_visible_bar-visible_bars;
//--- Если есть ошибка, выйдем
   if(GetLastError()!=0)
      return;
//--- Если значение некорректно, исправим
   if(last_visible_bar<0)
      last_visible_bar=0;
//--- Получим максимум и минимум текущего символа на видимой части графика
   for(int i=0; i<try; i++)
      if(CopyHigh(Symbol(),Period(),last_visible_bar,visible_bars,high)==visible_bars)
         break;
   for(int i=0; i<try; i++)
      if(CopyLow(Symbol(),Period(),last_visible_bar,visible_bars,low)==visible_bars)
         break;
//--- Выйдем, если данные не были получены
   if(ArraySize(high)<=0 || ArraySize(low)<=0)
      return;
//--- Если данные получены, то
//    определим в массивах текущего символа максимум и минимум
   else
     {
      min_price=low[ArrayMinimum(low)];
      max_price=high[ArrayMaximum(high)];
     }
//--- Получим максимальную и минимальную цены на всех ценовых массивах
   for(int s=0; s<SYMBOLS; s++)
     {
      //--- Если такого символа нет, то перейти к следующему
      if(symbols_names[s]==empty_symbol)
         continue;
      //---
      datetime time[];         // Массив времени
      int      number_bars =0; // Количество баров для расчёта
      //--- Установим нулевой размер массивам
      ArrayResize(high,0);
      ArrayResize(low,0);
      //--- Получим время первого видимого на графике бара
      for(int i=0; i<try; i++)
         if(CopyTime(Symbol(),Period(),last_visible_bar,visible_bars,time)==visible_bars)
            break;
      //--- Если данных меньше, чем видимых баров на графике, выйдем
      if(ArraySize(time)<visible_bars)
         return;
      //--- Если время первого "истинного" бара больше, чем
      //    время первого видимого бара на графике, то
      //    получим доступное количество баров текущего в цикле символа
      if(limit_time[s]>time[0])
        {
         //--- Получим размер массива
         size_array=ArraySize(time);
         //--- Получим количество баров от первого истинного
         if((number_bars=Bars(Symbol(),Period(),limit_time[s],time[size_array-1]))<=0)
            return;
        }
      //--- Иначе получим видимое количество баров на графике
      else
         number_bars=visible_bars;
      //--- Установим индексацию, как в таймсериях
      ArraySetAsSeries(low,true);
      ArraySetAsSeries(high,true);
      //--- Скопируем данные из индикаторных буферов
      //    Все режимы кроме Линия
      if(DrawType!=LINES)
        {
         ArrayCopy(low,buffer_data[s].low);
         ArrayCopy(high,buffer_data[s].high);
        }
      //--- Если в режиме Линия
      else
        {
         ArrayCopy(low,buffer_data[s].close);
         ArrayCopy(high,buffer_data[s].close);
        }
      //--- Получим размер массива
      size_array=ArraySize(high);
      //--- Заполним пустые значения,
      //    чтобы они не учитывались в определении максимума/минимума
      for(int i=0; i<size_array; i++)
        {
         if(high[i]==EMPTY_VALUE)
            high[i]=max_price;
         if(low[i]==EMPTY_VALUE)
            low[i]=min_price;
        }
      //--- Определим максимум/минимум учитывая инверсию
      if(inverse[s])
        {
         //--- Если ошибок нет, запомним значения
         if(ArrayMaximum(high,last_visible_bar,number_bars)>=0 && 
            ArrayMinimum(low,last_visible_bar,number_bars)>=0)
           {
            max_price=fmax(max_price,low[ArrayMaximum(low,last_visible_bar,number_bars)]);
            min_price=fmin(min_price,high[ArrayMinimum(high,last_visible_bar,number_bars)]);
           }
        }
      else
        {
         //--- Если ошибок нет, запомним значения
         if(ArrayMinimum(low,last_visible_bar,number_bars)>=0 && 
            ArrayMaximum(high,last_visible_bar,number_bars)>=0)
           {
            min_price=fmin(min_price,low[ArrayMinimum(low,last_visible_bar,number_bars)]);
            max_price=fmax(max_price,high[ArrayMaximum(high,last_visible_bar,number_bars)]);
           }
        }
     }
//--- Рассчитаем отступ (3%) от верха и низа графика
   offset_max_min=((max_price-min_price)*3)/100;
//--- Включим режим фиксированного масштаба графика
   ChartSetInteger(0,CHART_SCALEFIX,true);
//--- Установим максимум/минимум
   ChartSetDouble(0,CHART_FIXED_MAX,max_price+offset_max_min);
   ChartSetDouble(0,CHART_FIXED_MIN,min_price-offset_max_min);
//--- Обновим график
   ChartRedraw();
  }
//---

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

По умолчанию во внешних параметрах установлены символы GBPUSD, AUDUSD, NZDUSD, USDCAD, USDCHF. На скриншоте ниже показан недельный график EURUSD в режиме Vertical Lines с отключенной инверсией:

Недельный таймфрейм EURUSD в режиме Vertical Line

На скриншоте ниже 30-ти минутный таймфрейм в режиме Day, но только на этот раз у символов с базовой валютой USD включен режим инверсии. В данном случае это USDCAD (светло-голубые свечи) и USDCHF (сиреневые свечи).

30-минутный таймфрейм EURUSD в режиме Day

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

В статье "Мультивалютный индикатор Multi Symbol Price Divergence" можно посмотреть больше скриншотов этого индикатора и видео, в котором показаны все режимы. Возможно Вам интересно будет также посмотреть другие подобные мультивалютные и мультитаймфреймовые индикаторы на этом блоге:

Мультисимвольный индикатор волатильности ATR MULTI SYMBOLS
Мультитаймфреймовый индикатор TF PANEL




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


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

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