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

Разработка системы репликации - Моделирование рынка (Часть 20): ФОРЕКС (I)

MetaTrader 5Тестер | 20 ноября 2023, 10:54
851 0
Daniel Jose
Daniel Jose

Введение

В предыдущей статье "Разработка системы репликации - Моделирование рынка (Часть 19): Необходимые корректировки", мы реализовали некоторые моменты, необходимость которых становилась всё более насущной. Однако, несмотря на то, что с самого начала данной серии основное внимание уделялось фондовому рынку, я не хочу прекращать попытки охватить также и валютный рынок (ФОРЕКС). Причина моего первоначального отсутствия интереса к ФОРЕКС связана с тем, что сделки на нем осуществляются постоянно, поэтому нет смысла в присутствии репликации/моделировании для тестирования или обучения.

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

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


Давайте познакомимся с некоторыми аспектами рынка ФОРЕКС.

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

В приложении к этой статье можно найти актив (более известный как валютная пара) с рынка ФОРЕКС. Это будут настоящие тики, чтобы мы могли визуализировать ситуацию. Без сомнения, ФОРЕКС - это нелегкий рынок для работы с моделированием и репликацией. Хотя можно видеть один и тот же основной тип информации, на рынке ФОРЕКС есть свои особенности. Поэтому интересно наблюдать и анализировать.

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


Как осуществляются сделки

На валютном рынке торговля обычно происходит без реального спреда между значениями BID и ASK. В большинстве случаев эти два значения могут совпадать. Но как такое возможно? Как они могут быть одинаковыми? В отличие от фондового рынка, где всегда существует спред между BID и ASK, на рынке ФОРЕКС этого нет. Хотя иногда разброс имеет место, а иногда он значительно выше, обычно значения BID и ASK могут быть одинаковыми. Это начинает смущать тех, кто пришел с фондового рынка и желает выйти на рынок ФОРЕКС, поскольку торговые стратегии часто требуют значительных изменений.

Еще следует иметь в виду, что на рынке ФОРЕКС основными игроками являются центральные банки, те, кто работает на B3 (Бразильской фондовой бирже), уже видели и прекрасно знают, что иногда делает с долларом ЦБ. По этой причине многие избегают торговли этим активом из-за опасений возможной интервенции Центрального банка в валюту, ведь это может быстро превратить ранее выигрышную позицию в сильно проигрышную. Многие неопытные трейдеры в это время часто становятся банкротами, а в некоторых случаях даже сталкиваясь с судебными исками со стороны фондовой биржи и биржевого брокера. Это может произойти в ходе одной из интервенций, которые центральный банк может осуществить без предварительного предупреждения и безжалостно по отношению к тем, кто имеет позиции.

Однако, для нас это не имеет значения: нас интересует сама программа. Таким образом, на рынке ФОРЕКС отображение цен основано на значении цены BID, как видно на рисунке 01.


Рисунок 01 

Рисунок 01: Отображение графика на рынке ФОРЕКС

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

Рисунок 02 

Рисунок 02: Контракт на мини-доллар торгуется на Бразильской фондовой бирже (B3).

Система репликации/моделировании была разработана с целью популяризации использования этого типа анализа. То есть, когда мы используем последнюю торговую цену, у нас будет разница в том, как данные будут расположены в файле торгуемых тиков. И не только это, у нас даже может быть большая разница в виде информации, которая фактически будет доступна в тиковом файле или в 1-минутных барах. Из-за этих вариаций сейчас мы сосредоточимся исключительно на том, чтобы посмотреть, как выполнять репликацию, так как моделирование включает в себя другие, еще более сложные проблемы. Однако, как упоминалось в начале этой статьи: можно использовать данные 1-минутных баров, чтобы смоделировать то, что, вероятно, произошло во время торгов. Чтобы не говорить только о теории, давайте изучим разницу в информации между валютным рынком и фондовым рынком в случае B3, то есть Бразильской фондовой биржи, для которой была разработана система репликации/моделировании. На рисунке 03 у нас есть информация об одной из валютных пар на рынке ФОРЕКС.

Рисунок 03

Рисунок 03: Информация о реальных сделках на рынке ФОРЕКС

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

Рисунок 04

Рисунок 04: Реальная информация о тиках в B3

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


Начинаем реализовывать охват рынка ФОРЕКС

