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

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

MetaTrader 5Тестер | 23 ноября 2023, 10:07
776 0
Daniel Jose
Daniel Jose

Введение

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

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

Хотя это уже третья статья об этом, я должен объяснить для тех, кто еще не понял разницу между фондовым рынком и валютным рынком (ФОРЕКС): большая разница заключается в том, что в ФОРЕКС не существует, точнее, нам не дают информацию о некоторых моментах, которые действительно происходили в ходе торговли. Хотя может показаться, что это относится только к рынку Форекс, я имею не в виду только его. Это связано с тем, что на валютном рынке у нас есть свой метод получения определенной информации, а торговая модель полностью отличается от модели фондового рынка. Мне кажется, что проще провести это различие таким образом. Но всё, что в этих статьях относится к ФОРЕКС, вы должны понимать как нечто относящееся к любому типу рынка, на котором форма отображения осуществляется через значение BID, в отличие от того, что происходит на фондовом рынке, где значение является LAST.

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


Исправим систему быстрого позиционирования

Чтобы решить эту проблему, нам придется внести некоторые изменения в код не потому что он неправильный, а из-за того, что он не может работать с BID-отображением. Если объяснить точнее, то проблема заключается в том, что нецелесообразно считывать и преображать тики в 1-минутные бары (даже в классе C_FileTicks) для последующего использования в процессе быстрого позиционирования. Это из-за того, что мы не можем представлять значения цен на основе BID. Чтобы понять, почему это так, давайте посмотрим на код, который отвечает за выполнение этого преобразования. Код можно увидеть ниже:

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
    }

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

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

        string   szInfo;
            
        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);
            m_Ticks.nTicks++;
        }
        FileClose(m_File);
        if (m_Ticks.nTicks == def_LIMIT)
        {
            Print("Слишком много данных в тиковом файле.\nНевозможно продолжать...");
            return false;
        }
        return (!_StopFlag);
#undef def_Ticks
#undef def_LIMIT
    }

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

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

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

inline bool BuiderBar1Min(MqlRates &rate, const MqlTick &tick)
    {
        if (rate.time != macroRemoveSec(tick.time))
        {
            rate.real_volume = 0;
            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;
        rate.tick_volume += (tick.last > 0 ? 1 : 0);

        return false;
    }

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

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

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;
        BuiderBar1Min(MemNTicks);
        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;
    };

Обратите внимание на то, как мы решили первую часть нашей задачи. Размещая эту строку, мы обеспечиваем преобразование тиков в 1-минутные бары перед возможным вызовом CustomRatesUpdate. В то же время мы сообщаем функции преобразования, где должен начаться процесс преобразования. Причина, по которой мы поместили вызов функции преобразования здесь, а не в функцию чтения, состоит именно в том, чтобы избежать добавления, казалось бы, ненужной переменной в функцию чтения. Ведь здесь у нас есть доступ к переменной, которая нам нужна для определения диапазона, в котором мы должны работать. Хорошо, мы можем перейти к реализации функции, которая преобразует тики в 1-минутные бары. Нам придется сделать это таким образом, чтобы это работало как для рынка типа ФОРЕКС, так и для рынка БИРЖЕВОГО типа. Кажется сложным, не правда ли? Опять же, если вы не планируете то, что собираетесь делать, то можно застрять в цикле написания кода, из-за которого придется отказаться от попыток реализовать правильный код.

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

inline void BuiderBar1Min(const int iFirst)
    {
        MqlRates rate;
        double   dClose = 0;
        
        rate.time = 0;
        for (int c0 = iFirst; c0 < m_Ticks.nTicks; c0++)
        {
            switch (m_Ticks.ModePlot)
            {
                case PRICE_EXCHANGE:
                    if (m_Ticks.Info[c0].last == 0.0) continue;
                    dClose = m_Ticks.Info[c0].last;
                    break;
                case PRICE_FOREX:
                    dClose = (m_Ticks.Info[c0].bid > 0.0 ? m_Ticks.Info[c0].bid : dClose);
                    if (dClose == 0.0) continue;
                    break;
            }
            if (rate.time != macroRemoveSec(m_Ticks.Info[c0].time))
            {
                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
                rate.time = macroRemoveSec(m_Ticks.Info[c0].time);
                rate.real_volume = 0;
                rate.tick_volume = 0;
                rate.open = rate.low = rate.high = rate.close = dClose;
            }else
            {
                rate.close = dClose;
                rate.high = (rate.close > rate.high ? rate.close : rate.high);
                rate.low = (rate.close < rate.low ? rate.close : rate.low);
                rate.real_volume += (long) m_Ticks.Info[c0].volume_real;
                rate.tick_volume++;
            }
            m_Ticks.Rate[(m_Ticks.nRate += (rate.tick_volume == 0 ? 1 : 0))] = rate;
        }
    }

