English 中文 Español Deutsch 日本語 Português
preview
Разработка системы репликации - Моделирование рынка (Часть 16): Новая система классов

Разработка системы репликации - Моделирование рынка (Часть 16): Новая система классов

MetaTrader 5Тестер | 3 октября 2023, 14:26
743 0
Daniel Jose
Daniel Jose

Введение

В предыдущей статье "Разработка системы репликации - Моделирование рынка (Часть 15): Появление ТЕСТЕРА (V) - СЛУЧАЙНОЕ БЛУЖДАНИЕ", мы разработали способ, способствующий рандомизации данных. Это было сделано для того, чтобы результат был вполне соответствующим. Но даже несмотря на то, что система действительно способна правдоподобно имитировать ордеры, ей всё равно не хватает определенной информации, причем не только самого тестера, но и самой системы репликации. Правда заключается в том, что некоторые вещи, необходимые для их реализации, являются довольно сложными, особенно с точки зрения построения системы. Так что нам нужно внести некоторые изменения, чтобы хотя бы получить лучшую модель и не заблудиться на следующих этапах.

Поверьте мне, если СЛУЧАЙНОЕ БЛУЖДАНИЕ показалось вам сложным, то это лишь потому, что вы еще не видели или не представляете, что нужно будет разработать, чтобы опыт моделирования/репликации был наиболее подходящим. Одним из наиболее сложных аспектов является то, что мы должны знать хотя бы основы о том активе, на котором мы будем проводить моделирование или репликацию. На данном этапе разработки меня не беспокоит автоматическое решение некоторых проблем: дело не в том, что это сложно сделать, но есть более практичные способы для этого.

Если внимательно изучать предыдущую статью, то заметно, что для создания возможных рыночных сценариев можно использовать такие программы, как EXCEL. Но мы также можем использовать информацию из других активов и комбинировать их для создания более сложного моделирования. Это усложняет создание полностью автоматизированной системы, которая могла бы искать недостающую информацию о самом активе. Но у нас есть еще одна небольшая проблема. Поэтому здесь обрезаем и распределяем содержимое заголовочного файла C_Replay.mqh таким образом, чтобы облегчить обслуживание и улучшения. Я не люблю работать с очень большим классом, мне это кажется неудобным. Причина в том, что во многих случаях код может быть не полностью оптимизирован. Поэтому, чтобы упростить задачу, мы внесем оба изменения, т.е. распределим содержимое класса C_Replay по нескольким классам и заодно реализуем самые актуальные части.


Реализация сервиса с новой системой классов

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

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

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

#property service
#property icon "\\Images\\Market Replay\\Icon.ico"
#property copyright "Daniel Jose"
#property version   "1.16"
#property description "Сервис репликации-моделирования для платформы MT5."
#property description "Он независим от индикатора Market Replay."
#property description "Можно узнать по подробнее в данной статье:"
#property link "https://www.mql5.com/ru/articles/11095"
//+------------------------------------------------------------------+
#define def_Dependence  "\\Indicators\\Market Replay.ex5"
#resource def_Dependence
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
input string            user00 = "Config.txt";  //Конфигурационный файл "Replay".
input ENUM_TIMEFRAMES   user01 = PERIOD_M1;     //Начальный таймфрейм для графика.
input bool              user02 = true;          //Визуализация конструкции баров.
input bool              user03 = true;          //Визуализация метрик создания.
//+------------------------------------------------------------------+
void OnStart()
{
        C_Replay        *pReplay;

        pReplay = new C_Replay(user00);
        if (pReplay.ViewReplay(user01))
        {
                Print("Разрешение получено. Сервис репликации теперь может быть использован...");
                while (pReplay.LoopEventOnTime(user02, user03));
        }
        delete pReplay;
}
//+------------------------------------------------------------------+

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

Теперь, поскольку классы используются в качестве указателей, мы откажемся от одних вещей и начнем использовать другие. Первый момент: теперь класс будет всегда запускаться и закрываться, и уже не скрыто, а явно. Это делается с помощью операторов new и delete. Когда мы используем оператор "new" для создания класса, мы всегда должны вызывать конструктор класса. Они никогда не возвращают значение, поэтому мы не можем проверить возвращаемое значение напрямую. Нам придется сделать это в другой момент. То же самое происходит и при использовании оператора «delete», при этом будет вызван деструктор класса.  Как и конструктор класса, деструктор никогда не возвращает значение. Но в отличие от конструктора деструктор не получает никаких аргументов.

