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

Разработка системы репликации - Моделирование рынка (Часть 17): Тики и еще больше тиков (I)

MetaTrader 5Примеры | 3 ноября 2023, 09:39
769 0
Daniel Jose
Daniel Jose

Введение

В предыдущей статье "Разработка системы репликации — моделирование рынка (Часть 16): Новая система классов", в класс C_Replay были внесены необходимые изменения. Данные изменения призваны упростить несколько задач, которые нам нужно будет выполнить. Таким образом, класс C_Replay, который когда-то был слишком объёмным, прошел процесс упрощения, в ходе которого его сложность была распределена между другими классами. Таким образом, стало намного проще и легче реализовать новые функциональные возможности и улучшения в системе репликации/моделирования. Начиная с этой статьи, данные улучшения начнут проявляться и распространятся на семь последующих статей.

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

Большая проблема заключается в том, что во всех предыдущих статьях основное внимание уделялось исключительно построению графика, и этот график должен был быть представлен таким образом, чтобы актив репликации/моделирования вел себя очень похоже на то, что происходит на реальном рынке. Я знаю, что есть много тех, кто торгует, используя какие-то другие инструменты, например книгу заявок. Хотя лично я не считаю использование такого инструмента хорошей практикой; другие трейдеры считают, что существует некоторая корреляция между тем, что происходит в книге заявок, и тем, чем торгуется. Это нормально, у каждого человека своя точка зрения. Но, несмотря на это, существует инструмент, который многие используют в своей деятельности, — это тиковый график. Если не знаете, что это такое, то можете взглянуть на изображение на рисунке 01.


Рисунок 01

Рисунок 01 - Тиковый график

Данный график появляется в нескольких местах на платформе MetaTrader 5. Чтобы у вас было представление об этих местах, я упомяну несколько, которые входят в стандартную версию MetaTrader 5. Например: окно "Обзор рынка", как показано на рисунке 01. В книге заявок (рис. 02), а также в системе ордеров (рис. 03).

Помимо этих мест, также можно использовать какой-то индикатор, чтобы увидеть эту же информацию. Можно найти пример в «Разработка торгового советника с нуля (Часть 13): Times And Trade (II)". Все эти системы требуют, чтобы разрабатываемая нами система могла сообщать или передавать информацию о тиках соответствующим образом, но это не совсем та информация, которую мы видим на всех этих рисунках. Фактически мы видим изменение ценовых значений ASK и BID. Вот что на самом деле показывается. 


Рисунок 02

Рисунок 02 – Тиковый график в книге заявок


Рисунок 03

Рисунок 03 – Тиковый график в системе ордеров


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

Здесь мы начнем внедрять эту систему, но максимально простым способом. Сначала мы сделаем так, чтобы она появилась в окне "Обзор рынка" (рис. 01). После этого мы постараемся, чтобы она появилась и в других местах. Но просто заставить ее появиться в окне "Обзор рынка" будет непростой задачей. В то же время это будет интересно, так как при реализации и использовании моделирования движений с интервалом в 1 минуту, мы сможем наблюдать на тиковом графике в окне "Обзор рынка" то, как происходило СЛУЧАЙНОЕ БЛУЖДАНИЕ, созданное тестером. На самом деле это очень любопытно и интересно.

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

Давайте начнем реализовывать самую безумную вещь из всех, учитывая степень сложности. В системе, которая будет реализована, в первые моменты мы не будем использовать смоделированные данные. Поэтому в прикрепленном файле можно найти РЕАЛЬНЫЕ данные за 2 дня по 4 разным активам для того, чтобы у нас была хотя бы база для экспериментов. Вы не обязаны мне доверять, скорее наоборот. Я хочу, чтобы вы сами собрали реальные рыночные данные и сами протестировали их в системе. Таким образом, можно будет сделать собственные выводы о том, что происходит на самом деле, до того, как мы реализуем систему моделирования. Потому что на самом деле всё гораздо безумнее, чем может показаться на первый взгляд.


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

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

Итак, давайте начнем с небольшого изменения в служебном файле:

#property service
#property icon "\\Images\\Market Replay\\Icon.ico"
#property copyright "Daniel Jose"
#property version   "1.17"
#property description "Сервис репликации-моделирования для платформы MT5."
#property description "Он независим от индикатора Market Replay."
#property description "Можно узнать по подробнее в данной статье:"
#property link "https://www.mql5.com/ru/articles/11106"
//+------------------------------------------------------------------+
#define def_Dependence  "\\Indicators\\Market Replay.ex5"
#resource def_Dependence
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
input string            user00 = "Mini Indice.txt";     //Конфигурационный файл "Replay".
input ENUM_TIMEFRAMES   user01 = PERIOD_M5;             //Начальный таймфрейм для графика.
//input bool            user02 = false;                 //Визуализация конструкции баров. ( Временно заблокирована )
input bool              user03 = true;                  //Визуализация метрик создания.
//+------------------------------------------------------------------+
void OnStart()
{
        C_Replay        *pReplay;

        pReplay = new C_Replay(user00);
        if ((*pReplay).ViewReplay(user01))
        {
                Print("Разрешение получено. Сервис репликации теперь может быть использован...");
                while ((*pReplay).LoopEventOnTime(false, user03));
        }
        delete pReplay;
}
//+------------------------------------------------------------------+

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

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

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

inline bool ReadAllsTicks(void)
                        {
#define def_LIMIT (INT_MAX - 2)
                                string   szInfo;
                                MqlTick  tick;
                                MqlRates rate;
                                int      i0;
                                
                                Print("Загрузка тиков для репликации. Подождите...");
                                ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray);
                                i0 = m_Ticks.nTicks;
                                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);
                                        tick.time = StringToTime(StringSubstr(szInfo, 0, 19));
                                        tick.time_msc = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                        tick.bid = StringToDouble(FileReadString(m_File));
                                        tick.ask = StringToDouble(FileReadString(m_File));
                                        tick.last = StringToDouble(FileReadString(m_File));
                                        tick.volume_real = StringToDouble(FileReadString(m_File));
                                        tick.flags = (uchar)StringToInteger(FileReadString(m_File));
                                        if ((m_Ticks.Info[i0].last == tick.last) && (m_Ticks.Info[i0].time == tick.time) && (m_Ticks.Info[i0].time_msc == tick.time_msc))
                                                m_Ticks.Info[i0].volume_real += tick.volume_real;
                                        else
                                        {
                                                m_Ticks.Info[m_Ticks.nTicks] = tick;
                                                if (tick.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, tick) ? 1 : 0);
                                                        m_Ticks.Rate[m_Ticks.nRate] = rate;
                                                        m_Ticks.nTicks++;
                                                }
                                                i0 = (m_Ticks.nTicks > 0 ? m_Ticks.nTicks - 1 : i0);
                                        }
                                }
                                FileClose(m_File);
                                if (m_Ticks.nTicks == def_LIMIT)
                                {
                                        Print("Слишком много данных в тиковом файле.\nНевозможно продолжать...");
                                        return false;
                                }
                                return (!_StopFlag);
#undef def_LIMIT
                        }

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

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);
                                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 = (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));
                                        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 и ASK. Кроме того, он больше не накапливает значения, если позиция это позволяет, то есть он считывает данные целиком и сохраняет их в памяти, потому что изменения не сгенерировали никаких новых процедур внутри функции. Скорее, они упростили ее. Думаю, что у вас (вы ведь прочитали эту серию статей) не возникнет затруднений с пониманием того, что происходит на самом деле, но сам факт этих упрощений имеет последствия в других местах кода. Некоторые из этих пунктов сильно пострадают, и именно поэтому пришлось отключить некоторые компоненты, пока весь код снова не заработает стабильно.

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

После внесения этих первых изменений нам нужно будет внести еще одно немного странное, но все же необходимое изменение. Теперь, когда у нас будут значения, при которых объем не существует (значения BID и ASK), мы должны запустить систему в точке, где у нас есть некоторый указанный объем.

class C_ConfigService : protected C_FileTicks
{
        protected:
//+------------------------------------------------------------------+
                datetime m_dtPrevLoading;
                int      m_ReplayCount;
//+------------------------------------------------------------------+
inline void FirstBarNULL(void)
                        {
                                MqlRates rate[1];
                                
                                for(int c0 = 0; m_Ticks.Info[c0].volume_real == 0; c0++)
                                        rate[0].close = m_Ticks.Info[c0].last;
                                rate[0].open = rate[0].high = rate[0].low = rate[0].close;
                                rate[0].tick_volume = 0;
                                rate[0].real_volume = 0;
                                rate[0].time = m_Ticks.Info[0].time - 60;
                                CustomRatesUpdate(def_SymbolReplay, rate);
                                m_ReplayCount = 0;
                        }
//+------------------------------------------------------------------+

//... Остальная часть класса ...

}

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

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


Модификация класса C_Replay

