English 中文 Español Deutsch 日本語 Português
preview
Разработка системы репликации - Моделирование рынка (Часть 12): Появление СИМУЛЯТОРА (II)

Разработка системы репликации - Моделирование рынка (Часть 12): Появление СИМУЛЯТОРА (II)

MetaTrader 5Тестер | 27 сентября 2023, 10:50
632 0
Daniel Jose
Daniel Jose

Введение

В предыдущей статье "Разработка системы репликации - Моделирование рынка (Часть 11): Появление СИМУЛЯТОРА (I)", мы сделали нашу систему репликации/моделирования способной использовать 1-минутные бары для моделирования возможных движений рынка. Хотя, возможно, прочитав данный материал, вы заметили, что движения не так уж похожи на движения реального рынка. Тогда я вам показал моменты, которые необходимо изменить, чтобы система была еще ближе к тому, что вы найдете на реальном рынке. Однако, сколько бы попыток и экспериментов вы не делали, при использовании простых методов у вас не получится создать нечто похожее на возможные и вероятные движения рынка.


Начало реализации

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

void InitSymbolReplay(void)
        {
                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);
        }

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

srand(5);

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

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


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

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

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

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

inline int SimuleBarToTicks(const MqlRates &rate, MqlTick &tick[])
                        {
                                int t0 = 0;
                                long v0, v1, v2, msc;
                                bool b1 = ((rand() & 1) == 1);
                                double p0, p1;
                                                                
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                p0 = (b1 ? rate.low : rate.high);
                                p1 = (b1 ? rate.high : rate.low);
                                Pivot(rate.open, p0, t0, tick);
                                Pivot(p0, p1, t0, tick);
                                Pivot(p1, rate.close, t0, tick, true);
                                v0 = (long)(rate.real_volume / (t0 + 1));
                                v1 = 0;
                                msc = 5;
                                v2 = ((60000 - msc) / (t0 + 1));
                                for (int c0 = 0; c0 <= t0; c0++, v1 += v0)
                                {
                                        tick[c0].volume_real = (v0 * 1.0);
                                        tick[c0].time = rate.time + (datetime)(msc / 1000);
                                        tick[c0].time_msc = msc % 1000;
                                        msc += v2;
                                }
                                tick[t0].volume_real = ((rate.real_volume - v1) * 1.0);
                                
                                return t0;
                        }

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

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

#define def_NPASS 3
inline int SimuleBarToTicks(const MqlRates &rate, MqlTick &tick[])
                        {
                                int t0 = 0;
                                long v0, v1, v2, msc;
                                bool b1 = ((rand() & 1) == 1);
                                double p0, p1, p2;
                                                                
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                p0 = (b1 ? rate.low : rate.high);
                                p1 = (b1 ? rate.high : rate.low);
                                p2 = floor((rate.high - rate.low) / def_NPASS);
                                Pivot(rate.open, p0, t0, tick);
                                for (int c0 = 1; c0 < def_NPASS; c0++, p0 = (b1 ? p0 + p2 : p0 - p2)) Pivot(p0, (b1 ? p0 + p2 : p0 - p2), t0, tick);
                                Pivot(p0, p1, t0, tick);
                                Pivot(p1, rate.close, t0, tick, true);
                                v0 = (long)(rate.real_volume / (t0 + 1));
                                v1 = 0;
                                msc = 5;
                                v2 = ((60000 - msc) / (t0 + 1));
                                for (int c0 = 0; c0 <= t0; c0++, v1 += v0)
                                {
                                        tick[c0].volume_real = (v0 * 1.0);
                                        tick[c0].time = rate.time + (datetime)(msc / 1000);
                                        tick[c0].time_msc = msc % 1000;
                                        msc += v2;
                                }
                                tick[t0].volume_real = ((rate.real_volume - v1) * 1.0);
                                
                                return t0;
                        }
#undef def_NPASS

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

Чтобы понять, что здесь произошло, можно посмотреть на следующие изображения.

До добавления новых сегментов.


