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

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

MetaTrader 5Тестер | 23 ноября 2023, 09:17
686 0
Daniel Jose
Daniel Jose

Введение

В предыдущей статье "Разработка системы репликации - моделирование рынка (часть 20): ФОРЕКС (I), мы приступили к настройке, а точнее, к адаптации системы репликации/моделирования. Это сделано таким образом, чтобы можно было использовать данные рынка, например, ФОРЕКСА, для того чтобы, по крайней мере, иметь возможность производить репликации данного рынка.

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

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


Решаем проблему с конфигурационным файлом

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

В результате при попытке использования сервиса репликации/моделирования может возникнуть ситуация, подобная той, что показана на рис. 01:

Рисунок 01

Рисунок 01: Результат сбоя в последовательности загрузки


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

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

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

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

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

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

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

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

Давайте теперь посмотрим, что нам понадобится. Это показано в приведенном ниже коде:

#include "C_FileBars.mqh"
#include "C_FileTicks.mqh"
//+------------------------------------------------------------------+
#include <Arrays\ArrayString.mqh>
//+------------------------------------------------------------------+
class C_ConfigService : protected C_FileTicks
{
// ... Внутренний код класса ....
}

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

После этого нам понадобятся приватные глобальные переменные для класса:

    private :
//+------------------------------------------------------------------+
        enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE};
        string m_szPath;
        struct st001
        {
            CArrayString *pTicksToReplay, *pBarsToTicks, *pTicksToBars, *pBarsToPrev;
        }m_GlPrivate;

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

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

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

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

bool SetSymbolReplay(const string szFileConfig)
   {

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

      m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL;
// ... Остальной код ...

   }

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

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

bool SetSymbolReplay(const string szFileConfig)
   {
#define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo)
      int    file,
             iLine;
      char   cError,
             cStage;

          string szInfo;

          bool   bBarsPrev;
                                                                

          if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
      {

             Print("Не удалось открыть файл конфигурации [", szFileConfig, "]. Закрывем сервис...");

             return false;
      }

          Print("Загрузка тиков для репликации. Подождите....");

          ArrayResize(m_Ticks.Rate, def_BarsDiary);
      m_Ticks.nRate = -1;
      m_Ticks.Rate[0].time = 0;
      iLine = 1;
      cError = cStage = 0;
      bBarsPrev = false;
      m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL;

          while ((!FileIsEnding(file)) && (!_StopFlag) && (cError == 0))
      {

             switch (GetDefinition(FileReadString(file), szInfo))
         {

                case Transcription_DEFINE:
               cError = (WhatDefine(szInfo, cStage) ? 0 : 1);

                   break;

                case Transcription_INFO:

                   if (szInfo != "") switch (cStage)
               {

                      case 0:
                     cError = 2;

                         break;

                      case 1:

                         if (m_GlPrivate.pBarsToPrev == NULL) m_GlPrivate.pBarsToPrev = new CArrayString();
                     (*m_GlPrivate.pBarsToPrev).Add(macroFileName);
                     pFileBars = new C_FileBars(macroFileName);
                     if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == 0) cError = 3; else bBarsPrev = true;
                     delete pFileBars;
                     break;
                  case 2:
                     if (m_GlPrivate.pTicksToReplay == NULL) m_GlPrivate.pTicksToReplay = new CArrayString();
                     (*m_GlPrivate.pTicksToReplay).Add(macroFileName);
                     if (LoadTicks(macroFileName) == 0) cError = 4;
                     break;
                  case 3:
                     if (m_GlPrivate.pTicksToBars == NULL) m_GlPrivate.pTicksToBars = new CArrayString();
                     (*m_GlPrivate.pTicksToBars).Add(macroFileName);
                     if ((m_dtPrevLoading = LoadTicks(macroFileName, false)) == 0) cError = 5; else bBarsPrev = true;
                     break;
                  case 4:
                     if (m_GlPrivate.pBarsToTicks == NULL) m_GlPrivate.pBarsToTicks = new CArrayString();
                     (*m_GlPrivate.pBarsToTicks).Add(macroFileName);
                     if (!BarsToTicks(macroFileName)) cError = 6;
                     break;
                  case 5:
                     if (!Configs(szInfo)) cError = 7;
                     break;
               }
               break;
            };
            iLine += (cError > 0 ? 0 : 1);
         }
         FileClose(file);
         Cmd_TicksToReplay(cError);
         Cmd_BarsToTicks(cError);
         bBarsPrev = (Cmd_TicksToBars(cError) ? true : bBarsPrev);
         bBarsPrev = (Cmd_BarsToPrev(cError) ? true : bBarsPrev);
         switch(cError)
         {
            case 0:
               if (m_Ticks.nTicks <= 0)
               {
                  Print("Нет тиков для использования. Закрываем сервис...");
                  cError = -1;
               }else if (!bBarsPrev) FirstBarNULL();
               break;
            case 1  : Print("Команда в строке ", iLine, " не распознается системой...");    break;
            case 2  : Print("Содержимое строки неожиданно для системы ", iLine);                  break;
            default : Print("Возникла ошибка при доступе к одному из указанных файлов...");
         }
                                              
         return (cError == 0 ? !_StopFlag : false);
