Рецепты MQL5 - Разработка мультивалютного индикатора для анализа расхождения цен
Введение
В этой статье рассмотрим разработку мультивалютного индикатора для анализа расхождения цен за указанный период времени. Многие основные моменты уже рассматривались в предыдущей статье по программированию мультивалютных индикаторов Рецепты MQL5 - Разработка мультивалютного индикатора волатильности на MQL5. Поэтому на этот раз будем останавливаться только на новых функциях и тех функциях, которые претерпели сильные изменения. Если вы впервые рассматриваете тему программирования мультивалютных индикаторов, то рекомендуется в первую очередь прочитать предыдущую статью.
В этой статье мы рассмотрим следующие вопросы:
- изменение свойств графика;
- обработка событий CHARTEVENT_OBJECT_DRAG (перетаскивание графического объекта) и CHARTEVENT_CHART_CHANGE (изменение размеров графика или изменение свойств графика через диалог свойств);
- отрисовка индикаторных буферов более, чем одним цветом;
- определение максимумов и минимумов в индикаторных буферах в зоне видимости для установки максимума/минимума графика;
- инверсия ряда.
Для индикатора объем кода в итоге получится довольно большой, около 1500 строк. Поэтому распределим все функции по отдельным категориям в файлы и будем подключать их к главному файлу проекта. Всего получится три категории функций для внешних файлов:
- Checks.mqh - функции для различных проверок и загрузки доступных данных;
- Objects.mqh - функции для управления графическими объектами;
- Chart.mqh - функции для управления свойствами графика.
Функции, которые не относятся к перечисленным выше категориям, оставим в главном файле.
Процесс разработки индикатора
Далее приступим к программированию индикатора. Сначала нужно создать новый проект. Для этого создайте в директории Metatrader 5\MQL5\Indicators папку с именем индикатора, а в ней папку Include, в которой будут размещаться подключаемые файлы. Далее создайте главный файл в папке индикатора. Это можно сделать вручную, создав текстовый файл с расширением *.mq5 или с помощью Мастера MQL5 по готовому шаблону. Кроме основных функций программы OnInit(), OnDeinit() и OnCalculate(), будут еще использоваться OnChartEvent() и OnTimer().
Так же как и в предыдущей статье, кроме текущего символа будем выводить на график данные еще пяти указанных во внешних параметрах символов. Но на этот раз вместо рассчитанных по какой-либо формуле значений, будем выводить на график чистые ценовые данные. Тип отображения данных предоставим на выбор пользователю. То есть, во внешних параметрах можно будет выбрать в выпадающем списке один из трех вариантов: Линия, Бары или Японские свечи.
Если бы нужно было отобразить данные только в виде линий одним цветом, то в свойствах индикатора в разделе специфических параметров (#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_COUNT 5 // Количество символов //--- Подключим класс для работы с холстом #include <Canvas\Canvas.mqh> //--- Подключаем свои библиотеки #include "Include/Checks.mqh" #include "Include/Chart.mqh" #include "Include/Objects.mqh"
Для создания выпадающих списков во внешних параметрах для выбора типа отрисовки и режима начальной точки расхождения цен, создадим перечисления ENUM_DRAWTYPE и ENUM_START_POINT:
//--- Тип отрисовки ценовых данных enum ENUM_DRAWTYPE { LINE =0, // Линия BARS =1, // Бары CANDLES=2 // Японские свечи }; //--- Режим начальной точки расхождения цен enum ENUM_START_POINT { VERTICAL_LINE=0, // Вертикальная линия MONTH =1, // Месяц WEEK =2, // Неделя DAY =3, // День HOUR =4 // Час };
О типах отображения данных уже рассказывалось выше, теперь немного подробнее о том, что означает начальная точка расхождения цен.
Всего создадим пять режимов: Вертикальная линия, Месяц, Неделя, День и Час. Для режима Вертикальная линия при загрузке индикатора на график будет установлена вертикальная линия. Перемещением этой линии указываться бар, на котором цены всех символов будут смыкаться в одну точку. Этой точкой опоры будет уровень цены открытия указанного бара для текущего символа. Любой другой режим будет указывать программе, что цены должны смыкаться каждый раз в начале указанного периода. То есть, в начале каждого месяца, в начале каждой недели, в начале каждого дня или в начале каждого часа.
Ниже можно ознакомиться со списком внешних параметров индикатора:
//--- Внешние параметры input ENUM_DRAWTYPE DrawType =CANDLES; // Тип отрисовки input ENUM_START_POINT StartPriceDivergence =VERTICAL_LINE; // Начало расхождения цен input bool TwoColor =false; // Двухцветные бары/свечи sinput string dlm01=""; //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - input string Symbol02 ="GBPUSD"; // Символ 2 input bool Inverse02 =false; // Инверсия символа 2 input string Symbol03 ="AUDUSD"; // Символ 3 input bool Inverse03 =false; // Инверсия символа 3 input string Symbol04 ="NZDUSD"; // Символ 4 input bool Inverse04 =false; // Инверсия символа 4 input string Symbol05 ="USDCAD"; // Символ 5 input bool Inverse05 =false; // Инверсия символа 5 input string Symbol06 ="USDCHF"; // Символ 6 input bool Inverse06 =false; // Инверсия символа 6
Нумерация символов начинается с двойки, так как единица - это текущий символ на графике.
Для каждого подключаемого символа можно включить инверсию. Инверсия означает, что данные символа будут отображаться перевернутыми. Это может быть полезным, когда в списке используемых для анализа символов присутствуют такие, у которых одна и та же валюта (например, доллар США) может быть как базовой, так и котируемой. Например, в валютной паре EURUSD, доллар является котируемой валютой, а в валютной паре USDCHF - базовой. В таком случае, если на графике текущий символ EURUSD, то для USDCHF можно включить инверсию, получив, таким образом, более удобное представление данных для анализа.
Ниже представлен список глобальных переменных и массивов:
//--- Структура массивов индикаторных буферов struct buffers { double open[]; // Буфер для цен открытия double high[]; // Буфер для цен максимумов double low[]; // Буфер для цен минимумов double close[]; // Буфер для цен закрытия double icolor[]; // Буфер для определения цвета элемента }; buffers buffer_data[SYMBOLS_COUNT]; //--- Загрузка класса CCanvas canvas; //--- Переменные/массивы для копирования данных из OnCalculate() int OC_rates_total =0; // Размер входных таймсерий int OC_prev_calculated =0; // Обработано баров на предыдущем вызове datetime OC_time[]; // Время открытия double OC_open[]; // Цены открытия double OC_high[]; // Максимальные цены double OC_low[]; // Минимальные цены double OC_close[]; // Цены закрытия long OC_tick_volume[]; // Тиковые объемы long OC_volume[]; // Реальные объемы int OC_spread[]; // Спред //--- Для хранения и проверки времени первого бара в терминале datetime series_first_date[SYMBOLS_COUNT]; datetime series_first_date_last[SYMBOLS_COUNT]; //--- Массив времени бара, от которого начинать отрисовку datetime limit_time[SYMBOLS_COUNT]; //--- Массив названий символов string symbol_names[SYMBOLS_COUNT]; //--- Массив флагов инверсии символов bool inverse[SYMBOLS_COUNT]; //--- Цвета линий индикатора color line_colors[SYMBOLS_COUNT]={clrDodgerBlue,clrLimeGreen,clrGold,clrAqua,clrMagenta}; //--- Строка, символизирующая отсутствие символа string empty_symbol="EMPTY"; //--- Свойства графика int window_number =WRONG_VALUE; // Номер окна индикатора int chart_width =0; // Ширина графика int chart_height =0; // Высота графика int last_chart_width =0; // Последняя в памяти ширина графика int last_chart_height =0; // Последняя в памяти высота графика int chart_center_x =0; // Центр графика по горизонтали int chart_center_y =0; // Центр графика по вертикали color color_bar_up =clrRed; // Цвет бара вверх color color_bar_down =C'100,0,0'; // Цвет бара вниз string indicator_shortname ="MS_PriceDivergence"; // Короткое имя индикатора string prefix =indicator_shortname+"_"; // Префикс для объектов //--- Имя вертикальной линии начальной точки расхождения цен string start_price_divergence=prefix+"start_price_divergence"; //--- Свойства холста string canvas_name =prefix+"canvas"; // Название холста color canvas_background =clrBlack; // Цвет фона холста uchar canvas_opacity =190; // Степень прозрачности int font_size =16; // Размер шрифта string font_name ="Calibri"; // Шрифт ENUM_COLOR_FORMAT clr_format =COLOR_FORMAT_ARGB_RAW; // Компоненты цвета должны быть корректно заданы пользователем //--- Сообщения холста string msg_prepare_data ="Подготовка данных! Пожалуйста, подождите..."; string msg_not_synchronized ="Данные не синхронизированы! Пожалуйста, подождите..."; string msg_load_data =""; string msg_sync_update =""; string msg_last =""; //--- ENUM_TIMEFRAMES timeframe_start_point =Period(); // Таймфрейм для точки расхождения цен datetime first_period_time =NULL; // Время первого указанного периода данных на графике double divergence_price =0.0; // Цена начальной точки расхождения цен datetime divergence_time =NULL; // Время начальной точки расхождения цен double symbol_difference[SYMBOLS_COUNT]; // Разница в цене относительно текущего символа double inverse_difference[SYMBOLS_COUNT]; // Разница, которая образуется при расчете инверсии
Далее рассмотрим функции, которые используются во время инициализации индикатора. В принципе, больших изменений по сравнению с тем, что было в функции OnInit() в индикаторе из предыдущей статьи, нет.
Добавим проверку на то, где используется индикатор. Дело в том, что в тестере на текущий момент разработчиками терминала реализованы не все возможности для управления свойствами графика, поэтому ограничимся использованием индикатора только вне тестера. Для этого напишем простую функцию CheckTesterMode(). Располагаться она будет в файле Checks.mqh:
//+------------------------------------------------------------------+ //| Проверяет, используется ли индикатор в тестере | //+------------------------------------------------------------------+ bool CheckTesterMode() { //--- Если индикатор был запущен в тестере, сообщим, что он не предназначен для использования в тестере if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE) || MQLInfoInteger(MQL_OPTIMIZATION)) { Comment("На данный момент индикатор <- "+MQLInfoString(MQL_PROGRAM_NAME)+" -> не предназначен для использования в тестере!"); return(false); } //--- return(true); }
Еще одна новая функция SetBarsColors() предназначена для установки цвета барам/свечам текущего символа, располагается она в файле Chart.mqh.
//+------------------------------------------------------------------+ //| Устанавливает цвета баров текущего символа | //+------------------------------------------------------------------+ void SetBarsColors() { //--- Цвет бара вверх, тени и окантовки тела бычьей свечи 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. Если выбран режим Вертикальная линия, то в глобальной переменной timeframe_start_point останется значение по умолчанию, то есть текущий таймфрейм. Иначе будет определен таймфрейм, соответствующий выбору. Для этого напишем функцию InitStartPointTF():
//+------------------------------------------------------------------+ //| Определяет таймфрейм для режима начальной точки цен | //+------------------------------------------------------------------+ void InitStartPointTF() { //--- Если режим вертикальной линии, выйдем if(StartPriceDivergence==VERTICAL_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!=VERTICAL_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_COUNT; 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 InitSymbolNames() { symbol_names[0]=AddSymbolToMarketWatch(Symbol02); symbol_names[1]=AddSymbolToMarketWatch(Symbol03); symbol_names[2]=AddSymbolToMarketWatch(Symbol04); symbol_names[3]=AddSymbolToMarketWatch(Symbol05); symbol_names[4]=AddSymbolToMarketWatch(Symbol06); } //+------------------------------------------------------------------+ //| Инициализирует массив инверсий | //+------------------------------------------------------------------+ void InitInverse() { inverse[0]=Inverse02; inverse[1]=Inverse03; inverse[2]=Inverse04; inverse[3]=Inverse05; inverse[4]=Inverse06; }
Существенные изменения были произведены в функции SetIndicatorProperties(). Фактически это новая функция. Теперь в зависимости от того, какой режим выбран для отображения данных, во время инициализации устанавливаются соответствующие свойства.
//+------------------------------------------------------------------+ //| Устанавливает свойства индикатора | //+------------------------------------------------------------------+ void SetIndicatorProperties() { //--- Установим короткое имя IndicatorSetString(INDICATOR_SHORTNAME,indicator_shortname); //--- Установим количество знаков IndicatorSetInteger(INDICATOR_DIGITS,_Digits); //--- В режиме линия нужен только один буфер, отображающий цену открытия if(DrawType==LINE) { for(int s=0; s<SYMBOLS_COUNT; s++) SetIndexBuffer(s,buffer_data[s].close,INDICATOR_DATA); } //--- В других режимах используем все цены для построения // баров/свеч и дополнительный буфер для двухцветного режима else if(DrawType==BARS || DrawType==CANDLES) { for(int s=0; s<SYMBOLS_COUNT; s++) { static int buffer_number=0; SetIndexBuffer(buffer_number,buffer_data[s].open,INDICATOR_DATA); buffer_number++; SetIndexBuffer(buffer_number,buffer_data[s].high,INDICATOR_DATA); buffer_number++; SetIndexBuffer(buffer_number,buffer_data[s].low,INDICATOR_DATA); buffer_number++; SetIndexBuffer(buffer_number,buffer_data[s].close,INDICATOR_DATA); buffer_number++; SetIndexBuffer(buffer_number,buffer_data[s].icolor,INDICATOR_COLOR_INDEX); buffer_number++; } } //--- Установим метки для текущего таймфрейма // В режиме линия только цена открытия if(DrawType==LINE) { for(int s=0; s<SYMBOLS_COUNT; s++) PlotIndexSetString(s,PLOT_LABEL,symbol_names[s]+",Close"); } //--- В других режимах все цены баров/свеч // В качестве разделителя используется ";" else if(DrawType==BARS || DrawType==CANDLES) { for(int s=0; s<SYMBOLS_COUNT; s++) { PlotIndexSetString(s,PLOT_LABEL, symbol_names[s]+",Open;"+ symbol_names[s]+",High;"+ symbol_names[s]+",Low;"+ symbol_names[s]+",Close"); } } //--- Установим тип линий для индикаторных буферов // Линия if(DrawType==LINE) for(int s=0; s<SYMBOLS_COUNT; s++) PlotIndexSetInteger(s,PLOT_DRAW_TYPE,DRAW_LINE); //--- Бары if(DrawType==BARS) for(int s=0; s<SYMBOLS_COUNT; s++) PlotIndexSetInteger(s,PLOT_DRAW_TYPE,DRAW_COLOR_BARS); //--- Свечи if(DrawType==CANDLES) for(int s=0; s<SYMBOLS_COUNT; s++) PlotIndexSetInteger(s,PLOT_DRAW_TYPE,DRAW_COLOR_CANDLES); //--- Установим тип линий для данных текущего символа // Линия if(DrawType==LINE) 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_COUNT; s++) PlotIndexSetInteger(s,PLOT_LINE_WIDTH,1); //--- Установим цвет линий для режима Линия if(DrawType==LINE) for(int s=0; s<SYMBOLS_COUNT; s++) PlotIndexSetInteger(s,PLOT_LINE_COLOR,line_colors[s]); //--- Установим показ данных в Окне Данных только существующих символов for(int s=0; s<SYMBOLS_COUNT; s++) { if(symbol_names[s]!=empty_symbol) PlotIndexSetInteger(s,PLOT_SHOW_DATA,true); else PlotIndexSetInteger(s,PLOT_SHOW_DATA,false); } //--- Пустое значение для построения, для которого нет отрисовки for(int s=0; s<SYMBOLS_COUNT; s++) PlotIndexSetDouble(s,PLOT_EMPTY_VALUE,EMPTY_VALUE); }
И, наконец, еще одна новая функция SetDivergenceLine() для использования в OnInit(). Она устанавливает вертикальную зеленую линию для управления началом расхождения цен в режиме Вертикальная линия.
//+------------------------------------------------------------------+ //| Устанавливает вертикальную линию для начальной точки цен | //+------------------------------------------------------------------+ void SetDivergenceLine() { //--- Если вертикальной линии нет, установим ее if(StartPriceDivergence==VERTICAL_LINE && ObjectFind(0,start_price_divergence)<0) //--- Установим вертикальную линию на истинном баре CreateVerticalLine(0,0,TimeCurrent()+PeriodSeconds(),start_price_divergence, 2,STYLE_SOLID,clrGreenYellow,true,true,false,"","\n"); //--- Если не в режиме вертикальной линии if(StartPriceDivergence!=VERTICAL_LINE) DeleteObjectByName(start_price_divergence); }
А ниже представлено то, как выглядит все выше описанное в функции OnInit(). Когда все распределено по отдельным функциям и файлам, читать программу становится очень просто.
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Проверим, не используется ли сейчас индикатор в тестере if(!CheckTesterMode()) return(INIT_FAILED); //--- Установим цвет барам/свечам SetBarsColors(); //--- Определим период для начальной точки расхождения цен InitStartPointTF(); //--- Проверим корректность входных параметров if(!CheckInputParameters()) return(INIT_PARAMETERS_INCORRECT); //--- Включим таймер с интервалом 1 секунда EventSetMillisecondTimer(1000); //--- Установим шрифт для отображения на холсте canvas.FontSet(font_name,font_size,FW_NORMAL); //--- Инициализация массивов InitArrays(); //--- Инициализируем массив символов InitSymbolNames(); //--- Инициализируем массив инверсий InitInverse(); //--- Установим свойства индикатора SetIndicatorProperties(); //--- Установим вертикальную линию начала расхождения цен SetDivergenceLine(); //--- Очистим комментарий Comment(""); //--- Обновим график ChartRedraw(); //--- Инициализация прошла успешно return(INIT_SUCCEEDED); }
В функции OnCalculate() код программы остался практически неизменным. В предыдущей статье после того, как все проверки на доступность данных были произведены, программа сначала в первом цикле заполняла вспомогательные массивы и только потом уже готовыми данными заполняла индикаторные буферы. В этот раз все уложим в один цикл.
Функции для проверки и загрузки данных я сделал более жесткими. Теперь каждое значение, которое нужно получить, проходит через указанное количество попыток с остановкой цикла, если значение получено. И так как теперь есть режимы, где понадобится определять начало того или иного периода (месяц, неделя, день, час), то получать время начала периода мы будем через старший таймфрейм. Поэтому была написана дополнительная функция подобная LoadAndFormData(), которая имеет похожее название LoadAndFormDataHighTF(). Ее код очень похож на исходный аналог, поэтому я не буду его приводить здесь.
Контрольную проверку на доступность данных текущего и старшего таймфрейма я реализовал в одной функции CheckAvailableData():
//+------------------------------------------------------------------+ //| Проверяет количество доступных данных у всех символов | //+------------------------------------------------------------------+ bool CheckAvailableData() { int attempts=100; //--- for(int s=0; s<SYMBOLS_COUNT; s++) { //--- Если такой символ есть if(symbol_names[s]!=empty_symbol) { datetime time[]; // Массив для проверки количества баров int total_period_bars =0; // Количество баров текущего периода datetime terminal_first_date =NULL; // Первая дата имеющихся данных текущего периода в терминале //--- Получим первую дату данных текущего периода в терминале terminal_first_date=(datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_TERMINAL_FIRSTDATE); //--- Получим количество доступных баров от указанной даты total_period_bars=Bars(symbol_names[s],Period(),terminal_first_date,TimeCurrent()); //--- Проверим готовность данных баров for(int i=0; i<attempts; i++) { //--- Скопируем указанное количество данных if(CopyTime(symbol_names[s],Period(),0,total_period_bars,time)) { //--- Если скопировалось нужное количество, остановим цикл if(ArraySize(time)>=total_period_bars) break; } } //--- Если скопировано меньше данных значит нужно совершить еще одну попытку if(ArraySize(time)==0 || ArraySize(time)<total_period_bars) { msg_last=msg_prepare_data; ShowCanvasMessage(msg_prepare_data); OC_prev_calculated=0; return(false); } } } //--- Если в режиме вертикальной линии для начальной точки расхождения цен, выходим if(StartPriceDivergence==VERTICAL_LINE) return(true); else { datetime time[]; // Массив для проверки количества баров int total_period_bars =0; // Количество баров текущего периода datetime terminal_first_date =NULL; // Первая дата имеющихся данных текущего периода в терминале //--- Получим первую дату данных текущего периода в терминале for(int i=0; i<attempts; i++) if((terminal_first_date=(datetime)SeriesInfoInteger(Symbol(),Period(),SERIES_FIRSTDATE))>0) break; //--- Получим количество доступных баров от указанной даты for(int i=0; i<attempts; i++) if((total_period_bars=(int)SeriesInfoInteger(Symbol(),timeframe_start_point,SERIES_BARS_COUNT))>0) break; //--- Проверим готовность данных баров for(int i=0; i<attempts; i++) //--- Скопируем указанное количество данных if(CopyTime(Symbol(),timeframe_start_point, terminal_first_date+PeriodSeconds(timeframe_start_point),TimeCurrent(),time)>0) break; //--- Если скопировано меньше данных, значит нужно совершить еще одну попытку if(ArraySize(time)<=0 || total_period_bars<=0) { msg_last=msg_prepare_data; ShowCanvasMessage(msg_prepare_data); OC_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 attempts=100; // Количество попыток копирования datetime high_tf_time=NULL; // Время бара старшего таймфрейма //--- Если находимся вне зоны "истинных" баров символа, выйдем if(time[i]<limit_time[s]) return; //--- Сбросим последнюю ошибку ResetLastError(); //--- Получим данные текущего бара указанного символа for(int j=0; j<attempts; j++) if(CopyRates(symbol_names[s],Period(),time[i],1,rates)==1) { ResetLastError(); break; } //--- Если не удалось получить данные, выйдем if(ArraySize(rates)<1 || GetLastError()!=0) return; //--- Если текущее время раньше, чем время первого периода или // время бара неравно времени бара текущего символа или // получены пустые значения if(rates[0].time==NULL || time[i]!=rates[0].time || time[i]<first_period_time || rates[0].low==EMPTY_VALUE || rates[0].open==EMPTY_VALUE || rates[0].high==EMPTY_VALUE || rates[0].close==EMPTY_VALUE) { //--- Запишем пустое значение if(DrawType!=LINE) { 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; return; } //--- Если в режиме вертикальной линии для начальной точки расхождения цен if(StartPriceDivergence==VERTICAL_LINE) { //--- Получим время линии divergence_time=(datetime)ObjectGetInteger(0,start_price_divergence,OBJPROP_TIME); //--- Получим время первого бара first_period_time=time[0]; } //--- Если в других режимах, то будем отслеживать начало периода else { //--- Если зашли сюда в первый раз, запомним данные первого бара старшего таймфрейма if(divergence_time==NULL) { ResetLastError(); //--- Получим время открытия первого бара старшего таймфрейма for(int j=0; j<attempts; 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<attempts; j++) if(CopyOpen(Symbol(),timeframe_start_point,high_tf_time,1,period_open)==1) { ResetLastError(); break; } //--- Получим время открытия первого бара старшего таймфрейма for(int j=0; j<attempts; 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 || divergence_time!=period_time[0]) { symbol_difference[s] =0.0; // Обнулим разницу цен символов inverse_difference[s] =0.0; // Обнулим разницу инверсии //--- Запомним время начала расхождения цен divergence_time=period_time[0]; //--- Запомним уровень начала расхождения цен divergence_price=period_open[0]; //--- Установим вертикальную линию в начале периода расхождения цен CreateVerticalLine(0,0,period_time[0],start_price_divergence+"_"+TimeToString(divergence_time), 2,STYLE_SOLID,clrWhite,false,false,true,TimeToString(divergence_time),"\n"); } } //--- Если в режиме вертикальной линии и время бара раньше, чем время линии if(StartPriceDivergence==VERTICAL_LINE && time[i]<divergence_time) { //--- Держим нулевые значения разницы symbol_difference[s] =0.0; inverse_difference[s] =0.0; //--- Для режима отрисовки Линия только цена открытия if(DrawType==LINE) 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]; //--- Установим цвет текущему элементу индикаторного буфера SetBufferColorIndex(i,s,rates[0].close,rates[0].open); } } //--- Если в других режимах else { //--- Если нужна инверсия данных символа if(inverse[s]) { //--- Если начался новый период, пересчитаем переменные для расчета if(symbol_difference[s]==0.0) { //--- Если режим вертикальной линии if(StartPriceDivergence==VERTICAL_LINE) { //--- Рассчитаем разницу symbol_difference[s] =rates[0].open-OC_open[i]; inverse_difference[s] =OC_open[i]-(-OC_open[i]); } //--- Для других режимов else { //--- Рассчитаем разницу symbol_difference[s] =rates[0].open-divergence_price; inverse_difference[s] =divergence_price-(-divergence_price); } } //--- Для режима Линия только цена закрытия if(DrawType==LINE) 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]; //--- Установим цвет текущему элементу индикаторного буфера SetBufferColorIndex(i,s,rates[0].close,rates[0].open); } } //--- Если без инверсии, то в начале периода нужно вычитать только разницу между ценами символов else { //--- Если начался новый период if(symbol_difference[s]==0.0) { //--- Если режим вертикальной линии if(StartPriceDivergence==VERTICAL_LINE) symbol_difference[s]=rates[0].open-OC_open[i]; //--- Для других режимов else symbol_difference[s]=rates[0].open-divergence_price; } //--- Для режима отрисовки Линия только цена открытия if(DrawType==LINE) 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]; //--- Установим цвет текущему элементу индикаторного буфера SetBufferColorIndex(i,s,rates[0].close,rates[0].open); } } } //--- Контрольная проверка рассчитанных значений // Для режима Линия только цена открытия if(DrawType==LINE) { //--- Если текущее время раньше, чем время первого периода или // время бара неравно времени бара текущего символа, запишем пустое значение if(time[i]!=rates[0].time || time[i]<first_period_time) buffer_data[s].close[i]=EMPTY_VALUE; } //--- Для других режимов все цены else { //--- Если текущее время раньше, чем время первого периода или // время бара неравно времени бара текущего символа или // получены пустые значения if(rates[0].time==NULL || time[i]!=rates[0].time || time[i]<first_period_time || rates[0].low==EMPTY_VALUE || rates[0].open==EMPTY_VALUE || rates[0].high==EMPTY_VALUE || rates[0].close==EMPTY_VALUE) { //--- Запишем пустое значение 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; } } }
При изучении функции выше вы должны были заметить еще одну пользовательскую функцию SetBufferColorIndex(). С помощью этой функции устанавливается цвет в цветовой индикаторный буфер.
//+------------------------------------------------------------------+ //| Устанавливает цвет элементу буфера по условию | //+------------------------------------------------------------------+ void SetBufferColorIndex(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 есть возможность получать номер первого видимого бара в окне графика, а также количество видимых баров. Эти возможности и будем использовать в еще одной пользовательской функции CorrectChartMaxMin(). Функцию можно разделить на несколько этапов:
- Определение номера первого и последнего видимых баров;
- Определение максимума и минимума видимых баров текущего символа;
- Определение максимума и минимума среди всех массивов символов;
- Установка максимума и минимума в свойствах графика.
Ниже представлен код функции CorrectChartMaxMin(), которая находится в файле Chart.mqh.
//+------------------------------------------------------------------+ //| Корректирует максимум/минимум графика относительно всех буферов | //+------------------------------------------------------------------+ void CorrectChartMaxMin() { double low[]; // Массив минимумов double high[]; // Массив максимумов int attempts =10; // Кол-во попыток int array_size =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<attempts; i++) if(CopyHigh(Symbol(),Period(),last_visible_bar,visible_bars,high)==visible_bars) break; for(int i=0; i<attempts; 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_COUNT; s++) { //--- Если такого символа нет, то перейти к следующему if(symbol_names[s]==empty_symbol) continue; //--- datetime time[]; // Массив времени int bars_count=0; // Количество баров для расчета //--- Установим нулевой размер массивам ArrayResize(high,0); ArrayResize(low,0); //--- Получим время первого видимого на графике бара for(int i=0; i<attempts; 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]) { //--- Получим размер массива array_size=ArraySize(time); //--- Получим количество баров от первого истинного if((bars_count=Bars(Symbol(),Period(),limit_time[s],time[array_size-1]))<=0) return; } //--- Иначе получим видимое количество баров на графике else bars_count=visible_bars; //--- Установим индексацию, как в таймсериях ArraySetAsSeries(low,true); ArraySetAsSeries(high,true); //--- Скопируем данные из индикаторных буферов // Все режимы кроме Линия if(DrawType!=LINE) { 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); } //--- Получим размер массива array_size=ArraySize(high); //--- Заполним пустые значения, // чтобы они не учитывались в определении максимума/минимума for(int i=0; i<array_size; 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,bars_count)>=0 && ArrayMinimum(low,last_visible_bar,bars_count)>=0) { max_price=fmax(max_price,low[ArrayMaximum(low,last_visible_bar,bars_count)]); min_price=fmin(min_price,high[ArrayMinimum(high,last_visible_bar,bars_count)]); } } else { //--- Если ошибок нет, запомним значения if(ArrayMinimum(low,last_visible_bar,bars_count)>=0 && ArrayMaximum(high,last_visible_bar,bars_count)>=0) { min_price=fmin(min_price,low[ArrayMinimum(low,last_visible_bar,bars_count)]); max_price=fmax(max_price,high[ArrayMaximum(high,last_visible_bar,bars_count)]); } } } //--- Рассчитаем отступ (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(); }
Описанную выше функцию мы будем использовать при обработке события перетаскивания вертикальной линии (и, разумеется, при расчете значений индикатора в OnCalculate):
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Событие перетаскивания графического объекта if(id==CHARTEVENT_OBJECT_DRAG) { //--- Если сейчас режим вертикальной линии для начальной точки расхождения цен, обновим индикаторные буферы if(StartPriceDivergence==VERTICAL_LINE) OnCalculate(OC_rates_total, 0, OC_time, OC_open, OC_high, OC_low, OC_close, OC_tick_volume, OC_volume, OC_spread); } //--- Изменение размеров графика или изменение свойств графика через диалог свойств if(id==CHARTEVENT_CHART_CHANGE) //--- Скорректируем максимум и минимум графика относительно значений в индикаторных буферах CorrectChartMaxMin(); }
Все функции готовы. Более подробно с подробно прокомментированным кодом можно ознакомиться в приложении к статье.
Продемонстрируем, что же в итоге получилось. По умолчанию во внешних параметрах установлены символы GBPUSD, AUDUSD, NZDUSD, USDCAD, USDCHF. На скриншоте ниже показан недельный график EURUSD в режиме Вертикальная линия с отключенной инверсией:
Рис. 1 - Недельный таймфрейм в режиме "Вертикальная линия"
На скриншоте ниже 30-ти минутный таймфрейм в режиме День, но только на этот раз у символов с базовой валютой USD включен режим инверсии. В данном случае это USDCAD (светло-голубые свечи) и USDCHF (сиреневые свечи).
Рис. 2 - 30-минутный таймфрейм в режиме "День"
Заключение
Думаю, получился довольно интересный и информативный инструмент для мультивалютного анализа расхождения цен. Этот индикатор можно неограниченно развивать в лучшую сторону.
Спасибо за внимание!
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Много уважаемый автор мне в целом все понравилось . Вы помогли мне решить ряд задач не прибегая к "интенсивной мозговой деятельности". Но пожалуйста либо перепишите код либо опишите как , с вашей точки зрения, его устанавливать, а то мне понадобилось 2 часа для того чтобы подправить код и установить его по классической схеме. Да и в обсуждении можно поменьше сленга здесь не все ВОЛКИ программирования, к которым я себя тоже не отношу.
А что не получалось и что пришлось исправлять?
И еще я пишу серию статей и в одной из них хочу поднять вопрос корреляции и хотел бы сделать ссылку на ваш материал, а для иллюстрации своих слов вставить скриншот с изображением индюка написанного по вашему алгоритму с моими изменениями. Для этого прошу вашего разрешения. Обещаю что код данного индикатора публиковаться не будет.
Любой код из статей используйте в своих целях так, как вам хочется. Для этого код и выкладывается, чтобы каждый мог его использовать в своих целях. Просто, когда будете писать статью, указывайте откуда были взяты примеры. Это общепринятые везде правила.
Глянул статью. Там вроде не обойдена проблемма "пустых" баров. Без этого, думаю, мультивалютку трогать не стоит.
Эта статья была продолжением темы. Посмотрите также и предыдущую: Рецепты MQL5 - Разработка мультивалютного индикатора волатильности на MQL5.
Спасибо. Теперь ясно.
Самое интересное, что в обсуждаемой статье сразу говорится об этом
В этой статье рассмотрим разработку мультивалютного индикатора для анализа расхождения цен за указанный период времени. Многие основные моменты уже рассматривались в предыдущей статье по программированию мультивалютных индикаторов Рецепты MQL5 - Разработка мультивалютного индикатора волатильности на MQL5. Поэтому на этот раз будем останавливаться только на новых функциях и тех функциях, которые претерпели сильные изменения. Если вы впервые рассматриваете тему программирования мультивалютных индикаторов, то рекомендуется в первую очередь прочитать предыдущую статью.
Интересная статья, спасибо. Но прошло некоторое время и возможно что то изменилось в mql. Теперь индикатор выдаёт такую ошибку в файле Checks.mqh и зависает. Подскажите, в чём тут причина?