Хотя всё работало нормально, когда мы используем версию, которая позволяет нам разделить амплитуду на диапазоны, мы будем иметь следующий сценарий:

Рисунок 02

После изменения начинаем делить ширину бара на 3.


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

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

Если вы обратили внимание на изменения, внесенные на предыдущем этапе, вы должны были заметить кое-что любопытное в последнем коде. На мгновение мы получим под свой контроль всё тело бара и сможем делать с ним всё, что захотим. В отличие от других моментов, когда мы имеем относительно направленное движение, либо от открытия к максимуму или минимуму, либо в момент закрытия бара, оставляя максимум или минимум в сторону цены закрытия. Когда нам нужно работать со всем телом бара, мы выполняем внутри него очень мало работы.  Как бы мы ни старались, мы всегда застреваем в одной и той же ситуации, но если присмотреться, то можно заметить, что у нас всегда есть два значения, над которыми можно работать. Начальная точка и конечная точка. И почему надо обратить внимание на этот момент? Подумайте немного: У нас есть 60 тысяч миллисекунд для создания 1-минутного бара, если мы оставим запас в 5 миллисекунд в начале бара, у нас всё равно будет немало времени. Если мы проведем несколько простых расчетов, мы заметим, что теряем много времени, которое можно было бы использовать для того, чтобы сделать моделирование бара намного сложнее.

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

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

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

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


Если тиков нет, почему сервис активен?

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

                bool SetSymbolReplay(const string szFileConfig)
                        {
#define macroERROR(MSG) { FileClose(file); MessageBox((MSG != "" ? MSG : StringFormat("Допущена ошибка в строке %d", iLine)), "Market Replay", MB_OK); return false; }
                                int     file,
                                        iLine;
                                string  szInfo;
                                char    iStage;
                                bool    bBarPrev;
                                MqlRates rate[1];
                                
                                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, def_BarsDiary);
                                m_Ticks.nRate = -1;
                                m_Ticks.Rate[0].time = 0;
                                iStage = 0;
                                iLine = 1;
                                bBarPrev = false;
                                while ((!FileIsEnding(file)) && (!_StopFlag))
                                {
                                        switch (GetDefinition(FileReadString(file), szInfo))
                                        {
                                                case Transcription_DEFINE:
                                                        if (szInfo == def_STR_FilesBar) iStage = 1; else
                                                        if (szInfo == def_STR_FilesTicks) iStage = 2; else
                                                        if (szInfo == def_STR_TicksToBars) iStage = 3; else
                                                        if (szInfo == def_STR_BarsToTicks) iStage = 4; else
                                                        if (szInfo == def_STR_ConfigSymbol) iStage = 5; else
                                                                macroERROR(StringFormat("%s не распознается в системе\nв строке %d.", szInfo, iLine));
                                                        break;
                                                case Transcription_INFO:
                                                        if (szInfo != "") switch (iStage)
                                                        {
                                                                case 0:
                                                                        macroERROR(StringFormat("Команда не распознана в строке %d\nконфигурационного файла.", iLine));
                                                                        break;
                                                                case 1:
                                                                        if (!LoadPrevBars(szInfo)) macroERROR("");
                                                                        bBarPrev = true;
                                                                        break;
                                                                case 2:
                                                                        if (!LoadTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 3:
                                                                        if (!LoadTicksReplay(szInfo, false)) macroERROR("");
                                                                        bBarPrev = true;
                                                                        break;
                                                                case 4:
                                                                        if (!LoadBarsToTicksReplay(szInfo)) macroERROR("");
                                                                        break;
                                                                case 5:
                                                                        if (!Configs(szInfo)) macroERROR("");
                                                                        break;
                                                        }
                                                        break;
                                        };
                                        iLine++;
                                }
                                FileClose(file);
                                if (m_Ticks.nTicks <= 0)
                                {
                                        MessageBox("Тики отсутствуют.\nЗакрываем сервис...", "Market Replay", MB_OK);
                                        return false;
                                }
                                if (!bBarPrev)
                                {
                                        rate[0].close = rate[0].open =  rate[0].high = rate[0].low = m_Ticks.Info[0].last;
                                        rate[0].tick_volume = 0;
                                        rate[0].real_volume = 0;
                                        rate[0].time = m_Ticks.Info[0].time - 60;
                                        CustomRatesUpdate(def_SymbolReplay, rate, 1);
                                }
                                
                                return (!_StopFlag);
#undef macroERROR
                        }

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

