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

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

MetaTrader 5Примеры | 13 июля 2023, 16:08
473 0
Daniel Jose
Daniel Jose

Введение

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

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

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


Обмен советника (EA) на индикатор

Это изменение довольно просто реализовать. После этого мы сможем использовать свой собственный советник для проведения исследований в сервисе репликации рынка или для торговли на реальном рынке. Например, мы сможем использовать советник, который я показывал в предыдущих статьях. Подробнее читайте в серии "Разработка торгового советника с нуля". Несмотря на то, что он не предназначен для стопроцентной автоматизации, его можно адаптировать для использования в сервисе репликации. Но оставим это на будущее. Кроме того, мы также сможем использовать некоторые советники из серии "Как построить советник, работающий автоматически (Часть 01): Концепции и структуры", где мы просмотрели, как создать советник, работающий полностью автоматически.

Однако в настоящее время наше основное внимание уделяется не использованию советника (мы будем изучать это в будущем), а другому делу.

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

#property copyright "Daniel Jose"
#property indicator_chart_window
#property indicator_plots 0
//+------------------------------------------------------------------+
#include <Market Replay\C_Controls.mqh>
//+------------------------------------------------------------------+
C_Controls      Control;
//+------------------------------------------------------------------+
int OnInit()
{
        IndicatorSetString(INDICATOR_SHORTNAME, "Market Replay");
        Control.Init();
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        return rates_total;
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        Control.DispatchMessage(id, lparam, dparam, sparam);
}
//+------------------------------------------------------------------+

Единственная разница — это добавление короткого имени, которое желательно должно быть включено в индикаторы. Данная часть выделена в коде выше. Сделав это, мы получаем дополнительное преимущество: мы сможем использовать любого советника для практики и обучения на сервисе репликации. Прежде чем кто-то задаст возможный вопрос, я дам на него ответ: репликация рынка НЕ ЯВЛЯЕТСЯ тестером стратегий. Она предназначена для людей, которые хотят попрактиковаться в «чтении» рынка и, таким образом, достичь стабильности, улучшая свое восприятие движения активов. Репликация рынка не заменяет отличный тестер стратегий в MetaTrader 5. Однако тестер стратегий не подходит для отработки репликации рынка.

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

int OnInit()
{
        u_Interprocess Info;
        
        IndicatorSetString(INDICATOR_SHORTNAME, "Market Replay");
        if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.Value = 0;
        Control.Init(Info.s_Infos.isPlay);
        
        return INIT_SUCCEEDED;
}

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

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


Реализация управления позицией

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

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

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

Хорошо, теперь приступаем к работе!

Первое, что мы сделаем - это определить некоторые настройки.

#define def_ButtonLeft  "Images\\Market Replay\\Left.bmp"
#define def_ButtonRight "Images\\Market Replay\\Right.bmp"
#define def_ButtonPin   "Images\\Market Replay\\Pin.bmp"

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

struct st_00
{
        string  szBtnLeft,
                szBtnRight,
                szBtnPin,
                szBarSlider;
        int     posPinSlider,
                posY;
}m_Slider;

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

inline void CreateObjectBitMap(int x, int y, string szName, string Resource1, string Resource2 = NULL)
                        {
                                ObjectCreate(m_id, szName, OBJ_BITMAP_LABEL, 0, 0, 0);
                                ObjectSetInteger(m_id, szName, OBJPROP_XDISTANCE, x);
                                ObjectSetInteger(m_id, szName, OBJPROP_YDISTANCE, y);
                                ObjectSetString(m_id, szName, OBJPROP_BMPFILE, 0, "::" + Resource1);
                                ObjectSetString(m_id, szName, OBJPROP_BMPFILE, 1, "::" + (Resource2 == NULL ? Resource1 : Resource2));
                        }

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

inline void CreteBarSlider(int x, int size)
                        {
                                ObjectCreate(m_id, m_Slider.szBarSlider, OBJ_RECTANGLE_LABEL, 0, 0, 0);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_XDISTANCE, x);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_YDISTANCE, m_Slider.posY - 4);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_XSIZE, size);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_YSIZE, 9);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BGCOLOR, clrLightSkyBlue);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BORDER_COLOR, clrBlack);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_WIDTH, 3);
                                ObjectSetInteger(m_id, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT);
                        }

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

Функция, которая создает кнопки воспроизведения/паузы, теперь выглядит таким образом:

void CreateBtnPlayPause(long id, bool state)
{
        m_szBtnPlay = def_PrefixObjectName + "Play";
        CreateObjectBitMap(5, 25, m_szBtnPlay, def_ButtonPause, def_ButtonPlay);
        ObjectSetInteger(id, m_szBtnPlay, OBJPROP_STATE, state);
}

