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

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

MetaTrader 5Примеры | 1 сентября 2023, 10:07
687 0
Daniel Jose
Daniel Jose

Введение

В предыдущей статье, "Разработка системы репликации - Моделирование рынка (Часть 09): Пользовательские События, мы посмотрели, как запускать пользовательские события. При этом мы реализовали очень интересную систему с точки зрения взаимодействия индикатора с сервисом. В этой статье я подчеркну важность сохранения торгуемых тиков. Если вы еще не применяли данную практику, вам следует серьезно подумать о том, чтобы делать это ежедневно. Зарегистрируйте все торгуемые значения актива, которые вам нужно подробно изучить.

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

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

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

ПРИМЕЧАНИЕ: Некоторые могут сказать, что, сложив разницу в подгонке, значения будут эквивалентны. Однако вопрос не в этом. Дилемма заключается в том, что если рынок воспринимает цену в определенном диапазоне, он среагирует определенным образом. Если мы воспринимаем это в другом, то ответ другой.

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


Разработка кода

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

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

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

#define def_STR_FilesBar        "[BARS]"
#define def_STR_FilesTicks      "[TICKS]"
#define def_STR_TicksToBars     "[TICKS->BARS]"

// ... Остальной код ...

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

Таким образом, если пользователю придет в голову ввести в файл конфигурации следующее определение:

[ TICKS -> BARS ]

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

class C_Replay
{
        private :
                enum eTranscriptionDefine {Transcription_FAILED, Transcription_INFO, Transcription_DEFINE};
                int      m_ReplayCount;
                datetime m_dtPrevLoading;

// ... Остальной код....

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

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

inline eTranscriptionDefine GetDefinition(const string &In, string &Out)
{
        string szInfo;
        
        szInfo = In;
        Out = "";
        StringToUpper(szInfo);
        StringTrimLeft(szInfo);
        StringTrimRight(szInfo);
        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;
}

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

В этом конкретном случае и в данный момент мы просто вернем результат ранее выполненного процесса. Если это не так, то мы начнем искать данные определения. Даже если они в другом формате, содержание может быть подходящим и правильным. При чтении мы должны игнорировать любой символ, значение которого меньше пробела. Таким образом, даже если мы напишем: [ B A R S ], система интерпретирует [BARS]. При этом у ввода могут присутствовать небольшие различия в способе написания, но пока содержимое соответствует ожиданиям, мы получим соответствующий результат.

Теперь у нас есть новая система чтения и настройки, основанная на содержимом файла конфигурации,

bool SetSymbolReplay(const string szFileConfig)
{
        int     file;
        string  szInfo;
        bool    isBars = true;
                                
        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;
        while ((!FileIsEnding(file)) && (!_StopFlag)) switch (GetDefinition(FileReadString(file), szInfo))
        {
		case Transcription_DEFINE:
			if (szInfo == def_STR_FilesBar) isBars = true; else
                        if (szInfo == def_STR_FilesTicks) isBars = false;
                        break;
		case Transcription_INFO:
			if (szInfo != "") if (!(isBars ? LoadPrevBars(szInfo) : LoadTicksReplay(szInfo)))
			{
				if (!_StopFlag)
					MessageBox(StringFormat("Файл %s из %s\nне смог быть загружен.", szInfo, (isBars ? def_STR_FilesBar : def_STR_FilesTicks), "Market Replay", MB_OK));
				FileClose(file);
				return false;
			}
			break;
	}
        FileClose(file);

        return (!_StopFlag);
}

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

Если присмотреться, то мы увидим, что приобрели способность делать то, что раньше было невозможно. Подумайте: данные отправляются в процедуру, которая централизует всю предварительную обработку данных, которые считались из файла конфигурации. Данные, используемые в приведенном выше коде, уже «чисты». Значит ли это, что мы можем включать комментарии в файл конфигурации? Наш ответ: ДА. Теперь мы можем сделать это. Нам просто нужно определить формат комментария: есть ли у него одна или несколько строк. Начнем с однострочного. Смотрите, процедура проста и понятна:

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;
}

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