Изменения здесь начинаются более простым способом и становятся довольно странными. Начнем с самого простого изменения, приведенного ниже:

                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)
                                        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);
                                        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();
                                Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay);
                                Info.s_Infos.isWait = false;
                                GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value);
                        }

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

Чтобы объяснить, как это было и возможно ли добавлять тики в окно "Обзора рынка", необходимо взглянуть на исходную функцию создания бара. Можно увидеть это ниже:

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

                                static ulong _mdt = 0;
                                int i;
                                
                                if (m_MountBar.bNew = (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time)))
                                {
                                        if (bViewMetrics)
                                        {
                                                _mdt = (_mdt > 0 ? GetTickCount64() - _mdt : _mdt);
                                                i = (int) (_mdt / 1000);
                                                Print(TimeToString(m_Ticks.Info[m_ReplayCount].time, TIME_SECONDS), " - Metrica: ", i / 60, ":", i % 60, ".", (_mdt % 1000));
                                                _mdt = GetTickCount64();
                                        }
                                        m_MountBar.memDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                        def_Rate.real_volume = 0;
                                        def_Rate.tick_volume = 0;
                                }
                                def_Rate.close = m_Ticks.Info[m_ReplayCount].last;
                                def_Rate.open = (m_MountBar.bNew ? def_Rate.close : def_Rate.open);
                                def_Rate.high = (m_MountBar.bNew || (def_Rate.close > def_Rate.high) ? def_Rate.close : def_Rate.high);
                                def_Rate.low = (m_MountBar.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;
                                m_MountBar.bNew = false;
                                CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate, 1);
                                m_ReplayCount++;
                                
#undef def_Rate
                        }

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

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

                                bool bNew;

                                if (m_MountBar.memDT != macroRemoveSec(m_Ticks.Info[m_ReplayCount].time))
                                {                               
                                        if (bViewMetrics) Metrics();
                                        m_MountBar.memDT = macroRemoveSec(m_Ticks.Info[m_ReplayCount].time);
                                        def_Rate.real_volume = 0;
                                        def_Rate.tick_volume = 0;
                                }
                                bNew = (def_Rate.tick_volume == 0);
                                def_Rate.close = (m_Ticks.Info[m_ReplayCount].volume_real > 0.0 ? m_Ticks.Info[m_ReplayCount].last : 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);
                                ViewTick();
                                m_ReplayCount++;
                                
#undef def_Rate
                        }

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

inline void Metrics(void)
                        {
                                int i;
                                static ulong _mdt = 0;
                                
                                _mdt = (_mdt > 0 ? GetTickCount64() - _mdt : _mdt);
                                i = (int) (_mdt / 1000);
                                Print(TimeToString(m_Ticks.Info[m_ReplayCount].time, TIME_SECONDS), " - Metrica: ", i / 60, ":", i % 60, ".", (_mdt % 1000));
                                _mdt = GetTickCount64();
                                
                        }

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

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

Код для второго вызова можно увидеть ниже:

inline void ViewTick(void)
                        {
                                MqlTick tick[1];

                                tick[0] = m_Ticks.Info[m_ReplayCount];
                                tick[0].time_msc = (m_Ticks.Info[m_ReplayCount].time * 1000) + m_Ticks.Info[m_ReplayCount].time_msc;
                                CustomTicksAdd(def_SymbolReplay, tick);
                        }

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

Ниже приводим содержание документации:

Дополнительное примечание

Функция CustomTicksAdd работает только для пользовательских символов, открытых в окне "Market Watch" (Обзор рынка). Если символ не выбран в MarketWatch, то для вставки тиков необходимо использовать CustomTicksReplace.

Функция CustomTicksAdd позволяет транслировать тики так, как если бы они приходили от сервера брокера. Данные записываются не напрямую в базу тиков, а отправляются в окно "Обзор рынка". И уже из него терминал сохраняет тики в своей базе. При большом объеме данных, передаваемых за один вызов, функция меняет свое поведение для экономии ресурсов. Если передается более 256 тиков, данные делятся на две части. Первая часть (большая) сразу напрямую записывается в базу тиков (как это делает CustomTicksReplace). Вторая часть, состоящая из последних 128 тиков, передается в окно "Обзор рынка" и после этого сохраняется терминалом в базе.

