English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
preview
Несколько индикаторов на графике (Часть 03): Разработка пользовательских определений

Несколько индикаторов на графике (Часть 03): Разработка пользовательских определений

MetaTrader 5Примеры | 19 апреля 2022, 08:57
2 307 0
Daniel Jose
Daniel Jose

Введение

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


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

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

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

Начнем с новых определений частных переменных класса.

struct st
{
        string  szObjName,
                szSymbol;
        int     width;
}m_Info[def_MaxTemplates];
int             m_IdSubWin,
                m_Counter,
                m_CPre,
                m_Aggregate;
long            m_Id,
                m_handle;
ENUM_TIMEFRAMES m_Period;

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

void SetBase(const string szSymbol, int iScale, int iSize)
{
#define macro_SetInteger(A, B) ObjectSetInteger(m_Id, m_Info[m_Counter].szObjName, A, B)
        if (m_IdSubWin < 0)
        {
                m_Id = ChartID();
                m_IdSubWin = (int)ChartGetInteger(m_Id, CHART_WINDOWS_TOTAL) - 1;
                m_Aggregate = 0;
        }
        m_Info[m_Counter].szObjName = __FILE__ + (string) MathRand() + (string) ObjectsTotal(m_Id, -1, OBJ_CHART);
        ObjectCreate(m_Id, m_Info[m_Counter].szObjName, OBJ_CHART, m_IdSubWin, 0, 0);
        ObjectSetString(m_Id, m_Info[m_Counter].szObjName, OBJPROP_SYMBOL, (m_Info[m_Counter].szSymbol = szSymbol));

// ....

        macro_SetInteger(OBJPROP_PERIOD, m_Period);
        m_handle = ObjectGetInteger(m_Id, m_Info[m_Counter].szObjName, OBJPROP_CHART_ID);
        m_Aggregate += iSize;
        m_Info[m_Counter].width = iSize;
        m_CPre += (iSize > 0 ? 1 : 0);
        m_Counter++;
#undef macro_SetInteger
};

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

void Decode(string &szArg, int &iScale, int &iSize)
{
#define def_ScaleDefault 4
#define macro_GetData(A)                \
        b0 = false;                     \
        for (c0++; (c0 < max) && (szArg[c0] == ' '); c0++);     \
        for (i0 = 0, i1 = c0; (c0 < max) && (szArg[c0] != A); i0 = (szArg[c0] != ' ' ? c0 - i1 + 1 : i0), c0++);        \
        if (szArg[c0] == A) sz1 = StringSubstr(szArg, i1, i0); else sz1 = "";
                                                                
        string sz1;
        int i0, i1, c1 = StringLen(szArg);
        bool b0 = true;
        StringToUpper(szArg);
        iScale = def_ScaleDefault;
        m_Period = _Period;
        for (int c0 = 0, max = StringLen(szArg); c0 < max; c0++) switch (szArg[c0])
        {
                case ':':
                        b0 = false;
                        for (; (c0 < max) && ((szArg[c0] < '0') || (szArg[c0] > '9')); c0++);
                        iScale = (int)(szArg[c0] - '0');
                        iScale = ((iScale > 5) || (iScale < 0) ? def_ScaleDefault : iScale);
                        break;
                case ' ':
                        break;
                case '<':
                        macro_GetData('>');
                        if (sz1 == "1M") m_Period = PERIOD_M1; else

//....

                        if (sz1 == "1MES") m_Period = PERIOD_MN1;
                        break;
                case '[':
                        macro_GetData(']');
                        iSize = (int) StringToInteger(sz1);
                        break;
                default:
                        c1 = (b0 ? c0 : c1);
                        break;
        }
        szArg = StringSubstr(szArg, 0, c1 + 1);
#undef macro_GetData
#undef def_ScaleDefault
}

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

Разделитель Функциональность Пример  Результат 
< > Указывает графический период, который будет использоваться  < 15m > Зафиксирует период индикатора на 15 минут, исходный график может использовать другой период, но индикатор будет отображаться только с данными за 15 минут.
 [ ] Указывает ширину индикатора  [ 350 ] Зафиксирует ширину индикатора на уровне 350 пикселей. 

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

         

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

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