bool SetSymbolReplay(const string szFileConfig)
        {
#define macroERROR(MSG) { FileClose(file); if (MSG != "") MessageBox(MSG, "Market Replay", MB_OK); return false; }
                int     file,
                        iLine;
                string  szInfo;
                char    iStage;
                                
                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;
                iStage = 0;
                iLine = 1;
                while ((!FileIsEnding(file)) && (!_StopFlag))
                {
                        switch (GetDefinition(FileReadString(file), szInfo))
                        {
                                case Transcription_DEFINE:
                                        if (szInfo == def_STR_FilesBar) iStage = 1; else
                                        if (szInfo == def_STR_FilesTicks) iStage = 2; else
                                        if (szInfo == def_STR_TicksToBars) iStage = 3; else
                                                macroERROR(StringFormat("%s не распознан в системе\nв строке %d.", szInfo, iLine));
                                        break;
                                case Transcription_INFO:
                                        if (szInfo != "") switch (iStage)
                                        {
                                                case 0:
                                                        macroERROR(StringFormat("Не распознана команда в строке %d\nфайла кнофигурации.", iLine));
                                                        break;
                                                case 1:
                                                        if (!LoadPrevBars(szInfo)) macroERROR(StringFormat("Este arquivo esta declarado na linha %d", iLine));
                                                        break;
                                                case 2:
                                                        if (!LoadTicksReplay(szInfo)) macroERROR(StringFormat("Этот файл объявоен в строке %d", iLine));
                                                        break;
                                                case 3:
                                                        break;
                                        }
                                        break;
                        };
                        iLine++;
                }
                FileClose(file);
                return (!_StopFlag);
#undef macroERROR
        }

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

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

#Сначала ставим начальные бары, думаю 3 достаточно ....
[Bars]
WIN$N_M1_202108020900_202108021754
WIN$N_M1_202108030900_202108031754
WIN$N_M1_202108040900_202108041754

#У меня есть файл тиков, но я буду использовать его в качестве пре-баров ... в итоге у нас будет 4 файла с барами
[ Ticks -> Bars]
WINQ21_202108050900_202108051759

#Теперь используем файл торгуемых тиков для запуска репликации ...
[Ticks]
WINQ21_202108060900_202108061759

#Конец файла конфигурации...

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

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

bool LoadTicksReplay(const string szFileNameCSV)
{
        int     file,
                old;
        string  szInfo = "";
        MqlTick tick;
        MqlRates rate;
                        
        if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
        {
                ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                ArrayResize(m_Ticks.Rate, def_BarsDiary, def_BarsDiary);
                old = m_Ticks.nTicks;
                for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(file);
                if (szInfo != def_Header_Ticks)
                {
                        Print("Файл ", szFileNameCSV, ".csv - не файл торгуемых тиков.");
                        return false;
                }
                Print("Загрузка тиков для репликации. Ждите...");
                while ((!FileIsEnding(file)) && (m_Ticks.nTicks < (INT_MAX - 2)) && (!_StopFlag))
                {
                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                        szInfo = FileReadString(file) + " " + FileReadString(file);
                        tick.time = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19)));
                        tick.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                        tick.bid = StringToDouble(FileReadString(file));
                        tick.ask = StringToDouble(FileReadString(file));
                        tick.last = StringToDouble(FileReadString(file));
                        tick.volume_real = StringToDouble(FileReadString(file));
                        tick.flags = (uchar)StringToInteger(FileReadString(file));
                        if ((m_Ticks.Info[old].last == tick.last) && (m_Ticks.Info[old].time == tick.time) && (m_Ticks.Info[old].time_msc == tick.time_msc))
                                m_Ticks.Info[old].volume_real += tick.volume_real;
                        else
                        {                                                       
                                m_Ticks.Info[m_Ticks.nTicks] = tick;
                                if (tick.volume_real > 0.0)
                                {
                                        m_Ticks.nRate += (BuiderBar1Min(rate, tick) ? 1 : 0);
                                        rate.spread = m_Ticks.nTicks;
                                        m_Ticks.Rate[m_Ticks.nRate] = rate;
                                        m_Ticks.nTicks++;
                                }
                                old = (m_Ticks.nTicks > 0 ? m_Ticks.nTicks - 1 : old);
                        }
                }
                if ((!FileIsEnding(file)) && (!_StopFlag))
                {
                        Print("Слишком много данных в тиковом файле.\nНевозможно продолжить...");
                        return false;
                }
        }else
        {
                Print("Файл тиков ", szFileNameCSV,".csv не найден...");
                return false;
        }
        return (!_StopFlag);
};

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