#undef macroFileName
      }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ниже можно посмотреть код класса в полном объеме:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
class C_Array
{
    private :
//+------------------------------------------------------------------+
        string  m_Info[];
        int     m_nLine[];
        int     m_maxIndex;
//+------------------------------------------------------------------+
    public  :
        C_Array()
            :m_maxIndex(0)
        {};
//+------------------------------------------------------------------+
        ~C_Array()
        {
            if (m_maxIndex > 0)
            {
                ArrayResize(m_nLine, 0);
                ArrayResize(m_Info, 0);
            }
        };
//+------------------------------------------------------------------+
        bool Add(const string Info, const int nLine)
        {
            m_maxIndex++;
            ArrayResize(m_Info, m_maxIndex);
            ArrayResize(m_nLine, m_maxIndex);
            m_Info[m_maxIndex - 1] = Info;
            m_nLine[m_maxIndex - 1] = nLine;

            return true;
        }
//+------------------------------------------------------------------+
        string At(const int Index, int &nLine) const
        {
            if (Index >= m_maxIndex)
            {
                nLine = -1;
                return "";
            }
            nLine = m_nLine[Index];
            return m_Info[Index];
        }
//+------------------------------------------------------------------+
};
//+------------------------------------------------------------------+

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

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

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_FileBars.mqh"
#include "C_FileTicks.mqh"
#include "C_Array.mqh"
//+------------------------------------------------------------------+
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;
                }
//+------------------------------------------------------------------+
        private :
//+------------------------------------------------------------------+
                enum eTranscriptionDefine {Transcription_INFO, Transcription_DEFINE};
                string m_szPath;
                struct st001
                {
                        C_Array *pTicksToReplay, *pBarsToTicks, *pTicksToBars, *pBarsToPrev;
                        int     Line;
                }m_GlPrivate;

Необходимо было добавить в файл только эти пункты, но при этом кое-что подправить в функции конфигурации. Она будет выглядеть так:

bool SetSymbolReplay(const string szFileConfig)
    {
#define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo)
        int     file;
        char    cError,
                cStage;
        string  szInfo;
        bool    bBarsPrev;

        if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
            Print("Не удалось открыть конфигурационный файл [", szFileConfig, "]. Закрываем сервис...");
            return false;
        }
        Print("Загрузка тиков для репликации. Подождите....");
        ArrayResize(m_Ticks.Rate, def_BarsDiary);
        m_Ticks.nRate = -1;
        m_Ticks.Rate[0].time = 0;
        cError = cStage = 0;
        bBarsPrev = false;
        m_GlPrivate.Line = 1;
        m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL;
        while ((!FileIsEnding(file)) && (!_StopFlag) && (cError == 0))
        {
            switch (GetDefinition(FileReadString(file), szInfo))
            {
                case Transcription_DEFINE:
                    cError = (WhatDefine(szInfo, cStage) ? 0 : 1);
                    break;
                case Transcription_INFO:
                if (szInfo != "") switch (cStage)
                {
                    case 0:
                        cError = 2;
                        break;
                    case 1:
                        if (m_GlPrivate.pBarsToPrev == NULL) m_GlPrivate.pBarsToPrev = new C_Array();
                        (*m_GlPrivate.pBarsToPrev).Add(macroFileName, m_GlPrivate.Line);
                        break;
                    case 2:
                        if (m_GlPrivate.pTicksToReplay == NULL) m_GlPrivate.pTicksToReplay = new C_Array();
                        (*m_GlPrivate.pTicksToReplay).Add(macroFileName, m_GlPrivate.Line);
                        break;
                    case 3:
                        if (m_GlPrivate.pTicksToBars == NULL) m_GlPrivate.pTicksToBars = new C_Array();
                        (*m_GlPrivate.pTicksToBars).Add(macroFileName, m_GlPrivate.Line);
                        break;
                    case 4:
                        if (m_GlPrivate.pBarsToTicks == NULL) m_GlPrivate.pBarsToTicks = new C_Array();
                        (*m_GlPrivate.pBarsToTicks).Add(macroFileName, m_GlPrivate.Line);
                        break;
                    case 5:
                        if (!Configs(szInfo)) cError = 7;
                        break;
                }
                break;
            };
            m_GlPrivate.Line += (cError > 0 ? 0 : 1);
        }
        FileClose(file);
        Cmd_TicksToReplay(cError);
        Cmd_BarsToTicks(cError);
        bBarsPrev = (Cmd_TicksToBars(cError) ? true : bBarsPrev);
        bBarsPrev = (Cmd_BarsToPrev(cError) ? true : bBarsPrev);
        switch(cError)
        {
            case 0:
                if (m_Ticks.nTicks <= 0)
                {
                    Print("Нет тиков для использования. Закрываем сервис...");
                    cError = -1;
                }else if (!bBarsPrev) FirstBarNULL();
                break;
            case 1  : Print("Команда в строке ", m_GlPrivate.Line, " не распознается системой..."); break;
            case 2  : Print("Содержимое строки неожиданно для системы: ", m_GlPrivate.Line);              break;
            default : Print("Ошибка доступа в файле, указанный в строке: ", m_GlPrivate.Line);
        }
        
        return (cError == 0 ? !_StopFlag : false);
