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

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

MetaTrader 5Примеры | 31 августа 2023, 09:39
528 2
Daniel Jose
Daniel Jose

Введение

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


Угодить и грекам, и троянцам

Первым шагом является добавление новой переменной или параметра в служебный файл:

input string            user00 = "Config.txt";  //Конфигурационный файл репликации.
input ENUM_TIMEFRAMES   user01 = PERIOD_M5;     //Начальное время графики.
input bool              user02 = true;          //Визуализировать построение баров.

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

После завершения этого шага мы передадим данный параметр классу C_Replay на следующем этапе:

while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value)) && (!_StopFlag))
{
        if (!Info.s_Infos.isPlay)
        {
                if (!bTest) bTest = true;
        }else
        {
                if (bTest)
                {
                        delay = ((delay = Replay.AdjustPositionReplay(user02)) >= 0 ? 3 : delay);
                        bTest = false;
                        t1 = GetTickCount64();
                }else if ((GetTickCount64() - t1) >= (uint)(delay))
                {
                        if ((delay = Replay.Event_OnTime()) < 0) break;
                        t1 = GetTickCount64();
                }
        }
}

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

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

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

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

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

inline bool BuiderBar1Min(MqlRates &rate, const MqlTick &tick)
                {
                        if (rate.time != macroRemoveSec(tick.time))
                        {
                                rate.real_volume = (long) tick.volume_real;
                                rate.tick_volume = 0;
                                rate.time = macroRemoveSec(tick.time);
                                rate.open = rate.low = rate.high = rate.close = tick.last;
                
                                return true;
                        }
                        rate.close = tick.last;
                        rate.high = (rate.close > rate.high ? rate.close : rate.high);
                        rate.low = (rate.close < rate.low ? rate.close : rate.low);
                        rate.real_volume += (long) tick.volume_real;
        
                        return false;
                }

То, что мы здесь делаем, по сути, это то же самое, что выполнил бы Event_OnTime. Тем не менее, мы будем делать это тик за тиком. Дадим краткое объяснение происходящему: когда время, указанное на тике, отличается от времени, записанного на баре, у нас будет начальное построение бара. Мы вернем «true», чтобы сообщить вызывающей программе, что будет создан новый бар, что позволит ему внести любые необходимые изменения. При последующих вызовах мы будем поправлять значения соответствующим образом. В этом случае мы вернем «false», чтобы указать, что новый бар не создался. Сама функция довольно проста, но при ее использовании необходимо соблюдать некоторые меры предосторожности.

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

bool SetSymbolReplay(const string szFileConfig)
{
        int     file;
        string  szInfo;
        bool    isBars = true;
                                
        if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
                MessageBox("Не удалось открыть\nконфигурационный файл.", "Market Replay", MB_OK);
                return false;
        }
        Print("Загрузка данных для репликации. Подождите....");
        ArrayResize(m_Ticks.Rate, 540);
        m_Ticks.nRate = -1;
        m_Ticks.Rate[0].time = 0;
        while ((!FileIsEnding(file)) && (!_StopFlag))
        {
                szInfo = FileReadString(file);
                StringToUpper(szInfo);
                if (szInfo == def_STR_FilesBar) isBars = true; else
                if (szInfo == def_STR_FilesTicks) isBars = false; else
                if (szInfo != "") if (!(isBars ? LoadPrevBars(szInfo) : LoadTicksReplay(szInfo)))
                {
                        if (!_StopFlag)
                                MessageBox(StringFormat("Файл %s из %s\nне может быть загружен.", szInfo, (isBars ? def_STR_FilesBar : def_STR_FilesTicks), "Market Replay", MB_OK));
                        FileClose(file);
                        return false;
                }
        }
        FileClose(file);
        return (!_StopFlag);
}

Если это не сделать правильно и заблаговременно, то не получится правильно использовать функцию создания бара. Тогда возникает следующий вопрос: Почему мы указываем значение -1 в индексе первого массива? Разве 0 не должно быть начальным значением? Да, это 0, но мы начинаем с -1 для первого вызова, который всегда будет иметь значение true. Если бы он начинался с 0, нам пришлось бы выполнить дополнительный тест сразу после вызова построения бара. Однако при установке значения -1 данная дополнительная проверка становится ненужной. Важно отметить, что мы инициализируем массив с 540 позициями, что соответствует количеству 1-минутных баров, обычно присутствующих в типичный торговый день на B3 (Бразильская Фондовая Биржа).

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

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