Теперь давайте подумаем: Разве это не именно то, что мы хотим сделать? Мы хотим считать файл торгуемых тиков и создать 1-минутный бар, а затем сохранить его в активе, используемом для репликации/моделирования. Но не в качестве торгуемого тика, а как данные, которые будут интерпретироваться как предыдущий бар. Таким образом, если вместо простого хранения этих тиков мы внесем небольшие изменения в упомянутый код, то мы сможем сделать так, чтобы созданный во время чтения торгуемых тиков бар, оказался вставленным в актив для анализа в репликации/моделировании, как предыдущий бар.

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

bool SetSymbolReplay(const string szFileConfig)
        {
#define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("Произошла ошибка в строке %d", iLine)), "Market Replay", MB_OK); return false; }
                int     file,
                        iLine;
                string  szInfo;
                char    iStage;
                                
                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;
                iStage = 0;
                iLine = 1;
                while ((!FileIsEnding(file)) && (!_StopFlag))
                {
                        switch (GetDefinition(FileReadString(file), szInfo))
                        {
                                case Transcription_DEFINE:
                                        if (szInfo == def_STR_FilesBar) iStage = 1; else
                                        if (szInfo == def_STR_FilesTicks) iStage = 2; else
                                        if (szInfo == def_STR_TicksToBars) iStage = 3; else
                                                macroERROR(StringFormat("%s не распознан в системе\nв строке %d.", szInfo, iLine));
                                        break;
                                case Transcription_INFO:
                                        if (szInfo != "") switch (iStage)
                                        {
                                                case 0:
                                                        macroERROR(StringFormat("Нераспознанная команда в строке %d\nфайла конфигурации.", iLine));
                                                        break;
                                                case 1:
                                                        if (!LoadPrevBars(szInfo)) macroERROR("");
                                                        break;
                                                case 2:
                                                        if (!LoadTicksReplay(szInfo)) macroERROR("");
                                                        break;
                                                case 3:
                                                        if (!LoadTicksReplay(szInfo, false)) macroERROR("");
                                                        break;
                                        }
                                        break;
                        };
                        iLine++;
                }
                FileClose(file);
                        return (!_StopFlag);
#undef macroERROR
        }

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

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

bool LoadTicksReplay(const string szFileNameCSV, const bool ToReplay = true)
{
        int     file,
                old,
                MemNRates,
                MemNTicks;
        string  szInfo = "";
        MqlTick tick;
        MqlRates rate,
                RatesLocal[];
                                
        MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
        MemNTicks = m_Ticks.nTicks;
        if ((file = FileOpen("Market Replay\\Ticks\\" + szFileNameCSV + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
        {
                ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                ArrayResize(m_Ticks.Rate, def_BarsDiary, def_BarsDiary);
                old = m_Ticks.nTicks;
                for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(file);
                if (szInfo != def_Header_Ticks)
                {
                        Print("Файл", szFileNameCSV, ".csv - не файл с торгуемыми тиками.");
                        return false;
                }
                Print("Загрузка тиков для репликации. Ждите...");
                while ((!FileIsEnding(file)) && (m_Ticks.nTicks < (INT_MAX - 2)) && (!_StopFlag))
                {
                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                        szInfo = FileReadString(file) + " " + FileReadString(file);
                        tick.time = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19)));
                        tick.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                        tick.bid = StringToDouble(FileReadString(file));
                        tick.ask = StringToDouble(FileReadString(file));
                        tick.last = StringToDouble(FileReadString(file));
                        tick.volume_real = StringToDouble(FileReadString(file));
                        tick.flags = (uchar)StringToInteger(FileReadString(file));
                        if ((m_Ticks.Info[old].last == tick.last) && (m_Ticks.Info[old].time == tick.time) && (m_Ticks.Info[old].time_msc == tick.time_msc))
                                m_Ticks.Info[old].volume_real += tick.volume_real;
                        else
                        {                                                       
                                m_Ticks.Info[m_Ticks.nTicks] = tick;
                                if (tick.volume_real > 0.0)
                                {
                                        m_Ticks.nRate += (BuiderBar1Min(rate, tick) ? 1 : 0);
                                        rate.spread = (ToReplay ? m_Ticks.nTicks : 0);
                                        m_Ticks.Rate[m_Ticks.nRate] = rate;
                                        m_Ticks.nTicks++;
                                }
                                old = (m_Ticks.nTicks > 0 ? m_Ticks.nTicks - 1 : old);
                        }
                }
                if ((!FileIsEnding(file)) && (!_StopFlag))
                {
                        Print("Слишком много данных в тиковом файле.\nНевозможно продолжить...");
                        FileClose(file);
                        return false;
                }
                FileClose(file);
        }else
        {
                Print("Файл тиков ", szFileNameCSV,".csv не найден...");
                return false;
        }
        if ((!ToReplay) && (!_StopFlag))
        {
                ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
                ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
                CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
                m_dtPrevLoading = m_Ticks.Rate[m_Ticks.nRate].time;
                m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
                m_Ticks.nTicks = MemNTicks;
                ArrayFree(RatesLocal);
        }
        return (!_StopFlag);
};