Нам всегда придется делать это так: создаем класс с помощью оператора "new" и уничтожаем класс с помощью оператора "delete". Это будет единственная работа, которую нам действительно придется выполнить. Всё остальное сделает ОС, которая выделяет достаточно памяти для работы программы в определенной области памяти, делая ее максимально безопасной на протяжении всего ее существования. Но здесь кроется опасность для тех, кто привык использовать указатели в C++/C: мы говорим о нотации. В языках C++ и C каждый раз, когда мы ссылаемся на указатели, мы используем очень специфическое обозначение. Обычно мы используем стрелку (->). Для программиста C++/C это означает, что используется указатель. Но мы можем также использовать другую нотацию, которую можно увидеть в моих кодах при обращении к указателю.

Кроме того, конечно, используется имя переменной, которое обычно начинается с буквы "p" или комбинации типа "ptr" (хотя это не строгое правило, так что не придерживайтесь его). Хотя MQL5 принимает как обозначения, показанные в приведенном выше коде, так и обозначения, показанные ниже, лично я считаю, что код, использующий указатели, легче читается, когда он действительно использует правильное объявление. Поэтому в наших кодах обозначения будут такими, как показано ниже, благодаря моему знанию языка C++/C:

void OnStart()
{
        C_Replay        *pReplay;

        pReplay = new C_Replay(user00);
        if ((*pReplay).ViewReplay(user01))
        {
                Print("Разрешение получено. Сервис репликации теперь может быть использован...");
                while ((*pReplay).LoopEventOnTime(user02, user03));
        }
        delete pReplay;
}

На самом деле есть дополнительная работа по написанию кода. Но мне, уже много лет занимавшемуся программированием на C++/C, легче понять, что я имею в виду указатель, когда смотрю на код, подобный показанному выше. А поскольку MQL5 понимает это также, как C++/C, я не вижу проблем в использовании этой нотации. Каждый раз, когда мы видим код с обозначением, подобно тому, который показан выше, вам не стоит волноваться, потому что это всего лишь указатель.

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

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

Рисунок 01 – C_Replay.mqh

Pисунок 01 - Система подключения для классов Репликации


На рисунке 01 показано, как классы соединяются друг с другом для осуществления желаемой работы, запланированной сервисом репликации/моделирования. Зеленая стрелка указывает, что класс импортирован, поэтому некоторые внутренние элементы класса будут общедоступными. Красная стрелка указывает на то, что данные будут импортированы, но больше не будут видны в старших классах. Класс C_FilesBars на самом деле является автономным классом. Он не будет унаследован каким-либо иным классом, но его методы будут использованы другими классами.

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

Давайте разберемся в том, как реализовали структуру.