#undef macroFileName

    }

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

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

inline void Cmd_TicksToReplay(char &cError)
    {
        string szInfo;

        if (m_GlPrivate.pTicksToReplay != NULL)
        {
            for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++)
            {
                if ((szInfo = (*m_GlPrivate.pTicksToReplay).At(c0, m_GlPrivate.Line)) == "") break;
                if (LoadTicks(szInfo) == 0) cError = 4;                                         
            }
            delete m_GlPrivate.pTicksToReplay;
        }
    }
//+------------------------------------------------------------------+
inline void Cmd_BarsToTicks(char &cError)
    {
        string szInfo;

        if (m_GlPrivate.pBarsToTicks != NULL)
        {
            for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++)
            {
                if ((szInfo = (*m_GlPrivate.pBarsToTicks).At(c0, m_GlPrivate.Line)) == "") break;
                if (!BarsToTicks(szInfo)) cError = 6;
            }
            delete m_GlPrivate.pBarsToTicks;
        }
    }
//+------------------------------------------------------------------+
inline bool Cmd_TicksToBars(char &cError)
    {
        bool bBarsPrev = false;
        string szInfo;

        if (m_GlPrivate.pTicksToBars != NULL)
        {
            for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++)
            {
                if ((szInfo = (*m_GlPrivate.pTicksToBars).At(c0, m_GlPrivate.Line)) == "") break;
                if ((m_dtPrevLoading = LoadTicks(szInfo, false)) == 0) cError = 5; else bBarsPrev = true;
            }
            delete m_GlPrivate.pTicksToBars;
        }
        return bBarsPrev;
    }
//+------------------------------------------------------------------+
inline bool Cmd_BarsToPrev(char &cError)
    {
        bool bBarsPrev = false;
        string szInfo;
        C_FileBars      *pFileBars;

        if (m_GlPrivate.pBarsToPrev != NULL)
        {
            for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++)
            {
                if ((szInfo = (*m_GlPrivate.pBarsToPrev).At(c0, m_GlPrivate.Line)) == "") break;
                pFileBars = new C_FileBars(szInfo);
                if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == 0) cError = 3; else bBarsPrev = true;
                delete pFileBars;
            }
            delete m_GlPrivate.pBarsToPrev;
        }

        return bBarsPrev;
    }
//+------------------------------------------------------------------+

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

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

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

inline bool CMD_Array(char &cError, eWhatExec e1)
    {
        bool        bBarsPrev = false;
        string      szInfo;
        C_FileBars    *pFileBars;
        C_Array     *ptr = NULL;

        switch (e1)
        {
            case eTickReplay: ptr = m_GlPrivate.pTicksToReplay; break;
            case eTickToBar : ptr = m_GlPrivate.pTicksToBars;   break;
            case eBarToTick : ptr = m_GlPrivate.pBarsToTicks;   break;
            case eBarPrev   : ptr = m_GlPrivate.pBarsToPrev;    break;
        }                               
        if (ptr != NULL)
        {
            for (int c0 = 0; (c0 < INT_MAX) && (cError == 0); c0++)
            {
                if ((szInfo = ptr.At(c0, m_GlPrivate.Line)) == "") break;
                switch (e1)
                {
                    case eTickReplay:
                        if (LoadTicks(szInfo) == 0) cError = 4;
                        break;
                    case eTickToBar :
                        if ((m_dtPrevLoading = LoadTicks(szInfo, false)) == 0) cError = 5; else bBarsPrev = true;
                        break;
                    case eBarToTick :
                        if (!BarsToTicks(szInfo)) cError = 6;
                        break;
                    case eBarPrev   :
                        pFileBars = new C_FileBars(szInfo);
                        if ((m_dtPrevLoading = (*pFileBars).LoadPreView()) == 0) cError = 3; else bBarsPrev = true;
                        delete pFileBars;
                        break;
                }
            }
            delete ptr;
        }

        return bBarsPrev;
    }

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

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