Первое, что нам нужно сделать — это исправить систему нумерации с плавающей запятой. Но ведь система с плавающей запятой больше не работает из-за изменений, внесенных в предыдущей статье, верно? Да, это правильно, но она не подходит для валютного рынка (ФОРЕКС). Это связано с тем, что точность ограничена четырьмя знаками после запятой, а нам нужно сообщить системе, что мы собираемся использовать набор с большим количеством десятичных знаков. Тогда нам придется это исправить, чтобы избежать других проблем в дальнейшем. Данное исправление выполнено в следующем коде:

C_Replay(const string szFileConfig)
    {
        m_ReplayCount = 0;
        m_dtPrevLoading = 0;
        m_Ticks.nTicks = 0;
        Print("************** Serviço Market Replay **************");
        srand(GetTickCount());
        GlobalVariableDel(def_GlobalVariableReplay);
        SymbolSelect(def_SymbolReplay, false);
        CustomSymbolDelete(def_SymbolReplay);
        CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay), _Symbol);
        CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
        CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
        SymbolSelect(def_SymbolReplay, true);
        CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0);
        CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0);
        CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0);
        CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
        CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8);
        m_IdReplay = (SetSymbolReplay(szFileConfig) ? 0 : -1);
    }

Прямо здесь мы сообщим MetaTrader 5, что нам нужно большее количество десятичных знаков в нашей системе с плавающей запятой. В данном случае мы будем использовать 8 знаков после запятой, это более чем достаточно для охвата широкого спектра условий. Одна важная деталь: B3 хорошо справляется с 4 знаками после запятой, но для работы на ФОРЕКС нам необходимо 5 знаков. Используя 8, мы делаем систему свободной. Однако так будет не всегда. Нам придется изменить его позже из-за детали, которую мы еще не можем объяснить, но на данный момент этого достаточно.

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


Давайте поработаем с основными понятиями

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

Сначала, добавим следующее:

class C_FileTicks
{
    protected:
        enum ePlotType {PRICE_EXCHANGE, PRICE_FOREX};
        struct st00
        {
            MqlTick   Info[];
            MqlRates  Rate[];
            int       nTicks,
                      nRate;
            bool      bTickReal;
            ePlotType ModePlot;
        }m_Ticks;

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

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

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

                string   szInfo;
                MqlRates rate;

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

Начнем с указания, что типом отображения будет модель ФОРЕКС. Однако, если при чтении тикового файла будет обнаружен тик, содержащий торгуемый объем, эта модель будет изменена на БИРЖЕВОЙ тип. Важно понимать, что это происходит без какого-либо вмешательства пользователя. Но тут возникает важный момент: данная система будет работать только для тех случаев, когда чтение производится во время запуска репликации. В случае моделирования ситуация будет другой. Мы пока не будем беспокоиться о моделировании.

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

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

datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true)
    {
        int      MemNRates,
                 MemNTicks;
        datetime dtRet = TimeCurrent();
        MqlRates RatesLocal[];

        MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
        MemNTicks = m_Ticks.nTicks;
        if (!Open(szFileNameCSV)) return 0;
        if (!ReadAllsTicks(ToReplay)) return 0;
        if (!ToReplay)
        {
            ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
            ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
            CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
            dtRet = m_Ticks.Rate[m_Ticks.nRate].time;
            m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
            m_Ticks.nTicks = MemNTicks;
            ArrayFree(RatesLocal);
        }else
        {
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX);
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID);
        }
        m_Ticks.bTickReal = true;

        return dtRet;
    };

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

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

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

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

Рисунок 05

Рисунок 05: Отображение автоматического распознавания чтения тиков

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

class C_Replay : private C_ConfigService
{
    private :
        long         m_IdReplay;
        struct st01
        {
            MqlRates Rate[1];
            datetime memDT;
            int      delay;
        }m_MountBar;
        struct st02
        {
            bool     bInit;
        }m_Infos;

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

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

C_Replay(const string szFileConfig)
{
    m_ReplayCount = 0;
    m_dtPrevLoading = 0;
    m_Ticks.nTicks = 0;
    m_Infos.bInit = false;
    Print("************** Serviço Market Replay **************");
    srand(GetTickCount());
    GlobalVariableDel(def_GlobalVariableReplay);
    SymbolSelect(def_SymbolReplay, false);
    CustomSymbolDelete(def_SymbolReplay);
    CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay), _Symbol);
    CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
    CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
    SymbolSelect(def_SymbolReplay, true);
    CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0);
    CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0);
    CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0);
    CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
    CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8);
    m_IdReplay = (SetSymbolReplay(szFileConfig) ? 0 : -1);
}

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

