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

Разработка системы репликации — моделирование рынка (Часть 01): Первые эксперименты (I)

MetaTrader 5Тестер | 19 июня 2023, 16:43
1 033 0
Daniel Jose
Daniel Jose

Введение

Во время написания серии статей "Разработка торгового советника с нуля", некоторые моменты дали мне понять, что можно делать гораздо больше того, что делается при программировании на MQL5. Один конкретный момент заставил меня задуматься: когда я разработал систему построения графиков Times & Trade. В той статье я задал себе вопрос о том, можно ли выйти за рамки уже построенного.

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

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

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

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

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

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

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


Планирование

Сначала, необходимо понять, с чем мы имеем дело. Это может показаться странным, но действительно ли вы знаете, чего хотите добиться, когда используете систему репликации/моделирования?

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

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

"Чем больше места, тем больше беспорядка".

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

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

                       

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

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

Так, мы перейдем к действительно сложной части: внедрению системы репликации.


Реализация

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

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

В приложении я всегда буду предоставлять (на этом первом этапе) как минимум 2 НАСТОЯЩИЕ серии тиков любого актива за любой прошедший период. Эти данные нельзя больше получать, поскольку они были утеряны и не могут быть загружены. Это поможет нам изучить все детали. Однако вы также можете создать свою собственную базу НАСТОЯЩИХ тиков.


Создание собственной базы данных

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

Для этого откройте платформу MetaTrader 5, и нажмите клавиши быстрого доступа по умолчанию - CTRL + U. Перед вами появится экран. На этом экране вы должны указать актив и даты начала и окончания сбора данных, нажать кнопку для запроса данных и подождать несколько минут. Сервер вернет все нужные вам данные. Как только вы это сделаете, то просто экспортируйте эти данные и аккуратно сохраните их, так как они очень ценные.

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

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

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


Первый тест репликации

Некоторые могут подумать, что это простая задача, но мы скоро эту идею опровергнем. Другие могут спросить: почему бы не использовать тестер стратегий MetaTrader 5 для репликации? Причина в том, что он не позволяет нам выполнять репликации так, будто мы торгуем на рынке. Существует множество ограничений и трудностей при проведении репликации через тестер, и в итоге у нас не будет идеального погружения в репликацию, как будто мы по настоящему торгуем на рынке.

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

#property copyright "Daniel Jose"
#property icon "Resources\\App.ico"
#property description "Expert Advisor - Market Replay"
//+------------------------------------------------------------------+
int OnInit()
{
        EventSetTimer(60);
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
        EventKillTimer();
}
//+------------------------------------------------------------------+
void OnTick()
{
}
//+------------------------------------------------------------------+
void OnTimer()
{
}
//+------------------------------------------------------------------+

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


Ограничения функции EventSetMillisecondTimer

Давайте посмотрим, что говорится в документации:

"...  При работе в режиме реального времени события таймера генерируются не чаще, чем раз в 10-16 миллисекунд, из-за ограничений в оборудовании...".

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

Начнем с кода советника, который полностью представлен ниже:

#property copyright "Daniel Jose"
#property icon "Resources\\App.ico"
#property description "Expert Advisor - Market Replay"
//+------------------------------------------------------------------+
#include "Include\\C_Replay.mqh"
//+------------------------------------------------------------------+
input string user01 = "WINZ21_202110220900_202110221759";       //Arquivo de ticks
//+------------------------------------------------------------------+
C_Replay        Replay;
//+------------------------------------------------------------------+
int OnInit()
{
        Replay.CreateSymbolReplay(user01);
        EventSetMillisecondTimer(20);
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
        EventKillTimer();
}
//+------------------------------------------------------------------+
void OnTick() {}
//+------------------------------------------------------------------+
void OnTimer()
{
        Replay.Event_OnTime();
}
//+------------------------------------------------------------------+

