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

Разработка системы репликации - Моделирование рынка (Часть 02): Первые эксперименты (II)

MetaTrader 5Тестер | 21 июня 2023, 09:23
654 0
Daniel Jose
Daniel Jose

Введение

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

Прежде чем рассматривать что-либо, связанное с использованием времени в диапазоне нескольких миллисекунд, важно напомнить всем, что это не совсем простая задача. Дело в том, что таймер, предоставляемый самой операционной системой, не может достичь этих значений. Поэтому перед нами большая, если не сказать, ГИГАНТСКАЯ, проблема. В данной статье я постараюсь ответить на этот вопрос и покажу, как можно попытаться решить его, выйдя за пределы временного ограничения, установленного операционной системой. Я знаю: многие думают, что современный процессор способен выполнять миллиарды вычислений в секунду. Однако одно дело, когда процессор выполняет вычисления, и совершенно другое - справятся ли все процессы внутри нашего компьютера с нужными нам задачами. Важно отметить, что здесь предлагается использовать для этого исключительно MQL5, без применения какого-либо внешнего кода или DLL, только чистый MQL5.


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

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

Поэтому мы будем использовать как можно более обширный подход, и лучший способ, который я нашел, - это применение модели, похожей на модель клиент-сервер. Я обсуждал эту технику в другой статье под названием "Разработка торгового советника с нуля (Часть 16): Доступ к данным в Интернете (II)". В ней мы представили три способа передачи информации в MetaTrader 5. Здесь мы будем использовать один из таких способов, а именно СЕРВИС. Да, репликация рынка станет сервисом MetaTrader 5.

Пожалуй, вы подумаете, что я собираюсь создать всё с нуля, но я сам спрашиваю себя: «Зачем я собирался это делать?!» По сути, система уже работает, однако она не достигает желаемого временного интервала в 1 минуту. И в вашем взгляде будет читаться вопрос: «Думаешь, что переход на какой-то сервис решит эту проблему?!» На самом деле замена системы на сервис не решит нашу проблему. Однако если мы с самого начала изолируем создание 1-минутных баров от остальной системы советника, то у нас будет меньше работы в дальнейшем, потому что сам советник вызовет небольшую задержку в выполнении построения бара (причины такого объясним позже).

Теперь понятно, почему мы пользуемся сервисом? Он практичнее других методов, рассмотренных выше. Мы сможем управлять им так, как объясняли в статье об обмене сообщениями между экспертом и сервисом, под названием "Разработка торгового советника с нуля (Часть 17) Доступ к данным в Интернете (III)". Однако здесь мы не станем сосредотачиваться на генерации данного элемента управления, мы только хотим, чтобы сервис сгенерировал бары, которые будут размещены на графике. Но чтобы сделать эти моменты интересными, мы должны использовать платформу более творчески, не ограничиваясь только использованием советника и сервиса.

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

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

Поэтому не воспринимайте буквально всё, что вы здесь видите. Следуйте за этой серией статей, поскольку то, что мы будем здесь делать, далеко не так просто и легко выполнить.


Реализация

Мы начнем с создания основ нашей системы, которые включают в себя:

  1. Сервис для создания 1-минутных баров;
  2. Скрипт для запуска сервиса;
  3. Советник для проведения моделирования (этот вопрос будет рассмотрен ниже);


Определение сервиса репликации рынка

Для корректной работы с сервисом нам необходимо обновить наш класс C_Replay. Однако изменения настолько незначительны, что не стоит вдаваться в подробности, но в основном всё заключается в коде возврата. Однако есть один момент, который стоит отдельно отметить, так как он осуществляет кое-что дополнительное. Код показан ниже.

