Модульная архитектура индикаторов в MQL5 (Часть 1): Хватит копировать и вставлять — начните писать масштабируемый, переиспользуемый код
Введение
Некоторое время назад я решил написать статью о разработке собственного индикатора. Тогда я понял, что использование моего каркаса без предварительного представления вводило бы в заблуждение. Именно поэтому вы читаете эту вводную статью. Материал оказался обширным, поэтому он будет разделен на две части.
В этой первой статье мы разработаем каркас для переносимых, пригодных для повторного использования и удобных в сопровождении индикаторов. Я мог бы представить окончательное решение и лишь после этого объяснить его, но это не дало бы полного понимания механики процесса. Вместо этого начнем с примитивной реализации очень простого индикатора и постепенно будем выполнять его рефакторинг, шаг за шагом развивая кодовую базу в полнофункциональный шаблон. Выстраивая его с самых основ, вы увидите, какие именно проблемы мы решаем на каждом этапе и почему необходима именно такая архитектура.
Во второй статье мы воспользуемся этим каркасом для создания новых индикаторов. Мы расширим набор модулей и продемонстрируем, как объединять готовые модули в более сложные индикаторы. Кроме того, я создам шаблон AlgoForge, который позволит быстро и удобно настраивать новые проекты, чтобы все нужное было под рукой и никакие лишние компоненты не мешали работе.
Примечание об отслеживании кода
В этой статье каждый фрагмент кода сопровождается подписью, содержащей ссылку. Как показано на рисунке ниже, эти ссылки представляют собой сами сообщения коммитов и ведут непосредственно к соответствующему коммиту на AlgoForge.