Вот одна важная деталь: первоначальное значение и резервное значение необходимо будет скорректировать, в случае, если количество минутных баров больше указанного здесь. Это значение подходит для торгового периода с 9:00 до 18:00, что соответствует 540 минутам, но если этот период больше, необходимо заранее его увеличить. Однако важно отметить, что временем, которое следует учитывать, должно быть время открытия и закрытия торгового окна. Это относится к файлу торгуемых тиков, а не к файлу баров. Это связано с тем, что бары генерируются на основе тикового файла и если в конкретном файле данное окно отличается, то могут возникать проблемы во время исполнения ( RUN TIME ). Однако, поскольку окно B3 обычно составляет 540 минут, данного значения достаточно.

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

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

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

Давайте теперь рассмотрим основную функцию данной темы.

int AdjustPositionReplay(const bool bViewBuider)
{
        u_Interprocess Info;
        MqlRates       Rate[1];
        int            iPos = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks);
        datetime       dt_Local;
                                
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == iPos) return 0;
        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / def_MaxPosSlider));
        if (iPos < m_ReplayCount)
        {
                dt_Local = m_dtPrevLoading;
                m_ReplayCount = 0;
                if (!bViewBuider) for (int c0 = 1; (c0 < m_Ticks.nRate) && (m_Ticks.Rate[c0 - 1].spread < iPos); c0++)
                {
                        dt_Local = m_Ticks.Rate[c0].time;
                        m_ReplayCount = m_Ticks.Rate[c0 - 1].spread;
                }
                CustomRatesDelete(def_SymbolReplay, dt_Local, LONG_MAX);
                if (m_dtPrevLoading == 0)
                {
                        Rate[0].close = Rate[0].open = Rate[0].high = Rate[0].low = m_Ticks.Info[m_ReplayCount].last;
                        Rate[0].tick_volume = 0;
                        Rate[0].time = m_Ticks.Info[m_ReplayCount].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                }
        }
        for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag); m_ReplayCount++) Event_OnTime();
        return Event_OnTime();
}

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

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

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

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

int AdjustPositionReplay(const bool bViewBuider)
{
#define macroSearchPosition     {                                                                                               \
                dt_Local = m_dtPrevLoading; m_ReplayCount = count = 0;                                                          \
                if (!bViewBuider) for (count = 1; (count < m_Ticks.nRate) && (m_Ticks.Rate[count - 1].spread < iPos); count++)  \
                        { dt_Local = m_Ticks.Rate[count].time;  m_ReplayCount = m_Ticks.Rate[count - 1].spread; }               \
                                }

        u_Interprocess  Info;
        MqlRates        Rate[def_BarsDiary];
        int             iPos = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks),
                        count;
        datetime        dt_Local;
                                
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == iPos) return 0;
        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
        if (iPos < m_ReplayCount)
        {
                macroSearchPosition;
                CustomRatesDelete(def_SymbolReplay, dt_Local, LONG_MAX);
                if (m_dtPrevLoading == 0)
                {
                        Rate[0].close = Rate[0].open = Rate[0].high = Rate[0].low = m_Ticks.Info[m_ReplayCount].last;
                        Rate[0].tick_volume = 0;
                        Rate[0].time = m_Ticks.Info[m_ReplayCount].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                }
        }if ((iPos > m_ReplayCount) && (!bViewBuider))
        {
                macroSearchPosition;                    
                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, count);
        }
        for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag); m_ReplayCount++) Event_OnTime();
        return Event_OnTime();
}

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


Оповещение пользователю

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

void OnStart()
{
        ulong t1;
        int delay = 3;
        long id = 0;
        u_Interprocess Info;
        bool bTest = false;
        
        Replay.InitSymbolReplay();
        if (!Replay.SetSymbolReplay(user00))
        {
                Finish();
                return;
        }
        Print("Ожидаем разрешения индикатора [Market Replay] для начала репликации ...");
        id = Replay.ViewReplay(user01);
        while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(id) != "")) Sleep(750);
        if ((_StopFlag) || (ChartSymbol(id) == ""))
        {
                Finish();
                return;
        }
        Print("Разрешение получено. Сервис репликации можно использовать...");
        t1 = GetTickCount64();
        while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value)) && (!_StopFlag))
        {
                if (!Info.s_Infos.isPlay)
                {
                        if (!bTest) bTest = true;
                }else
                {
                        if (bTest)
                        {
                                if ((delay = Replay.AdjustPositionReplay(user02)) < 0) AlertToUser(); else
                                {
                                        delay = (delay >= 0 ? 3 : delay);
                                        bTest = false;
                                        t1 = GetTickCount64();
                                }                               
                        }else if ((GetTickCount64() - t1) >= (uint)(delay))
                        {
                                if ((delay = Replay.Event_OnTime()) < 0) AlertToUser();
                                t1 = GetTickCount64();
                        }
                }
        }
        Finish();
}
//+------------------------------------------------------------------+
void AlertToUser(void)
{
        u_Interprocess Info;
        
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        Info.s_Infos.isPlay = false;
        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        MessageBox("No more data to use in replay-simulation", "Service Replay", MB_OK);
}
//+------------------------------------------------------------------+
void Finish(void)
{
        Replay.CloseReplay();
        Print("Сервис репликации завершен...");
}
//+------------------------------------------------------------------+

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

