English 中文 Español Deutsch 日本語 Português
preview
Разработка системы репликации - Моделирование рынка (Часть 11): Появление СИМУЛЯТОРА (I)

Разработка системы репликации - Моделирование рынка (Часть 11): Появление СИМУЛЯТОРА (I)

MetaTrader 5Примеры | 4 сентября 2023, 10:03
730 0
Daniel Jose
Daniel Jose

Введение

Пока, включая предыдущую статью: Разработка системы репликации - Моделирование рынка (Часть 10): Только реальные данные для репликации, всё, что мы делали, включало реальные данные, то есть мы использовали реально торгуемые тики. Благодаря этому движения становятся точными и их легко создавать, поскольку нам не нужно беспокоиться о сборе информации. Всё, что нам нужно было сделать — это преобразовывать торгуемые тики в 1-минутные бары, а платформа MetaTrader 5 заботилась обо всем остальном за нас.

Однако теперь перед нами стоит другая, более сложная задача.


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

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

Эта деталь может показаться не такой важной, поскольку обычно мы видим на рынке движение типа ЗИГ-ЗАГ. Независимо от сложности движения, всё сводится к созданию зигзага между точками OHCL. Он начинается в точке открытия бара, и для создания этого внутреннего зигзага выполняется не менее 9 сегментов, всегда заканчивающихся при закрытии бара и повторяющих процесс на следующем баре. Такую же логику использует тестер стратегий MetaTrader 5. Для более подробной информации смотрите Реальные и сгенерированные тики: Алгоритмический трейдинг. Первоначально мы примем эту стратегию; хотя она и не идеально подходящая нам, но она послужит отправной точкой для разработки более подходящих подходов.

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


Подготовка почвы

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

inline bool CheckFileIsBar(int &file, const string szFileName)
                        {
                                string  szInfo = "";
                                bool    bRet;
                                
                                for (int c0 = 0; (c0 < 9) && (!FileIsEnding(file)); c0++) szInfo += FileReadString(file);
                                if ((bRet = (szInfo == def_Header_Bar)) == false)
                                {
                                        Print("File ", szFileName, ".csv is not a file with bars.");
                                        FileClose(file);
                                }
                                
                                return bRet;
                        }

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

inline void FileReadBars(int &file, MqlRates &rate[])
                        {
                                rate[0].time = StringToTime(FileReadString(file) + " " + FileReadString(file));
                                rate[0].open = StringToDouble(FileReadString(file));
                                rate[0].high = StringToDouble(FileReadString(file));
                                rate[0].low = StringToDouble(FileReadString(file));
                                rate[0].close = StringToDouble(FileReadString(file));
                                rate[0].tick_volume = StringToInteger(FileReadString(file));
                                rate[0].real_volume = StringToInteger(FileReadString(file));
                                rate[0].spread = (int) StringToInteger(FileReadString(file));
                        }

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