#define macroGetMin(A)  (int)((A - (A - ((A % 3600) - (A % 60)))) / 60)
                int Event_OnTime(void)
                        {
                                bool isNew;
                                int mili;
                                static datetime _dt = 0;
                                
                                if (m_ReplayCount >= m_ArrayCount) return -1;
                                if (m_dt == 0)
                                {
                                        m_Rate[0].close = m_Rate[0].open =  m_Rate[0].high = m_Rate[0].low = m_ArrayInfoTicks[m_ReplayCount].Last;
                                        m_Rate[0].tick_volume = 0;
                                        m_Rate[0].time = m_ArrayInfoTicks[m_ReplayCount].dt - 60;
                                        CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
                                        _dt = TimeLocal();
                                }
                                isNew = m_dt != m_ArrayInfoTicks[m_ReplayCount].dt;
                                m_dt = (isNew ? m_ArrayInfoTicks[m_ReplayCount].dt : m_dt);
                                mili = m_ArrayInfoTicks[m_ReplayCount].milisec;
                                while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec)
                                {
                                        m_Rate[0].close = m_ArrayInfoTicks[m_ReplayCount].Last;
                                        m_Rate[0].open = (isNew ? m_Rate[0].close : m_Rate[0].open);
                                        m_Rate[0].high = (isNew || (m_Rate[0].close > m_Rate[0].high) ? m_Rate[0].close : m_Rate[0].high);
                                        m_Rate[0].low = (isNew || (m_Rate[0].close < m_Rate[0].low) ? m_Rate[0].close : m_Rate[0].low);
                                        m_Rate[0].tick_volume = (isNew ? m_ArrayInfoTicks[m_ReplayCount].Vol : m_Rate[0].tick_volume + m_ArrayInfoTicks[m_ReplayCount].Vol);
                                        isNew = false;
                                        m_ReplayCount++;
                                }
                                m_Rate[0].time = m_dt;
                                CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
                                mili = (m_ArrayInfoTicks[m_ReplayCount].milisec < mili ? m_ArrayInfoTicks[m_ReplayCount].milisec + (1000 - mili) : m_ArrayInfoTicks[m_ReplayCount].milisec - mili);
                                if ((macroGetMin(m_dt) == 1) && (_dt > 0))
                                {
                                        Print("Tempo decorrido : ", TimeToString(TimeLocal() - _dt, TIME_SECONDS));
                                        _dt = 0;
                                }                               
                                return (mili < 0 ? 0 : mili);
                        };
#undef macroGetMin

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

С учетом этих изменений давайте рассмотрим код сервиса, который приведен ниже в полном объеме:

#property service
#property copyright "Daniel Jose"
#property version   "1.00"
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
input string    user01 = "WINZ21_202110220900_202110221759"; //Arquivo com ticks
//+------------------------------------------------------------------+
C_Replay Replay;
//+------------------------------------------------------------------+
void OnStart()
{
        ulong t1;
        int delay = 3;
        
        if (!Replay.CreateSymbolReplay(user01)) return;
        Print("Aguardando permissão para iniciar replay ...");
        GlobalVariableTemp(def_GlobalVariable01);
        while (!GlobalVariableCheck(def_SymbolReplay)) Sleep(750);
        Print("Serviço de replay iniciado ...");
        t1 = GetTickCount64();
        while (GlobalVariableCheck(def_SymbolReplay))
        {
                if ((GetTickCount64() - t1) >= (uint)(delay))
                {
                        if ((delay = Replay.Event_OnTime()) < 0) break;
                        t1 = GetTickCount64();
                }
        }
        GlobalVariableDel(def_GlobalVariable01);
        Print("Serviço de replay finalizado ...");
}
//+------------------------------------------------------------------+

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

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

Следующим шагом является скрипт, который полностью показан ниже:

#property copyright "Daniel Jose"
#property version   "1.00"
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
C_Replay Replay;
//+------------------------------------------------------------------+
void OnStart()
{
        Print("Aguardando Serviço de Replay ...");
        while((!GlobalVariableCheck(def_GlobalVariable01)) && (!IsStopped())) Sleep(500);
        if (IsStopped()) return;
        Replay.ViewReplay();
        GlobalVariableTemp(def_SymbolReplay);
        while ((!IsStopped()) && (GlobalVariableCheck(def_GlobalVariable01))) Sleep(500);
        GlobalVariableDel(def_SymbolReplay);
        Print("Script de Replay finalizado...");
        Replay.CloseReplay();
}
//+------------------------------------------------------------------+

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

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

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

Как можно заметить, результат очень близок к идеальному. Лишние 9 секунд можно легко устранить с помощью настроек системы. В идеале время должно быть меньше 1 минуты, что облегчит настройку, поскольку нам надо только добавить задержку в систему. Легче добавить задержку, чем уменьшить ее. Но если вы считаете, что системное время нельзя сокращать, давайте подробнее разберемся с этим.

Если внимательно посмотреть - есть фрагмент, который продуцирует задержку в системе, а именно в сервисе. Данный фрагмент, который фактически порождает эту задержку, выделен в следующем отрывке. Но что произойдет, если мы превратим эту строку в комментарий? Что произойдет с системой?

        t1 = GetTickCount64();
        while (GlobalVariableCheck(def_SymbolReplay))
        {
// ...  COMENTARIO ...  if ((GetTickCount64() - t1) >= (uint)(delay))
                {
                        if ((delay = Replay.Event_OnTime()) < 0) break;
                        t1 = GetTickCount64();
                }
        }
        GlobalVariableDel(def_GlobalVariable01);

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



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


Максимальная скорость. Это действительно так?

Здесь мы сделаем последнюю попытку заставить систему работать менее 1 минуты.

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