Однако, внеся исправления, упомянутые выше, всё будет решено. 


Реализация ТИКОВОГО ОБЪЕМА

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

В нем можно увидеть два значения объема. Один из них — тиковый объем, а другой — объем (в данном случае фактический объем). Но, глядя на это изображение, не могли бы вы сказать мне, в чем разница между реальным и тиковым объемами? Если вы не знаете разницы, сейчас самое время наконец-то узнать ее.

ОБЪЕМ или РЕАЛЬНЫЙ ОБЪЕМ – это, по сути, количество контрактов, торгуемых в данный конкретный момент времени. Он всегда будет кратным значению, которое зависит от рассматриваемого актива. Например, некоторые активы не позволяют торговать со значениями меньше 5, а другие принимают дробные значения. Не пытайтесь понять, почему это возможно, просто надо знать, что можно торговать дробными значениями. Это значение легко понять и, возможно, именно поэтому многие его используют. Теперь, если мы возьмем значение РЕАЛЬНОГО ОБЪЕМА и умножим его на минимальную стоимость каждого контракта, мы получим другое значение, называемое ФИНАНСОВЫМ ОБЪЕМОМ. MetaTrader 5 напрямую не предоставляет это значение, но, как вы видели, его легко получить. Таким образом, торговый сервер понимает, что ему не нужно сообщать этот ФИНАНСОВЫЙ ОБЪЕМ торговым терминалам. Программисты или пользователи платформы должны реализовать указанный расчет.

Теперь ТИКОВЫЙ ОБЪЕМ , это уже совершенно другой объем. Он предусмотрен только в содержимом баров по простой причине: мы не сможем сказать, что произошло во время торговли, просто взглянув на фактический объем. Нам нужна дополнительная информация — тиковый объем. Но почему тиковый объем присутствует, когда мы запрашиваем бары, но не присутствует, когда мы запрашиваем тики? А какой объем появляется, когда мы запрашиваем тики? Если вы никогда не обращали внимание на это (или еще не видели), можно посмотреть на изображение ниже:

Значения, указанные в поле ОБЪЕМ, ,повторяю, НЕ представляют тиковый объем. Это значение, по сути, и есть РЕАЛЬНЫЙ ОБЪЕМ. Однако, как мы можем узнать тиковый объем, если он не сообщается при запросе тиков? Он появляется только тогда, когда мы запрашиваем бары. Дело в том, что точно так же, как сервер понимает, что ему не нужно сообщать ФИНАНСОВЫЙ ОБЪЕМ, он также понимает, что, предоставляя торгуемые тики, мы сможем рассчитать ТИКОВЫЙ ОБЪЕМ. Это отличается от того, что произошло бы, если бы мы запросили бары, когда у нас нет доступа к фактически торгуемым тикам.

Всё еще не понимаете? Имея данные о фактически торгуемых тиках, мы можем рассчитать тиковый объем. Но как? Разве есть какая-то таинственная формула? Потому что каждый раз, когда я пытаюсь, я не могу добиться того, чтобы значения совпали. Не волнуйтесь, дорогие читатели. Волшебной формулы не существует. Дело в том, что вы, пожалуй, не совсем понимаете, что на самом деле представляет из себя ТИКОВЫЙ ОБЪЕМ. В этой и предыдущей статьях мы использовали метод моделирования движения внутри минутного бара. Хотя данное движение приводит к затрагиванию всех цен, фактически создаваемый нами тиковый объем намного меньше тикового объема, сообщаемого на 1-минутном баре.