Независимый класс: C_FileBars

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

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "Interprocess.mqh"
//+------------------------------------------------------------------+
#define def_BarsDiary   1440
//+------------------------------------------------------------------+
class C_FileBars
{
        private :
                int     m_file;
                string  m_szFileName;
//+------------------------------------------------------------------+
inline void CheckFileIsBar(void)
                        {
                                string  szInfo = "";
                                
                                for (int c0 = 0; (c0 < 9) && (!FileIsEnding(m_file)); c0++) szInfo += FileReadString(m_file);
                                if (szInfo != "<DATE><TIME><OPEN><HIGH><LOW><CLOSE><TICKVOL><VOL><SPREAD>")
                                {
                                        Print("Файл ", m_szFileName, ".csv не является файлом баров.");
                                        FileClose(m_file);
                                        m_file = INVALID_HANDLE;
                                }
                        }
//+------------------------------------------------------------------+
        public  :
//+------------------------------------------------------------------+
                C_FileBars(const string szFileNameCSV)
                        :m_szFileName(szFileNameCSV)
                        {
                                if ((m_file = FileOpen("Market Replay\\Bars\\" + m_szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
                                        Print("Не удалось получить доступ ", m_szFileName, ".csv баров.");
                                else
                                        CheckFileIsBar();
                        }
//+------------------------------------------------------------------+
                ~C_FileBars()
                        {
                                if (m_file != INVALID_HANDLE) FileClose(m_file);
                        }
//+------------------------------------------------------------------+
                bool ReadBar(MqlRates &rate[])
                        {
                                if (m_file == INVALID_HANDLE) return false;
                                if (FileIsEnding(m_file)) return false;
                                rate[0].time = StringToTime(FileReadString(m_file) + " " + FileReadString(m_file));
                                rate[0].open = StringToDouble(FileReadString(m_file));
                                rate[0].high = StringToDouble(FileReadString(m_file));
                                rate[0].low = StringToDouble(FileReadString(m_file));
                                rate[0].close = StringToDouble(FileReadString(m_file));
                                rate[0].tick_volume = StringToInteger(FileReadString(m_file));
                                rate[0].real_volume = StringToInteger(FileReadString(m_file));
                                rate[0].spread = (int) StringToInteger(FileReadString(m_file));
                                
                                return true;
                        }
//+------------------------------------------------------------------+
                datetime LoadPreView(const string szFileNameCSV)
                        {
                                int      iAdjust = 0;
                                datetime dt = 0;
                                MqlRates Rate[1];
                                
                                Print("Загрузка предыдущих баров для Replay. Подождите ....");
                                while (ReadBar(Rate) && (!_StopFlag))
                                {
                                        iAdjust = ((dt != 0) && (iAdjust == 0) ? (int)(Rate[0].time - dt) : iAdjust);
                                        dt = (dt == 0 ? Rate[0].time : dt);
                                        CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                                }
                                return ((_StopFlag) || (m_file == INVALID_HANDLE) ? 0 : Rate[0].time + iAdjust);
                        }
//+------------------------------------------------------------------+
};

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

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

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

Данный вид моделирования может показаться не таким безопасным и стабильным, но поверьте мне, он значительно более безопасен и стабилен, чем кажется. Однако, если бы можно было получить доступ к некоторым другим операторам, присутствующим в C++, всё было бы ещё интереснее. Конечно, для этого нужно правильно написать код, иначе всё стало бы полной катастрофой. Но поскольку MQL5 — это не C++, давайте изучим и будем максимально использовать возможности данного языка. Таким образом, у нас будет система, которая будет использовать что-то очень близкое к пределам, которые позволяет нам достичь язык.


Глубокий класс: C_FileTicks

Следующий класс, который мы собираемся рассмотреть, — это класс C_FileTicks. Он намного сложнее класса C_FileBars, и это связано с тем, что у нас есть публичные элементы, приватные элементы и элементы, которые находятся где-то посередине. Они получают особое название: PROTECTED. Термин "protected" имеет особый уровень, когда речь идет о наследовании между классами. В случае с C++ всё довольно сложно, по крайней мере, в начале обучения. Это связано с некоторыми операторами, присутствующими в C++. Но, к счастью, MQL5 решает проблему намного более простым путем. Таким образом, будет гораздо проще понять, как наследуются элементы, объявленные как защищенные, и можно ли к ним получить доступ или нет, в зависимости от того, конечно, как происходит наследование. Для этого нужно смотреть таблицу ниже:

Определение в базовом классе Тип наследования базового класса Доступ внутри производного класса Доступ с помощью вызова производного класса 
private public: Доступ запрещен Не удалось получить доступ к данным или процедурам базового класса
public: public: Доступ разрешен Разрешен доступ к данным или процедурам базового класса
protected public: Доступ разрешен Не удалось получить доступ к данным или процедурам базового класса
private private Доступ запрещен Не удалось получить доступ к данным или процедурам базового класса
public: private Доступ разрешен Не удалось получить доступ к данным или процедурам базового класса
protected private Доступ разрешен Не удалось получить доступ к данным или процедурам базового класса
private protected Доступ запрещен Не удалось получить доступ к данным или процедурам базового класса
public: protected Доступ разрешен  Не удалось получить доступ к данным или процедурам базового класса
protected protected Доступ разрешен Не удалось получить доступ к данным или процедурам базового класса

Таблица уровней доступа к элементам и процедурам классов

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

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

Это кажется довольно сложным, не так ли? Однако, не стоит паниковать. На практике всё гораздо проще. Однако нам придется несколько раз испытать этот механизм в работе, чтобы по-настоящему понять, как он функционирует. Но поверьте, в MQL5 это гораздо проще сделать, чем в C++, там дела обстоят намного сложнее. И причина в том, что у нас есть способы изменить уровень доступа к данным или процедурам, объявленным как защищенные, в некоторых случаях, даже приватным, в процессе наследования классов. Вот это полное безумие. Однако в MQL5 всё работает гладко.

Основываясь на этом, мы наконец можем увидеть класс C_FileTicks, который, несмотря на то, что теоретически более сложен, имеет относительно простой код. Давайте приступим к рассмотрению первых элементов внутри класса, и начнем с его объявления:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_FileBars.mqh"
//+------------------------------------------------------------------+
#define def_MaxSizeArray        16777216 // 16 Mbytes позиций
//+------------------------------------------------------------------+
#define macroRemoveSec(A) (A - (A % 60))
//+------------------------------------------------------------------+
class C_FileTicks
{
        protected:
                struct st00
                {
                        MqlTick  Info[];
                        MqlRates Rate[];
                        int      nTicks,
                                 nRate;
                }m_Ticks;
                double          m_PointsPerTick;

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

        public  :
//+------------------------------------------------------------------+
                bool BarsToTicks(const string szFileNameCSV)
                        {
                                C_FileBars      *pFileBars;
                                int             iMem = m_Ticks.nTicks;
                                MqlRates        rate[1];
                                MqlTick         local[];
                                
                                pFileBars = new C_FileBars(szFileNameCSV);
                                ArrayResize(local, def_MaxSizeArray);
                                Print("Преобразуем бары в тики. Подождите...");
                                while ((*pFileBars).ReadBar(rate) && (!_StopFlag)) Simulation(rate[0], local);
                                ArrayFree(local);
                                delete pFileBars;
                                
                                return ((!_StopFlag) && (iMem != m_Ticks.nTicks));
                        }
//+------------------------------------------------------------------+
                datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true)
                        {
                                int             MemNRates,
                                                MemNTicks;
                                datetime dtRet = TimeCurrent();
                                MqlRates RatesLocal[];
                                
                                MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
                                MemNTicks = m_Ticks.nTicks;
                                if (!Open(szFileNameCSV)) return 0;
                                if (!ReadAllsTicks()) return 0;
                                if (!ToReplay)
                                {
                                        ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
                                        ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
                                        CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
                                        dtRet = m_Ticks.Rate[m_Ticks.nRate].time;
                                        m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
                                        m_Ticks.nTicks = MemNTicks;
                                        ArrayFree(RatesLocal);
                                }
                                return dtRet;
                        };
//+------------------------------------------------------------------+

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

И вот один из таких моментов. Сначала мы объявляем класс очень специфическим образом. Теперь вызываем конструктор класса с именем файла, из которого хотим получить значения баров. Помните, что этот вызов не вернет никакого значения. Мы используем оператор NEW, чтобы класс получил место в памяти, зарезервированное только для него. В этом пространстве будет содержаться класс, так как MetaTrader 5 на самом деле не контролирует, где этот класс может находиться. Эта информация есть только у операционной системы.

Но что более удобно, мы получаем значение от оператора NEW, и это значение является «указателем», который мы можем использовать для прямой ссылки на наш класс (ПРИМЕЧАНИЕ: Слово «указатель» взято в кавычки, потому что на самом деле это не указатель, это просто переменная, которая может ссылаться на класс, как будто этот класс был другой переменной или константой. В будущем я покажу, как можно использовать этот ресурс для создания постоянного класса, в котором мы можем только получать доступ к данным, но не выполнять вычисления. Поскольку у него очень специфическое применение, мы оставим его на другое время.). Получив этот «указатель», мы можем выполнять работу в нашем классе, но опять же, мы всё еще не уверены, что файл открыт и его можно прочитать. Поэтому нам нужно будет выполнить какую-то проверку, прежде чем пытаться читать и использовать какие-либо данные. К счастью, в этом нет необходимости, поскольку во время попытки чтения мы можем проверить, правильно ли выполнился вызов или нет. То есть, действительно ли произошло чтение данных или оно не произошло, и это делается здесь.

Если по какой-либо причине чтение не получилось, цикл WHILE просто закроется. Поэтому нет необходимости проводить дальнейший анализ. Сама попытка чтения данных послужит для проверки того, успешным ли было чтение или нет. Таким образом, мы можем управлять вещами за пределами класса C_FileBars, но мы должны явно завершить класс, чтобы та память, в которой он находился, была возвращена в ОС. Это делается с помощью вызова деструктора через оператор DELETE. Таким образом мы гарантируем, что класс был правильно удален и на него больше нет ссылок.

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


Один класс, несколько функций: C_ConfigService

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

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

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_FileBars.mqh"
#include "C_FileTicks.mqh"
//+------------------------------------------------------------------+
class C_ConfigService : protected C_FileTicks
{
        protected:
//+------------------------------------------------------------------+
                datetime m_dtPrevLoading;
//+------------------------------------------------------------------+
        private :
//+------------------------------------------------------------------+
                enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE};
//+------------------------------------------------------------------+
inline eTranscriptionDefine GetDefinition(const string &In, string &Out)
                        {
                                string szInfo;
                                
                                szInfo = In;
                                Out = "";
                                StringToUpper(szInfo);
                                StringTrimLeft(szInfo);
                                StringTrimRight(szInfo);
                                if (StringSubstr(szInfo, 0, 1) == "#") return Transcription_INFO;
                                if (StringSubstr(szInfo, 0, 1) != "[")
                                {
                                        Out = szInfo;
                                        return Transcription_INFO;
                                }
                                for (int c0 = 0; c0 < StringLen(szInfo); c0++)
                                        if (StringGetCharacter(szInfo, c0) > ' ')
                                                StringAdd(Out, StringSubstr(szInfo, c0, 1));                                    
                                
                                return Transcription_DEFINE;
                        }
//+------------------------------------------------------------------+
inline bool Configs(const string szInfo)
                        {
                                const string szList[] = {
                                        
						"POINTSPERTICK"
                                                        };
                                string  szRet[];
                                char    cWho;
                                
                                if (StringSplit(szInfo, '=', szRet) == 2)
                                {
                                        StringTrimRight(szRet[0]);
                                        StringTrimLeft(szRet[1]);
                                        for (cWho = 0; cWho < ArraySize(szList); cWho++) if (szList[cWho] == szRet[0]) break;
                                        switch (cWho)
                                        {
                                                case 0:
                                                        m_PointsPerTick = StringToDouble(szRet[1]);
                                                        return true;                                            
                                        }
                                        Print("Переменная >>", szRet[0], "<< не определена.");
                                }else
                                        Print("Определение конфигурации >>", szInfo, "<< недействительна.");
                                        
                                return false;
                        }
//+------------------------------------------------------------------+
inline void FirstBarNULL(void)
                        {
                                MqlRates rate[1];
                                
                                rate[0].close = rate[0].open =  rate[0].high = rate[0].low = m_Ticks.Info[0].last;
                                rate[0].tick_volume = 0;
                                rate[0].real_volume = 0;
                                rate[0].time = m_Ticks.Info[0].time - 60;
                                CustomRatesUpdate(def_SymbolReplay, rate, 1);
                        }
//+------------------------------------------------------------------+
inline bool WhatDefine(const string szArg, char &cStage)
                        {
                                const string szList[] = {
                                        "[BARS]",
                                        "[TICKS]",
                                        "[TICKS->BARS]",
                                        "[BARS->TICKS]",
                                        "[CONFIG]"
                                                        };
                                                                                                
                                cStage = 1;
                                for (char c0 = 0; c0 < ArraySize(szList); c0++, cStage++)
                                        if (szList[c0] == szArg) return true;
                                        
                                return false;
                        }
//+------------------------------------------------------------------+
        public  :
//+------------------------------------------------------------------+
                bool SetSymbolReplay(const string szFileConfig)
                        {
                                int             file,
                                                iLine;
                                char            cError,
                                                cStage;
                                string          szInfo;
                                bool            bBarPrev;
                                C_FileBars      *pFileBars;
                                
                                if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
                                {
                                        MessageBox("Не удалось открыть\nконфигурационный файл.", "Market Replay", MB_OK);
                                        return false;
                                }
                                Print("Загрузка данных для репликации. Подождите....");
                                ArrayResize(m_Ticks.Rate, def_BarsDiary);
                                m_Ticks.nRate = -1;
                                m_Ticks.Rate[0].time = 0;
                                bBarPrev = false;

                                iLine = 1;
                                cError = cStage = 0;
                                while ((!FileIsEnding(file)) && (!_StopFlag) && (cError == 0))
                                {
                                        switch (GetDefinition(FileReadString(file), szInfo))
                                        {
                                                case Transcription_DEFINE:
                                                        cError = (WhatDefine(szInfo, cStage) ? 0 : 1);
                                                        break;
                                                case Transcription_INFO:
                                                        if (szInfo != "") switch (cStage)
                                                        {
                                                                case 0:
                                                                        cError = 2;
                                                                        break;
                                                                case 1:
                                                                        pFileBars = new C_FileBars(szInfo);
                                                                        if ((m_dtPrevLoading = (*pFileBars).LoadPreView(szInfo)) == 0) cError = 3; else bBarPrev = true;
                                                                        delete pFileBars;
                                                                        break;
                                                                case 2:
                                                                        if (LoadTicks(szInfo) == 0) cError = 4;
                                                                        break;
                                                                case 3:
                                                                        if ((m_dtPrevLoading = LoadTicks(szInfo, false)) == 0) cError = 5; else bBarPrev = true;
                                                                        break;
                                                                case 4:
                                                                        if (!BarsToTicks(szInfo)) cError = 6;
                                                                        break;
                                                                case 5:
                                                                        if (!Configs(szInfo)) cError = 7;
                                                                        break;
                                                        }
                                                        break;
                                        };
                                        iLine += (cError > 0 ? 0 : 1);
                                }
                                FileClose(file);
                                switch(cError)
                                {
                                        case 0:
                                                if (m_Ticks.nTicks <= 0)
                                                {
                                                        Print("Нет тиков для использования. Закрываем сервис...");
                                                        cError = -1;
                                                }else   if (!bBarPrev) FirstBarNULL();
                                                break;
                                        case 1  : Print("Команда в строке ", iLine, " не распознается системой...");    break;
                                        case 2  : Print("Система не ожидала содержимого строки ", iLine);                  break;
                                        default : Print("В строке имеется ошибка ", iLine);
                                }
                                                                
                                return (cError == 0 ? !_StopFlag : false);
                        }
//+------------------------------------------------------------------+
};

Именно здесь мы начинаем использовать эту таблицу наследования. Все эти пункты унаследованы от класса C_FileTicks. Таким образом, мы фактически расширяем функциональные возможности самого класса C_ConfigService. Но дело не только в этом, так как если внимательно посмотреть, то существует вполне специфическая ситуация, когда нам необходимо загрузить данные из предыдущих баров. Для этого нам нужно использовать класс C_FileBars. Итак, мы использовали тот же способ, что и в классе C_FileTicks, когда нужно было загрузить данные из файла баров, чтобы преобразовать их в тики. Объяснение, которое мы дали там, применимо и здесь.

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

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

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

Хотя всё кажется очень красивым, нам еще предстоит увидеть последний класс.


Класс C_Replay - Ничего не понимаю... Где все вещи?!

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

Однако, если внимательно посмотреть на код класса и начать что-то искать, можно задаться вопросом: где находятся вызываемые переменные, структуры и функции? Я не могу их нигде найти! Такое мышление на самом деле является начальным этапом. Это всего лишь значит, что вы еще не очень хорошо знакомы с наследованием между классами. Не волнуйтесь, изучайте код спокойно и вскоре вы начнете понимать, как работает данное наследование. То, что вы начали сейчас, это хорошо, ведь скоро я покажу кое-что еще более сложное и оно может вас сильно запутать. Одной из таких вещей является ПОЛИМОРФИЗМ. Это нечто очень полезное, но одновременно создает большую путаницу для тех, кто не понимает проблем, связанных с работой наследования. Так что рекомендую вам изучать этот код должным образом.

Ну а пока оставим тему полиморфизма на будущее. Давайте сосредоточимся на текущем коде, который можно посмотреть ниже:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_ConfigService.mqh"
//+------------------------------------------------------------------+
class C_Replay : private C_ConfigService
{
        private :
                int             m_ReplayCount;
                long            m_IdReplay;
                struct st01
                {
                        MqlRates Rate[1];
                        bool     bNew;
                        datetime memDT;
                        int      delay;
                }m_MountBar;
//+------------------------------------------------------------------+
                void AdjustPositionToReplay(const bool bViewBuider)
                        {
                                u_Interprocess  Info;
                                MqlRates        Rate[def_BarsDiary];
                                int             iPos, nCount;
                                
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                if (Info.s_Infos.iPosShift == (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks)) return;
                                iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
                                Rate[0].time = macroRemoveSec(m_Ticks.Info[iPos].time);
                                if (iPos < m_ReplayCount)
                                {
                                        CustomRatesDelete(def_SymbolReplay, Rate[0].time, LONG_MAX);
                                        if ((m_dtPrevLoading == 0) && (iPos == 0))
                                        {
                                                m_ReplayCount = 0;
                                                Rate[m_ReplayCount].close = Rate[m_ReplayCount].open = Rate[m_ReplayCount].high = Rate[m_ReplayCount].low = m_Ticks.Info[iPos].last;
                                                Rate[m_ReplayCount].tick_volume = Rate[m_ReplayCount].real_volume = 0;
                                                CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                                        }else
                                        {
                                                for(Rate[0].time -= 60; (m_ReplayCount > 0) && (Rate[0].time <= macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)); m_ReplayCount--);
                                                m_ReplayCount++;
                                        }
                                }else if (iPos > m_ReplayCount)
                                {
                                        if (bViewBuider)
                                        {
                                                Info.s_Infos.isWait = true;
                                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                        }else
                                        {
                                                for(; Rate[0].time > m_Ticks.Info[m_ReplayCount].time; m_ReplayCount++);
                                                for (nCount = 0; m_Ticks.Rate[nCount].time < macroRemoveSec(m_Ticks.Info[iPos].time); nCount++);
                                                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount);
                                        }
                                }
                                for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag);) CreateBarInReplay();
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                Info.s_Infos.isWait = false;
                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                        }