int AdjustPositionReplay(const bool bViewBuider)
{

// ... Код ...

        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));

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

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


Добавляем уведомление об ожидании

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

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

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

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#define def_GlobalVariableReplay        "Replay Infos"
#define def_GlobalVariableIdGraphics    "Replay ID"
#define def_SymbolReplay                "RePlay"
#define def_MaxPosSlider                400
#define def_ShortName                   "Market Replay"
//+------------------------------------------------------------------+
union u_Interprocess
{
        union u_0
        {
                double  df_Value;       // Значение глобальной переменной терминала...
                ulong   IdGraphic;      // Содержит ID графика актива....
        }u_Value;
        struct st_0
        {
                bool    isPlay;         // Указывает, находимся ли мы в режиме воспроизведения или паузы ...
                bool    isWait;         // Предлагает пользователю подождать...
                ushort  iPosShift;      // Значение от 0 до 400 ...
        }s_Infos;
};
//+------------------------------------------------------------------+

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

int AdjustPositionReplay(const bool bViewBuider)
{
#define macroSearchPosition     {                                                                                               \
                dt_Local = m_dtPrevLoading; m_ReplayCount = count = 0;                                                          \
                if (!bViewBuider) for (count = 1; (count < m_Ticks.nRate) && (m_Ticks.Rate[count - 1].spread < iPos); count++)  \
                        { dt_Local = m_Ticks.Rate[count].time;  m_ReplayCount = m_Ticks.Rate[count - 1].spread; }               \
                                }

        u_Interprocess  Info;
        MqlRates        Rate[def_BarsDiary];
        int             iPos = (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks),
                        count;
        datetime        dt_Local;
                                
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (Info.s_Infos.iPosShift == iPos) return 0;
        iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
        if (iPos < m_ReplayCount)
        {
                macroSearchPosition;
                CustomRatesDelete(def_SymbolReplay, dt_Local, LONG_MAX);
                if (m_dtPrevLoading == 0)
                {
                        Rate[0].close = Rate[0].open = Rate[0].high = Rate[0].low = m_Ticks.Info[m_ReplayCount].last;
                        Rate[0].tick_volume = 0;
                        Rate[0].time = m_Ticks.Info[m_ReplayCount].time - 60;
                        CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                }
        }if ((iPos > m_ReplayCount) && (!bViewBuider))
        {
                macroSearchPosition;                    
                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, count);
        }
        if (bViewBuider)
        {
                Info.s_Infos.isWait = true;
                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        }
        for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag); m_ReplayCount++) Event_OnTime();
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        Info.s_Infos.isWait = false;
        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        return Event_OnTime();
}

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

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

enum EventCustom {Ev_WAIT_ON, Ev_WAIT_OFF};

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

#define def_ButtonPlay  "Images\\Market Replay\\Play.bmp"
#define def_ButtonPause "Images\\Market Replay\\Pause.bmp"
#define def_ButtonLeft  "Images\\Market Replay\\Left.bmp"
#define def_ButtonRight "Images\\Market Replay\\Right.bmp"
#define def_ButtonPin   "Images\\Market Replay\\Pin.bmp"
#define def_ButtonWait  "Images\\Market Replay\\Wait.bmp"
#resource "\\" + def_ButtonPlay
#resource "\\" + def_ButtonPause
#resource "\\" + def_ButtonLeft
#resource "\\" + def_ButtonRight
#resource "\\" + def_ButtonPin
#resource "\\" + def_ButtonWait

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

Далее в работе с файлом класса, мы добавляем в класс частную внутреннюю переменную для управления внутренними действиями. 

