Понравилась статья?
Поставьте ссылку на нее —
пусть другие почитают
Используй новые возможности MetaTrader 5

Несколько индикаторов на графике (Часть 02): Первые эксперименты

11 апреля 2022, 10:50
Daniel Jose
0
1 869

Введение

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


Планирование

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

На этом первом этапе мы будем использовать индикатор, поэтому создадим его для этого окна:

Но зачем использовать индикатор, а не какой-либо другой тип файла?! Причина в том, что с индикатором нам не нужно добавлять внутреннюю логику для создания подокна, мы можем указать индикатору сделать это за нас, что экономит нам время и ускоряет создание системы. Таким образом, заголовок нашего индикатора будет выглядеть следующим образом:

#property indicator_plots 0
#property indicator_separate_window

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

Код Описание
indicador_plots 0 Эта строка укажет компилятору, что мы не будем строить никаких данных, и не позволит компилятору выводить предупреждающие сообщения.
indicator_separate_window Эта строка указывает компилятору добавить необходимую логику для создания подокна.

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

 #include <Auxiliar\C_TemplateChart.mqh>

Эта директива указывает компилятору добавить (include) заголовочный файл, который присутствует в определенном месте. Но подождите, разве полный путь не должен быть таким: Includes \ Auxiliary \ C_TemplateChart.mqh !? Да, это полный путь, но поскольку он уже структурирован, а MQL5 знает, что любой заголовочный файл должен находиться в папке includes, то мы можем удалить этот пункт и просто указать остальной путь. Если путь заключен в угловые скобки, значит это абсолютный путь, если в кавычки, то путь будет относительным, т.е. <Auxiliary \ C_TemplateChart. mqh> отличается от "Auxiliary \ C_TemplateChart.mqh", это простая деталь, которая все меняет.

Продолжая работу с кодом, мы получим следующие строки:

input string user01 = "" ;       //Используемые индикаторы
input string user02 = "" ;       //Сопровождаемые активы

Они позволяют вводить строки, и если мы уже понимаем, как использовать в качестве команды каждое открытие индикатора, мы можем оставить эту команду по умолчанию. Например, если мы всегда хотим использовать RSI с толщиной линии 3 и MACD с толщиной линии 2, мы можем оставить это по умолчанию в нашей системе, так что код выше будет выглядеть следующим образом:

input string user01 = "RSI:3;MACD:2" ;  //Используемые индикаторы
input string user02 = "" ;              //Сопровождаемые активы

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

C_TemplateChart SubWin;

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

 //+------------------------------------------------------------------+
int OnInit ()
{
         SubWin.AddThese(C_TemplateChart::INDICATOR, user01);
         SubWin.AddThese(C_TemplateChart::SYMBOL, user02);

         return INIT_SUCCEEDED ;
}
//+------------------------------------------------------------------+

//...... остальные строки не представляют интереса для нас ......

//+------------------------------------------------------------------+
void OnChartEvent ( const int id,
                   const long &lparam,
                   const double &dparam,
                   const string &sparam)
{
         if (id == CHARTEVENT_CHART_CHANGE ) SubWin.Resize();
}
//+------------------------------------------------------------------+

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

Функции
Описание
SetBase Создаст объект, необходимый для отображения данных индикатора
decode Декодирует переданную ему команду
AddTemplate Производит настройку в зависимости от типа представленных данных
C_Template Конструктор класса по умолчанию
~ C_Template Деструктор класса
resize
Изменение масштаба подокна
AddThese Функция, отвечающая за доступ к внутренним объектам и их построение.

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

 #define def_MaxTemplates         6

Эта строка очень важна для нашего класса объекта, она определяет максимальное количество указателей, которые мы можем создать. Если мы хотим добавить больше или меньше, просто нужно изменить данное число в этом пункте, так всё становится намного проще, поскольку мы получаем динамическое распределение памяти и мы не хотим слишком много указателей на нашем экране. Это, пожалуй, единственный пункт, который действительно нужно будет изменить, если мы этого захотим, но я думаю, что 6 - подходящее число для большинства людей и используемых мониторов.

Следующая строка - это перечисление, которое облегчает управление данными в некоторых пунктах программы:

 enum eTypeChart {INDICATOR, SYMBOL};

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

 private :

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

 void SetBase( const string szSymbol, int scale)
{
#define macro_SetInteger(A, B) ObjectSetInteger (m_Id, m_szObjName[m_Counter], A, B)

...

         ObjectCreate (m_Id, m_szObjName[m_Counter], OBJ_CHART , m_IdSubWin, 0 , 0 );
         ObjectSetString (m_Id, m_szObjName[m_Counter], OBJPROP_SYMBOL , szSymbol);
        macro_SetInteger( OBJPROP_CHART_SCALE , scale);
...
        macro_SetInteger( OBJPROP_PERIOD , _Period );
        m_handle = ObjectGetInteger (m_Id, m_szObjName[m_Counter], OBJPROP_CHART_ID );
        m_Counter++;
#undef macro_SetInteger
};

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

