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

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

MetaTrader 5Тестер | 15 ноября 2023, 10:24
652 0
Daniel Jose
Daniel Jose

Введение

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

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

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

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

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

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


Реализуем исправление времени создания 1-минутного бара.

Начнем с таймера. Для его исправления, мы изменим всего лишь одну маленькую деталь во всем коде. Это изменение можно увидеть в коде ниже:

inline bool ReadAllsTicks(const bool ToReplay)
                        {
#define def_LIMIT (INT_MAX - 2)
#define def_Ticks m_Ticks.Info[m_Ticks.nTicks]

                                string   szInfo;
                                MqlRates rate;
                                
                                Print("Загрузка тиков для репликации. Подождите...");
                                ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                                while ((!FileIsEnding(m_File)) && (m_Ticks.nTicks < def_LIMIT) && (!_StopFlag))
                                {
                                        ArrayResize(m_Ticks.Info, m_Ticks.nTicks + 1, def_MaxSizeArray);
                                        szInfo = FileReadString(m_File) + " " + FileReadString(m_File);
                                        def_Ticks.time = StringToTime(StringSubstr(szInfo, 0, 19));
                                        def_Ticks.time_msc = (def_Ticks.time * 1000) + (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                        def_Ticks.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                        def_Ticks.bid = StringToDouble(FileReadString(m_File));
                                        def_Ticks.ask = StringToDouble(FileReadString(m_File));
                                        def_Ticks.last = StringToDouble(FileReadString(m_File));
                                        def_Ticks.volume_real = StringToDouble(FileReadString(m_File));
                                        def_Ticks.flags = (uchar)StringToInteger(FileReadString(m_File));                                       
                                        if (def_Ticks.volume_real > 0.0)
                                        {
                                                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
                                                m_Ticks.nRate += (BuiderBar1Min(rate, def_Ticks) ? 1 : 0);
                                                m_Ticks.Rate[m_Ticks.nRate] = rate;
                                        }
                                        m_Ticks.nTicks++;
                                }
                                FileClose(m_File);
                                if (m_Ticks.nTicks == def_LIMIT)
                                {
                                        Print("Слишком много данных в тиковом файле.\nНевозможно продолжать...");
                                        return false;
                                }
                                return (!_StopFlag);
#undef def_Ticks
#undef def_LIMIT
                        }

Готово. Теперь таймер будет работать более правильно. Вы могли бы подумать: но как же это может быть? Я не понимаю 🤔. Простое удаление строки (она удалена) и замена ее небольшим расчетом уже полностью решает проблему таймера, но дело не только в этом. Мы также могли бы удалить значение времени, оставив его равным нулю.

Это сэкономит нам несколько машинных циклов при добавлении тиков на график Обзора рынка. Но (и это «но» действительно заставляет меня задуматься), нам пришлось бы выполнить дополнительный расчет при создании 1-минутных баров, которые потом будут строиться в MetaTrader 5. В итоге нам пришлось бы потратить несколько машинных циклов только на расчет. Сделав это так, как это делается, затраты были бы намного меньше.

Благодаря этому изменению мы можем сразу осуществить еще одно:

inline void ViewTick(void)
                        {
                                MqlTick tick[1];

                                tick[0] = m_Ticks.Info[m_ReplayCount];
                                tick[0].time_msc = (m_Ticks.Info[m_ReplayCount].time * 1000) + m_Ticks.Info[m_ReplayCount].time_msc;
                                CustomTicksAdd(def_SymbolReplay, tick);
                        }

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

Теперь возникает вопрос, который может немного беспокоить:

Возможно ли, что в активе с низкой ликвидностью, где сделки могут происходить в течение нескольких секунд, может произойти «заморозка сервиса»? Может ли случиться так, что если мы решим остановить его, пока он останавливается, на самом деле он не остановится полностью? Может ли это произойти из-за того, что таймер находится в режиме ожидания несколько секунд?

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

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

Настоящая проблема заключается в том, что при достижении функции Sleep, сервис на некоторое время «приостанавливается», и это факт, но ни в коем случае сервис не может быть остановлен. Фактически его можно остановить запросом STOP, когда вызов меняет состояние флага Stop. Откуда я это знаю? Просто потому что я видел это в документации по функции Sleep. Ниже приведен фрагмент, из которого это становится ясно.

Примечание

Функцию Sleep() нельзя вызывать из пользовательских индикаторов, так как индикаторы выполняются в интерфейсном потоке и не должны его тормозить. Функция встроена для проверки состояния флага остановки эксперта каждую 0.1 секунды.

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


Внесение исправлений в систему быстрой навигации.

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

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

#property service
#property icon "\\Images\\Market Replay\\Icon.ico"
#property copyright "Daniel Jose"
#property version   "1.18"
#property description "Сервис репликации-моделирования для платформы MT5."
#property description "Он независим от индикатора Market Replay."
#property description "Можно узнать по подробнее в данной статье."
#property link "https://www.mql5.com/ru/articles/11113"
//+------------------------------------------------------------------+
#define def_Dependence  "\\Indicators\\Market Replay.ex5"
#resource def_Dependence
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
input string            user00 = "Mini Dolar.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;
}
//+------------------------------------------------------------------+

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

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