inline bool OpenFileBars(int &file, const string szFileName)
                        {
                                if ((file = FileOpen("Market Replay\\Bars\\" + szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
                                {
                                        if (!CheckFileIsBar(file, szFileName))
                                                return false;
                                        return true;
                                }
                                Print("Falha ao acessar ", szFileName, ".csv de barras.");
                                
                                return false;
                        }

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

bool LoadPrevBars(const string szFileNameCSV)
        {
                int     file,
                        iAdjust = 0;
                datetime dt = 0;
                MqlRates Rate[1];
                                
                if (OpenFileBars(file, szFileNameCSV))
                {
                        Print("Carregando barras previas para Replay. Aguarde ....");
                        while ((!FileIsEnding(file)) && (!_StopFlag))
                        {
                                FileReadBars(file, Rate);
                                iAdjust = ((dt != 0) && (iAdjust == 0) ? (int)(Rate[0].time - dt) : iAdjust);
                                dt = (dt == 0 ? Rate[0].time : dt);
                                CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                        }
                        m_dtPrevLoading = Rate[0].time + iAdjust;
                        FileClose(file);
                        
                        return (!_StopFlag);
                }
                m_dtPrevLoading = 0;
                        
                return false;
        }

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

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

Это уже позволяет нам провести простой тест, а это именно то, что нам нужно, чтобы начать работу над моделированием тика.

                bool SetSymbolReplay(const string szFileConfig)
                        {
#define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("An error occurred in line %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("Failed to open the\nconfiguration file.", "Market Replay", MB_OK);
                                        return false;
                                }
                                Print("Loading data for replay. Please wait....");
                                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
                                                        if (szInfo == def_STR_BarsToTicks) iStage = 4; else
                                                                macroERROR(StringFormat("%s is not recognized in the system\nin line %d.", szInfo, iLine));
                                                        break;
                                                case Transcription_INFO:
                                                        if (szInfo != "") switch (iStage)
                                                        {
                                                                case 0:
                                                                        macroERROR(StringFormat("Command not recognized in line %d\nof the configuration file.", iLine));
                                                                        break;
                                                                case 1:
                                                                        if (!LoadPrevBars(szInfo)) macroERROR("");
                                                                        break;
                                                                case 2:
                                                                        if (!LoadTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 3:
                                                                        if (!LoadTicksReplay(szInfo, false)) macroERROR("");
                                                                        break;
                                                                case 4:
                                                                        if (!LoadBarsToTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                        }
                                                        break;
                                        };
                                        iLine++;
                                }
                                FileClose(file);

                                return (!_StopFlag);
#undef macroERROR
                        }

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

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


Небольшие размышления перед реализацией

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


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

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

Давайте подумаем: Нецелесообразно использовать только 1 тик в любой системе, поскольку это будет представлять собой только сделку на покупку или продажу, а не само движение. Поэтому мы можем исключить эту ​​возможность. Мы могли бы придумать минимум два тика, которые будут символизировать точку открытия и точку закрытия. Хотя это кажется логичным, реального движения у нас тоже не будет, поскольку нам нужно будет сгенерировать только один тик для открытия бара и второй для его закрытия. 

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

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

Важно: Возможно, читатель путает некоторые используемые здесь термины. Давайте уточним это, чтобы избежать недоразумений: Когда я упоминаю термин TICK, я фактически имею в виду торговое событие, то есть событие покупки или продажи актива по указанной цене. Что касается термина TICK, я имею в виду наименьшее отклонение относительно торговой цены. Чтобы понять эту разницу, надо учитывать следующее: 1 тик на фондовом рынке стоит 0,01 пункта, 1 тик в долларовом фьючерсе стоит 0,5 пункта, а в фьючерсе на индекс — 5 пунктов.

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

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

Изображение выше может сбить с толку, и вы, возможно, не поняли его правильно. Оно представляет собой следующую информацию: В левом углу мы смотрим на фактическое движение в тиках. Маленькие горизонтальные полоски представляют каждый тик. Круги обозначают цены, на которых актив фактически останавливался между тиками. Зеленая линия указывает на скачок цены. Обратите внимание, что были случаи, когда по определенным тикам не было сделок, но при анализе значения OHCL мы не видим явных скачков тиков. Смоделировав движение с помощью только свечи баров, мы увидим то, что показано на следующем изображении.

Синяя линия представляет смоделированное движение. В этом случае мы собираемся пройти все тики, независимо от того, что на самом деле произошло во время реальной торговли. Поэтому всегда имейте в виду: моделирование — это не то же самое, что использование реальных данных. Какой бы сложной ни была система моделирования, она никогда не сможет точно отразить реальность.


О реализации базовой системы преобразования

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

#define def_STR_FilesBar        "[BARS]"
#define def_STR_FilesTicks      "[TICKS]"
#define def_STR_TicksToBars     "[TICKS->BARS]"
#define def_STR_BarsToTicks     "[BARS->TICKS]"
#define def_STR_ConfigSymbol    "[CONFIG]"
#define def_STR_PointsPerTicks  "POINTSPERTICK"
#define def_Header_Bar          "<DATE><TIME><OPEN><HIGH><LOW><CLOSE><TICKVOL><VOL><SPREAD>"
#define def_Header_Ticks        "<DATE><TIME><BID><ASK><LAST><VOLUME><FLAGS>"
#define def_BarsDiary           540

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

                bool SetSymbolReplay(const string szFileConfig)
                        {
#define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("An error occurres in line %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("Failed to open the\nconfiguration file.", "Market Replay", MB_OK);
                                        return false;
                                }
                                Print("Loading data for replay. Please wait....");
                                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
                                                        if (szInfo == def_STR_BarsToTicks) iStage = 4; else
                                                        if (szInfo == def_STR_ConfigSymbol) iStage = 5; else
                                                                macroERROR(StringFormat("%s is not recognized in the system\nin line %d.", szInfo, iLine));
                                                        break;
                                                case Transcription_INFO:
                                                        if (szInfo != "") switch (iStage)
                                                        {
                                                                case 0:
                                                                        macroERROR(StringFormat("Command not recognized in line %d\nof the configuration file.", iLine));
                                                                        break;
                                                                case 1:
                                                                        if (!LoadPrevBars(szInfo)) macroERROR("");
                                                                        break;
                                                                case 2:
                                                                        if (!LoadTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 3:
                                                                        if (!LoadTicksReplay(szInfo, false)) macroERROR("");
                                                                        break;
                                                                case 4:
                                                                        if (!LoadBarsToTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 5:
                                                                        if (!Configs(szInfo)) macroERROR("");
                                                                        break;
                                                        }
                                                        break;
                                        };
                                        iLine++;
                                }
                                FileClose(file);
                                
                                return (!_StopFlag);
#undef macroERROR
                        }

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

inline bool Configs(const string szInfo)
                        {
                                string szRet[];
                                
                                if (StringSplit(szInfo, '=', szRet) == 2)
                                {
                                        StringTrimRight(szRet[0]);
                                        StringTrimLeft(szRet[1]);
                                        if (szRet[0] == def_STR_PointsPerTicks) m_PointsPerTick = StringToDouble(szRet[1]); else
                                        {
                                                Print("Variable >>", szRet[0], "<< not defined.");
                                                return false;
                                        }
                                        return true;
                                }
                                Print("Definition of configuration >>", szInfo, "<< is invalid.");
                                return false;
                        }

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

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

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

[Config]
PointsPerTick = 5

[Bars]
WIN$N_M1_202112060900_202112061824
WIN$N_M1_202112070900_202112071824

[ Ticks -> Bars]

[Ticks]

[ Bars -> Ticks ]
WIN$N_M1_202112080900_202112081824

#Fim do arquivo de configuração...

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

inline int Event_OnTime(void)
        {
                bool    bNew;
                int     mili, iPos;
                u_Interprocess Info;
                static MqlRates Rate[1];
                static datetime _dt = 0;
                datetime tmpDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                
                if (m_ReplayCount >= m_Ticks.nTicks) return -1;
                if (bNew = (_dt != tmpDT))
                {
                        _dt = tmpDT;
                        Rate[0].real_volume = 0;
                        Rate[0].tick_volume = 0;
                }
                mili = (int) m_Ticks.Info[m_ReplayCount].time_msc;
                do
                {
                        while (mili == m_Ticks.Info[m_ReplayCount].time_msc)
                        {
                                Rate[0].close = m_Ticks.Info[m_ReplayCount].last;
                                Rate[0].open = (bNew ? Rate[0].close : Rate[0].open);
                                Rate[0].high = (bNew || (Rate[0].close > Rate[0].high) ? Rate[0].close : Rate[0].high);
                                Rate[0].low = (bNew || (Rate[0].close < Rate[0].low) ? Rate[0].close : Rate[0].low);
                                Rate[0].real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real;
                                bNew = false;
                                m_ReplayCount++;
                        }
                        mili++;
                }while (mili == m_Ticks.Info[m_ReplayCount].time_msc);
                Rate[0].time = _dt;
                CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                iPos = (int)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
                GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                if (Info.s_Infos.iPosShift != iPos)
                {
                        Info.s_Infos.iPosShift = (ushort) iPos;
                        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                }
                return (int)(m_Ticks.Info[m_ReplayCount].time_msc < mili ? m_Ticks.Info[m_ReplayCount].time_msc + (1000 - mili) : m_Ticks.Info[m_ReplayCount].time_msc - mili);
        }

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

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


Сейчас мы приступаем к этапу реализации.

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

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

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

                bool LoadBarsToTicksReplay(const string szFileNameCSV)
                        {
//#define DEBUG_SERVICE_CONVERT
                                int file, max;
                                MqlRates rate[1];
                                MqlTick tick[];
                                
                                if (OpenFileBars(file, szFileNameCSV))
                                {
                                        Print("Convertendo barras em ticks. Aguarde...");
                                        ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                                        ArrayResize(m_Ticks.Rate, def_BarsDiary);
                                        ArrayResize(tick, def_MaxSizeArray);
                                        while ((!FileIsEnding(file)) && (!_StopFlag))
                                        {
                                                FileReadBars(file, rate);
                                                max = SimuleBarToTicks(rate[0], tick);
                                                for (int c0 = 0; c0 <= max; c0++)
                                                {
                                                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                                                        m_Ticks.Info[m_Ticks.nTicks++] = tick[c0];
                                                }
                                        }
                                        FileClose(file);
                                        ArrayFree(tick);
                                        
#ifdef DEBUG_SERVICE_CONVERT
        file = FileOpen("Infos.txt", FILE_ANSI | FILE_WRITE);
        for (long c0 = 0; c0 < m_Ticks.nTicks; c0++)
                FileWriteString(file, StringFormat("%s.%03d %f --> %f\n", TimeToString(m_Ticks.Info[c0].time, TIME_DATE | TIME_SECONDS), m_Ticks.Info[c0].time_msc, m_Ticks.Info[c0].last, m_Ticks.Info[c0].volume_real));
        FileClose(file);
#endif

                                        return (!_StopFlag);
                                }
                                
                                return false;
                        }

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

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

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

inline int SimuleBarToTicks(const MqlRates &rate, MqlTick &tick[])
                        {
                                int t0 = 0;
                                long v0, v1, v2, msc;
                                                                
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                Pivot(rate.open, rate.low, t0, tick);
                                Pivot(rate.low, rate.high, t0, tick);
                                Pivot(rate.high, rate.close, t0, tick, true);
                                v0 = (long)(rate.real_volume / (t0 + 1));
                                v1 = 0;
                                msc = 5;
                                v2 = ((60000 - msc) / (t0 + 1));
                                for (int c0 = 0; c0 <= t0; c0++, v1 += v0)
                                {
                                        tick[c0].volume_real = (v0 * 1.0);
                                        tick[c0].time = rate.time + (datetime)(msc / 1000);
                                        tick[c0].time_msc = msc % 1000;
                                        msc += v2;
                                }
                                tick[t0].volume_real = ((rate.real_volume - v1) * 1.0);
                                
                                return t0;
                        }

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

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

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

//+------------------------------------------------------------------+
#define macroCreateLeg(A, B, C) if (A < B)      {               \
                while (A < B)   {                               \
                        tick[C++].last = A;                     \
                        A += m_PointsPerTick;                   \
                                }                               \
                                                } else {        \
                while (A > B)   {                               \
                        tick[C++].last = A;                     \
                        A -= m_PointsPerTick;                   \
                                }               }
                        