Исходная функция выделена зеленым цветом для того, чтобы можно было определить, где она присутствует. Эти коды — новая часть функции. Обратите внимание, что данная часть полностью отвечает за обмен LAST на BID или наоборот, чтобы цена закрытия была подходящей для создания 1-минутного бара. Цикл, который я упомянул раньше, находится на этом этапе. Хотя данная функция решает большинство наших проблем, она не решает проблему тикового объема, если мы используем отображение типа BID. Чтобы решить эту проблему, нам придется немного изменить предыдущую функцию, чтобы окончательный код выглядел так:

inline void BuiderBar1Min(const int iFirst)
    {
        MqlRates rate;
        double  dClose = 0;
        bool    bNew;
        
        rate.time = 0;
        for (int c0 = iFirst; c0 < m_Ticks.nTicks; c0++)
        {
            switch (m_Ticks.ModePlot)
            {
                case PRICE_EXCHANGE:
                    if (m_Ticks.Info[c0].last == 0.0) continue;
                    dClose = m_Ticks.Info[c0].last;
                    break;
                case PRICE_FOREX:
                    dClose = (m_Ticks.Info[c0].bid > 0.0 ? m_Ticks.Info[c0].bid : dClose);
                    if ((dClose == 0.0) || (m_Ticks.Info[c0].bid == 0.0)) continue;
                    break;
            }
            if (bNew = (rate.time != macroRemoveSec(m_Ticks.Info[c0].time)))
            {
                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
                rate.time = macroRemoveSec(m_Ticks.Info[c0].time);
                rate.real_volume = 0;
                rate.tick_volume = (m_Ticks.ModePlot == PRICE_FOREX ? 1 : 0);
                rate.open = rate.low = rate.high = rate.close = dClose;
            }else
            {
                rate.close = dClose;
                rate.high = (rate.close > rate.high ? rate.close : rate.high);
                rate.low = (rate.close < rate.low ? rate.close : rate.low);
                rate.real_volume += (long) m_Ticks.Info[c0].volume_real;
                rate.tick_volume++;
            }
            m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
        }
    }

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

Теперь один важный момент: Когда мы используем режим отображение по LAST, чтобы тиковый объем был правильным, мы должны запустить счетчик с нуля. Но когда мы используем отображение по BID, мы должны начинать значение с единицы. В противном случае у нас получатся неверные данные в тиковом объеме, а это достигается запуском данного кода.

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


Исправление тикового объема

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

Дело не в том, что код неправильный, совсем наоборот. Если вы используете систему для репликации данных или моделирования актива, который использует значение LAST в качестве цены отображения, сообщаемый тиковый объем будет правильным. Но в случае использования цены BID, как это происходит на ФОРЕКС, данный объем будет неверным. Нам теперь нужно решить эту проблему, чтобы информация об объеме была верной. Чтобы понять, в чем проблема, давайте посмотрим на код, который отвечает за выполнение данного расчета:

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
    }

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

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

inline void CreateBarInReplay(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);            
            m_MountBar.memDT = (datetime) macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
            if (m_Ticks.ModePlot == PRICE_FOREX) CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate, (def_Rate.time < m_MountBar.memDT ? 1 : 0));
            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.ModePlot == PRICE_FOREX) && (m_Ticks.Info[m_ReplayCount].bid > 0.0) ? 1 : (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
    }

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

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

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


Готовим почву для следующего испытания