Первое, на что следует обратить внимание — добавить некоторые дополнительные переменные: Эти локальные переменные будут временно хранить важную информацию, чтобы при необходимости позже восстановить исходное состояние системы. Важно понимать, что весь код останется идентичным оригиналу. Мы могли бы добавить 1-минутные бары по мере их появления, но я применил другой подход. Естественно, мне пришлось внести изменения в конкретном месте, именно там, где мы оцениваем, будут ли данные считаться предыдущими барами или нет. Если они используются таким образом, важно, чтобы представление было последовательным и логичным.

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

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

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

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


Удаление всей графики репликации

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

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

void CloseReplay(void)
{
        ArrayFree(m_Ticks.Info);
        ArrayFree(m_Ticks.Rate);
        ChartClose(m_IdReplay);
        SymbolSelect(def_SymbolReplay, false);
        CustomSymbolDelete(def_SymbolReplay);
        GlobalVariableDel(def_GlobalVariableReplay);
        GlobalVariableDel(def_GlobalVariableIdGraphics);
}

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

void CloseReplay(void)
{                       
        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);
}

Многим из вас этот код может показаться странным, но его функция довольно простая. Сначала мы захватываем идентификатор первого графика. Следует отметить, что он не обязательно откроется первым. Далее запускаем цикл. В этом цикле мы выполняем проверку, чтобы определить актив, на который ссылается график. Если это актив репликации, мы закрываем график. Чтобы определить, завершится цикл или нет, мы запросим платформу сообщить нам об идентификаторе следующего графика в списке. Если в списке нет других графиков, вернется значение меньше нуля и цикл завершится. В ином случае цикл будет выполнен снова. Это гарантирует, что все окна, активом которых является тот, который мы используем в качестве репликации, будут закрыты, независимо от их количества. Затем мы делаем две попытки удалить актив, используемый в качестве репликации, из окна обзора рынка. Причина, по которой выполняются две попытки заключается в следующем: когда у нас открыто только окно сервиса репликации/моделирования, то одной попытки достаточно для удаления актива, но если пользователь открыл другие окна, может потребоваться повторная попытка.

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


Заключение

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



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


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

Прикрепленные файлы |
Market_Replay.zip (13061.7 KB)
Готовые шаблоны для подключения индикаторов в экспертах (Часть 1): Осцилляторы Готовые шаблоны для подключения индикаторов в экспертах (Часть 1): Осцилляторы
В статье рассмотрим стандартные индикаторы из категории осцилляторов. Создадим готовые к применению шаблоны их использования в советниках — объявление и установка параметров, инициализация, деинициализация индикаторов и получение данных и сигналов из индикаторных буферов в советниках.
Теория категорий в MQL5 (Часть 11): Графы Теория категорий в MQL5 (Часть 11): Графы
Статья продолжает серию о реализации теории категорий в MQL5. Здесь мы рассмотрим, как теория графов может быть интегрирована с моноидами и другими структурами данных при разработке стратегии закрытия торговой системы.
Разработка системы репликации - Моделирование рынка (Часть 11): Появление СИМУЛЯТОРА (I) Разработка системы репликации - Моделирование рынка (Часть 11): Появление СИМУЛЯТОРА (I)
Для того, чтобы использовать данные, формирующие бары, мы должны отказаться от репликации и заняться разработкой симулятора. Мы будем использовать 1-минутные бары именно потому, что они предлагают минимальный уровень сложности.
Разработка системы репликации - Моделирование рынка (Часть 09): Пользовательские события Разработка системы репликации - Моделирование рынка (Часть 09): Пользовательские события
Здесь мы увидим, как активировать пользовательские события и проработать вопрос о том, как индикатор сообщает о состоянии сервиса репликации/моделирования.