inline void CreateBarInReplay(const bool bViewMetrics, const bool bViewTicks)
                        {
#define def_Rate m_MountBar.Rate[0]

                                bool bNew;
                                MqlTick tick[1];

                                if (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))
                                {                               
                                        if (bViewMetrics) Metrics();
                                        m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                        def_Rate.real_volume = 0;
                                        def_Rate.tick_volume = 0;
                                }
                                bNew = (def_Rate.tick_volume == 0);
                                def_Rate.close = (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close);
                                def_Rate.open = (bNew ? def_Rate.close : def_Rate.open);
                                def_Rate.high = (bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
                                def_Rate.low = (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;
                                CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
                                tick = m_Ticks.Info[m_ReplayCount];
                                if (bViewTicks) CustomTicksAdd(def_SymbolReplay, tick);
                                m_ReplayCount++;
                                
#undef def_Rate
                        }

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

Тогда вы могли бы подумать: значит, мы не можем действительно изменить содержимое окна Обзора рынка? Ответ ДА, можем, но не в любом случае. Всё, что мы действительно можем и будем делать, это удалять старые тики. Но это получится не без труда, по крайней мере, до тех пор, пока разработчики платформы MetaTrader 5 не исправят проблему, связанную с использованием пользовательских активов в окне Обзора рынка. Это связано с тем, что тики, установленные на пользовательском ресурсе, не исчезают из окна, где мы можем видеть пользовательские тики. Как ни странно, они остаются там, что затрудняет понимание того, когда мы возвращаемся на какую-нибудь прежнюю позицию.

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

                void AdjustPositionToReplay(const bool bViewBuider)
                        {
                                u_Interprocess Info;
                                MqlRates       Rate[def_BarsDiary];
                                int            iPos, nCount;
                                
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                if (m_ReplayCount == 0)
                                        for (; m_Ticks.Info[m_ReplayCount].volume_real == 0; m_ReplayCount++);
                                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);
                                        CustomTicksDelete(def_SymbolReplay, m_Ticks.Info[iPos].time_msc, LONG_MAX);
                                        if ((m_dtPrevLoading == 0) && (iPos == 0)) FirstBarNULL(); 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(false, false);
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                Info.s_Infos.isWait = false;
                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                        }

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

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


Использование смоделированных данных в Обзоре рынка

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

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

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

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

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

Первое, что нужно сделать, это добавить в систему новую переменную.

struct st00
        {
                MqlTick  Info[];
                MqlRates Rate[];
                int      nTicks,
                         nRate;
                bool     bTickReal;
        }m_Ticks;

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

Как только у нас появится новая переменная, нам необходимо будет правильно ее инициализировать. Есть два места, где он действительно будет инициализирован. Первый — когда указываем, что мы работаем с тиками, которые смоделированы.

                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("Convertendo barras em ticks. Aguarde...");
                                while ((*pFileBars).ReadBar(rate) && (!_StopFlag)) Simulation(rate[0], local);
                                ArrayFree(local);
                                delete pFileBars;
                                m_Ticks.bTickReal = false;
                                
                                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(ToReplay)) 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);
                                }
                                m_Ticks.bTickReal = true;
                                                                        
                                return dtRet;
                        };

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

Чтобы исправить этот момент, мы внесем следующие изменения:

inline void Simulation(const MqlRates &rate, MqlTick &tick[])
                        {
#define macroRandomLimits(A, B) (int)(MathMin(A, B) + (((rand() & 32767) / 32767.0) * MathAbs(B - A)))

                                long     il0, max, i0, i1;
                                bool     b1 = ((rand() & 1) == 1);
                                double   v0, v1;
                                MqlRates rLocal;
                                
                                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                max = rate.tick_volume - 1;     
                                v0 = 4.0;
                                v1 = (60000 - v0) / (max + 1.0);
                                for (int c0 = 0; c0 <= max; c0++, v0 += v1)
                                {
                                        tick[c0].last = 0;
                                        tick[c0].flags = 0;
                                        il0 = (long)v0;
                                        tick[c0].time = rate.time + (datetime) (il0 / 1000);
                                        tick[c0].time_msc = (tick[c0].time * 1000) + (il0 % 1000);
                                        tick[c0].time_msc = il0 % 1000;
                                        tick[c0].volume_real = 1.0;
                                }
                                tick[0].last = rate.open;
                                tick[max].last = rate.close;
                                for (int c0 = (int)(rate.real_volume - rate.tick_volume); c0 > 0; c0--)
                                        tick[macroRandomLimits(0, max)].volume_real += 1.0;                                     
                                i0 = (long)(MathMin(max / 3.0, max * 0.2));
                                i1 = max - i0;
                                rLocal = rate;  
                                rLocal.open = rate.open;
                                rLocal.close = (b1 ? rate.high : rate.low);
                                i0 = RandomWalk(1, i0, rLocal, tick, 0);
                                rLocal.open = tick[i0].last;
                                rLocal.close = (b1 ? rate.low : rate.high);
                                RandomWalk(i0, i1, rLocal, tick, 1);
                                rLocal.open = tick[i1].last;
                                rLocal.close = rate.close;
                                RandomWalk(i1, max, rLocal, tick, 2);
                                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];
                                }
#undef macroRandomLimits
                        }

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

В классе C_Replay мы сосредоточимся на внесении изменений в одну функцию, как показано в следующем коде:

inline void CreateBarInReplay(const bool bViewMetrics, const bool bViewTicks)
                        {
#define def_Rate m_MountBar.Rate[0]

                                bool bNew;
                                MqlTick tick[1];

                                if (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))
                                {                               
                                        if (bViewMetrics) Metrics();
                                        m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                        def_Rate.real_volume = 0;
                                        def_Rate.tick_volume = 0;
                                }
                                bNew = (def_Rate.tick_volume == 0);
                                def_Rate.close = (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close);
                                def_Rate.open = (bNew ? def_Rate.close : def_Rate.open);
                                def_Rate.high = (bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
                                def_Rate.low = (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;
                                CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                tick[0].bid = tick[0].last - m_PointsPerTick;
                                                tick[0].ask = tick[0].last + m_PointsPerTick;
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }
                                m_ReplayCount++;
                                
#undef def_Rate
                        }

Здесь у нас довольно простое изменение. На самом деле это предназначено только для создания и отображения значений BID и ASK. Но имейте в виду, что это создание основано на значении последней торговой цены и будет выполняться только тогда, когда мы имеем дело со смоделированными значениями. Запустив этот код, мы получим внутреннее представление графика, созданного системой СЛУЧАЙНОГО БЛУЖДАНИЯ, точно так же, как это делалось, когда мы использовали для этого EXCEL. Здесь стоит упомянуть одну деталь: когда я объяснял в статье "Разработка системы репликации — моделирование рынка (Часть 15): Появление ТЕСТЕРА (V) – СЛУЧАЙНОЕ БЛУЖДАНИЕ", что были другие способы сделать ту же самую визуализацию, я имел в виду эту модель. Но тогда было не уместно говорить, как это сделать, а сейчас уже другой случай.

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

И чтобы не повторять весь приведенный выше код, мы сосредоточимся на выделенном сегменте и изменим его, как показано ниже:

                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                static double BID, ASK;
                                                
                                                if (tick[0].last > ASK)
                                                {
                                                        ASK = tick[0].ask = tick[0].last;
                                                        BID = tick[0].bid = tick[0].last - m_PointsPerTick;
                                                }
                                                if (tick[0].last < BID)
                                                {
                                                        ASK = tick[0].ask = tick[0].last + m_PointsPerTick;
                                                        BID = tick[0].bid = tick[0].last;
                                                }
                                                tick[0].ask = tick[0].last + m_PointsPerTick;
                                                tick[0].bid = tick[0].last - m_PointsPerTick;
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }

Мы вычеркнули выделенную часть исходного кода и добавили еще несколько моментов. Следует отметить, что значения BID и ASK статичны, и это необходимо для того, чтобы мы могли построить небольшой индикатор. Что-то совсем простое, но достаточное, чтобы вызвать революцию. А так как при запуске системы весьма вероятно, что данные значения будут равны нулю (linkeditor всегда создаёт способ, чтобы эти значения начинались с нуля), то изначально у нас будет выполнение вызова, где акцент торгов сначала окажется на ASK, и будет создан довольно узкий канал всего в 1 тик. Таким образом, пока последняя торгуемая цена не уйдет из этого канала, она останется там.