bool LoopEventOnTime(const bool bViewBuider, const bool bViewMetrics)
        {
                u_Interprocess Info;
                int iPos, iTest;

                if (!m_Infos.bInit)
                {
                        ChartSetInteger(m_IdReplay, CHART_SHOW_ASK_LINE, m_Ticks.ModePlot == PRICE_FOREX);
                        ChartSetInteger(m_IdReplay, CHART_SHOW_BID_LINE, m_Ticks.ModePlot == PRICE_FOREX);
                        ChartSetInteger(m_IdReplay, CHART_SHOW_LAST_LINE, m_Ticks.ModePlot == PRICE_EXCHANGE);
                        m_Infos.bInit = true;
                }
                iTest = 0;
                while ((iTest == 0) && (!_StopFlag))
                {
                        iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -1);
                        iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -1);
                        iTest = (iTest == 0 ? (Info.s_Infos.isPlay ? 1 : iTest) : iTest);
                        if (iTest == 0) Sleep(100);
                }
                if ((iTest < 0) || (_StopFlag)) return false;
                AdjustPositionToReplay(bViewBuider);
                m_MountBar.delay = 0;
                while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag))
                {
                        CreateBarInReplay(bViewMetrics, true);
                        iPos = (int)(m_ReplayCount < m_Ticks.nTicks ? m_Ticks.Info[m_ReplayCount].time_msc - m_Ticks.Info[m_ReplayCount - 1].time_msc : 0);
                        m_MountBar.delay += (iPos < 0 ? iPos + 1000 : iPos);
                        if (m_MountBar.delay > 400)
                        {
                                if (ChartSymbol(m_IdReplay) == "") break;
                                GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                if (!Info.s_Infos.isPlay) return true;
                                Info.s_Infos.iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                Sleep(m_MountBar.delay - 20);
                                m_MountBar.delay = 0;
                        }
                }                               
                return (m_ReplayCount == m_Ticks.nTicks);
        }

Данная функция, описанная выше, вызывает у нас некоторую головную боль. Именно поэтому нам нужна переменная, которая указывает, инициализирована система или нет. Обратите внимание: при первом запуске данной функции переменная по-прежнему будет указывать, что система НЕ была полностью инициализирована. В это время её инициализация будет завершена. Здесь мы следим за тем, чтобы на экране отображались правильные ценовые линии. Если система определяет режим отображения как ФОРЕКС, будут отображаться ценовые линии BID и ASK, а последняя ценовая линия будет скрыта. Обратное произойдет, если режимом отображения является БИРЖЕВОЙ. В этом случае ценовые линии BID и ASK будут скрыты, а будет отображаться последняя ценовая линия.

Было бы всё очень красиво и хорошо, если бы не тот факт, что некоторые пользователи предпочитают устанавливать другую настройку. Даже если они работают в БИРЖЕВОМ стиле отображения, им нравится отображать линии BID или ASK, а в некоторых случаях и обе линии. Поэтому, если пользователь приостанавливает работу системы после настройки по своему усмотрению, а затем перезапускает систему, то система проигнорирует настройки пользователя и вернется к своим внутренним настройкам. Однако, указав, что система уже инициализирована (и используя для этого переменную), она не вернется к внутренним настройкам, а останется такой, какой ее только что настроил пользователь.

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


Отображение баров

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

Первое исправление находится в функции ниже:

void AdjustPositionToReplay(const bool bViewBuider)
        {
                u_Interprocess Info;
                MqlRates       Rate[def_BarsDiary];
                int            iPos, nCount;

                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                if ((m_ReplayCount == 0) && (m_Ticks.ModePlot == PRICE_EXCHANGE))
                for (; m_Ticks.Info[m_ReplayCount].volume_real == 0; m_ReplayCount++);
                if (Info.s_Infos.iPosShift == (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks)) return;
                iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1)));
                Rate[0].time = macroRemoveSec(m_Ticks.Info[iPos].time);
                if (iPos < m_ReplayCount)
                {
                        CustomRatesDelete(def_SymbolReplay, Rate[0].time, LONG_MAX);
                        CustomTicksDelete(def_SymbolReplay, m_Ticks.Info[iPos].time_msc, LONG_MAX);
                        if ((m_dtPrevLoading == 0) && (iPos == 0)) FirstBarNULL(); else
                        {
                                for(Rate[0].time -= 60; (m_ReplayCount > 0) && (Rate[0].time <= macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)); m_ReplayCount--);
                                m_ReplayCount++;
                        }
                }else if (iPos > m_ReplayCount)
                {
                        if (bViewBuider)
                        {
                                Info.s_Infos.isWait = true;
                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                        }else
                        {
                                for(; Rate[0].time > (m_Ticks.Info[m_ReplayCount].time); m_ReplayCount++);
                                for (nCount = 0; m_Ticks.Rate[nCount].time < macroRemoveSec(m_Ticks.Info[iPos].time); nCount++);
                                CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount);
                        }
                }
                for (iPos = (iPos > 0 ? iPos - 1 : 0); (m_ReplayCount < iPos) && (!_StopFlag);) CreateBarInReplay(false, false);
                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                Info.s_Infos.isWait = false;
                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
        }

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