//+------------------------------------------------------------------+
inline void CreateBarInReplay(const bool bViewMetrics = false)
                        {
#define def_Rate m_MountBar.Rate[0]

                                static ulong _mdt = 0;
                                int i;
                                
                                if (m_MountBar.bNew = (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)))
                                {
                                        if (bViewMetrics)
                                        {
                                                _mdt = (_mdt > 0 ? GetTickCount64() - _mdt : _mdt);
                                                i = (int) (_mdt / 1000);
                                                Print(TimeToString(m_Ticks.Info[m_ReplayCount].time, TIME_SECONDS), " - Metrica: ", i / 60, ":", i % 60, ".", (_mdt % 1000));
                                                _mdt = GetTickCount64();
                                        }
                                        m_MountBar.memDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                        def_Rate.real_volume = 0;
                                        def_Rate.tick_volume = 0;
                                }
                                def_Rate.close = m_Ticks.Info[m_ReplayCount].last;
                                def_Rate.open = (m_MountBar.bNew ? def_Rate.close : def_Rate.open);
                                def_Rate.high = (m_MountBar.bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
                                def_Rate.low = (m_MountBar.bNew || (def_Rate.close < def_Rate.low) ? def_Rate.close : def_Rate.low);
                                def_Rate.real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real;
                                def_Rate.tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0);
                                def_Rate.time = m_MountBar.memDT;
                                m_MountBar.bNew = false;
                                CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate, 1);
                                m_ReplayCount++;
                                