Намного проще, не правда ли? Теперь давайте посмотрим на функцию, которая создаст ползунки. Это можно увидеть ниже:

void CreteCtrlSlider(void)
{
        u_Interprocess Info;
                                
        m_Slider.szBarSlider = def_PrefixObjectName + "Slider Bar";
        m_Slider.szBtnLeft   = def_PrefixObjectName + "Slider BtnL";
        m_Slider.szBtnRight  = def_PrefixObjectName + "Slider BtnR";
        m_Slider.szBtnPin    = def_PrefixObjectName + "Slider BtnP";
        m_Slider.posY = 40;
        CreteBarSlider(82, 436);
        CreateObjectBitMap(52, 25, m_Slider.szBtnLeft, def_ButtonLeft);
        CreateObjectBitMap(516, 25, m_Slider.szBtnRight, def_ButtonRight);
        CreateObjectBitMap(def_MinPosXPin, m_Slider.posY, m_Slider.szBtnPin, def_ButtonPin);
        ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_ANCHOR, ANCHOR_CENTER);
        if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.Value = 0;
        PositionPinSlider(Info.s_Infos.iPosShift);
}

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

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

inline void PositionPinSlider(int p)
{
        m_Slider.posPinSlider = (p < 0 ? 0 : (p > def_MaxPosSlider ? def_MaxPosSlider : p));
        ObjectSetInteger(m_id, m_Slider.szBtnPin, OBJPROP_XDISTANCE, m_Slider.posPinSlider + def_MinPosXPin);
        ChartRedraw();
}

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

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

void Init(const bool state = false)
{
        if (m_szBtnPlay != NULL) return;
        m_id = ChartID();
        ChartSetInteger(m_id, CHART_EVENT_MOUSE_MOVE, true);
        CreateBtnPlayPause(m_id, state);
        GlobalVariableTemp(def_GlobalVariableReplay);
        if (!state) CreteCtrlSlider();
        ChartRedraw();
}

Теперь мы также добавляем код для пересылки в индикатор событий движения мыши. Без этого дополнения события мыши будут утеряны и не передадутся MetaTrader 5 в индикатор. Чтобы ползунок не был виден, когда он не нужен, мы добавили небольшую проверку. Если эта проверка подтвердит необходимость отображения ползунка, он будет отображен на экране.

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

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

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        u_Interprocess Info;

//... другие локальные переменные ....
                                
        switch (id)
        {
                case CHARTEVENT_OBJECT_CLICK:
                        if (sparam == m_szBtnPlay)
                        {
                                Info.s_Infos.isPlay = (bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE);
                                if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else
                                {
                                        ObjectsDeleteAll(m_id, def_PrefixObjectName + "Slider");
                                        m_Slider.szBtnPin = NULL;
                                }
                                Info.s_Infos.iPosShift = m_Slider.posPinSlider;
                                GlobalVariableSet(def_GlobalVariableReplay, Info.Value);
                                ChartRedraw();
                        }else   if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - 1);
                        else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + 1);                                                   
                break;

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

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

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

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

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        u_Interprocess Info;
        static int six = -1, sps;
        int x, y, px1, px2;
                                
        switch (id)
        {

// ... СОБЫТИЕ нажатия по объекту ...

                case CHARTEVENT_MOUSE_MOVE:
                        x = (int)lparam;
                        y = (int)dparam;
                        px1 = m_Slider.posPinSlider + def_MinPosXPin - 14;
                        px2 = m_Slider.posPinSlider + def_MinPosXPin + 14;
                        if ((((uint)sparam & 0x01) == 1) && (m_Slider.szBtnPin != NULL))
                        {
                                if ((y >= (m_Slider.posY - 14)) && (y <= (m_Slider.posY + 14)) && (x >= px1) && (x <= px2) && (six == -1))
                                {
                                        six = x;
                                        sps = m_Slider.posPinSlider;
                                        ChartSetInteger(m_id, CHART_MOUSE_SCROLL, false);
                                }
                                if (six > 0) PositionPinSlider(sps + x - six);
                        }else if (six > 0)
                        {
                                six = -1;
                                ChartSetInteger(m_id, CHART_MOUSE_SCROLL, true);
                        }
                        break;
        }
}

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

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

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

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

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


Настройка класса C_Replay

Вещи никогда не бывают такими, как некоторые себе представляют. То, что мы создали ползунок и настроили что-то в классе управления (C_Control), не означает, что всё работает идеально. Нам нужно внести небольшие поправки в класс, отвечающий за настройку репликации.

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

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