С учетом всех изменений, реализованных на данный момент, мы можем перейти к реальной задаче: созданию способа моделирования тиков рынка ФОРЕКС, основываясь исключительно на содержимом, присутствующем в файлах 1-минутных баров. Поверьте мне: испытание будет довольно значительным, но в то же время очень интересным и захватывающим, чтобы его преодолеть. Для облегчения задачи мы разделим класс C_FileTicks на 2 класса, но это просто для того, чтобы облегчить вопрос. В этом разделении нет необходимости, но поскольку нам придется иметь дело с довольно утомительными задачами, а мне не нравится, чтобы у классов было больше 1000 строк, давайте разделим C_FileTicks на два класса.

В этом разделе мы собираемся удалить часть, отвечающую за моделирование тиков, из класса C_FileTicks. И таким образом появляется класс C_Simulation, который будет отвечать за выполнение этого создания, преобразование 1-минутных баров в тики, чтобы это можно было отображать (и работать) без проблем, поскольку класс C_Simulation будет невидим для системы репликации. Для сервиса репликации данные всегда будут поступать из файла реальных тиков. На самом деле они могут возникнуть в результате моделирования. Даже если попытаться получить доступ к классу C_Simulation из класса C_Replay, он будет недоступен. Таким образом, всё будет работать так, как мы ожидаем, так как класс C_Replay сможет видеть только класс C_FileTicks и делает он именно это: загружает реальные тики, присутствующие в файле, чтобы класс C_Replay мог отобразить их в графическом терминале MetaTrader 5.

Новое объявление класса C_FileTicks выглядит теперь так:

#include "C_FileBars.mqh"
#include "C_Simulation.mqh"
//+------------------------------------------------------------------+
#define macroRemoveSec(A) (A - (A % 60))
//+------------------------------------------------------------------+
class C_FileTicks : private C_Simulation
{

// ... Внутренний код класса 

};

В результате класс C_FileTicks будет приватным образом наследовать класс C_Simulation. Так мы добьемся именно того, что мы описали выше.

Но нам нужно внести небольшие изменения в код класса C_FileTicks. И это потому что мы унаследуем класс C_Simulation. Но я не хочу отправлять данные, объявленные как protected, в класс C_Simulation. Это сделано для того, чтобы класс C_Simulation оставался скрытым от остальной части системы. Однако нам по-прежнему необходимо разрешить использование работы, выполненной классом, другими классами, поэтому, нам нужно изменить следующий код, добавив эти строки:

bool BarsToTicks(const string szFileNameCSV)
    {
        C_FileBars  *pFileBars;
        int         iMem = m_Ticks.nTicks,
                    iRet;
        MqlRates    rate[1];
        MqlTick     local[];
        
        pFileBars = new C_FileBars(szFileNameCSV);
        ArrayResize(local, def_MaxSizeArray);
        Print("Преобразуем бары в тики. Подождите...");
        while ((*pFileBars).ReadBar(rate) && (!_StopFlag))
        {
            ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
            iRet = Simulation(rate[0], local);
            for (int c0 = 0; c0 <= iRet; c0++)
            {
                ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                m_Ticks.Info[m_Ticks.nTicks++] = local[c0];
            }
        }
        ArrayFree(local);
        delete pFileBars;
        m_Ticks.bTickReal = false;
        
        return ((!_StopFlag) && (iMem != m_Ticks.nTicks));
    }

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

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


Класс C_Simulation

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

Но при работе с данными файла 1-минутных баров, на первый взгляд, не существует способа узнать, получены ли данные в этом конкретном файле с рынка, типа ФОРЕКС, или рынка, подобного фондовому. По крайней мере, в первый момент. Но присмотревшись к этим файлам и сравнив данные в них, вскоре будет заметна определенная закономерность. Благодаря этому паттерну можно очень четко и эффективно определить тип отображения, который следует использовать: BID или LAST. Это просто просмотр содержимого файла баров.

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

Я думаю, вы поняли, как система может узнать, будем ли мы проводить репликацию данных с рынка ФОРЕКС или с фондового рынка. Если вам непонятно, я рекомендую перечитать предыдущие статьи, пока вы действительно не поймете, как система может проводить такое различие. Но если вы уже поняли часть о репликации, давайте перейдем к важному вопросу: как система может узнать, будет ли она работать с данными с рынка, подобного ФОРЕКС, или фондового рынка, когда единственной информацией является файл 1-минутных баров? 

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