#undef def_Rate
                        }
//+------------------------------------------------------------------+
        public  :
//+------------------------------------------------------------------+
                C_Replay(const string szFileConfig)
                        {
                                m_ReplayCount = 0;
                                m_dtPrevLoading = 0;
                                m_Ticks.nTicks = 0;
                                m_PointsPerTick = 0;
                                Print("************** Serviço Market Replay **************");
                                srand(GetTickCount());
                                GlobalVariableDel(def_GlobalVariableReplay);
                                SymbolSelect(def_SymbolReplay, false);
                                CustomSymbolDelete(def_SymbolReplay);
                                CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay), _Symbol);
                                CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
                                CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
                                SymbolSelect(def_SymbolReplay, true);
                                m_IdReplay = (SetSymbolReplay(szFileConfig) ? 0 : -1);
                        }
//+------------------------------------------------------------------+
                ~C_Replay()
                        {
                                ArrayFree(m_Ticks.Info);
                                ArrayFree(m_Ticks.Rate);
                                m_IdReplay = ChartFirst();
                                do
                                {
                                        if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
                                                ChartClose(m_IdReplay);
                                }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0);
                                for (int c0 = 0; (c0 < 2) && (!SymbolSelect(def_SymbolReplay, false)); c0++);
                                CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
                                CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
                                CustomSymbolDelete(def_SymbolReplay);
                                GlobalVariableDel(def_GlobalVariableReplay);
                                GlobalVariableDel(def_GlobalVariableIdGraphics);
                                Print("Сервис репликации совершен...");
                        }