class C_Controls
{
        private :
//+------------------------------------------------------------------+
                string  m_szBtnPlay;
                long    m_id;
                bool    m_bWait;
                struct st_00
                {
                        string  szBtnLeft,
                                szBtnRight,
                                szBtnPin,
                                szBarSlider;
                        int     posPinSlider,
                                posY;
                }m_Slider;
//+------------------------------------------------------------------+

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

C_Controls() : m_id(0), m_bWait(false)
        {
                m_szBtnPlay             = NULL;
                m_Slider.szBarSlider    = NULL;
                m_Slider.szBtnPin       = NULL;
                m_Slider.szBtnLeft      = NULL;
                m_Slider.szBtnRight     = NULL;
        }

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

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

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

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_OBJECT_CLICK:
                                if (m_bWait) break;
                                if (sparam == m_szBtnPlay)
                                {
                                        Info.s_Infos.isPlay = (bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE);
                                        if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else
                                        {
                                                RemoveCtrlSlider();
                                                m_Slider.szBtnPin = NULL;
                                        }
                                        Info.s_Infos.iPosShift = (ushort) m_Slider.posPinSlider;
                                        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_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;

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

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

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

void CreateBtnPlayPause(bool state)
{
        m_szBtnPlay = def_PrefixObjectName + "Play";
        CreateObjectBitMap(5, 25, m_szBtnPlay, (m_bWait ? def_ButtonWait : def_ButtonPause), (m_bWait ? def_ButtonWait : def_ButtonPlay));
        ObjectSetInteger(m_id, m_szBtnPlay, OBJPROP_STATE, state);
}

Обратите внимание, что мы просто проверяем переменную. В зависимости от его значения мы применим кнопку «воспроизведение/пауза» или кнопку, которая будет представлять сигнал ожидания. Однако у вас может возникнуть вопрос: как управлять этой кнопкой? Будет ли она постоянно считывать значение глобальной переменной с терминала? Будет что-то похожее. Помните следующий момент: каждый раз, когда сервис добавляет новую запись в актив репликации рынка, это будет отражаться в индикаторе. Таким образом, MetaTrader 5 сгенерирует событие, которое запустит функцию OnCalculate. Вот именно тут мы и вступим в игру, но постоянно следить за индикатором мы не будем. Мы сделаем это более элегантно. Чтобы понять ход действий, посмотрите на изображение ниже, на котором представлен поток вызовов в коде:

Именно такая последовательность действий будет выполнена для корректного управления кнопкой на ​​индикаторе управления. Процедура CreateBtnPlayPause уже была представлена ​​ранее, так что я думаю, что она довольно понятна. Теперь мы собираемся рассмотреть другие моменты, пока эта схема не будет полностью описана. Мы поступим наоборот, поскольку процедура OnCalculate включает в себя немного более сложную логику и требует нашего понимания тех шагов, которые происходят в DispatchMessage

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

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_CUSTOM + Ev_WAIT_ON):
                        m_bWait = true;
                        CreateBtnPlayPause(true);
                        break;
                case (CHARTEVENT_CUSTOM + Ev_WAIT_OFF):
                        m_bWait = false;
                        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                        CreateBtnPlayPause(Info.s_Infos.isPlay);
                        break;

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

Когда функция DispatchMessage вызывается OnChartEvent, присутствующим в индикаторе управления, то будут предоставлены данные, которые позволят нам обрабатывать как сообщения о событиях, передаваемые платформой MetaTrader 5, так и пользовательские события, запускаемые нашим кодом в определенных точках. Пользовательские события обсудим позже. Функция будет искать соответствующий код, если используется пользовательское событие Ev_WAIT_ON. Это сообщит нам о том, что сервис занят, в результате чего переменная m_bWait получит значение true. Далее вызываем создание кнопки «воспроизведение/пауза», которая фактически будет отображать фигуру, обозначающую состояние занятости. Когда запускается пользовательское событие Ev_WAIT_OFF, мы хотим, чтобы указывалось текущее состояние сервиса, то есть находится ли он в режиме воспроизведения или паузы. Следовательно, переменная m_bWait получит значение, которое указывает на то, что сервис доступен для приема запросов. Нам также необходимо получить данные из глобальной переменной терминала, которая будет содержать текущее состояние сервиса. Далее мы вызываем функцию, которая создает кнопку «воспроизведение/пауза», чтобы пользователь мог взаимодействовать с системой.

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

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        Control.DispatchMessage(id, lparam, dparam, sparam);
}