Но как так? Не волнуйтесь. Вы это лучше поймете в следующей статье, где мы фактически смоделируем тот же тиковый объем. Упомянув это, я думаю, вы поняли, что такое тиковый объем. Тиковый объем — это количество сделок, которые фактически произошли в пределах данного бара. У нас этот объем в среднем составляет около 150. На самом деле в среднем оно часто составляет около 12 890.

Однако, можно подумать: Как тогда мне рассчитать этот тиковый объем? Это очень легко сделать. Давайте посмотрим, сможет ли наша система выполнить этот расчет. Ведь чтобы понять это, вам действительно нужно увидеть расчеты в действии.

В настоящее время, этот расчет по разным причинам производится в двух местах. Первое место чуть ниже:

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

На данном этапе мы рассчитываем объем тиков, которые будут присутствовать в баре. Второй пункт находится ниже:

inline int Event_OnTime(void)
                        {
                                bool    bNew;
                                int     mili, iPos;
                                u_Interprocess Info;
                                static MqlRates Rate[1];
                                static datetime _dt = 0;
                                datetime tmpDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                
                                if (m_ReplayCount >= m_Ticks.nTicks) return -1;
                                if (bNew = (_dt != tmpDT))
                                {
                                        _dt = tmpDT;
                                        Rate[0].real_volume = 0;
                                        Rate[0].tick_volume = 0;
                                }
                                mili = (int) m_Ticks.Info[m_ReplayCount].time_msc;
                                do
                                {
                                        while (mili == m_Ticks.Info[m_ReplayCount].time_msc)
                                        {
                                                Rate[0].close = m_Ticks.Info[m_ReplayCount].last;
                                                Rate[0].open = (bNew ? Rate[0].close : Rate[0].open);
                                                Rate[0].high = (bNew || (Rate[0].close > Rate[0].high) ? Rate[0].close : Rate[0].high);
                                                Rate[0].low = (bNew || (Rate[0].close < Rate[0].low) ? Rate[0].close : Rate[0].low);
                                                Rate[0].real_volume += (long) m_Ticks.Info[m_ReplayCount].volume_real;
                                                Rate[0].tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > 0 ? 1 : 0);
                                                bNew = false;
                                                m_ReplayCount++;
                                        }
                                        mili++;
                                }while (mili == m_Ticks.Info[m_ReplayCount].time_msc);
                                Rate[0].time = _dt;
                                CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                                iPos = (int)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
                                GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                if (Info.s_Infos.iPosShift != iPos)
                                {
                                        Info.s_Infos.iPosShift = (ushort) iPos;
                                        GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                                }
                                return (int)(m_Ticks.Info[m_ReplayCount].time_msc < mili ? m_Ticks.Info[m_ReplayCount].time_msc + (1000 - mili) : m_Ticks.Info[m_ReplayCount].time_msc - mili);
                        }

На этом этапе мы делаем то же самое, т.е. рассчитываем тиковый объем. Но так ли это? Да, верно. Тиковый объем рассчитывается таким образом, то есть будут учитываться только тики, которые действительно указывают на совершение сделок. По одному на каждую выполненную операцию. То есть тики, у которых активны флаги BID или ASK, в расчете не участвуют, а будут рассчитываться только те, у которых есть флаг SELL или BUY. Но поскольку данные флаги будут активны только тогда, когда значение цены или фактический объем больше нуля, мы не выполняем проверку флагов, ведь в этом нет необходимости.

ПРИМЕЧАНИЕ: Когда мы говорим о форексе, мы изменяем это. Но это будет описано только в специальных статьях по ФОРЕКСУ.

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