На следующем этапе мы фактически вставляем тики на график. Здесь единственное, о чем мы должны побеспокоиться, — это о том, чтобы сообщить системе, какой будет цена закрытия бара; остальное обрабатывается функцией моделирования бара. В этом случае оно будет одинаковым как для отображения биржевого режима, так и для отображения ФОРЕКС. Код для его реализации можно увидеть ниже:

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

        bool bNew;
        MqlTick tick[1];
        static double PointsPerTick = 0.0;

        if (bNew = (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)))
        {
            PointsPerTick = (PointsPerTick == 0.0 ? SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) : PointsPerTick);                    
            if (bViewMetrics) Metrics();
            m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
            def_Rate.real_volume = 0;
            def_Rate.tick_volume = 0;
        }
        def_Rate.close = (m_Ticks.ModePlot == PRICE_EXCHANGE ? (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : def_Rate.close) :
                                                               (m_Ticks.Info[m_ReplayCount].bid > 0.0 ? m_Ticks.Info[m_ReplayCount].bid : def_Rate.close));
        def_Rate.open = (bNew ? def_Rate.close : def_Rate.open);
        def_Rate.high = (bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
        def_Rate.low = (bNew || (def_Rate.close < def_Rate.low) ? def_Rate.close : def_Rate.low);
        def_Rate.real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real;
        def_Rate.tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0);
        def_Rate.time = m_MountBar.memDT;
        CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
        if (bViewTicks)
        {
            tick = m_Ticks.Info[m_ReplayCount];
            if (!m_Ticks.bTickReal)
            {
                static double BID, ASK;
                double  dSpread;
                int     iRand = rand();

                dSpread = PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? PointsPerTick : 0 ) : 0 );
                if (tick[0].last > ASK)
                {
                    ASK = tick[0].ask = tick[0].last;
                    BID = tick[0].bid = tick[0].last - dSpread;
                }
                if (tick[0].last < BID)
                {
                    ASK = tick[0].ask = tick[0].last + dSpread;
                    BID = tick[0].bid = tick[0].last;
                }
            }
            CustomTicksAdd(def_SymbolReplay, tick); 
        }
        m_ReplayCount++;

#undef def_Rate
    }

Данная строка делает именно это. Она сгенерирует цену закрытия бара в зависимости от типа используемого отображения. В остальном функция остается такой же, как и раньше. Таким образом, нам удастся охватить систему отображения ФОРЕКС и выполнить репликацию с данными, представленными в тиковом файле. У нас пока нет возможности проводить моделирование.

Можно подумать, что система уже закончена, но это не так. У нас всё еще есть две проблемы, и они весьма актуальны, прежде чем мы даже подумаем о моделировании ФОРЕКСа.

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

Поскольку вторая проблема является более насущной и неотложной, начнем с нее.


Исправление таймера

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

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

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

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

bool LoopEventOnTime(const bool bViewBuider, const bool bViewMetrics)
    {
        u_Interprocess Info;
        int iPos, iTest;

        iTest = 0;
        while ((iTest == 0) && (!_StopFlag))
        {
            iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -1);
            iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -1);
            iTest = (iTest == 0 ? (Info.s_Infos.isPlay ? 1 : iTest) : iTest);
            if (iTest == 0) Sleep(100);
        }
        if ((iTest < 0) || (_StopFlag)) return false;
        AdjustPositionToReplay(bViewBuider);
        iPos = 0;
        while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag))
        {
            iPos = (int)(m_ReplayCount < m_Ticks.nTicks ? m_Ticks.Info[m_ReplayCount].time_msc - m_Ticks.Info[m_ReplayCount - 1].time_msc : 0);
            m_MountBar.delay += (iPos < 0 ? iPos + 1000 : iPos);
            iPos += (int)(m_ReplayCount < (m_Ticks.nTicks - 1) ? m_Ticks.Info[m_ReplayCount + 1].time_msc - m_Ticks.Info[m_ReplayCount].time_msc : 0);
            CreateBarInReplay(bViewMetrics, true);
            if (m_MountBar.delay > 400)
            while ((iPos > 200) && (!_StopFlag))
            {
                if (ChartSymbol(m_IdReplay) == "") break;
                if (ChartSymbol(m_IdReplay) == "") return false;
                GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                if (!Info.s_Infos.isPlay) return true;
                Info.s_Infos.iPosShift = (ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                Sleep(195);
                iPos -= 200;
                Sleep(m_MountBar.delay - 20);
                m_MountBar.delay = 0;
            }
        }                               
        return (m_ReplayCount == m_Ticks.nTicks);
    }