Идея заключается в том, чтобы сначала загрузить моделируемые данные, а затем, если это нужно, загрузить предыдущие бары. Но у нас есть проблема: как тики, которые будут использоваться в репликации/моделировании, так и предыдущие бары могут поступать из файлов типа «Tick» или типа «Bar», но нам нужно как-то это указать. Поэтому я предлагаю использовать переменную, которую может определить пользователь, но которая не будет следовать какому-либо паттерну. Давайте будем предельно конкретны, чтобы не усложнять ситуацию и делать вещи жизнеспособными в долгосрочной перспективе. Для этого воспользуемся следующей таблицей:

Значение Режим чтения
1 Режим Tick - Tick
2 Режим Tick - Bar
3 Режим Bar - Tick 
4 Режим работы Bar - Bar 

Таблица 01 - Данные для модели внутреннего чтения

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

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

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

class C_ConfigService : protected C_FileTicks
{

       protected:
//+------------------------------------------------------------------+

          datetime m_dtPrevLoading;

          int      m_ReplayCount,
               m_ModelLoading;

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

C_ConfigService()
   :m_szPath(NULL), m_ModelLoading(1)
   {
   }

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

inline bool Configs(const string szInfo)
    {
        const string szList[] = 
        {
            "PATH",
            "POINTSPERTICK",
            "VALUEPERPOINTS",
            "VOLUMEMINIMAL",
            "LOADMODEL"
        };
        string  szRet[];
        char    cWho;

        if (StringSplit(szInfo, '=', szRet) == 2)
        {
            StringTrimRight(szRet[0]);
            StringTrimLeft(szRet[1]);
            for (cWho = 0; cWho < ArraySize(szList); cWho++) if (szList[cWho] == szRet[0]) break;
            switch (cWho)
            {
                case 0:
                    m_szPath = szRet[1];
                    return true;
                case 1:
                    CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, StringToDouble(szRet[1]));
                    return true;
                case 2:
                    CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, StringToDouble(szRet[1]));
                    return true;
                case 3:
                    CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, StringToDouble(szRet[1]));
                    return true;
                case 4:
                    m_ModelLoading = StringInit(szRet[1]);
                    m_ModelLoading = ((m_ModelLoading < 1) && (m_ModelLoading > 4) ? 1 : m_ModelLoading);
                    return true;                            
            }
            Print("Переменная >>", szRet[0], "<< не определена.");
        }else
        Print("Определение настройки >>", szInfo, "<< недействительно.");

        return false;
    }

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

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