inline void Pivot(const double p1, const double p2, int &t0, MqlTick &tick[], bool b0 = false)
                        {
                                double v0, v1, v2;
                                
                                v0 = (p1 > p2 ? p1 - p2 : p2 - p1);
                                v1 = p1 + (MathFloor((v0 * 0.382) / m_PointsPerTick) * m_PointsPerTick * (p1 > p2 ? -1 : 1));
                                v2 = p1 + (MathFloor((v0 * 0.618) / m_PointsPerTick) * m_PointsPerTick * (p1 > p2 ? -1 : 1));
                                v0 = p1;
                                macroCreateLeg(v0, v2, t0);
                                macroCreateLeg(v0, v1, t0);
                                macroCreateLeg(v0, p2, t0);
                                if (b0) tick[t0].last = v0;
                        }
#undef macroCreateLeg
//+------------------------------------------------------------------+

Вышеупомянутая функция проста с точки зрения ее работы. Хотя его расчеты могут показаться многим странными, глядя на значения, использованные для создания точки пивота, вы заметите, что мы всегда пытаемся установить пивот, используя первый и третий линии Фибоначчи. Во-первых, важно отметить, что не имеет значения, направлен ли пивот вверх или вниз; функция выполнит вычисления соответствующим образом. Затем появляется аспект, который может напугать тех, у кого мало знаний в области программирования: МАКРОС. Причина использования макроса заключается в том, что создать часть пивота проще с помощью макроса. Однако ничто не мешает вам создать для этого функцию. На самом деле, если бы мы использовали чистый C++, то этот макрос, вероятно, имел бы совсем другой код. Но здесь, используя MQL5 в том виде, в котором он был создан, он работает как обходной путь.