Настоятельно рекомендую вам переходить по этим ссылкам! При рефакторинге кода зачастую недостаточно просто рассматривать отдельные статические фрагменты. Перейдя по этим ссылкам, вы увидите точные построчные изменения на каждом этапе — что было добавлено (зеленым цветом) и что было удалено (красным цветом). Отслеживание различий в коммитах — наилучший способ понять логику изменений и наглядно увидеть, как развивается проект.
1. Отправная точка: Примитивная реализация
Задача очень простая: пользователь выбирает цену из списка ENUM_APPLIED_PRICE в настройках индикатора, а индикатор строит линию, основываясь на этой цене. Нам понадобится массив для буфера индикатора, функция, которая вычисляет применяемую цену, и один входной параметр.
input(name="Price") ENUM_APPLIED_PRICE inpAppliedPrice = PRICE_CLOSE; double buffer[]; double calculate(int barIdx, ENUM_APPLIED_PRICE a_mode, const double &open[], const double &high[], const double &low[], const double &close[]) { switch(a_mode) { case PRICE_OPEN: return open[barIdx]; case PRICE_HIGH: return high[barIdx]; case PRICE_LOW: return low[barIdx]; case PRICE_MEDIAN: return (high[barIdx] + low[barIdx]) / 2.0; case PRICE_TYPICAL: return (high[barIdx] + low[barIdx] + close[barIdx]) / 3.0; case PRICE_WEIGHTED: return (high[barIdx] + low[barIdx] + close[barIdx] + close[barIdx]) / 4.0; default: return close[barIdx]; } }
Код № 1 | Evolution.mq5 |Первоначальный коммит
Почти готово. Осталось лишь присвоить каждому элементу буферного массива значение, которое вернула наша функция. Но чтобы избежать пересчета всего буфера индикатора при каждом событии Calculate, нужно выяснить, с чего начать перерасчет. Вы, вероятно, уже видели много подобного кода на форуме или в CodeBase:
int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { int limit, counted_bars = prev_calculated; if(counted_bars < 0) // checking for possible errors return(-1); if(counted_bars > 0) counted_bars--; // the last calculated bar must be recalculated limit = rates_total - counted_bars - 1; for(int bar = limit; bar >= 0; bar--) { //... } return(rates_total); }
До сих пор вспоминаю времена, когда сам был начинающим и часами зарисовывал бары в блокноте, пытаясь понять, как на самом деле работает подобная логика. Но на самом деле все довольно просто — существует всего три возможных сценария:
- Изменился только текущий бар — пересчитываем только текущий бар.
- Появился новый бар — пересчитываем предыдущий и текущий бары.
- Во всех остальных случаях пересчитываем весь буфер индикатора.
Поскольку сценарий № 3 по сути представляет собой «все остальные случаи», нам нужно обнаружить только первые два: «изменился только текущий бар» и «появился новый бар». Для этого вычисляем разницу между rates_total и prev_calculated:
- Изменился только текущий бар → rates_total - prev_calculated == 0
- Появился новый бар → rates_total - prev_calculated == 1
Теперь мы полностью формализовали логику и можем визуализировать ее в виде блок-схемы:

Приведенный ниже код представляет собой прямой перевод этой блок-схемы на язык MQL5:
int OnCalculate(const int32_t rates_total, const int32_t prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int32_t &spread[]) { int diff = rates_total - prev_calculated; //--- Only the current bar changed if(diff == 0) { //--- Recalculate current bar int curBarIdx = rates_total - 1; buffer[curBarIdx] = calculate(curBarIdx, inpAppliedPrice, open, high, low, close); } //--- A new bar appeared else if(diff == 1) { //--- Recalculate previous bar int prevBarIdx = rates_total - 2; buffer[prevBarIdx] = calculate(prevBarIdx, inpAppliedPrice, open, high, low, close); //--- Recalculate current bar int curBarIdx = rates_total - 1; buffer[curBarIdx] = calculate(curBarIdx, inpAppliedPrice, open, high, low, close); } //--- In all other cases else { //--- Recalculate entire buffer for(int i = 0; i < rates_total; i++) buffer[i] = calculate(i, inpAppliedPrice, open, high, low, close); } return rates_total; }
Код № 2 | Evolution.mq5 | Первоначальный коммит
Разумеется, код, представляющий собой непосредственный перевод блок-схемы на язык MQL5, не особенно хорош с точки зрения программирования. Ниже представлена функция OnCalculate после небольшого рефакторинга. Я просто перенес пересчет буфера за пределы оператора ветвления.
int OnCalculate(const int32_t rates_total, const int32_t prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int32_t &spread[]) { int barIdxToRecalcFrom; switch(rates_total - prev_calculated) { case 0: barIdxToRecalcFrom = rates_total - 1; break; case 1: barIdxToRecalcFrom = rates_total - 2; break; default: barIdxToRecalcFrom = 0; break; } for(int i = barIdxToRecalcFrom; i < rates_total; i++) buffer[i] = calculate(i, inpAppliedPrice, open, high, low, close); return rates_total; }
Код № 3 | Evolution.mq5 | рефакторинг: унификация цикла пересчета буфера в OnCalculate
Мы можем еще больше упростить оператор switch:
int OnCalculate(const int32_t rates_total, const int32_t prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int32_t &spread[]) { int barIdxToRecalcFrom; switch(rates_total - prev_calculated) { case 0: case 1: barIdxToRecalcFrom = prev_calculated - 1; break; default: barIdxToRecalcFrom = 0; break; } for(int i = barIdxToRecalcFrom; i < rates_total; i++) buffer[i] = calculate(i, inpAppliedPrice, open, high, low, close); return rates_total; }
Код № 4 | Evolution.mq5 | рефакторинг: свертывание операторов ветвления в OnCalculate
Следует отметить, что данная логика несколько упрощена по сравнению с традиционным подходом. Она надежнее, менее многословна и легче читается. Кроме того, упрощение логики перерасчета открывает возможности для повышения производительности — например, за счет отказа от вспомогательных буферов индикаторов, как в Tillson T3 (вернемся к этому в следующих частях).
Однако есть один сценарий, который мы до сих пор не учли. В тестере стратегий с отключенным визуальным режимом советник может запрашивать значения индикатора реже, чем один раз за бар — это означает, что между вызовами CopyBuffer() могут появиться несколько новых баров. Чтобы избежать повторного пересчета буфера в этом случае, просто добавьте директиву свойств tester_everytick_calculate (см. также Расчет индикаторов при тестировании):
#property tester_everytick_calculate
Код № 5 | Evolution.mq5 | исправление: включение tester_everytick_calculate
В редких случаях функцию OnCalculate() можно вызвать с параметрами rates_total = 1 и prev_calculated = 0. Это может произойти, если пользователь переключится на не использованный ранее сервер во время работы индикатора. В результате barIdxToRecalcFrom будет устанавливаться в значение -1, что вызовет ошибку выполнения «массив выходит за пределы диапазона» и приведет к сбою в работе индикатора. Описанная ниже простая проверка достаточна для предотвращения этого. Я, честно говоря, не ожидаю, что значение rates_total когда-либо будет меньше значения prev_calculated, но если это все же произойдет, такой случай тоже будет обработан.
if(rates_total < 2 || rates_total < prev_calculated) { PrintFormat(__FUNCTION__" Something went wrong. rates_total %i, prev_calculated %i", rates_total, prev_calculated); return 0; }
Код № 6 | Evolution.mq5 | исправление: добавление проверок корректности значений rates_total и prev_calculated
На этом создание примитивного индикатора, реализованного в непереносимом виде, завершено. Полный исходный код доступен на AlgoForge.
2. Ключевые составляющие: Заголовочные файлы и объектно-ориентированное программирование.
Чтобы повторно использовать этот код в другом проекте, вам придется просто скопировать и вставить его. Поскольку разработанный нами код очень короткий и примитивный, у вас, вероятно, не возникнет с этим никаких проблем.
Теперь представьте, что в новом проекте вы хотите повторно использовать большой фрагмент длинного, запутанного кода — кода, где необходимые вам данные и функции безнадежно переплетены с другими компонентами системы. Это происходит по той причине, что при создании этого кода вы не потрудились подумать о том, как сделать его легко пригодным для повторного использования в будущем. Более того, новый проект тоже не пустой — он уже сам по себе содержит немало запутанного кода типа «спагетти». При таких условиях повторное использование кода не доставит удовольствия. Вероятно, вы проведете за отладкой часы, если не дни. Когда я начинал, иной раз случалось, что я тратил три дня на отладку кода, написание которого занимало всего два часа.
Заголовочные файлы
Если вы хотите, чтобы ваш код можно было использовать многократно, вам не обойтись без файлов .mqh. В этой статье я буду называть файлы с расширением .mqh заголовочными файлами. Заголовочные файлы нужны не только для повторного использования кода — они значительно упрощают разработку и сокращают время, затрачиваемое на написание кода.
- Представьте себе библиотеку без полок, где все книги просто свалены в огромную кучу.
- Представьте себе супермаркет площадью в несколько тысяч квадратных метров, продающий огромное количество товаров, но без полок, секций и какой-либо организации. Все хаотично разбросано по полу. Сколько времени вам понадобится, чтобы найти кофе и корм для кошки, за которыми вы пришли?
Вот так выглядит проект, когда он состоит из одного файла, содержащего тысячи — или даже десятки тысяч — строк кода. Я тоже когда-то писал большие проекты, упаковывая их в один файл. Cписок функций занимал несколько экранов, поэтому мне было проще находить функции с помощью <Ctrl+F>. Но иногда я даже не мог вспомнить название функции, которую искал. Хуже того, иногда я забывал, что эта функция уже существует, и писал другую, делающую то же самое. Очевидно, что такой код было очень сложно поддерживать и практически невозможно масштабировать.
Компиляция любого заголовочного файла должна выполняться без ошибок и предупреждений.
Мне это кажется очевидным. В большинстве случаев код компилируют не из-за того, что готовы запустить программу, а потому, что хотят, чтобы компилятор проверил код на наличие ошибок. Кстати, компиляция всего решения может занять секунды или даже десятки секунд, если ваш проект большой. Но если ваш проект разбит на небольшие заголовочные файлы, то после внесения изменений в один из них вам нужно будет скомпилировать только этот конкретный файл, чтобы проверить наличие ошибок и сохранить изменения. В отличие от компиляции всего решения, компиляция отдельного заголовочного файла происходит практически мгновенно (за редкими исключениями).
Объектно-ориентированное программирование
Вы когда-нибудь пробовали разбивать код на заголовочные файлы без использования объектно-ориентированного программирования? В те времена, когда я скептически относился к ООП — и, честно говоря, был слишком ленив, чтобы его изучать, — я перепробовал все возможные способы организации кода в заголовочные файлы без его использования. Ничего не помогло.
Не объектно-ориентированный код, по сути, состоит из двух основных частей: функций и данных в глобальной области видимости. Типичные примеры данных в глобальной области видимости:
- Параметры конфигурации.
- Массивы индикаторных буферов.
- Переменные, которые хранят значения между вызовами функции OnCalculate() (простой пример — имена созданных объектов графика).
- Переменные, созданные для уменьшения количества передаваемых функциям параметров. Все мы это делали — создавали глобальную переменную только для того, чтобы иметь к ней доступ из разных функций.
Конечно, вы можете перенести объявления глобальных данных в заголовочный файл. Но это всего лишь объявление! Недостаточно просто объявить данные — необходимо также правильно вызывать функции, работающие с этими данными, в нужном месте и в правильном порядке. В противном случае вы рискуете создать ошибки, даже не написав новый код, а просто случайно нарушив последовательность вызовов функций или непреднамеренно изменив общие данные.
До того, как я начал программировать в объектно-ориентированной парадигме, мне постоянно приходилось выбирать между «спагетти-кодом» и раздутыми сигнатурами функций. Чем больше переменных я делал глобальными, тем запутаннее становился код. Пытаясь избежать «спагетти-кода», я переместил переменные внутрь функций, что привело к созданию громоздких сигнатур функций.
Без объектно-ориентированного программирования ваши идентификаторы (имена функций и переменных) становятся ужасно длинными и неудобными. Иногда мне хотелось разбить длинную функцию на несколько меньших, но я отказался от этой идеи, потому что это потребовало бы изобретения нескольких длинных и неуклюжих названий.
Все описанные выше проблемы легко решаются с помощью инкапсуляции — механизма в объектно-ориентированном программировании, который объединяет данные и функции, работающие с этими данными. Инкапсуляция — это не просто ключ к эффективному разделению кода на заголовочные файлы, это ключ к объектно-ориентированному программированию в целом.
3. Инкапсуляция логики: Первый модуль
Теперь давайте сделаем написанный нами код немного более переносимым (без изменения его поведения). Я обернул код применяемой цены в класс под названием CAppliedPrice и переместил этот класс в отдельный файл: src/AppliedPrice.mqh. Полный код показан ниже.
class CAppliedPrice { public: static double calculate(int barIdx, ENUM_APPLIED_PRICE mode, const double &open[], const double &high[], const double &low[], const double &close[]) { switch(mode) { case PRICE_OPEN: return open[barIdx]; case PRICE_HIGH: return high[barIdx]; case PRICE_LOW: return low[barIdx]; case PRICE_MEDIAN: return (high[barIdx] + low[barIdx]) / 2.0; case PRICE_TYPICAL: return (high[barIdx] + low[barIdx] + close[barIdx]) / 3.0; case PRICE_WEIGHTED: return (high[barIdx] + low[barIdx] + close[barIdx] + close[barIdx]) / 4.0; default: return close[barIdx]; } } int onCalculate(const int rates_total, const int prev_calculated, const double &open[], const double &high[], const double &low[], const double &close[]) { if(rates_total < 2 || rates_total < prev_calculated) { PrintFormat(__FUNCTION__" Something went wrong. rates_total %i, prev_calculated %i", rates_total, prev_calculated); return 0; } int barIdxToRecalcFrom; switch(rates_total - prev_calculated) { case 0: case 1: barIdxToRecalcFrom = prev_calculated - 1; break; default: barIdxToRecalcFrom = 0; break; } for(int i = barIdxToRecalcFrom; i < rates_total; i++) buffer[i] = calculate(i, mode, open, high, low, close); return rates_total; } private: const ENUM_APPLIED_PRICE mode; public: double buffer[]; public: CAppliedPrice(ENUM_APPLIED_PRICE a_mode) : mode(a_mode) {} };
Код № 7 | AppliedPrice.mqh |рефакторинг: извлечение применяемой цены в CAppliedPrice
Функция calculate() была перемещена без каких-либо изменений. Я сделал метод CAppliedPrice::calculate() публичным и статическим на случай, если он мне когда-нибудь понадобится, но я захочу избежать хранения применяемой цены в буфере. Тело функции OnCalculate() стало телом функции CAppliedPrice::onCalculate().
Вот все, что осталось в файле Evolution.mq5 после извлечения CAppliedPrice (я не цитирую директивы #property):
#include "src\AppliedPrice.mqh" input(name="Price") ENUM_APPLIED_PRICE inpAppliedPrice = PRICE_CLOSE; CAppliedPrice* appliedPrice; int OnInit() { appliedPrice = new CAppliedPrice(inpAppliedPrice); SetIndexBuffer(0, appliedPrice.buffer, INDICATOR_DATA); return INIT_SUCCEEDED; } int OnCalculate(const int32_t rates_total, const int32_t prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int32_t &spread[]) { return appliedPrice.onCalculate(rates_total, prev_calculated, open, high, low, close); } void OnDeinit(const int reason) { delete appliedPrice; }
Код № 8 | Evolution.mq5 |рефакторинг: извлечение применяемой цены в CAppliedPrice
В OnInit() мы создаем экземпляр класса CAppliedPrice, сохраняем указатель на него в глобальной переменной appliedPrice и связываем appliedPrice.buffer с буфером индикатора. В OnDeinit() экземпляр удаляется, а OnCalculate() содержит всего один вызов метода.
Теперь CAppliedPrice — это переносимый модуль. Его код находится в отдельном файле, что исключает риск случайного внесения в него непреднамеренных изменений. Кроме того, код применяемой цены больше не мешает при работе с другими частями индикатора.
Договоримся отныне называть модули типа CAppliedPrice «субиндикаторами». Иными словами, субиндикатор — это класс, содержащий один или несколько массивов индикаторных буферов и код для их вычисления. Другой отличительной чертой субиндикаторов является публичный метод onCalculate(), что вполне логично, поскольку массивы индикаторных буферов должны обновляться всякий раз, когда происходит событие Calculate.
4. Устранение глобального состояния: введение CIndicator
Как уже упоминал, я тогда начал изучать ООП, потому что мне нужна была инкапсуляция для разделения исходного кода на переносимые модульные файлы.
Я помню период, когда я все еще изучал объектно-ориентированное программирование. Имеющаяся у меня база кодов находилась в переходном состоянии: часть кода уже была инкапсулирована в классы, а остальная часть представляла собой процедурный «спагетти-код» с большим количеством глобального состояния. В процессе преобразования существующего процедурного кода в классы у меня возник вопрос, который меня серьезно беспокоил: что произойдет, если имя члена класса войдет в конфликт с идентификатором, уже занятым в глобальной области видимости? Я не мог ответить на него, потому что все еще не был знаком со значительной частью синтаксиса ООП, но эта проблема засела у меня в голове.
Спустя несколько дней, перед сном, я снова об этом подумал. Всю ночь мне снился код, а утром меня внезапно осенила идея: «Глобальное состояние нужно убрать; в глобальной области видимости оставаться ничего не должно». Я сел за компьютер, обернул всю программу в один большой класс, CAdvisor (я тогда работал над советником) и заключил сам с собой соглашение, что глобальная область видимости будет содержать только указатель на экземпляр CAdvisor.
С тех пор прошло немало времени, но это соглашение по-прежнему действует. Итак, сразу же создадим компонент CIndicator в отдельном файле (src/Indicator.mqh) и будем работать исключительно внутри него.

#include "AppliedPrice.mqh" class CIndicator { public: int onCalculate(const int rates_total, const int prev_calculated, const double &open[], const double &high[], const double &low[], const double &close[], const datetime &time[]) { return appliedPrice.onCalculate(rates_total, prev_calculated, open, high, low, close); } private: CAppliedPrice appliedPrice; public: CIndicator(ENUM_APPLIED_PRICE mode) : appliedPrice(mode) { SetIndexBuffer(0, appliedPrice.buffer, INDICATOR_DATA); } };
Код № 9 | Indicator.mqh |рефакторинг: введение CIndicator в качестве корневого объекта
#include "src\Indicator.mqh" input(name="Price") ENUM_APPLIED_PRICE inpAppliedPrice = PRICE_CLOSE; CIndicator* Indicator; int OnInit() { Indicator = new CIndicator(inpAppliedPrice); return INIT_SUCCEEDED; } int OnCalculate(const int32_t rates_total, const int32_t prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int32_t &spread[]) { return Indicator.onCalculate(rates_total, prev_calculated, open, high, low, close, time); } void OnDeinit(const int reason) { delete Indicator; }
Код № 10 | Evolution.mq5 | рефакторинг: введение CIndicator в качестве корневого объекта
5. Изоляция входных параметров
По мере роста числа входных параметров передавать их в конструктор CIndicator становится все неудобнее, поскольку каждый входной параметр требует соответствующего параметра конструктора. Что мне особенно неудобно при добавлении или удалении входных данных, так это обновление списка аргументов в месте вызова. Довольно часто аргументы, которые необходимо добавить или удалить, разбросаны по разным частям списка, что вынуждает меня заново просматривать весь список аргументов и сверять его с сигнатурой конструктора.
Обернуть параметры в класс кажется удачной идеей. Ниже приведен пример того, как это может выглядеть:
#include "src\Indicator.mqh" input(name="Price") ENUM_APPLIED_PRICE inpAppliedPrice = PRICE_CLOSE; class CIndiParams { public: ENUM_APPLIED_PRICE appliedPrice; CIndiParams() { appliedPrice = inpAppliedPrice; } }; CIndicator* Indicator; int OnInit() { CIndiParams params; Indicator = new CIndicator(params); return INIT_SUCCEEDED; }
Однако этот код не скомпилируется, поскольку класс CIndicator определен в файле src/Indicator.mqh и не может увидеть определение класса CIndiParams в файле Evolution.mq5.
Если переместить CIndiParams в отдельный файл, у него больше не будет доступа к входным переменным из Evolution.mq5.
Конечно, мы могли бы переместить в общий заголовочный файл как CIndiParams, так и входные переменные. В таком случае, однако, входные переменные также станут видимыми из файла src/Indicator.mqh. Объективно говоря, это не критическая проблема, но я предпочел бы более высокую степень изоляции.
Таким образом, требования примерно следующие:
- входные параметры следует передавать конструктору CIndicator, обернутому в класс CIndiParams, расположенный в отдельном заголовочном файле;
- входные переменные должны быть доступны для CIndiParams;
- класс CIndicator должен иметь доступ к классу CIndiParams, но не к самим входным переменным.
Если понимать буквально, то это означает, что класс CIndiParams каким-то образом должен одновременно видеть и не видеть входные переменные. На первый взгляд это звучит абсурдно, но этого можно достичь с помощью наследования.

CIndiParamsBase ничего не знает о входных переменных и находится в отдельном файле:
class CIndiParamsBase { public: ENUM_APPLIED_PRICE appliedPrice; };
Код № 11 | IndiParamsBase.mqh |рефакторинг: замена аргументов конструктора CIndicator на объект params.
Затем класс CIndicator получает в своем конструкторе ссылку на класс CIndiParamsBase:
#include "IndiParamsBase.mqh" #include "AppliedPrice.mqh" class CIndicator { private: CAppliedPrice appliedPrice; public: CIndicator(const CIndiParamsBase ¶ms) : appliedPrice(params.appliedPrice) { SetIndexBuffer(0, appliedPrice.buffer, INDICATOR_DATA); } };
Код № 12 | Indicator.mqh |рефакторинг: замена аргументов конструктора CIndicator на объект params
Файл src/InputsAndRelated.mqh содержит входные переменные и производный класс CIndiParams, который присваивает значения этих входных переменных унаследованным полям базового класса:
#include "IndiParamsBase.mqh" input(name="Price") ENUM_APPLIED_PRICE inpAppliedPrice = PRICE_CLOSE; class CIndiParams : public CIndiParamsBase { public: CIndiParams() { appliedPrice = inpAppliedPrice; } };
Код № 13 | InputsAndRelated.mqh |рефакторинг: замена аргументов конструктора CIndicator на объект params
В результате Evolution.mq5 больше не объявляет входные переменные напрямую; вместо этого он подключает заголовочный файл, в котором они определены:
#include "src\Indicator.mqh" #include "src\InputsAndRelated.mqh" CIndicator* Indicator; int OnInit() { CIndiParams params; Indicator = new CIndicator(params); return INIT_SUCCEEDED; }
Код № 14 | Evolution.mq5 |рефакторинг: замена аргументов конструктора CIndicator на объект params
Приведенная ниже диаграмма включения позволяет легко увидеть, что входные переменные видны только файлу .mq5.

6. Извлечение общих характеристик: CSubIndiBase
У нас уже есть применяемая цена; далее я предлагаю построить поверх нее простую скользящую среднюю (SMA). Но прежде чем перейти к ней, давайте еще раз вернемся к CAppliedPrice.
class CAppliedPrice { public: int onCalculate(const int rates_total, const int prev_calculated, const double &open[], const double &high[], const double &low[], const double &close[]) { if(!checkRatesTotal(rates_total, prev_calculated, 1)) return 0; int barIdxToRecalcFrom; switch(rates_total - prev_calculated) { case 0: case 1: barIdxToRecalcFrom = prev_calculated - 1; break; default: barIdxToRecalcFrom = 0; break; } for(int i = barIdxToRecalcFrom; i < rates_total; i++) buffer[i] = calculate(i, mode, open, high, low, close); return rates_total; } private: static bool checkRatesTotal(const int rates_total, const int prev_calculated, int barsRequired) { if(barsRequired < 2) barsRequired = 2; if(rates_total < barsRequired) { PrintFormat("Not enough bars on the chart to calculate the indicator. Required: %i, available: %i.", barsRequired, rates_total); return false; } if(rates_total < prev_calculated) { PrintFormat(__FUNCTION__" Something went wrong. rates_total: %i, prev_calculated: %i", rates_total, prev_calculated); return false; } return true; } };
Код № 15 | AppliedPrice.mqh |рефакторинг: извлечение проверок rates_total в checkRatesTotal
Я извлек код, проверяющий rates_total, в отдельный метод checkRatesTotal() и сделал его немного понятнее. Как мы установили ранее, для работы индикатора всегда требовалось как минимум 2 бара на графике. Но, строго говоря, не сам класс CAppliedPrice требует наличия двух баров. С технической точки зрения, CAppliedPrice можно рассчитать, даже если на графике имеется всего один бар. Именно поэтому параметру barsRequired функции checkRatesTotal() передается значение 1, а строгое требование о наличии как минимум двух баров обеспечивается внутри логики метода.
Для скользящей средней (SMA), которую мы собираемся реализовать, количество баров на графике должно быть не меньше периода SMA. Вновь извлеченный метод checkRatesTotal() достаточно гибок, чтобы мы могли повторно использовать его для SMA, просто передав период индикатора в качестве аргумента barsRequired. Кроме того, для любого субиндикатора характерно установление минимального количества баров на графике. Поэтому я создам класс CSubIndiBase, и отныне все субиндикаторы должны быть производными от него.

class CSubIndiBase { private: static int correctBarsRequired(int barsRequired) { if(barsRequired >= 1) return barsRequired; const int prevValue = barsRequired; barsRequired = 1; PrintFormat(__FUNCTION__": invalid barsRequired value (%i); corrected to %i", prevValue, barsRequired); return barsRequired; } protected: static bool checkRatesTotal(const int rates_total, const int prev_calculated, int barsRequired) { if(barsRequired < 2) barsRequired = 2; if(rates_total < barsRequired) { PrintFormat("Not enough bars on the chart to calculate the indicator. Required: %i, available: %i.", barsRequired, rates_total); return false; } if(rates_total < prev_calculated) { PrintFormat(__FUNCTION__" Something went wrong. rates_total: %i, prev_calculated: %i", rates_total, prev_calculated); return false; } return true; } public: const int barsRequired; const int drawBegin; const double emptyValue; protected: CSubIndiBase(int a_barsRequired, double a_emptyValue) : barsRequired(correctBarsRequired(a_barsRequired)), drawBegin(barsRequired - 1), emptyValue(a_emptyValue) {} };
Код № 16 | SubIndiBase.mqh |рефакторинг: введение CSubIndiBase для субиндикаторов
#include "SubIndiBase.mqh" class CAppliedPrice : public CSubIndiBase { public: int onCalculate(const int rates_total, const int prev_calculated, const double &open[], const double &high[], const double &low[], const double &close[]) { if(!checkRatesTotal(rates_total, prev_calculated, barsRequired)) return 0; int barIdxToRecalcFrom; switch(rates_total - prev_calculated) { case 0: case 1: barIdxToRecalcFrom = prev_calculated - 1; break; default: barIdxToRecalcFrom = 0; break; } for(int i = barIdxToRecalcFrom; i < rates_total; i++) buffer[i] = calculate(i, mode, open, high, low, close); return rates_total; } private: const ENUM_APPLIED_PRICE mode; public: double buffer[]; public: CAppliedPrice(ENUM_APPLIED_PRICE a_mode) : CSubIndiBase(1, EMPTY_VALUE), mode(a_mode) {} };
Код № 17 | AppliedPrice.mqh |рефакторинг: добавление CSubIndiBase для субиндикаторов
Помимо определения метода checkRatesTotal(), CSubIndiBase вводит три важных поля:
- Под drawBegin я подразумеваю индекс бара, где можно ожидать первого непустого значения индикатора. Например, для скользящей средней с периодом 5 требуется 5 баров на графике (barsRequired = 5), и первое непустое значение скользящей средней будет на баре с индексом 4.
- Поле barsRequired в основном используется только для того, чтобы убедиться, что на графике достаточно баров для расчета субиндикатора.
- Поле emptyValue представляет собой, грубо говоря, значение, которое обычно передается свойству PLOT_EMPTY_VALUE графика индикатора.

Теперь, когда класс CAppliedPrice наследует от класса CSubIndiBase, вызов функции checkRatesTotal() передает значение поля barsRequired, а не целочисленный литерал 1.
7. Централизация требований: CSubIndiRegistry
Использование общего метода checkRatesTotal() лучше, чем создание индивидуальной копии для каждого субиндикатора, но все же это несколько неудобно. Это вынуждает нас вставлять этот же самый шаблонный код в начало метода onCalculate() для каждого субиндикатора:
if(!checkRatesTotal(rates_total, prev_calculated, barsRequired)) return 0;
Кроме того, это единственное, что заставляет субиндикаторы возвращать значение из метода onCalculate(), что не очень удобно с точки зрения CIndicator::onCalculate().
Представьте себе индикатор, состоящий из 3 субиндикаторов со значениями barsRequired 12, 28 и 5. Мы могли бы просто вызвать функцию checkRatesTotal() один раз, передав значение 28 — наибольшее значение barsRequired среди всех субиндикаторов. Для этого нам необходим программный элемент, который отслеживает максимальное количество необходимых баров (barsRequired). Для этой цели создадим класс с именем CSubIndiRegistry. Я сделал его полностью статическим, потому что мне точно не понадобится больше одного экземпляра. Преимущество чисто статического класса заключается в том, что мне не нужно объявлять экземпляр и передавать ссылки на него, что упрощает код.
#include "SubIndiBase.mqh" class CSubIndiBase; class CSubIndiRegistry { public: static void clear() { // Even with drawBegin = 0, at least 2 bars on the chart are required barsRequired = 2; } static void register(CSubIndiBase& instance) { if(instance.barsRequired > barsRequired) barsRequired = instance.barsRequired; } static bool checkRatesTotal(const int rates_total, const int prev_calculated) { if(rates_total < barsRequired) { PrintFormat("Not enough bars on the chart to calculate the indicator. Required: %i, available: %i.", barsRequired, rates_total); return false; } if(rates_total < prev_calculated) { PrintFormat(__FUNCTION__" Something went wrong. rates_total: %i, prev_calculated: %i", rates_total, prev_calculated); return false; } return true; } private: static int barsRequired; }; int CSubIndiRegistry::barsRequired;
Код № 18 | SubIndiRegistry.mqh |рефакторинг: централизация проверок rates_total в CSubIndiRegistry
Метод checkRatesTotal() перемещен из класса CSubIndiBase в новый класс CSubIndiRegistry. Общее требование о наличии как минимум двух баров на графике теперь выполняется в методе clear(). Метод register() используется для того, чтобы реестр получал информацию обо всех субиндикаторах.
Я просто добавил вызов CSubIndiRegistry::register() в конструктор CSubIndiBase. Таким образом, всякий раз при создании очередного экземпляра субиндикатора, CSubIndiRegistry узнает о нем «автоматически».
#include "SubIndiRegistry.mqh" class CSubIndiBase { protected: CSubIndiBase(int a_barsRequired, double a_emptyValue) : barsRequired(correctBarsRequired(a_barsRequired)), drawBegin(barsRequired - 1), emptyValue(a_emptyValue) { CSubIndiRegistry::register(this); } };
Код № 19 | SubIndiBase.mqh |рефакторинг: централизация проверок rates_total в CSubIndiRegistry
Вызовите CSubIndiRegistry::clear() во время инициализации, перед созданием CIndicator. Субиндикаторы создаются в списке инициализаторов класса CIndicator, который вызывает конструктор CSubIndiBase. Именно поэтому метод CSubIndiRegistry::clear() вызывается непосредственно из метода OnInit().
int OnInit() { CSubIndiRegistry::clear(); CIndiParams params; Indicator = new CIndicator(params); return INIT_SUCCEEDED; }
Код № 20 | Evolution.mq5 |рефакторинг: централизация проверок rates_total в CSubIndiRegistry
Функция CSubIndiRegistry::checkRatesTotal() теперь вызывается из функции CIndicator::onCalculate(). Это позволило нам изменить тип возвращаемого значения метода onCalculate() в классе CAppliedPrice (и всех будущих субиндикаторах) с int на void. Описанные в этом абзаце изменения можно найти в файлах на AlgoForge: AppliedPrice.mqh, Indicator.mqh.
8. Скрытие шаблона инфраструктуры: CIndicatorBase
Файл Indicator.mqh — один из наиболее часто редактируемых файлов, поскольку именно в нем все компоненты связываются между собой. Спустя некоторое время мне надоело постоянно видеть вызов CSubIndiRegistry::checkRatesTotal(). Поэтому я решил вынести это в отдельный базовый класс, чтобы освободить место на экране от кода, который никогда не меняется и не требует внимания. После того представления класса CIndicatorBase я понял, что его конструктор — идеальное место для вызова метода CSubIndiRegistry::clear().
#include "SubIndiRegistry.mqh" class CIndicatorBase { public: int onCalculate(const int rates_total, const int prev_calculated, const double &open[], const double &high[], const double &low[], const double &close[], const datetime &time[]) { if(!CSubIndiRegistry::checkRatesTotal(rates_total, prev_calculated)) return 0; onCalculateImpl(rates_total, prev_calculated, open, high, low, close, time); return rates_total; } private: virtual void onCalculateImpl(const int rates_total, const int prev_calculated, const double &open[], const double &high[], const double &low[], const double &close[], const datetime &time[]) = 0; protected: CIndicatorBase() { CSubIndiRegistry::clear(); } };
Код № 21 | IndicatorBase.mqh | рефакторинг: извлечение логики инфраструктуры в CIndicatorBase
#include "IndicatorBase.mqh" class CIndicator : public CIndicatorBase { public: void onCalculateImpl(const int rates_total, const int prev_calculated, const double &open[], const double &high[], const double &low[], const double &close[], const datetime &time[]) override { appliedPrice.onCalculate(rates_total, prev_calculated, open, high, low, close); } };
Код № 22 | Indicator.mqh |рефакторинг: извлечение логики инфраструктуры в CIndicatorBase
9. Создание второго модуля: SMA
Вернемся к нашей цели — созданию простой скользящей средней (SMA) в добавление к модулю CAppliedPrice, разработанному нами ранее. Для начала подготовим параметры индикатора:
- Я переименовал единственный существующий параметр, чтобы он органично смотрелся рядом с периодом SMA. См. рефакторинг: переименуйте параметр «применяемая цена» в «Применить к».
- Я добавил второй параметр для периода SMA. См. функция: добавление входного параметра периода SMA.
Вот как сейчас выглядят настройки индикатора:

Для создания нового модуля (субиндикатора) я сделал заготовку класса CSma (разумеется, в отдельном файле), который наследует от CSubIndiBase. Метод onCalculate() я пока оставил пустым; мы к нему вернемся немного позже. Что касается полей класса, нам понадобится переменная для периода SMA и массив для индикаторного буфера (период и буфер соответственно)...
#include "SubIndiBase.mqh" class CSma : public CSubIndiBase { private: static int correctPeriod(int p, bool mute = false) { if(p >= 2) return p; const int prevValue = p; p = 2; if(!mute) PrintFormat(__FUNCTION__": invalid period value (%i); corrected to %i", prevValue, p); return p; } public: void onCalculate(const int rates_total, const int prev_calculated, const double &source[]) { Print(__FUNCTION__" not implemented"); } private: const int period; public: double buffer[]; public: CSma(int a_period, int sourceBarsRequired) : CSubIndiBase(sourceBarsRequired + correctPeriod(a_period) - 1, EMPTY_VALUE), period(correctPeriod(a_period, true)) {} };
Код № 23 | Sma.mqh |функция: заготовка субиндикатора CSma
Обратите внимание, что помимо emptyValue, конструктор CSubIndiBase ожидает значение для barsRequired. На первый взгляд может показаться очевидным, что достаточно просто передать сюда период SMA. Однако лучше подумать на опережение: сам источник данных может потребовать наличия более одного бара на графике. Чтобы это учесть, я ввел параметр sourceBarsRequired. Для расчета фактического значения barsRequired мы просто складываем собственное требование SMA (период) с требованием источника (sourceBarsRequired) и вычитаем 1. Для наглядности рассмотрим пример, где период скользящей средней равен 3, а источнику требуется 5 баров:

Теперь подключим наш новый субиндикатор:
- Добавьте экземпляр класса CSma в класс CIndicator. См. функция: интеграция CSma в CIndicator.
- Привяжите индикаторные буферы. См. функция: перенастройка графиков и буферов.
Небольшое отступление
Во время написания этого раздела я просматривал файл AppliedPrice.mqh и внезапно почувствовал непреодолимое желание улучшить одно из имен переменных. Я не стал сдерживаться и сделал это в небольшом отдельном коммите:
рефакторинг: переименуйте barIdxToRecalcFrom в recalcStartIdx
Кроме того, я понял: когда наследовал CAppliedPrice от CSubIndiBase, я забыл заменить жестко заданное значение 0 на drawBegin. Поэтому я решил эту проблему с помощью еще одного быстрого микрорефакторинга:
рефакторинг: замените 0 на drawBegin в CAppliedPrice
10. Расчет простой скользящей средней (SMA)
Осталось только реализовать метод onCalculate(). Возьму готовую логику из функции SimpleMAOnBuffer(), которая находится в файле MQL5/Include/MovingAverages.mqh, и просто адаптирую ее к своей архитектуре. На первый взгляд, функция может показаться несколько запутанной, но лежащая в ее основе логика на самом деле довольно проста. Вот что нам нужно сделать:
- Заполните бары слева от drawBegin пустыми значениями.
- Рассчитайте значение для бара в точке drawBegin, используя базовую формулу.
- Рассчитайте значения для всех оставшихся баров (справа от drawBegin), используя рекуррентную формулу.
Пункты 1 и 2 кажутся отличными кандидатами на выделение в отдельные методы (это просто сделает код более удобным и лучше читаемым):
class CSma : public CSubIndiBase { private: void clearLeftFromDrawBegin() { for(int i = 0; i < drawBegin; i++) buffer[i] = emptyValue; } void computeValueAtDrawBegin(const double &source[]) { double result = 0.0; for(int i = drawBegin - (period - 1); i <= drawBegin; i++) result += source[i]; buffer[drawBegin] = result / period; } };
Код № 24 | Sma.mqh | функция: реализация CSma::onCalculate
Благодаря наличию готовых методов clearLeftFromDrawBegin() и computeValueAtDrawBegin(), реализация метода onCalculate() становится значительно проще. Тем более что у нас уже есть именно такой код, который нам нужен для вычисления recalcStartIdx в CAppliedPrice.
class CSma : public CSubIndiBase { public: void onCalculate(const int rates_total, const int prev_calculated, const double &source[]) { int recalcStartIdx; switch(rates_total - prev_calculated) { case 0: case 1: recalcStartIdx = prev_calculated - 1; break; default: recalcStartIdx = drawBegin; clearLeftFromDrawBegin(); break; } if(rates_total - prev_calculated > 1) { computeValueAtDrawBegin(source); recalcStartIdx++; } for(int i = recalcStartIdx; i < rates_total; i++) buffer[i] = buffer[i - 1] + (source[i] - source[i - period]) / period; } };
Код № 25 | Sma.mqh | функция: реализация CSma::onCalculate
Я просто скопировал и вставил весь оператор switch, который вычисляет recalcStartIdx из CAppliedPrice::onCalculate(). Но с одним важным отличием — я добавил внутри него вызов функции clearLeftFromDrawBegin().
Все. Индикатор работает, и его значения идеально совпадают со значениями SMA, встроенной в MetaTrader 5.
11. Применяем принцип DRY к субиндикаторам: подтягивание логики
В предыдущем разделе я скопировал и вставил оператор switch из CAppliedPrice в CSma. Однако, учитывая, что эта статья буквально называется «Хватит копировать и вставлять», мне пора уже самому начать применять на практике то, о чем сам проповедую. Мы можем обернуть этот оператор switch в отдельный метод — назовем его prepareBeforeCalculating() — и включить в базовый класс CSubIndiBase. В перспективе этот метод будет использоваться практически во всех наших субиндикаторах. Единственное, что мешает мне извлечь его прямо сейчас, — это всего лишь одна строка кода:
class CSma : public CSubIndiBase { private: void clearLeftFromDrawBegin() { for(int i = 0; i < drawBegin; i++) buffer[i] = emptyValue; } };
Проблема состоит в том, что базовый класс совершенно не имеет представления, какие индикаторные буферы используются в производных классах и сколько их всего. Решение очевидно: объявим в базовом классе чисто виртуальный метод с именем clearBuffersAt(). Это вынуждает каждый субиндикатор реализовывать собственную специфическую логику очистки буфера:
class CSubIndiBase { private: virtual void clearBuffersAt(int barIdx) = 0; };
Код № 26 | SubIndiBase.mqh | рефакторинг: реализация для подклассов обработчика clearBuffersAt
class CAppliedPrice : public CSubIndiBase { private: void clearBuffersAt(int barIdx) override { buffer[barIdx] = emptyValue; } };
Код № 27 | AppliedPrice.mqh | рефакторинг: реализация для подклассов обработчика clearBuffersAt
class CSma : public CSubIndiBase { private: void clearBuffersAt(int barIdx) override { buffer[barIdx] = emptyValue; } void clearLeftFromDrawBegin() { for(int i = 0; i < drawBegin; i++) clearBuffersAt(i); } };
Код № 28 | Sma.mqh | рефакторинг: реализация для подклассов обработчика clearBuffersAt
Преодолев это препятствие, мы можем смело перенести в базовый класс методы prepareBeforeCalculating() и clearLeftFromDrawBegin(). Вот диаграмма классов:

Фактические изменения в коде показаны ниже. Обратите внимание, насколько чище и понятнее стали методы onCalculate() внутри субиндикаторов.
class CSubIndiBase { private: virtual void clearBuffersAt(int barIdx) = 0; protected: int prepareBeforeCalculating(const int rates_total, const int prev_calculated) { int recalcStartIdx; switch(rates_total - prev_calculated) { case 0: case 1: recalcStartIdx = prev_calculated - 1; break; default: recalcStartIdx = drawBegin; clearLeftFromDrawBegin(); break; } return recalcStartIdx; } void clearLeftFromDrawBegin() { for(int i = 0; i < drawBegin; i++) clearBuffersAt(i); } };
Код № 29 | SubIndiBase.mqh | рефакторинг: перенос prepareBeforeCalculating в базовый класс
class CAppliedPrice : public CSubIndiBase { public: void onCalculate(const int rates_total, const int prev_calculated, const double &open[], const double &high[], const double &low[], const double &close[]) { for(int i = prepareBeforeCalculating(rates_total, prev_calculated); i < rates_total; i++) buffer[i] = calculate(i, mode, open, high, low, close); } };
Код № 30 | AppliedPrice.mqh | рефакторинг: перенос prepareBeforeCalculating в базовый класс
class CSma : public CSubIndiBase { public: void onCalculate(const int rates_total, const int prev_calculated, const double &source[]) { int recalcStartIdx = prepareBeforeCalculating(rates_total, prev_calculated); if(rates_total - prev_calculated > 1) { computeValueAtDrawBegin(source); recalcStartIdx++; } for(int i = recalcStartIdx; i < rates_total; i++) buffer[i] = buffer[i - 1] + (source[i] - source[i - period]) / period; } };
Код № 31 | Sma.mqh | рефакторинг: перенос prepareBeforeCalculating в базовый код
Заключение
Мы прошли долгий путь от примитивной, процедурной реализации, с которой начинали. Благодаря целенаправленному, поэтапному подходу к рефакторингу нам удалось успешно превратить простую задачу в надежный объектно-ориентированный каркас.
Вместо того чтобы просто наспех «слепить» код и надеяться, что он будет работать, мы сосредоточились на архитектурной целостности. Мы устранили хрупкое глобальное состояние, инкапсулируя данные и логику в рамках специализированных классов. Мы решили проблему корректной передачи параметров через наследование с помощью класса CIndiParamsBase. Кроме того, благодаря внедрению классов CSubIndiBase, CIndicatorBase и CSubIndiRegistry мы ушли от повторяющегося шаблонного кода. Теперь важнейшие требования, такие как обеспечение достаточного количества баров на графике и расчет начальных индексов, обрабатываются автоматически, с точки зрения внутренней структуры.
В результате получилась чистая, модульная архитектура, в которой такие субиндикаторы, как CAppliedPrice и CSma, полностью самодостаточны и легко могут быть использованы повторно. Их методы onCalculate() больше не перегружены инфраструктурной логикой; они содержат только те математические вычисления, которые действительно имеют значение.
Мы заложили прочный фундамент. В следующей статье рассмотрим, как применить этот каркас на практике. Мы перестанем беспокоиться о лежащей в ее основе механике и полностью сосредоточимся на создании новых индикаторов, расширении нашей библиотеки модулей и демонстрации возможности с легкостью комбинировать их для создания сложных аналитических инструментов.
Полный исходный код и история коммитов описанного в этой статье проекта находятся в репозитории Git:
https://forge.mql5.io/modular-indicator-architecture/evolution
Файлы, прикрепленные к статье
| Название файла | Описание |
|---|---|
| Evolution.mq5 | Основной файл индикатора, выступающий в качестве точки входа и содержащий обработчики событий MQL5. |
| Indicator.mqh | Корневой объект, который связывает все субиндикаторы воедино, устраняя необходимость в глобальном состоянии. |
| IndicatorBase.mqh | Упрощает стандартный код инфраструктуры (например, проверки rates_total) по сравнению с основной логикой индикатора. |
| InputsAndRelated.mqh | Определяет глобальные входные переменные и производный класс, сопоставляющий их с объектом параметров. |
| IndiParamsBase.mqh | Определяет базовую структуру для параметров индикатора, отделяя основную логику от глобальных входных переменных. |
| SubIndiRegistry.mqh | Статический реестр, который централизованно управляет требованием к количеству баров (barsRequired) для всех модулей. |
| SubIndiBase.mqh | Базовый класс для всех субиндикаторов, который извлекает общие характеристики и обрабатывает шаблонный служебный код. |
| AppliedPrice.mqh | Субиндикатор, рассчитывающий применяемую базовую цену. |
| Sma.mqh | Субиндикатор, который рассчитывает простую скользящую среднюю. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17566
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Возможности Мастера MQL5, которые вам нужно знать (Часть 95): Использование DSU и DBN в пользовательском классе сигнала
Моделирование рынка: Position View (IX)
Автоматизация торговых стратегий на MQL5 (Часть 34): Система пробоя линии тренда с оценкой качества аппроксимации по коэффициенту R²
Анализ CSV-данных (Часть 5): Потоковая передача CSV-данных в реальном времени из активных сессий MetaTrader 5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Привет, Владислав! Твоя работа абсолютно четкая с концептуальной точки зрения — настоящий шедевр программной инженерии.
У меня к тебе вопрос: сможешь ли ты расширить этот проект, добавив поддержку MTF и многосимвольного режима?
С уважением, Сабино
С уважением, Сабино
Привет, Сабино, спасибо за отзыв! Давным-давно я пытался разработать индикаторы с поддержкой нескольких таймфреймов, но у меня возникли сложности с подготовкой синхронизированных массивов временных рядов для всех необходимых графиков. Я не захотел продолжать и отложил работу над поддержкой нескольких таймфреймов на потом. До сих пор к этому не вернулся.
В архитектуре, описанной в этой статье, я не учитывал использование нескольких графиков. Но я думаю, что её можно расширить для поддержки нескольких графиков, если удастся подготовить синхронизированные массивы временных рядов.