Обратите внимание, что у нас событие OnTime будет происходить каждые 20 миллисекунд или около того, на что указывает выделенная строка в коде советника. Пожалуй вам покажется, что это слишком быстро, но так ли это на самом деле? Давайте это проверим. Помните, что в документации указано, что нельзя опускаться ниже 10-16 миллисекунд. Поэтому нет смысла устанавливать значение в 1 миллисекунду, поскольку событие не будет сгенерировано за это время.

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

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#define def_MaxSizeArray 134217727 // 128 Mbytes de posições
#define def_SymbolReplay "Replay"
//+------------------------------------------------------------------+
class C_Replay
{
};

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

В следующей строке задается имя актива, который будет использоваться для репликации. Довольно креативно с моей стороны назвать актив REPLAY, да ведь? 😂 Давайте продолжим с изучением класса. Вот следующий фрагмент кода, на который стоит обратить внимание:

void CreateSymbolReplay(const string FileTicksCSV)
{
        SymbolSelect(def_SymbolReplay, false);
        CustomSymbolDelete(def_SymbolReplay);
        CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\Replay\\%s", def_SymbolReplay), _Symbol);
        CustomRatesDelete(def_SymbolReplay, 0, LONG_MAX);
        CustomTicksDelete(def_SymbolReplay, 0, LONG_MAX);
        SymbolSelect(def_SymbolReplay, true);
        m_IdReplay = ChartOpen(def_SymbolReplay, PERIOD_M1);
        LoadFile(FileTicksCSV);
        Print("Executando teste de velocidade.");
}

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

#define macroRemoveSec(A) (A - (A % 60))
                void LoadFile(const string szFileName)
                        {
                                int file;
                                string szInfo;
                                double last;
                                long    vol;
                                uchar flag;
                                
                                if ((file = FileOpen("Market Replay\\Ticks\\" + szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
                                {
                                        ArrayResize(m_ArrayInfoTicks, def_MaxSizeArray);
                                        m_ArrayCount = 0;
                                        last = 0;
                                        vol = 0;
                                        for (int c0 = 0; c0 < 7; c0++) FileReadString(file);
                                        Print("Carregando dados para Replay.\nAguarde ....");
                                        while ((!FileIsEnding(file)) && (m_ArrayCount < def_MaxSizeArray))
                                        {
                                                szInfo = FileReadString(file);
                                                szInfo += " " + FileReadString(file);                                           
                                                m_ArrayInfoTicks[m_ArrayCount].dt = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19)));
                                                m_ArrayInfoTicks[m_ArrayCount].milisec = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                                m_ArrayInfoTicks[m_ArrayCount].Bid = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Ask = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Last = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Vol = StringToInteger(FileReadString(file));
                                                flag = m_ArrayInfoTicks[m_ArrayCount].flag = (uchar)StringToInteger(FileReadString(file));
                                                if (((flag & TICK_FLAG_ASK) == TICK_FLAG_ASK) || ((flag & TICK_FLAG_BID) == TICK_FLAG_BID)) continue;
                                                m_ArrayCount++;
                                        }
                                        FileClose(file);
                                        Print("Carregamento concluido.\nIniciando Replay.");
                                        return;
                                }
                                Print("Falha no acesso ao arquivo de dados de ticks.");
                                ExpertRemove();
                        };
#undef macroRemoveSec

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

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

Но есть кое-что довольно интересное в приведенной выше программе: функция FileReadString. Он считывает данные до тех пор, пока не найдет какой-либо разделитель. Интересно отметить, что когда мы анализируем бинарные данные тикового файла, сгенерированного MetaTrader 5 и сохраненного в формате CSV, как было описано в начале статьи, мы получаем следующий результат.


Желтая область представляет собой заголовок файла и показывает нам организацию внутренней структуры, которая следует далее. Зеленая область представляет собой первую строку данных. Теперь давайте посмотрим на синие точки (они являются разделителями), присутствующими в этом формате. 0D и 0A обозначают новую строку, а 09 - табуляцию (КЛАВИША KEY). Когда используем функцию FileReadString, не нужно накапливать данные для ее проверки. Сама функция делает это за нас. Всё, что мне нужно сделать - это преобразовать данные в правильный тип: это очень упрощает задачу. Однако посмотрите на следующий фрагмент кода.