//+------------------------------------------------------------------+
                bool ViewReplay(ENUM_TIMEFRAMES arg1)
                        {
                                u_Interprocess info;
                                
                                if (m_IdReplay == -1) return false;
                                if ((m_IdReplay = ChartFirst()) > 0) do
                                {
                                        if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
                                        {
                                                ChartClose(m_IdReplay);
                                                ChartRedraw();
                                        }
                                }while ((m_IdReplay = ChartNext(m_IdReplay)) > 0);
                                Print("Ожидаем разрешения от индикатора [Market Replay] для начала репликации ...");
                                info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1);
                                ChartApplyTemplate(m_IdReplay, "Market Replay.tpl");
                                ChartRedraw(m_IdReplay);
                                GlobalVariableDel(def_GlobalVariableIdGraphics);
                                GlobalVariableTemp(def_GlobalVariableIdGraphics);
                                GlobalVariableSet(def_GlobalVariableIdGraphics, info.u_Value.df_Value);
                                while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(750);
                                
                                return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != ""));
                        }
//+------------------------------------------------------------------+
                bool LoopEventOnTime(const bool bViewBuider, const bool bViewMetrics)
                        {

                                u_Interprocess Info;
                                int iPos, iTest;
                                
                                iTest = 0;
                                while ((iTest == 0) && (!_StopFlag))
                                {
                                        iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -1);
                                        iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -1);
                                        iTest = (iTest == 0 ? (Info.s_Infos.isPlay ? 1 : iTest) : iTest);
                                        if (iTest == 0) Sleep(100);
                                }
                                if ((iTest < 0) || (_StopFlag)) return false;
                                AdjustPositionToReplay(bViewBuider);
                                m_MountBar.delay = 0;
                                while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag))
                                {
                                        CreateBarInReplay(bViewMetrics);
                                        iPos = (int)(m_ReplayCount < m_Ticks.nTicks ? m_Ticks.Info[m_ReplayCount].time_msc - m_Ticks.Info[m_ReplayCount - 1].time_msc : 0);
                                        m_MountBar.delay += (iPos < 0 ? iPos + 1000 : iPos);
                                        if (m_MountBar.delay > 400)
                                        {
                                                if (ChartSymbol(m_IdReplay) == "") break;
                                                GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                                if (!Info.s_Infos.isPlay) return true;
                                                Info.s_Infos.iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
                                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                                Sleep(m_MountBar.delay - 20);
                                                m_MountBar.delay = 0;
                                        }
                                }                               
                                return (m_ReplayCount == m_Ticks.nTicks);
                        }