#define macro_GetData(A)                \
        b0 = false;                     \
        for (c0++; (c0 < max) && (szArg[c0] == ' '); c0++);     \
        for (i0 = 0, i1 = c0; (c0 < max) && (szArg[c0] != A); i0 = (szArg[c0] != ' ' ? c0 - i1 + 1 : i0), c0++);        \
        if (szArg[c0] == A) sz1 = StringSubstr(szArg, i1, i0); else sz1 = "";
           
//....                                                     
                case '<':
                        macro_GetData('>');

Обратите внимание, что мы определяем нечто, что для многих может показаться странным, но его название очень наводит на размышления: macro_GetData(A), это создаст часть кода, которая является макросом. Когда компилятор найдет это определение в коде, то компилятор заменит объявление на код макроса, что будет очень полезным, когда мы собирёмся повторить определенную часть кода в нескольких местах, но с минимальными изменениями между одним объявлением и другим. В предыдущем примере зеленая строка будет заменена и код, сгенерированный компилятором, будет таким, как показано ниже:

case '<':
	b0 = false;
        for (c0++; (c0 < max) && (szArg[c0] == ' '); c0++);
        for (i0 = 0, i1 = c0; (c0 < max) && (szArg[c0] != '>'); i0 = (szArg[c0] != ' ' ? c0 - i1 + 1 : i0), c0++);
        if (szArg[c0] == A) sz1 = StringSubstr(szArg, i1, i0); else sz1 = "";
//....

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

//.....

if (sz1 == "1M") m_Period = PERIOD_M1; else

//.....

Выделенная информация является важной деталью. Согласно шаблону, который я применил, пользователь, если он хочет использовать одноминутный период, должен сообщить об этом, используя следующий синтаксис: MIN_1. Но это можно изменить, если мы хотим оставить всё в индивидуальном стиле, тогда буквы должны быть в верхнем регистре и без пробелов. Мы можем, например, заменить выделенный фрагмент на "1MIN", "MIN_1", "1_MINUTE" или что-то еще более очевидное, например: "LOCK_IN_1_MIN" или на русском "ЗАФИКСИРОВАТЬ_НА_1_МИН", это может быть сделано и будет работать, пока нет пробелов между словами. Это ограничение на пробелы - то, что можно убрать, но, как мне кажется, нет реальной необходимости делать это. Благодаря тому, что мы умеем программировать, мы можем оставить всё с пользовательским стилем. Следующим кодом, котоый я изменил, был деструктор по умолчанию.

~C_TemplateChart() 
{
        for (char c0 = 0; c0 < m_Counter; c0++)
        {
                ObjectDelete(m_Id, m_Info[c0].szObjName);
                SymbolSelect(m_Info[c0].szSymbol, false);
        }
}

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

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

Как я уже говорил, выделенная строка делает то, что можно сделать, а именно: использовать файл настроек по умолчанию, если все наблюдаемые активы используют одни и те же настройки. Нехорошо делать что-то, без понимания того, как действовать, если всё идет не так, как ожидалось. Но если настройки ТОЧНО такие же, почему бы не использовать эти же самые настройки? Обратите внимание, что когда файл настроек указанного актива не найден, будет считаться, что вы хотите использовать настройки MetaTrader 5 по умолчанию, а это определяется как файл под названием DEFAULT.TPL, который находится в каталоге Profiles\Template, но давайте раньше разберем одну важную вещь. Почему в функции ChartApplyTemplate я не сообщил ни об одном каталоге поиска? Причина в том, что поиск выполняется MetaTrader 5 по определенной логике, и знание того, как эта логика работает, может помочь вам разобраться с ситуацией очень интересным и менее напряженным способом.

Представим следующий сценарий, в котором мы заменим выделенную строку на эту:

if (!ChartApplyTemplate(m_handle, "MyTemplates\\" + szTemplate + ".tpl")) if (type == SYMBOL) ChartApplyTemplate(m_handle, ""MyTemplates\\Default.tpl");

MetaTrader 5 сначала будет искать файл настроек в подкаталоге MYTEMPLATES того каталога, где находится исполняемый файл пользовательского индикатора, т.е. если в той же папке, где находится исполняемый файл, который мы используем для создания нескольких индикаторов, есть папка MYTEMPLATES, то MetaTrader 5 будет искать нужный нам файл там. Однако если там ничего не найдено, он будет искать тот же файл, но теперь в папке MQL5\Profiles\Templates\MyTemplates, вот почему я не показал его раньше. Но дело не только в этом, потому что в том же самом коде есть еще одна деталь, предположим, что она показана ниже:

if (!ChartApplyTemplate(m_handle, "\\MyTemplates\\" + szTemplate + ".tpl")) if (type == SYMBOL) ChartApplyTemplate(m_handle, ""\\MyTemplates\\Default.tpl");

Одна маленькая деталь, которая меняет всё: теперь MetaTrader 5 сначала будет искать нужный нам файл в директории MQL5 MyTemplates, и если он не найдет нужный файл, то выполнит шаги, описанные выше. Это можно посмотреть в документации к ChartApplyTemplate, потому что я не хочу запутать тех, кто не знает, как работает MetaTrader 5 , сформировав ложное представление о том, как это работает. Но теперь, когда вы понимаете, как проводится поиск файла настроек, вы можете создавать вариации и знать, куда вы должны поместить файлы.

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

void Resize(void)
{
#define macro_SetInteger(A, B) ObjectSetInteger(m_Id, m_Info[c0].szObjName, A, B)
        int x0 = 0, x1, y = (int)(ChartGetInteger(m_Id, CHART_HEIGHT_IN_PIXELS, m_IdSubWin));
        x1 = (int)((ChartGetInteger(m_Id, CHART_WIDTH_IN_PIXELS, m_IdSubWin) - m_Aggregate) / (m_Counter > 0 ? (m_CPre == m_Counter ? m_Counter : (m_Counter - m_CPre)) : 1));
        for (char c0 = 0; c0 < m_Counter; x0 += (m_Info[c0].width > 0 ? m_Info[c0].width : x1), c0++)
        {
                macro_SetInteger(OBJPROP_XDISTANCE, x0);
                macro_SetInteger(OBJPROP_XSIZE, (m_Info[c0].width > 0 ? m_Info[c0].width : x1));
                macro_SetInteger(OBJPROP_YSIZE, y);
        }
        ChartRedraw();
#undef macro_SetInteger
}

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

void AddThese(const eTypeChart type, string szArg)
{
        string szLoc;
        int i0, iSize;
//....
        Decode(szLoc, i0, iSize);
        AddTemplate(type, szLoc, i0, iSize);
//....
}

Обратите внимание, что единственное изменение выделено, как видете — это практически ничего.


Заключение

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



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

Прикрепленные файлы |
DoEasy. Элементы управления (Часть 3): Создание привязанных элементов управления DoEasy. Элементы управления (Часть 3): Создание привязанных элементов управления
В статье разберём создание подчинённых элементов управления, привязанных к базовому элементу, создаваемых непосредственно при помощи функционала базового элемента управления. Помимо поставленной выше задачи, немного поработаем над объектом-тенью графического элемента, так как при её использовании для любого из объектов, позволяющих иметь тень, до сих пор есть неисправленные ошибки логики
DoEasy. Элементы управления (Часть 2): Продолжаем работу над классом CPanel DoEasy. Элементы управления (Часть 2): Продолжаем работу над классом CPanel
В статье избавимся от некоторых ошибок при работе с графическими элементами и продолжим разработку элемента управления CPanel. Это будут методы для установки параметров шрифта, который используется по умолчанию для всех текстовых объектов панели, которые в свою очередь могут быть на ней расположены в дальнейшем.
DoEasy. Элементы управления (Часть 4): Элемент управления "Панель", параметры Padding и Dock DoEasy. Элементы управления (Часть 4): Элемент управления "Панель", параметры Padding и Dock
В статье реализуем работу таких параметров панели как Padding (внутренние отступы/поля со всех сторон элемента) и Dock (способ расположения объекта внутри контейнера).
Как сделать график более интересным: добавление фона Как сделать график более интересным: добавление фона
Многие рабочие терминалы содержат некое репрезентативное изображение, которое показывает что-то о пользователе, эти изображения делают рабочий стол более красивым и разнообразным. Давайте посмотрим, как сделать графики более интересными, добавив фон.