if (((flag & TICK_FLAG_ASK) == TICK_FLAG_ASK) || ((flag & TICK_FLAG_BID) == TICK_FLAG_BID)) continue;

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

Ниже показываем последнюю процедуру в нашей тестовой системе:

#define macroGetMin(A)  (int)((A - (A - ((A % 3600) - (A % 60)))) / 60)
                void Event_OnTime(void)
                        {
                                bool isNew;
                                static datetime _dt = 0;
                                
                                if (m_ReplayCount >= m_ArrayCount) return;
                                if (m_dt == 0)
                                {
                                        m_Rate[0].close = m_Rate[0].open =  m_Rate[0].high = m_Rate[0].low = m_ArrayInfoTicks[m_ReplayCount].Last;
                                        m_Rate[0].tick_volume = 0;
                                        _dt = TimeLocal();
                                }
                                isNew = m_dt != m_ArrayInfoTicks[m_ReplayCount].dt;
                                m_dt = (isNew ? m_ArrayInfoTicks[m_ReplayCount].dt : m_dt);
                                m_Rate[0].close = m_ArrayInfoTicks[m_ReplayCount].Last;
                                m_Rate[0].open = (isNew ? m_Rate[0].close : m_Rate[0].open);
                                m_Rate[0].high = (isNew || (m_Rate[0].close > m_Rate[0].high) ? m_Rate[0].close : m_Rate[0].high);
                                m_Rate[0].low = (isNew || (m_Rate[0].close < m_Rate[0].low) ? m_Rate[0].close : m_Rate[0].low);
                                m_Rate[0].tick_volume = (isNew ? m_ArrayInfoTicks[m_ReplayCount].Vol : m_Rate[0].tick_volume + m_ArrayInfoTicks[m_ReplayCount].Vol);
                                m_Rate[0].time = m_dt;
                                CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
                                m_ReplayCount++;
                                if ((macroGetMin(m_dt) == 1) && (_dt > 0))
                                {
                                        Print(TimeToString(_dt, TIME_DATE | TIME_SECONDS), " ---- ", TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));
                                        _dt = 0;
                                }
                        };
#undef macroGetMin

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

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



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


Улучшение кода

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

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

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

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

Чтобы проверить этот момент, внесем первое изменение в код:

#define macroRemoveSec(A) (A - (A % 60))
                void LoadFile(const string szFileName)
                        {

// ... Código interno ...
                                        FileClose(file);
                                        Print("Carregamento concluido.\nFoi gerados ", m_ArrayCount, " posições de movimento.\nIniciando Replay.");
                                        return;
                                }
                                Print("Falha no acesso ao arquivo de dados de ticks.");
                                ExpertRemove();
                        };
#undef macroRemoveSec

Указанная дополнительная часть сделает это за нас. Давайте теперь посмотрим на первый запуск и на то, что происходит. Всё это можно увидеть на изображении ниже:

Теперь у нас есть некоторый параметр, который позволяет проверить, помогают ли модификации или нет. Если сделать расчет, то мы увидим, что на генерацию 1 минуты данных ушло почти 3 минуты. Другими словами, система очень далека от приемлемой.

Поэтому мы внесем небольшие доработки в код:

#define macroRemoveSec(A) (A - (A % 60))
                void LoadFile(const string szFileName)
                        {
                                int file;
                                string szInfo;
                                double last;
                                long    vol;
                                uchar flag;
                                
                                
                                if ((file = FileOpen("Market Replay\\Ticks\\" + szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
                                {
                                        ArrayResize(m_ArrayInfoTicks, def_MaxSizeArray);
                                        m_ArrayCount = 0;
                                        last = 0;
                                        vol = 0;
                                        for (int c0 = 0; c0 < 7; c0++) FileReadString(file);
                                        Print("Carregando dados para Replay.\nAguarde ....");
                                        while ((!FileIsEnding(file)) && (m_ArrayCount < def_MaxSizeArray))
                                        {
                                                szInfo = FileReadString(file);
                                                szInfo += " " + FileReadString(file);                                           
                                                m_ArrayInfoTicks[m_ArrayCount].dt = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19)));
                                                m_ArrayInfoTicks[m_ArrayCount].milisec = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                                m_ArrayInfoTicks[m_ArrayCount].Bid = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Ask = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Last = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Vol = vol + StringToInteger(FileReadString(file));
                                                flag = m_ArrayInfoTicks[m_ArrayCount].flag = (uchar)StringToInteger(FileReadString(file));
                                                if (((flag & TICK_FLAG_ASK) == TICK_FLAG_ASK) || ((flag & TICK_FLAG_BID) == TICK_FLAG_BID)) continue;
                                                if (m_ArrayInfoTicks[m_ArrayCount].Last != last)
                                                {
                                                        last = m_ArrayInfoTicks[m_ArrayCount].Last;
                                                        vol = 0;
                                                        m_ArrayCount++;
                                                }else
                                                        vol += m_ArrayInfoTicks[m_ArrayCount].Vol;
                                        }
                                        FileClose(file);
                                        Print("Carregamento concluido.\nFoi gerados ", m_ArrayCount, " posições de movimento.\nIniciando Replay.");
                                        return;
                                }
                                Print("Falha no acesso ao arquivo de dados de ticks.");
                                ExpertRemove();
                        };
#undef macroRemoveSec

Добавление выделенных линий значительно улучшает результаты, как видно на изображении ниже:


Здесь мы улучшили производительность системы. Может показаться, что это не так много, но это всё же показывает, что ранние изменения сыграли решающую роль. Мы достигли приблизительного времени 2 минуты 29 секунд для генерации 1-минутного бара. Иными словами, произошло общее улучшение системы, но, хотя это и звучит обнадеживающе, у нас есть проблема, которая усложняет дело. Мы не можем уменьшить время между событиями, генерируемыми функцией EventSetMillisecondTimer, что заставляет нас задуматься о другом подходе.

Тем не менее, в систему внедрили небольшое улучшение, как показано ниже:

                void Event_OnTime(void)
                        {
                                bool isNew;
                                static datetime _dt = 0;
                                
                                if (m_ReplayCount >= m_ArrayCount) return;
                                if (m_dt == 0)
                                {
                                        m_Rate[0].close = m_Rate[0].open =  m_Rate[0].high = m_Rate[0].low = m_ArrayInfoTicks[m_ReplayCount].Last;
                                        m_Rate[0].tick_volume = 0;
                                        m_Rate[0].time = m_ArrayInfoTicks[m_ReplayCount].dt - 60;
                                        CustomRatesUpdate(def_SymbolReplay, m_Rate, 1);
                                        _dt = TimeLocal();
                                }

// ... Código interno ....

                        }

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

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


Дойти до крайности

Для этого нам необходимо внести последнее изменение в код. Посмотрите ниже:

#define macroRemoveSec(A) (A - (A % 60))
#define macroGetMin(A)  (int)((A - (A - ((A % 3600) - (A % 60)))) / 60)
                void LoadFile(const string szFileName)
                        {
                                int file;
                                string szInfo;
                                double last;
                                long    vol;
                                uchar flag;
                                datetime mem_dt = 0;
                                
                                if ((file = FileOpen("Market Replay\\Ticks\\" + szFileName + ".csv", FILE_CSV | FILE_READ | FILE_ANSI)) != INVALID_HANDLE)
                                {
                                        ArrayResize(m_ArrayInfoTicks, def_MaxSizeArray);
                                        m_ArrayCount = 0;
                                        last = 0;
                                        vol = 0;
                                        for (int c0 = 0; c0 < 7; c0++) FileReadString(file);
                                        Print("Carregando dados para Replay.\nAguarde ....");
                                        while ((!FileIsEnding(file)) && (m_ArrayCount < def_MaxSizeArray))
                                        {
                                                szInfo = FileReadString(file);
                                                szInfo += " " + FileReadString(file);                                           
                                                m_ArrayInfoTicks[m_ArrayCount].dt = macroRemoveSec(StringToTime(StringSubstr(szInfo, 0, 19)));
                                                m_ArrayInfoTicks[m_ArrayCount].milisec = (int)StringToInteger(StringSubstr(szInfo, 20, 3));
                                                m_ArrayInfoTicks[m_ArrayCount].Bid = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Ask = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Last = StringToDouble(FileReadString(file));
                                                m_ArrayInfoTicks[m_ArrayCount].Vol = vol + StringToInteger(FileReadString(file));
                                                flag = m_ArrayInfoTicks[m_ArrayCount].flag = (uchar)StringToInteger(FileReadString(file));
                                                if (((flag & TICK_FLAG_ASK) == TICK_FLAG_ASK) || ((flag & TICK_FLAG_BID) == TICK_FLAG_BID)) continue;
                                                if ((mem_dt == macroGetMin(m_ArrayInfoTicks[m_ArrayCount].dt)) && (last == m_ArrayInfoTicks[m_ArrayCount].Last)) vol += m_ArrayInfoTicks[m_ArrayCount].Vol; else
                                                {
                                                        mem_dt = macroGetMin(m_ArrayInfoTicks[m_ArrayCount].dt);
                                                        last = m_ArrayInfoTicks[m_ArrayCount].Last;
                                                        vol = 0;
                                                        m_ArrayCount++;
                                                }
                                        }
                                        FileClose(file);
                                        Print("Carregamento concluido.\nFoi gerados ", m_ArrayCount, " posições de movimento.\nIniciando Replay.");
                                        return;
                                }
                                Print("Falha no acesso ao arquivo de dados de ticks.");
                                ExpertRemove();
                        };
#undef macroRemoveSec
#undef macroGetMin

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

После этого мы протестировали систему с EventSetMillisecondTimer, равным 20, и получили следующий результат:

 

В данном случае результат составил 2 минуты 34 секунды для события длительностью 20 миллисекунд... Затем мы изменили значение EventSetMillisecondTimer на 10 (что является минимальным значением, указанным в документации) и получили результат:

 

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


Заключение

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

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

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

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


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

Прикрепленные файлы |
Replay.zip (10910.23 KB)
Нейросети — это просто (Часть 46): Обучение с подкреплением, направленное на достижение целей (GCRL) Нейросети — это просто (Часть 46): Обучение с подкреплением, направленное на достижение целей (GCRL)
Предлагаю Вам познакомиться с ещё одним направлением в области обучения с подкреплением. Оно называется обучением с подкреплением, направленное на достижение целей (Goal-conditioned reinforcement learning, GCRL). В этом подходе агент обучается достигать различных целей в определенных сценариях.
MQL5 — Вы тоже можете стать мастером этого языка MQL5 — Вы тоже можете стать мастером этого языка
В этой статье я проведу нечто вроде интервью с самим собой и расскажу, как я делал свои первые шаги в языке MQL5. С помощью данного руководства я хочу помочь вам стать выдающимся программистом на MQL5, поэтому мы рассмотрим необходимые основы, чтобы достичь этого. Всё, что вам нужно иметь при себе - это искреннее желание учиться.
Разработка системы репликации - Моделирование рынка (Часть 02): Первые эксперименты (II) Разработка системы репликации - Моделирование рынка (Часть 02): Первые эксперименты (II)
В этот раз попробуем другой подход для достижения цели в 1 минуту. Однако эта задача не так проста, как можно подумать.
Интеграция ML-моделей с тестером стратегий (Часть 3): Управление файлами CSV(II) Интеграция ML-моделей с тестером стратегий (Часть 3): Управление файлами CSV(II)
Данный материал - полное руководство по созданию класса в MQL5 для эффективного управления CSV-файлами. Вы поймете, как реализуются методы открытия, записи, чтения и преобразования данных и как можно использовать их для хранения и доступа к информации. Кроме того, мы обсудим ограничения и важнейшие аспекты использования такого класса. Это ценный материал для тех, кто хочет научиться обрабатывать CSV-файлы в MQL5.