То есть, когда событие запускается, его обработка делегируется классу C_Control, а выполняемой функцией является DispatchMessage. Вы заметили, как всё устроено? Если бы код, который содержится в функции DispatchMessage, находился внутри функции обработки событий, результат был бы таким же. Однако важно отметить один немаловажный фактор: функция OnChartEvent получает 4 параметра, но функция, которая запускает пользовательские события, будет использовать больше параметров. Фактически, для запуска пользовательских событий используются 5 параметров. Таким образом, мы можем отличить пользовательские события от событий, поступающих из MetaTrader 5. Если мы присмотримся, то заметим, что значение, используемое при выборе, представляет собой сумму значения, указанного в перечислении EventCustom, и других данных CHARTEVENT_CUSTOM. Таким образом мы получим правильное значение. 

Но как создается данное значение? Как мы можем генерировать собственные события с помощью MQL5? Чтобы понять это, нужно посмотреть на основной код нашего индикатора управления: функцию OnCalcule. Её можно увидеть чуть ниже:

int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        static bool bWait = false;
        u_Interprocess Info;
        
        Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
        if (!bWait)
        {
                if (Info.s_Infos.isWait)
                {
                        EventChartCustom(m_id, Ev_WAIT_ON, 0, 0, "");
                        bWait = true;
                }
        }else if (!Info.s_Infos.isWait)
        {
                EventChartCustom(m_id, Ev_WAIT_OFF, 0, Info.u_Value.df_Value, "");
                bWait = false;
        }
        
        return rates_total;
}

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


Заключение

Надеюсь, вы поняли общую идею, поскольку она является основой для всего остального. Таким образом, при каждом вызове OnCalcule мы запишем значение, присутствующее в глобальной переменной терминала, и проверим, имеет ли локальная статическая переменная значение true или нет. Если ее значение не равно true, мы проверим, занят ли сервис. Если это условие выполнено, мы создадим специальное событие, чтобы сообщить об этом. Сразу после этого мы изменим значение локальной статической переменной, чтобы указать, что индикатор управления знает, что сервис репликации/моделирования занят. Таким образом, при следующем вызове OnCalcule мы проверим, свободен ли сервис репликации/моделирования для выполнения своей деятельности. Как только это произойдет, мы запустим специальное событие, сообщающее, что сервис готов получать запросы индикаторов управления. И цикл повторится, как только локальная статическая переменная будет иметь значение true.

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

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



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

Прикрепленные файлы |
Market_Replay.zip (13060.83 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
fxsaber
fxsaber | 31 авг. 2023 в 14:27

Видео напомнило (не реклама) давнюю работу, т.к. тоже используются кастомные символы в динамике с регулировкой скорости.

Возможно, автор увидит полезности в некоторых сценариях использования для своего проекта.

Daniel Jose
Daniel Jose | 4 сент. 2023 в 10:37
fxsaber # :

Видео напомнило (не реклама) давнюю работу , т.к. тоже используются кастомные символы в динамике с регулировкой скорости.

Возможно, автор увидит полезности в некоторых сценариях использования для своего проекта.

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

Теория категорий в MQL5 (Часть 11): Графы Теория категорий в MQL5 (Часть 11): Графы
Статья продолжает серию о реализации теории категорий в MQL5. Здесь мы рассмотрим, как теория графов может быть интегрирована с моноидами и другими структурами данных при разработке стратегии закрытия торговой системы.
Делаем информационную панель для отображения данных в индикаторах и советниках Делаем информационную панель для отображения данных в индикаторах и советниках
В статье рассмотрим создание класса информационной панели для использования её в индикаторах и советниках. Это вводная статья в небольшой серии статей с шаблонами подключения и использования стандартных индикаторов в советниках. Начнем мы с создания панели — аналога окна данных MetaTrader 5.
Разработка системы репликации - Моделирование рынка (Часть 10): Только реальные данные для репликации Разработка системы репликации - Моделирование рынка (Часть 10): Только реальные данные для репликации
Здесь мы рассмотрим, как более надежные данные (торгуемые тики) можно использовать в системе репликации, не беспокоясь о том, скорректированы они или нет.
Разработка системы репликации - Моделирование рынка (Часть 08): Блокировка индикатора Разработка системы репликации - Моделирование рынка (Часть 08): Блокировка индикатора
В этой статье мы рассмотрим, как заблокировать индикатор при простом использовании языка MQL5, и сделаем это очень интересным и удивительным способом.