Как я уже говорил, это что-то довольно простое, но функциональное. Теперь подумаем о следующем: Не следует допускать, чтобы значение BID сталкивалось со значением ASK (это на ФОНДОВОМ РЫНКЕ, на рынке FOREX другая история, но это будет видно в другом моменте). Что действительно вызывает данное столкновение, так это стоимость последней заключенной сделки. А что, если мы внесем небольшое изменение в показанный выше фрагмент? Нечто очень тонкое. Что произойдет, если значение BID или ASK изменится без фактического изменения цены последней сделки?

Чтобы убедиться в этом, мы еще раз изменим код, чтобы фрагмент выглядел так:

                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                static double BID, ASK;
                                                
                                                if (tick[0].last > ASK)
                                                {
                                                        ASK = tick[0].ask = tick[0].last;
                                                        BID = tick[0].bid = tick[0].last - (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                }
                                                if (tick[0].last < BID)
                                                {
                                                        ASK = tick[0].ask = tick[0].last + (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                        BID = tick[0].bid = tick[0].last;
                                                }
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }

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

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

                                if (bViewTicks)
                                {
                                        tick = m_Ticks.Info[m_ReplayCount];
                                        if (!m_Ticks.bTickReal)
                                        {
                                                static double BID, ASK;
                                                double  dSpread;
                                                int     iRand = rand();
                                                
                                                dSpread = m_PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_PointsPerTick : 0 ) : 0 );
                                                if (tick[0].last > ASK)
                                                {
                                                        ASK = tick[0].ask = tick[0].last;
                                                        BID = tick[0].bid = tick[0].last - dSpread;
                                                        BID = tick[0].bid = tick[0].last - (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                }
                                                if (tick[0].last < BID)
                                                {
                                                        ASK = tick[0].ask = tick[0].last + (m_PointsPerTick * ((rand() & 1) == 1 ? 2 : 1));
                                                        ASK = tick[0].ask = tick[0].last + dSpread;
                                                        BID = tick[0].bid = tick[0].last;
                                                }
                                        }
                                        CustomTicksAdd(def_SymbolReplay, tick); 
                                }

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


Заключение

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

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


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

Прикрепленные файлы |
Market_Replay_rvt_18.zip (12899.62 KB)
Стоп-лосс и тейк-профит, дружелюбные к трейдеру Стоп-лосс и тейк-профит, дружелюбные к трейдеру
Стоп-лосс и тейк-профит могут оказать значительное влияние на результаты трейдинга. В этой статье мы рассмотрим несколько способов поиска оптимальных значений стоп-приказов.
Нейросети — это просто (Часть 63): Предварительное обучение Трансформера решений без учителя (PDT) Нейросети — это просто (Часть 63): Предварительное обучение Трансформера решений без учителя (PDT)
Продолжаем рассмотрение семейства методов Трансформера решений. Из предыдущих работ мы уже заметили, что обучение трансформера, лежащего в основе архитектуры данных методов, довольно сложная задача и требует большого количества размеченных обучающих данных. В данной статье мы рассмотрим алгоритм использования не размеченных траекторий для предварительного обучения моделей.
Нейросети — это просто (Часть 64): Метод Консервативного Весового Поведенческого Клонирования (CWBC) Нейросети — это просто (Часть 64): Метод Консервативного Весового Поведенческого Клонирования (CWBC)
В результате тестов, проведенных в предыдущих статьях, мы пришли к выводу, что оптимальность обученной стратегии во многом зависит от используемой обучаемой выборки. В данной статье я предлагаю вам познакомиться с довольно простым и эффективном методе выбора траекторий для обучения моделей.
Регрессионные модели библиотеки Scikit-learn и их экспорт в ONNX Регрессионные модели библиотеки Scikit-learn и их экспорт в ONNX
В данной статье мы рассмотрим применение регрессионных моделей пакета Scikit-learn, попробуем их сконвертировать в ONNX-формат и использовать полученные модели в программах на MQL5. Также мы сравним точность работы оригинальных моделей и их ONNX-версий для float и double. Кроме того, мы рассмотрим ONNX-представление регресионных моделей, это позволит лучше понять их внутреннее устройство и принцип работы.