Все удаленные части были заменены другими кодами. Таким образом, мы можем решить первую проблему, связанную с отображением баров. Ранее мы использовали обратный расчет, но теперь мы собираемся использовать прямой расчет. Это предотвращает некоторые странные вещи на графике, пока мы находимся в режиме ожидания. Обратите внимание на то, что когда мы запускаем систему, она всегда ждет некоторое время, прежде чем отобразить тик. Раньше делалось ровно наоборот (ОШИБКА - МОЯ). Поскольку теперь время может быть очень долгим, вплоть до нескольких часов, у нас есть другой способ управления таймером. Чтобы лучше разобраться, следует знать, что раньше система оставалась в режиме ожидания до полного истечения времени. Если мы попытались внести какие-либо изменения, например закрыть график или попытаться изменить точку исполнения, система просто не реагировала должным образом. Это происходило потому, что в тех файлах, которые были прикреплены ранее, не было риска использования набора, где актив долгое время оставался «неторгуемым». Но когда я начал писать эту статью, система показала данную ошибку. Поэтому были внесены исправления.

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


Заключение

Несмотря на все неудачи, теперь можно начать экспериментировать с использованием данных ФОРЕКС в системе. Начнем с этой версии системы репликации/моделирования. В подтверждение этого в приложении у нас будет доступ к некоторым данным ФОРЕКС. В некоторых моментах система всё еще нуждается в доработках. Так как пока я не хочу вдаваться в подробности (поскольку это может потребовать радикальных изменений в некоторых моментах, показанных в этой статье), на этом я закончу модификации.

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

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


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

Прикрепленные файлы |
Market_Replay_yvg20.zip (14386.04 KB)
Торговая техника RSI Deep Three Move Торговая техника RSI Deep Three Move
В статье представлена техника торговли RSI Deep Three Move в MetaTrader 5. Статья основана на новой серии исследований, демонстрирующих несколько торговых методов, основанных на RSI - техническом индикаторе для измерения силы и импульса ценных бумаг, включая акции, валюты и товары.
Разработка системы репликации - Моделирование рынка (Часть 19): Необходимые корректировки Разработка системы репликации - Моделирование рынка (Часть 19): Необходимые корректировки
Здесь мы подготовим почву для того, чтобы при необходимости добавления новых функций в код это происходило плавно и легко. Текущий код пока не может охватывать или обрабатывать некоторые моменты, которые будут необходимы для значимого прогресса. Нам нужно, чтобы всё было построено так, чтобы усилия по реализации некоторых вещей были минимальными. Если сделаем всё правильно, мы сможем получить действительно универсальную систему, способную очень легко адаптироваться к любой ситуации, которую необходимо охватить.
Популяционные алгоритмы оптимизации: Алгоритм интеллектуальных капель воды (Intelligent Water Drops, IWD) Популяционные алгоритмы оптимизации: Алгоритм интеллектуальных капель воды (Intelligent Water Drops, IWD)
В статье рассматривается интересный алгоритм - интеллектуальные капли воды, IWD, подсмотренный у неживой природы, симулирующий процесс формирования русла реки. Идеи этого алгоритма позволили значительно улучшить прошлого лидера рейтинга - SDS, а нового лидера (модифицированный SDSm), как обычно, найдёте в архиве к статье.
Нейросети — это просто (Часть 64): Метод Консервативного Весового Поведенческого Клонирования (CWBC) Нейросети — это просто (Часть 64): Метод Консервативного Весового Поведенческого Клонирования (CWBC)
В результате тестов, проведенных в предыдущих статьях, мы пришли к выводу, что оптимальность обученной стратегии во многом зависит от используемой обучаемой выборки. В данной статье я предлагаю вам познакомиться с довольно простым и эффективном методе выбора траекторий для обучения моделей.