Изменения показаны ниже:

#define macroGetMin(A)  (int)((A - (A - ((A % 3600) - (A % 60)))) / 60)
inline int Event_OnTime(void)
                        {
                                bool isNew;
                                int mili;
                                static datetime _dt = 0;
                                
                                if (m_ReplayCount >= m_ArrayCount) return -1;
                                if (m_dt == 0)
                                {
                                        m_Rate[0].close = m_Rate[0].open =  m_Rate[0].high = m_Rate[0].low = m_ArrayInfoTicks[m_ReplayCount].Last;
                                        m_Rate[0].tick_volume = 0;
                                        m_Rate[0].time = m_ArrayInfoTicks[m_ReplayCount].dt - 60;
                                        CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
                                        _dt = TimeLocal();
                                }
                                isNew = m_dt != m_ArrayInfoTicks[m_ReplayCount].dt;
                                m_dt = (isNew ? m_ArrayInfoTicks[m_ReplayCount].dt : m_dt);
                                mili = m_ArrayInfoTicks[m_ReplayCount].milisec;
                                do
                                {
                                        while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec)
                                        {
                                                m_Rate[0].close = m_ArrayInfoTicks[m_ReplayCount].Last;
                                                m_Rate[0].open = (isNew ? m_Rate[0].close : m_Rate[0].open);
                                                m_Rate[0].high = (isNew || (m_Rate[0].close > m_Rate[0].high) ? m_Rate[0].close : m_Rate[0].high);
                                                m_Rate[0].low = (isNew || (m_Rate[0].close < m_Rate[0].low) ? m_Rate[0].close : m_Rate[0].low);
                                                m_Rate[0].tick_volume = (isNew ? m_ArrayInfoTicks[m_ReplayCount].Vol : m_Rate[0].tick_volume + m_ArrayInfoTicks[m_ReplayCount].Vol);
                                                isNew = false;
                                                m_ReplayCount++;
                                        }
                                        mili++;
                                }while (mili == m_ArrayInfoTicks[m_ReplayCount].milisec);
                                m_Rate[0].time = m_dt;
                                CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
                                mili = (m_ArrayInfoTicks[m_ReplayCount].milisec < mili ? m_ArrayInfoTicks[m_ReplayCount].milisec + (1000 - mili) : m_ArrayInfoTicks[m_ReplayCount].milisec - mili);
                                if ((macroGetMin(m_dt) == 1) && (_dt > 0))
                                {
                                        Print("Tempo decorrido : ", TimeToString(TimeLocal() - _dt, TIME_SECONDS));
                                        _dt = 0;
                                }                               
                                return (mili < 0 ? 0 : mili);
                        };
#undef macroGetMin

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

Мы внесли всего лишь одно изменение. Вы увидите результат на видео ниже.



Для тех, кто хочет что-то еще более быстрое, посмотрите на результат:

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


Заключение

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

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

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

Прикрепленные файлы |
Replay.zip (10746.69 KB)
Реализация алгоритма обучения ARIMA на MQL5 Реализация алгоритма обучения ARIMA на MQL5
В этой статье мы реализуем алгоритм, который применяет интегрированную модель авторегрессии скользящей средней (модель Бокса-Дженкинса) с использованием метода минимизации функции Пауэллса. Бокс и Дженкинс утверждали, что большинство временных рядов можно смоделировать с помощью одной или обеих из двух структур.
Нейросети — это просто (Часть 46): Обучение с подкреплением, направленное на достижение целей (GCRL) Нейросети — это просто (Часть 46): Обучение с подкреплением, направленное на достижение целей (GCRL)
Предлагаю Вам познакомиться с ещё одним направлением в области обучения с подкреплением. Оно называется обучением с подкреплением, направленное на достижение целей (Goal-conditioned reinforcement learning, GCRL). В этом подходе агент обучается достигать различных целей в определенных сценариях.
Возможности Мастера MQL5, которые вам нужно знать (Часть 6): Преобразование Фурье Возможности Мастера MQL5, которые вам нужно знать (Часть 6): Преобразование Фурье
Преобразование Фурье, введенное Жозефом Фурье, является средством разложения сложных волновых точек данных на простые составляющие волны. Эта особенность может быть полезной для трейдеров, и именно ее мы и рассмотрим в этой статье.
Разработка системы репликации — моделирование рынка (Часть 01): Первые эксперименты (I) Разработка системы репликации — моделирование рынка (Часть 01): Первые эксперименты (I)
Что вы думаете: создавать системы для изучения рынка, когда он закрыт, или создать систему, которая позволит моделировать рыночные ситуации? Здесь мы начнем новую серию статей, посвященных этому вопросу.