Структура MqlTick имеет два поля со значением времени – time (время тика в секундах) и time_msc (время тика в миллисекундах) – которые ведут отсчет от 01 января 1970 года. Обработка этих полей в добавляемых тиках производится по следующим правилам в указанном порядке:

  1. если значение ticks[k].time_msc!=0, то используем его для заполнения поля ticks[k].time, то есть для тика выставляется время ticks[k].time=ticks[k].time_msc/1000 (деление целочисленное)
  2. если ticks[k].time_msc==0 и ticks[k].time!=0, то время в миллисекундах получается умножением на 1000, то есть ticks[k].time_msc=ticks[k].time*1000
  3. если ticks[k].time_msc==0 и ticks[k].time==0, то в эти поля записывается текущее время торгового сервера с точностью до миллисекунд на момент вызова функции CustomTicksAdd.

Если значение полей ticks[k].bid, ticks[k].ask, ticks[k].last или ticks[k].volume больше нуля, то в поле ticks[k].flags пишется комбинация соответствующих флагов:

  • TICK_FLAG_BID — тик изменил цену Bid.
  • TICK_FLAG_ASK — тик изменил цену Ask.
  • TICK_FLAG_LAST — тик изменил цену последней сделки.
  • TICK_FLAG_VOLUME — тик изменил объем.

Если значение какого-то поля меньше или равно нулю, соответствующий ему флаг не записывается в поле ticks[k].flags. 

Флаги TICK_FLAG_BUY и TICK_FLAG_SELL в историю пользовательского инструмента не добавляются.

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

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

Таким образом, функция CustomTicksAdd сможет вставлять данные в "Обзор рынка". Но дело не только в этом: при вводе этих данных в систему, на строящемся графике также появятся ценовые линии BID, цена ASK и последняя ценовая линия. Иными словами, в качестве бонуса за то, что удалось вставить тики в "Обзор рынка", мы еще получили ценовые линии на графике. Этого не было из-за отсутствия такого вида функциональности. Однако пока рано радоваться, ведь система еще не завершена. Есть еще кое-что, что нужно проверить, исправить и собрать. Вот почему мы используем и предоставляем данные из REAL TICKS, чтобы проверить этот новый этап системы репликации/моделирования.


Заключительные мысли

Статья подходит к концу, поскольку выполнение последующих действий может привести к некой путанице в уже представленном материале. Таким образом, в следующей статье мы рассмотрим, как исправить некоторые моменты, которые не работают должным образом в текущей системе. Однако можно использовать систему без быстрой «перемотки» вперед или назад. Если это сделать, тиковые данные в "Обзоре рынка" или информация о ценовых линиях могут не соответствовать текущей ситуации на графике репликации/моделирования.

Чтобы не отдавать предпочтение только использованию контрактов типа мини-индекса, я хочу, чтобы вы протестировали данную систему на других активах. Это прояснит, как система репликации/моделирования будет вести себя по отношению к тому, что мы в нее внедряем. Я просто хочу прояснить один факт: в системе быстрой прокрутки всё еще есть некоторые недостатки. Поэтому я предлагаю вам, по крайней мере на данный момент, избегать использования быстрой «перемотки» вперед.

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

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


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

Прикрепленные файлы |
Разработка показателя качества советников Разработка показателя качества советников
В этой статье мы объясним, как разработать показатель качества, который ваш советник сможет отображать в тестере стратегии. Мы познакомимся с двумя известными методами расчета (Ван Тарп и Санни Харрис).
Популяционные алгоритмы оптимизации: Алгоритм поиска системой зарядов (Charged System Search, CSS) Популяционные алгоритмы оптимизации: Алгоритм поиска системой зарядов (Charged System Search, CSS)
В этой статье рассмотрим ещё один алгоритм оптимизации, инспирированный неживой природой - алгоритм поиска системой зарядов (CSS). Цель этой статьи - представить новый алгоритм оптимизации, основанный на принципах физики и механики.
Нейросети — это просто (Часть 62): Использование Трансформера решений в иерархических моделях Нейросети — это просто (Часть 62): Использование Трансформера решений в иерархических моделях
В последних статьях мы познакомились с несколькими вариантами использования метода Decision Transformer. Который позволяет анализировать не только текущее состояние, но и траекторию предшествующих состояний и, совершенных в них, действий. В данной статье я предлагаю Вам познакомиться с вариантом использования данного метода в иерархических моделях.
Квантование в машинном обучении (Часть 1): Теория, пример кода, разбор реализации в CatBoost Квантование в машинном обучении (Часть 1): Теория, пример кода, разбор реализации в CatBoost
В настоящей статье речь пойдёт о теоретическом применении квантования при построении древовидных моделей. Рассмотрены реализованные методы квантования в CatBoost. Материал будет подан без сложных математических формул, доступным языком.