//+------------------------------------------------------------------+
};
//+------------------------------------------------------------------+
#undef macroRemoveSec
#undef def_SymbolReplay
//+------------------------------------------------------------------+

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

Но прежде чем говорить о конструкторе и деструкторе класса, давайте взглянем на две функции, к которым можно получить доступ извне класса. В какой-то момент я решил оставить только одну из них, но по практическим соображениям я оставил обе функции. Так будет легче в работе. Функция LoopEventOnTime уже довольно подробно рассматривалась в предыдущих статьях. А поскольку здесь она не претерпела никаких модификаций, то нет смысла давать дополнительные пояснения. Мы можем пропустить ее и сосредоточиться на той, которая претерпела изменения: функции ViewReplay.

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


Конечные выводы

Учитывая все изменения, я предлагаю вам изучить новые материалы, и также сравнить прикрепленный код с другими кодами, представленными в предыдущих статьях. В следующей статье мы начнем совсем другую, довольно интересную тему. До встречи!

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

Прикрепленные файлы |
Создавать графические панели в MQL5 стало проще Создавать графические панели в MQL5 стало проще
В этой статье мы предоставим простое и понятное руководство для всех, кто хочет создать один из самых ценных и полезных инструментов в трейдинге — графическую панель, упрощающую выполнение торговых задач. Графические панели позволяют сэкономить время и больше сосредоточиться на самой торговле.
Может ли Heiken Ashi давать хорошие сигналы в сочетании со скользящими средними? Может ли Heiken Ashi давать хорошие сигналы в сочетании со скользящими средними?
Комбинации стратегий могут повысить эффективность торговли. Мы можем комбинировать индикаторы и паттерны, чтобы получать дополнительные подтверждения. Скользящие средние помогают нам подтвердить тренд и следовать ему. Это самые известный технический индикатор, что объясняется его простотой и доказанной эффективностью анализа.
Вспоминаем старую трендовую стратегию: два стохастических осциллятора, MA и Фибоначчи Вспоминаем старую трендовую стратегию: два стохастических осциллятора, MA и Фибоначчи
Старые торговые стратегии. В этой статье представлена стратегия отслеживания тренда. Стратегия исключительно техническая и использует несколько индикаторов и инструментов для подачи сигналов и определения целевых уровней. Компоненты стратегии включают в себя: 14-периодный стохастический осциллятор, пятипериодный стохастический осциллятор, скользящую среднюю с периодом 200 и проекцию Фибоначчи (для установки целевых уровней).
Сделайте торговые графики лучше с интерактивным графическим интерфейсом на основе MQL5 (Часть II): Перемещаемый интерфейс (II) Сделайте торговые графики лучше с интерактивным графическим интерфейсом на основе MQL5 (Часть II): Перемещаемый интерфейс (II)
Раскройте потенциал динамического представления данных в своих торговых стратегиях и утилитах с помощью нашего подробного руководства по созданию перемещаемых графических интерфейсов в MQL5. Погрузитесь в фундаментальные принципы объектно-ориентированного программирования и узнайте, как легко и эффективно разрабатывать и использовать один или несколько перемещаемых графических интерфейсов на одном графике.