Сделав это, мы создаем объект типа CHART. Возможно, вы удивитесь. Мы будем использовать что-то такое, что нельзя изменить для того,чтобы изменить ситуацию? Да, это так. Следующим шагом будет объявление актива для использования, и здесь есть первый момент - если во время сохранения настроек графика не существует никакого актива, то актив, с которым связан этот объект, будет активом который мы используем; если актив присутствует при создании настроек, этот актив будет использоваться. Теперь важная деталь: мы можем указать другой актив, и с ним использовать общую настроек, но я объясню это более подробно в следующей статье, потому что мы собираемся реализовать некоторые улучшения в этом коде, чтобы иметь возможность делать те вещи, которые здесь невозможны. Далее у нас есть уровень плотности информации, который указывается в свойстве OBJPROP_CHART_SCALE, поэтому мы используем значения от 0 до 5. Хотя мы можем использовать значения и вне этого диапазона, всё же луяше, чтобы был соблюден этот диапазон.

Следующее, на что стоит обратить внимание, это свойство OBJPROP_PERIOD. Мы используем таймфрейм текущего графика, и это означает, что если мы изменим его, он также изменится. Позже мы внесем некоторые изменения, которые позволят зафиксировать значение. Но если захотите попробовать, то можно использовать таймфрейм от MetaTrader 5, например: PERIOD_M10, который может указывать на отображение данных за фиксированный период в 10 минут. Чуть позже мы поработаем над этим, пока не будем углубляться. После этого мы увеличиваем количество подпоказателей на единицу и уничтожаем макрос. Это означает, что после этого он больше не будет иметь никакого представления и должен быть переопределен для использования в другом месте. Я ничего не забыл? Конечно, — это линия, возможно, самая важная часть функции.

m_handle = ObjectGetInteger (m_Id, m_szObjName[m_Counter], OBJPROP_CHART_ID );

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

 void AddTemplate( const eTypeChart type, const string szTemplate, int scale)
{
	if (m_Counter >= def_MaxTemplates) return ;
	if (type == SYMBOL) SymbolSelect (szTemplate, true );
	SetBase((type == INDICATOR ? _Symbol : szTemplate), scale);
	ChartApplyTemplate (m_handle, szTemplate + ".tpl" );
	ChartRedraw (m_handle);
}

Мы увидим, что сначала мы проверяем возможность или её отсутствие для добавления нового индикатора, и если это возможно, то мы проверяем, является ли он SYMBOL, и если да, то SYMBOL должен присутствовать в окне Market Watch, что гарантируется функцией. Из этого мы создаем объект, который будет получать информацию. После выполнения, шаблон применяется к OBJ_CHART, и именно в этот момент происходит волшебство: мы вызываем объект повторно, но теперь он будет содержать данные в соответствии с определениями, содержащимися в файле настроек, который был использован для определения OBJ_CHART. Теперь он простой, красивый и понятный и восхищает своей простотой.

С помощью только этих двух функций можно было бы сделать многое, но нам нужна, по крайней мере, еще одна функция, которая приводится ниже в полном объеме:

 void Resize( void )
{
         int x0 = 0 , x1 = ( int )( ChartGetInteger (m_Id, CHART_WIDTH_IN_PIXELS , m_IdSubWin) / (m_Counter > 0 ? m_Counter : 1 ));
         for ( char c0 = 0 ; c0 < m_Counter; c0++, x0 += x1)
        {
                 ObjectSetInteger (m_Id, m_szObjName[c0], OBJPROP_XDISTANCE , x0);
                 ObjectSetInteger (m_Id, m_szObjName[c0], OBJPROP_XSIZE , x1);
                 ObjectSetInteger (m_Id, m_szObjName[c0], OBJPROP_YSIZE , ChartGetInteger (m_Id, CHART_HEIGHT_IN_PIXELS , m_IdSubWin));
        }
         ChartRedraw ();
}

То, что делает вышеупомянутая функция - она расставляет всё по своим местам, сохраняя данные всегда внутри области подокна и здесь больше нечего добавить. На этом мы закончили весь необходимый код, чтобы всё отлично работало. Но вы можете задать вопрос: а как же другие функции?! Будьте спокойны, остальные функции совсем не нужны, они только поддерживают интерпретацию командных строк. Сначала давайте посмотрим кое-что, что также важно для тех, кто захочет изменить этот код в будущем, а именно следующее зарезервированное слово, которое появляется в коде нашего класса объектов:

 public   :