Если вы этого не поняли, предлагаем заглянуть на изображения ниже, где вы можете увидеть фрагменты файла с 1-минутными барами.


ФОРЕКС

Рисунок 02 – Файл актива ФОРЕКС


Фондовый рынок

Рисунок 03 – Файл актива фондового рынка

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


Заключительные соображения по данной статье

Теперь давайте разберемся в еще одном моменте: В статье "Разработка системы репликации - Моделирование рынка (Часть 11): Появление ТЕСТЕРА (I)", когда мы начали разрабатывать систему моделирования, мы использовали некоторые ресурсы, присутствующие в 1-минутных барах, для моделирования случайного блуждания. Как сообщается в файле, это будет вероятным движением рынка в пределах этого 1-минутного бара. Однако ни в одном из моментов построения указанного механизма, в том числе и в статье "Разработка системы репликации - Моделирование рынка (Часть 15): Появление ТЕСТЕРА (V) – СЛУЧАЙНОЕ БЛУЖДАНИЕ", где было построено случайное блуждание, не было необходимости, точнее, я не считал нужным иметь дело со случаями, когда объем был равен нулю или тиковый объем был очень мал. Но поскольку существуют рынки и активы, которые имеют довольно своеобразные значения, возникает необходимость охватить и такие случаи. Если этого не сделать, сервис в какой-то момент зависнет или выйдет из строя, оставив вас в затруднительном положении, если вы захотите запустить моделирование. Помните: проблема возникает в случае моделирования. В случае репликации всё будет работать нормально, лишь бы всё было в полной гармонии, так как не нужно будет работать с экзотическими ситуациями, вроде того, что мы будем делать в дальнейшем, где нам придется создавать, а точнее моделировать возможные движения рынка.

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



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

Прикрепленные файлы |
Market_Replay_7vc22.zip (14387.78 KB)
Популяционные алгоритмы оптимизации: Алгоритм оптимизации спиральной динамики (Spiral Dynamics Optimization, SDO) Популяционные алгоритмы оптимизации: Алгоритм оптимизации спиральной динамики (Spiral Dynamics Optimization, SDO)
В статье представлен алгоритм оптимизации, основанный на закономерностях построения спиральных траекторий в природе, таких как раковины моллюсков - алгоритм оптимизации спиральной динамики, SDO. Алгоритм, предложенный авторами, был мной основательно переосмыслен и модифицирован, в статье будет рассмотрено, почему эти изменения были необходимы.
Разработка системы репликации - Моделирование рынка (Часть 21):  ФОРЕКС (II) Разработка системы репликации - Моделирование рынка (Часть 21): ФОРЕКС (II)
Мы продолжим строить систему для работы на рынке ФОРЕКС. Поэтому для того, чтобы решить эту проблему необходимо сначала объявить загрузку тиков до загрузки предыдущих баров. Это решает проблему, но в то же время заставляет пользователя следовать некой структуре в конфигурационном файле, которая, лично для меня, не имеет особого смысла. Причина в том, что, разработав программу, которая отвечает за анализ и выполнение того, что находится в конфигурационном файле, мы можем позволить пользователю объявлять нужные ему элементы в любом порядке.
Разработка системы репликации - Моделирование рынка (Часть 23): ФОРЕКС (IV) Разработка системы репликации - Моделирование рынка (Часть 23): ФОРЕКС (IV)
Теперь создание происходит в той же точке, где мы преобразовывали тики в бары. Таким образом, если в процессе преобразования что-то пойдет не так, мы сразу же заметим ошибку. Это связано с тем, что тот же код, который размещает на графике 1-минутные бары при быстрой перемотке, также используется для системы позиционирования и для размещения баров при обычной перемотке. Другими словами, код, который отвечает за эту задачу, больше нигде не дублируется. Таким образом, мы получаем гораздо более совершенную систему как для поддержания, так и для улучшения.
Кросс-валидация и основы причинно-следственного вывода в моделях CatBoost, экспорт в ONNX формат Кросс-валидация и основы причинно-следственного вывода в моделях CatBoost, экспорт в ONNX формат
В данной статье предложен авторский способ создания ботов с использованием машинного обучения.