bool SetSymbolReplay(const string szFileConfig)
    {
#define macroFileName ((m_szPath != NULL ? m_szPath + "\\" : "") + szInfo)
        int     file;
        char    cError,
                cStage;
        string  szInfo;
        bool    bBarsPrev;

        if ((file = FileOpen("Market Replay\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
            Print("Не удалось открыть файл конфигурации [", szFileConfig, "]. Закрываем сервис...");
            return false;
        }
        Print("Загрузка тиков для репликации. Подождите....");
        ArrayResize(m_Ticks.Rate, def_BarsDiary);
        m_Ticks.nRate = -1;
        m_Ticks.Rate[0].time = 0;
        cError = cStage = 0;
        bBarsPrev = false;
        m_GlPrivate.Line = 1;
        m_GlPrivate.pTicksToReplay = m_GlPrivate.pTicksToBars = m_GlPrivate.pBarsToTicks = m_GlPrivate.pBarsToPrev = NULL;
        while ((!FileIsEnding(file)) && (!_StopFlag) && (cError == 0))
        {
            switch (GetDefinition(FileReadString(file), szInfo))
            {
                case Transcription_DEFINE:
                    cError = (WhatDefine(szInfo, cStage) ? 0 : 1);
                    break;
                case Transcription_INFO:
                    if (szInfo != "") switch (cStage)
                    {
                        case 0:
                            cError = 2;
                            break;
                        case 1:
                            if (m_GlPrivate.pBarsToPrev == NULL) m_GlPrivate.pBarsToPrev = new C_Array();
                            (*m_GlPrivate.pBarsToPrev).Add(macroFileName, m_GlPrivate.Line);
                            break;
                        case 2:
                            if (m_GlPrivate.pTicksToReplay == NULL) m_GlPrivate.pTicksToReplay = new C_Array();
                            (*m_GlPrivate.pTicksToReplay).Add(macroFileName, m_GlPrivate.Line);
                            break;
                        case 3:
                            if (m_GlPrivate.pTicksToBars == NULL) m_GlPrivate.pTicksToBars = new C_Array();
                            (*m_GlPrivate.pTicksToBars).Add(macroFileName, m_GlPrivate.Line);
                            break;
                        case 4:
                            if (m_GlPrivate.pBarsToTicks == NULL) m_GlPrivate.pBarsToTicks = new C_Array();
                            (*m_GlPrivate.pBarsToTicks).Add(macroFileName, m_GlPrivate.Line);
                            break;
                        case 5:
                            if (!Configs(szInfo)) cError = 7;
                            break;
                    }
                break;
            };
            m_GlPrivate.Line += (cError > 0 ? 0 : 1);
        }
        FileClose(file);
        CMD_Array(cError, (m_ModelLoading <= 2 ? eTickReplay : eBarToTick));
        CMD_Array(cError, (m_ModelLoading <= 2 ? eBarToTick : eTickReplay));
        bBarsPrev = (CMD_Array(cError, ((m_ModelLoading & 1) == 1 ? eTickToBar : eBarPrev)) ? true : bBarsPrev);
        bBarsPrev = (CMD_Array(cError, ((m_ModelLoading & 1) == 1 ? eBarPrev : eTickToBar)) ? true : bBarsPrev);
        switch(cError)
        {
            case 0:
                if (m_Ticks.nTicks <= 0)
                {
                    Print("Нет тиков для использования. Закрываем сервис...");
                    cError = -1;
                }else if (!bBarsPrev) FirstBarNULL();
                break;
            case 1  : Print("Команда в строке ", m_GlPrivate.Line, " не распознается системой..."); break;
            case 2  : Print("Содержимое строки неожиданно для системы: ", m_GlPrivate.Line);              break;
            default : Print("Ошибка доступа в файле, указанный в строке: ", m_GlPrivate.Line);
        }

        return (cError == 0 ? !_StopFlag : false);
#undef macroFileName
    }

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


Конечные выводы

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

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

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

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

Прикрепленные файлы |
Market_Replay_ev221.zip (14387.15 KB)
Разработка системы репликации - Моделирование рынка (Часть 22): ФОРЕКС (III) Разработка системы репликации - Моделирование рынка (Часть 22): ФОРЕКС (III)
Хотя это уже третья статья об этом, я должен объяснить для тех, кто еще не понял разницу между фондовым рынком и валютным рынком (ФОРЕКС): большая разница заключается в том, что в ФОРЕКС не существует, точнее, нам не дают информацию о некоторых моментах, которые действительно происходили в ходе торговли.
Кросс-валидация и основы причинно-следственного вывода в моделях CatBoost, экспорт в ONNX формат Кросс-валидация и основы причинно-следственного вывода в моделях CatBoost, экспорт в ONNX формат
В данной статье предложен авторский способ создания ботов с использованием машинного обучения.
Популяционные алгоритмы оптимизации: Алгоритм оптимизации спиральной динамики (Spiral Dynamics Optimization, SDO) Популяционные алгоритмы оптимизации: Алгоритм оптимизации спиральной динамики (Spiral Dynamics Optimization, SDO)
В статье представлен алгоритм оптимизации, основанный на закономерностях построения спиральных траекторий в природе, таких как раковины моллюсков - алгоритм оптимизации спиральной динамики, SDO. Алгоритм, предложенный авторами, был мной основательно переосмыслен и модифицирован, в статье будет рассмотрено, почему эти изменения были необходимы.
Популяционные алгоритмы оптимизации: Алгоритм интеллектуальных капель воды (Intelligent Water Drops, IWD) Популяционные алгоритмы оптимизации: Алгоритм интеллектуальных капель воды (Intelligent Water Drops, IWD)
В статье рассматривается интересный алгоритм - интеллектуальные капли воды, IWD, подсмотренный у неживой природы, симулирующий процесс формирования русла реки. Идеи этого алгоритма позволили значительно улучшить прошлого лидера рейтинга - SDS, а нового лидера (модифицированный SDSm), как обычно, найдёте в архиве к статье.