Это слово гарантирует, что с этого момента все данные и функции могут быть доступны и просмотрены другими частями кода, даже если они не являются частью класса объекта, поэтому здесь мы объявляем, что на самом деле может быть изменено или доступно другим объектам. На самом деле, поведение хорошего объектно-ориентированного кода заключается в том, чтобы никогда не разрешать прямой доступ к данным объекта. В хорошо разработанном коде мы будем иметь доступ только к методам, и причина проста - безопасность. Когда мы позволяем внешнему коду изменять данные внутри класса, мы рискуем тем, что эти данные не будут соответствовать тому, что ожидает объект, и это вызывает множество проблем и головной боли при попытке решить несоответствия или дефекты, когда всё кажется правильным. Поэтому, если я могу дать совет человека, который годами программировал на C++, НИКОГДА не позволяйте внешним объектам изменять или напрямую обращаться к данным в созданном вами классе, предоставьте функции или процедуры, чтобы к данным можно было получить доступ, но никогда не позволяйте обращаться к данным напрямую и убедитесь, что функции и процедуры поддерживают данные в соответствии с ожиданиями созданного вами класса. Учитывая это сообщение, давайте перейдем к последним двум функциям нашего урока, одна из которых является публичной (AddThese), а другая - приватной (Decode), полностью их можно увидеть ниже:

void Decode( string &szArg, int &iScale)
{
#define def_ScaleDefault 4
         StringToUpper (szArg);
        iScale = def_ScaleDefault;
         for ( int c0 = 0 , c1 = 0 , max = StringLen (szArg); c0 < max; c0++) switch (szArg[c0])
        {
                 case ':' :
                         for (; (c0 < max) && ((szArg[c0] < '0' ) || (szArg[c0] > '9' )); c0++);
                        iScale = ( int )(szArg[c0] - '0' );
                        iScale = ((iScale > 5 ) || (iScale < 0 ) ? def_ScaleDefault : iScale);
                        szArg = StringSubstr (szArg, 0 , c1 + 1 );
                         return ;
                 case ' ' :
                         break ;
                 default :
                        c1 = c0;
                         break ;
        }
#undef def_ScaleDefault
}
//+------------------------------------------------------------------+
// ... Коды, не относящиеся к этой части ...
//+------------------------------------------------------------------+
void AddThese( const eTypeChart type, string szArg)
{
         string szLoc;
         int i0;
         StringToUpper (szArg);
         StringAdd (szArg, ";" );
         for ( int c0 = 0 , c1 = 0 , c2 = 0 , max = StringLen (szArg); c0 < max; c0++) switch (szArg[c0])
        {
                 case ';' :
                         if (c1 != c2)
                        {
                                szLoc = StringSubstr (szArg, c1, c2 - c1 + 1 );
                                Decode(szLoc, i0);
                                AddTemplate(type, szLoc, i0);
                        }
                        c1 = c2 = (c0 + 1 );
                         break ;
                 case ' ' :
                        c1 = (c1 >= c2 ? c0 + 1 : c1);
                         break ;
                 default :
                        c2 = c0;
                         break ;
        }
}

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

Конечный результат будет выглядеть так, как показано ниже:



Заключение:

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


Перевод с португальского произведен MetaQuotes Software Corp.
Оригинальная статья: https://www.mql5.com/pt/articles/10230

Прикрепленные файлы |
Как сделать график более интересным: добавление фона Как сделать график более интересным: добавление фона
Многие рабочие терминалы содержат некое репрезентативное изображение, которое показывает что-то о пользователе, эти изображения делают рабочий стол более красивым и разнообразным. Давайте посмотрим, как сделать графики более интересными, добавив фон.
DoEasy. Элементы управления (Часть 1): Первые шаги DoEasy. Элементы управления (Часть 1): Первые шаги
С этой статьи начинаем обширную тему по созданию на MQL5 элементов управления в стиле Windows Forms. И начнём тему с создания класса панели. Без наличия элементов управления уже становится сложно обходиться. Поэтому мы создадим все возможные элементы управления в стиле Windows Forms.
DoEasy. Элементы управления (Часть 2): Продолжаем работу над классом CPanel DoEasy. Элементы управления (Часть 2): Продолжаем работу над классом CPanel
В статье избавимся от некоторых ошибок при работе с графическими элементами и продолжим разработку элемента управления CPanel. Это будут методы для установки параметров шрифта, который используется по умолчанию для всех текстовых объектов панели, которые в свою очередь могут быть на ней расположены в дальнейшем.
Несколько индикаторов на графике (Часть 01): Понимание концепций Несколько индикаторов на графике (Часть 01): Понимание концепций
Сегодня разберем, как можно добавить несколько индикаторов в график одновременно, не занимая при этом отдельную его область. При торговле много трейдеров чувствуют себя более уверенно, если одновременно смотрят на несколько индикаторов (например, RSI, STOCASTIC, MACD, ADX и другие), а в некоторых случаях даже на разные активы, составляющие тот или иной индекс.