Гораздо эффективнее использовать этот макрос, чем встраивать код внутрь областей, где он объявлен.


Заключение

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

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



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

Прикрепленные файлы |
Market_Replay.zip (50.42 KB)
Теория категорий в MQL5 (Часть 12): Порядок Теория категорий в MQL5 (Часть 12): Порядок
Статья является частью серии о реализации графов средствами теории категорий в MQL5 и посвящена отношению порядка (Order Theory). Мы рассмотрим два основных типа упорядочения и исследуем, как концепции отношения порядка могут поддерживать моноидные множества при принятии торговых решений.
Готовые шаблоны для подключения индикаторов в экспертах (Часть 1): Осцилляторы Готовые шаблоны для подключения индикаторов в экспертах (Часть 1): Осцилляторы
В статье рассмотрим стандартные индикаторы из категории осцилляторов. Создадим готовые к применению шаблоны их использования в советниках — объявление и установка параметров, инициализация, деинициализация индикаторов и получение данных и сигналов из индикаторных буферов в советниках.
Нейросети — это просто (Часть 57): Стохастический маргинальный актор-критик (SMAC) Нейросети — это просто (Часть 57): Стохастический маргинальный актор-критик (SMAC)
Предлагаем познакомиться с довольно новым алгоритмом Stochastic Marginal Actor-Critic (SMAC), который позволяет строить политики латентных переменных в рамках максимизации энтропии.
Разработка системы репликации - Моделирование рынка (Часть 10): Только реальные данные для репликации Разработка системы репликации - Моделирование рынка (Часть 10): Только реальные данные для репликации
Здесь мы рассмотрим, как более надежные данные (торгуемые тики) можно использовать в системе репликации, не беспокоясь о том, скорректированы они или нет.