#define macroGetMin(A)  (int)((A - (A - ((A % 3600) - (A % 60)))) / 60)
inline int Event_OnTime(void)
{
        bool isNew;
        int mili, test;
        static datetime _dt = 0;
        u_Interprocess Info;
                                
        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);
        test = (int)((m_ReplayCount * def_MaxPosSlider) / m_ArrayCount);
        GlobalVariableGet(def_GlobalVariableReplay, Info.Value);
        if (Info.s_Infos.iPosShift != test)
        {
                Info.s_Infos.iPosShift = test;
                GlobalVariableSet(def_GlobalVariableReplay, Info.Value);
        }
                        
        return (mili < 0 ? 0 : mili);
};
#undef macroGetMin

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

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

На этом мы завершаем первый этап. Однако нам предстоит решить ещё одну проблему. Как расположить систему репликации в нужном относительном положении после корректировки значения во время паузы?

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

int AdjustPositionReplay()
{
        u_Interprocess Info;
        int test = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_ArrayCount);
                                
        Info.Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == test) return 0;
        test = (int)(m_ArrayCount * ((Info.s_Infos.iPosShift * 1.0) / def_MaxPosSlider));
        if (test < m_ReplayCount)
        {
                CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
                CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
                m_ReplayCount = 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);
        };
        for (test = (test > 0 ? test - 1 : 0); m_ReplayCount < test; m_ReplayCount++)
                Event_OnTime();

        return Event_OnTime();
}

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

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

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

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

#property service
#property copyright "Daniel Jose"
#property version   "1.00"
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
input string    user01 = "WINZ21_202110220900_202110221759"; //Файл с тиками
//+------------------------------------------------------------------+
C_Replay        Replay;
//+------------------------------------------------------------------+
void OnStart()
{
        ulong t1;
        int delay = 3;
        long id;
        u_Interprocess Info;
        bool bTest = false;
        
        if (!Replay.CreateSymbolReplay(user01)) return;
        id = Replay.ViewReplay();
        Print("Ожидаем разрешения для начала репликации ...");
        while (!GlobalVariableCheck(def_GlobalVariableReplay)) Sleep(750);
        Print("Сервис репликации запущен ...");
        t1 = GetTickCount64();
        while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.Value)))
        {
                if (!Info.s_Infos.isPlay)
                {
                        if (!bTest) bTest = (Replay.Event_OnTime() > 0);
                }else
                {
                        if (bTest)
                        {
                                delay = ((delay = Replay.AdjustPositionReplay()) == 0 ? 3 : delay);
                                bTest = false;
                                t1 = GetTickCount64();
                        }else if ((GetTickCount64() - t1) >= (uint)(delay))
                        {
                                if ((delay = Replay.Event_OnTime()) < 0) break;
                                t1 = GetTickCount64();
                        }
                }
        }
        Replay.CloseReplay();
        Print("Сервис репликации завершен ...");
}
//+------------------------------------------------------------------+

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

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


Заключение

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

Данная ситуация будет исправлена ​​в будущем. А пока мы можем двигаться дальше, так как нам ещё многое предстоит выяснить.



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

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


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

Прикрепленные файлы |
Market_Replay.zip (10795.89 KB)
Разработка системы репликации - Моделирование рынка (Часть 05): Предварительный просмотр Разработка системы репликации - Моделирование рынка (Часть 05): Предварительный просмотр
Нам удалось разработать способ осуществления репликации рынка достаточно реалистичным и доступным образом. Теперь давайте продолжим наш проект и добавим данные для улучшения поведения репликации.
Нейросети — это просто (Часть 49): Мягкий Актор-Критик (Soft Actor-Critic) Нейросети — это просто (Часть 49): Мягкий Актор-Критик (Soft Actor-Critic)
Мы продолжаем рассмотрение алгоритмов обучения с подкреплением в решении задач непрерывного пространства действий. И в данной статье предлагаю познакомиться с алгоритмом Soft Аctor-Critic (SAC). Основное преимущество SAC заключается в способности находить оптимальные политики, которые не только максимизируют ожидаемую награду, но и имеют максимальную энтропию (разнообразие) действий.
Разработка системы репликации - Моделирование рынка (Часть 06): Первые улучшения (I) Разработка системы репликации - Моделирование рынка (Часть 06): Первые улучшения (I)
В этой статье мы приступим к стабилизации всей системы, иначе мы рискуем не выполнить следующие шаги.
Разработка системы репликации - Моделирование рынка (Часть 03):  Внесение корректировок (I) Разработка системы репликации - Моделирование рынка (Часть 03): Внесение корректировок (I)
Начнем с прояснения нынешней ситуации, потому что мы начали не самым лучшим образом. Если не сделать этого сейчас, то вскоре мы окажемся в беде.