Настройка контрольной точки

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

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

                bool LoadTicksReplay(const string szFileNameCSV, const bool ToReplay = true)
                        {
                                int     file,
                                        old,
                                        MemNRates,
                                        MemNTicks;
                                string  szInfo = "";
                                MqlTick tick;
                                MqlRates rate,
                                        RatesLocal[];
                                
                                MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
                                MemNTicks = m_Ticks.nTicks;
                                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, def_BarsDiary, def_BarsDiary);
                                        old = m_Ticks.nTicks;
                                        for (int c0 = 0; c0 < 7; c0++) szInfo += FileReadString(file);
                                        if (szInfo != def_Header_Ticks)
                                        {
                                                Print("Arquivo ", 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 = 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 = (ToReplay ? m_Ticks.nTicks : 0);
                                                                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Невозможно продолжать...");
                                                FileClose(file);
                                                return false;
                                        }
                                        FileClose(file);
                                }else
                                {
                                        Print("Тиковый файл ", szFileNameCSV,".csv не найден...");
                                        return false;
                                }
                                if ((!ToReplay) && (!_StopFlag))
                                {
                                        ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
                                        ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
                                        CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
                                        m_dtPrevLoading = m_Ticks.Rate[m_Ticks.nRate].time;
                                        m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
                                        m_Ticks.nTicks = MemNTicks;
                                        ArrayFree(RatesLocal);
                                }
                                return (!_StopFlag);
                        };

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

Чтобы выполнить правильное преобразование, нам нужно будет изменить очень конкретную процедуру. Она расположена чуть ниже:

                long AdjustPositionReplay(const bool bViewBuider)
                        {
                                u_Interprocess  Info;
                                MqlRates        Rate[def_BarsDiary];
                                int             iPos,
                                                nCount;
                                
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                if (Info.s_Infos.iPosShift == (int)((m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks)) return 0;
                                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);
                                        if ((m_dtPrevLoading == 0) && (iPos == 0))
                                        {
                                                m_ReplayCount = 0;
                                                Rate[m_ReplayCount].close = Rate[m_ReplayCount].open = Rate[m_ReplayCount].high = Rate[m_ReplayCount].low = m_Ticks.Info[iPos].last;
                                                Rate[m_ReplayCount].tick_volume = Rate[m_ReplayCount].real_volume = 0;
                                                CustomRatesUpdate(def_SymbolReplay, Rate, 1);
                                        }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);) 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();
                        }

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

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


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

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

А теперь, посмотрите на видео. Пожалуйста, знайте, что я в курсе происходящего.



В приложении можно найти используемые здесь файлы. У вас также будет дополнительный файл, в котором показаны как 1-минутные бары, так и тики, торгуемые в один и тот же день. Запустите обе конфигурации и проверьте результаты, но прежде всего нужно понять, что происходит на графике.

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

Прикрепленные файлы |
Разработка системы репликации - Моделирование рынка (Часть 13): Появление СИМУЛЯТОРА (III) Разработка системы репликации - Моделирование рынка (Часть 13): Появление СИМУЛЯТОРА (III)
Здесь мы немного упростим несколько элементов, связанных с работой в следующей статье. Я также объясню, как можно визуализировать то, что генерирует симулятор с точки зрения случайности.
Как обнаруживать тренды и графические паттерны с помощью MQL5 Как обнаруживать тренды и графические паттерны с помощью MQL5
В статье представлен метод автоматического обнаружения моделей ценовых действий с помощью MQL5, таких как тренды (восходящий, нисходящий, боковой) и графические модели (двойная вершина, двойное дно).
Готовые шаблоны для подключения индикаторов в экспертах (Часть 3): Трендовые индикаторы Готовые шаблоны для подключения индикаторов в экспертах (Часть 3): Трендовые индикаторы
В этой справочной статье рассмотрим стандартные индикаторы из категории "Трендовые индикаторы". Создадим готовые к применению шаблоны использования этих индикаторов в советниках — объявление и установка параметров, инициализация и деинициализация индикаторов и получение данных и сигналов из индикаторных буферов в советниках.
Разработка MQTT-клиента для MetaTrader 5: методология TDD Разработка MQTT-клиента для MetaTrader 5: методология TDD
Статья представляет собой первую попытку разработать нативный MQTT-клиент для MQL5. MQTT - это протокол обмена данными по принципу "издатель - подписчик". Он легкий, открытый, простой и разработан так, чтобы его было легко внедрить. Это позволяет применять его во многих ситуациях.