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

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

MetaTrader 5Тестер | 24 ноября 2023, 12:39
901 0
Daniel Jose
Daniel Jose

Введение

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

В той же статье есть момент, в котором я ясно выражаю свое недовольство:

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

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


Решаем проблему с тиковым объемом

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

datetime LoadTicks(const string szFileNameCSV, const bool ToReplay = true)
    {
        int      MemNRates,
                 MemNTicks;
        datetime dtRet = TimeCurrent();
        MqlRates RatesLocal[],
                 rate;
        bool     bNew;
        
        MemNRates = (m_Ticks.nRate < 0 ? 0 : m_Ticks.nRate);
        MemNTicks = m_Ticks.nTicks;
        if (!Open(szFileNameCSV)) return 0;
        if (!ReadAllsTicks(ToReplay)) return 0;         
        rate.time = 0;
        for (int c0 = MemNTicks; c0 < m_Ticks.nTicks; c0++)
        {
            if (!BuildBar1Min(c0, rate, bNew)) continue;
            if (bNew) ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 2 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[(m_Ticks.nRate += (bNew ? 1 : 0))] = rate;
        }
        if (!ToReplay)
        {
            ArrayResize(RatesLocal, (m_Ticks.nRate - MemNRates));
            ArrayCopy(RatesLocal, m_Ticks.Rate, 0, 0);
            CustomRatesUpdate(def_SymbolReplay, RatesLocal, (m_Ticks.nRate - MemNRates));
            dtRet = m_Ticks.Rate[m_Ticks.nRate].time;
            m_Ticks.nRate = (MemNRates == 0 ? -1 : MemNRates);
            m_Ticks.nTicks = MemNTicks;
            ArrayFree(RatesLocal);
        }else
        {
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX);
            CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID);
        }
        m_Ticks.bTickReal = true;
        
        return dtRet;
    };

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

Давайте рассмотрим код преобразования:

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

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

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

Теперь давайте рассмотрим новую функцию создания бара.

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

      bool    bNew;
      double  dSpread;
      int     iRand = rand();
                                
      if (BuildBar1Min(m_ReplayCount, def_Rate, bNew))
      {
         m_Infos.tick[0] = m_Ticks.Info[m_ReplayCount];
         if ((!m_Ticks.bTickReal) && (m_Ticks.ModePlot == PRICE_EXCHANGE))
         {                                               
            dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
            if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
            {
               m_Infos.tick[0].ask = m_Infos.tick[0].last;
               m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
            }else   if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
            {
               m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
               m_Infos.tick[0].bid = m_Infos.tick[0].last;
            }
         }
         if (bViewTicks) CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
         CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate);
      }
      m_ReplayCount++;
#undef def_Rate
   }

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


Начнем моделирование отображения на основе BID (режим ФОРЕКС).

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

bool BarsToTicks(const string szFileNameCSV)
   {
      C_FileBars *pFileBars;
      int         iMem = m_Ticks.nTicks,
                  iRet;
      MqlRates    rate[1];
      MqlTick     local[];
                                
      pFileBars = new C_FileBars(szFileNameCSV);
      ArrayResize(local, def_MaxSizeArray);
      Print("Convertendo barras em ticks. Aguarde...");
      while ((*pFileBars).ReadBar(rate) && (!_StopFlag))
      {
         ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
         m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
         if ((iRet = Simulation(rate[0], local)) < 0)
         {
            ArrayFree(local);
            delete pFileBars;
            return false;
         }
         for (int c0 = 0; c0 <= iRet; c0++)
         {
            ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
            m_Ticks.Info[m_Ticks.nTicks++] = local[c0];
         }
      }
      ArrayFree(local);
      delete pFileBars;
      m_Ticks.bTickReal = false;
                                
      return ((!_StopFlag) && (iMem != m_Ticks.nTicks));
   }

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

int SetSymbolInfos(void)
   {
      int iRet;
                                
      CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, iRet = (m_Ticks.ModePlot == PRICE_EXCHANGE ? 4 : 5));
      CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TRADE_CALC_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CALC_MODE_EXCH_STOCKS : SYMBOL_CALC_MODE_FOREX);
      CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_CHART_MODE, m_Ticks.ModePlot == PRICE_EXCHANGE ? SYMBOL_CHART_MODE_LAST : SYMBOL_CHART_MODE_BID);
                                
      return iRet;
   }
//+------------------------------------------------------------------+
   public  :
//+------------------------------------------------------------------+
      bool BarsToTicks(const string szFileNameCSV)
      {
         C_FileBars      *pFileBars;
         C_Simulation    *pSimulator = NULL;
         int             iMem = m_Ticks.nTicks,
                         iRet = -1;
         MqlRates        rate[1];
         MqlTick         local[];
         bool            bInit = false;
                                
         pFileBars = new C_FileBars(szFileNameCSV);
         ArrayResize(local, def_MaxSizeArray);
         Print("Convertendo barras em ticks. Aguarde...");
         while ((*pFileBars).ReadBar(rate) && (!_StopFlag))
         {
            if (!bInit)
            {
               m_Ticks.ModePlot = (rate[0].real_volume > 0 ? PRICE_EXCHANGE : PRICE_FOREX);
               pSimulator = new C_Simulation(SetSymbolInfos());
               bInit = true;
            }
            ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
            m_Ticks.Rate[++m_Ticks.nRate] = rate[0];
            if (pSimulator == NULL) iRet = -1; else iRet = (*pSimulator).Simulation(rate[0], local);
            if (iRet < 0) break;
            for (int c0 = 0; c0 <= iRet; c0++)
            {
               ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
               m_Ticks.Info[m_Ticks.nTicks++] = local[c0];
            }
         }
         ArrayFree(local);
         delete pFileBars;
         delete pSimulator;
         m_Ticks.bTickReal = false;
                                
         return ((!_StopFlag) && (iMem != m_Ticks.nTicks) && (iRet > 0));
      }

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

  • Устранение наследования класса C_Simulation. Это сделает систему еще более гибкой.
  • Инициализация данных об активах, что выполнялось ранее только при использовании реальных тиков.
  • Соответствующая ширина символов, используемых в графическом отображении.
  • Использование класса C_Simulation в качестве указателя. То есть более эффективное использование памяти системы, так как после выполнения классом своей работы память, которую он занимал, будет освобождена.
  • Гарантия наличия только одной точки входа и одной точки выхода из функции.
При этом некоторые вещи изменятся по сравнению с предыдущей статьей. Но давайте продолжим реализацию класса C_Simulation. Главной деталью для разработки класса C_Simulation является то, что мы можем иметь любое количество тиков в системе. Но хотя это и не является проблемой (по крайней мере, на данный момент), сложность заключается в том, что во многих случаях диапазон, который мы должны будем охватить между максимумом и минимумом, уже будет намного больше, чем количество тиков, о которых сообщается или которые возможно создать. Это не считая участка, который начинается с цены открытия и идет к одному из экстремумов, и того участка, который начинается с одного экстремумов и идет до самого закрытия. Если проводить такой расчет с помощью СЛУЧАЙОГО БЛУЖДАНИЯ, то в огромном количестве случаев это будет невозможным. Поэтому нам придется отказаться от случайного блуждания, которое мы создали в предыдущих статьях, и разработать новый метод создания тиков. Я сказал, что проблема с ФОРЕКС далеко не так однозначна.

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

C_Simulation(const int nDigits)
   {
      m_NDigits       = nDigits;
      m_IsPriceBID    = (SymbolInfoInteger(def_SymbolReplay, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_BID);
      m_TickSize      = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
   }

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

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

template < typename T >
inline T RandomLimit(const T Limit01, const T Limit02)
   {
      T a = (Limit01 > Limit02 ? Limit01 - Limit02 : Limit02 - Limit01);
      return (Limit01 >= Limit02 ? Limit02 : Limit01) + ((T)(((rand() & 32767) / 32737.0) * a));
   }

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

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

Таким образом, мы всегда будем выполнять один и тот же расчет, но он будет корректироваться в зависимости от вида используемой переменной. Это сделает компилятор, поскольку именно он будет решать, какой тип является правильным. Таким образом, мы сможем генерировать псевдослучайное число в каждом вызове, независимо от используемого типа, но учтите, что тип обоих пределов должна быть одинаковой. Иными словами, нельзя смешивать double с integer или long integer с short integer. Это не сработает. Это единственное ограничение такого подхода, когда мы используем перегрузку типов.

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

inline void Simulation_Time(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      for (int c0 = 0, iPos, v0 = (int)(60000 / rate.tick_volume), v1 = 0, v2 = v0; c0 <= imax; c0++, v1 = v2, v2 += v0)
      {
         iPos = RandomLimit(v1, v2);
         tick[c0].time = rate.time + (iPos / 1000);
         tick[c0].time_msc = iPos % 1000;
      }
   }

Здесь мы моделируем время таким образом, чтобы оно было слегка случайным. Должен признаться, на первый взгляд это выглядит довольно запутанно. Но, поверьте, время здесь случайное, хотя оно всё равно не соответствует логике, ожидаемой классом C_Replay. Это связано с тем, что значение в миллисекундах задано неверно. Данная корректировка будет произведена в другом месте. Здесь мы просто хотим, чтобы время генерировалось случайным образом, но в пределах 1-минутного бара. И как нам это сделать? Во-первых, мы делим время 60 секунд, которое на самом деле составляет 60 000 миллисекунд, на количество тиков, которые необходимо сгенерировать. Это значение важно для нас, так как оно подскажет, какой предельный диапазон мы будем использовать. После этого в каждой итерации цикла мы выполним несколько простых присваиваний. Теперь секрет генерации случайного таймера заключается в этих трех строчках внутри цикла. В первой строке мы просим компилятор сгенерировать вызов, в котором мы будем использовать целочисленные данные, и этот вызов будет возвращать значение в указанном диапазоне. Затем мы выполним два очень простых расчета. Сначала мы подгоняем сгенерированное значение к времени минутного бара, а затем используем это же сгенерированное значение для подгонки времени в миллисекундах. Таким образом, у каждого из тиков будет совершенно случайное значение по времени. Помните, что на этом раннем этапе мы только исправляем время. Цель данной настройки - избежать излишней предсказуемости.

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

Давайте теперь рассмотрим первый из вызовов, который будет выполнен для создания моделирования по BID:

inline void Simulation_BID(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      bool    bHigh  = (rate.open == rate.high) || (rate.close == rate.high), 
      bLow = (rate.open == rate.low) || (rate.close == rate.low);
                                                        
      Mount_BID(0, rate.open, rate.spread, tick);     
      for (int c0 = 1; c0 < imax; c0++)
      {
         Mount_BID(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), rate.spread, tick); 
         bHigh = (rate.high == tick[c0].bid) || bHigh;
         bLow = (rate.low == tick[c0].bid) || bLow;
      }
      if (!bLow) Mount_BID(Unique(imax, rate.high, tick), rate.low, rate.spread, tick);
      if (!bHigh) Mount_BID(Unique(imax, rate.low, tick), rate.high, rate.spread, tick);
      Mount_BID(imax, rate.close, rate.spread, tick);
   }

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

Если внимательно присмотреться, то можно заметить, что у нас есть две функции, которые часто упоминаются в части моделирования: MOUNT_BID и UNIQUE. Каждая из них служит определенной цели. Но для начала рассмотрим функцию Unique, код которой приведен ниже:

inline int Unique(const int imax, const double price, const MqlTick &tick[])
   {
      int iPos = 1;
                                
      do
      {
         iPos = (imax > 20 ? RandomLimit(1, imax - 1) : iPos + 1);
      }while ((m_IsPriceBID ? tick[iPos].bid : tick[iPos].last) == price);
                                
      return iPos;
   }

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

Теперь рассмотрим функцию Mount_BID, код которой приведен ниже:

inline void Mount_BID(const int iPos, const double price, const int spread, MqlTick &tick[])
   {
      tick[iPos].bid = price;
      tick[iPos].ask = NormalizeDouble(price + (m_TickSize * spread), m_NDigits);
   }

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

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

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

inline void CorretTime(int imax, MqlTick &tick[])
   {
      for (int c0 = 0; c0 <= imax; c0++)
         tick[c0].time_msc += (tick[c0].time * 1000);
   }

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

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

Чтобы убедиться в этом, мы воспользуемся одним из приемов программирования, это будет что-то очень сложное и продуманное. О чем идет речь, можно посмотреть в приведенном ниже фрагменте:

inline int Simulation(const MqlRates &rate, MqlTick &tick[])
   {
      int imax;
                        
      imax = (int) rate.tick_volume - 1;
      Simulation_Time(imax, rate, tick);
      if (m_IsPriceBID) Simulation_BID(imax, rate, tick); else return -1;
      CorretTime(imax, tick);

      return imax;
   }

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

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

inline void Simulation_BID(int imax, const MqlRates &rate, MqlTick &tick[])
   {
      bool    bHigh  = (rate.open == rate.high) || (rate.close == rate.high), 
      bLow = (rate.open == rate.low) || (rate.close == rate.low);

      Mount_BID(0, rate.open, rate.spread, tick);     
      for (int c0 = 1; c0 < imax; c0++)
      {
         Mount_BID(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (imax & 0xF)), 0)), tick);
         bHigh = (rate.high == tick[c0].bid) || bHigh;
         bLow = (rate.low == tick[c0].bid) || bLow;
      }
      if (!bLow) Mount_BID(Unique(imax, rate.high, tick), rate.low, rate.spread, tick);
      if (!bHigh) Mount_BID(Unique(imax, rate.low, tick), rate.high, rate.spread, tick);
      Mount_BID(imax, rate.close, rate.spread, tick);
   }

Здесь мы обеспечиваем рандомизацию величины разброса, правда, данная рандомизация носит лишь демонстрационный характер. При желании можно поступить несколько иначе в плане пределов. Нам просто придется немного подправить ситуацию. Теперь вы должны понять, что я использую эту рандомизацию, которая кажется немного странной для некоторых, но вот что я делаю на самом деле: я убеждаюсь, что максимально возможное значение может быть использовано для рандомизации спреда. Данное значение основано на расчете, в котором мы побитно объединяем значение спреда со значением, которое может варьироваться от 1 до 16, так как мы используем только часть всех битов. Но следует обратить внимание на следующее: если спред равен нулю (а в некоторые моменты он действительно будет равен нулю), мы всё равно получим значение, которое будет не меньше 3, так как значения 1 и 2 фактически не создают рандомизации спреда. Это связано с тем, что значение 1 указывает только на открытие, равное закрытию, а значение 2 указывает на то, что открытие может быть как равным, так и отличным от закрытия. Но в данном случае именно значение 2 будет реально создавать значение. Во всех остальных случаях мы будем иметь дело с созданием рандомизации в спреде.

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


Заключение

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

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

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

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

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

Прикрепленные файлы |
Market_Replay_7vx23.zip (14388.45 KB)
Нейросети — это просто (Часть 65): Дистанционно-взвешенное обучение с учителем (DWSL) Нейросети — это просто (Часть 65): Дистанционно-взвешенное обучение с учителем (DWSL)
В данной статье я предлагаю Вам познакомиться с интересным алгоритмом, который построен на стыке методов обучения с учителем и подкреплением.
Популяционные алгоритмы оптимизации: Алгоритм оптимизации спиральной динамики (Spiral Dynamics Optimization, SDO) Популяционные алгоритмы оптимизации: Алгоритм оптимизации спиральной динамики (Spiral Dynamics Optimization, SDO)
В статье представлен алгоритм оптимизации, основанный на закономерностях построения спиральных траекторий в природе, таких как раковины моллюсков - алгоритм оптимизации спиральной динамики, SDO. Алгоритм, предложенный авторами, был мной основательно переосмыслен и модифицирован, в статье будет рассмотрено, почему эти изменения были необходимы.
Популяционные алгоритмы оптимизации: Дифференциальная эволюция (Differential Evolution, DE) Популяционные алгоритмы оптимизации: Дифференциальная эволюция (Differential Evolution, DE)
В этой статье поговорим об алгоритме, который демонстрирует самые противоречивые результаты из всех рассмотренных ранее, алгоритм дифференциальной эволюции (DE).
Разработка системы репликации - Моделирование рынка (Часть 22): ФОРЕКС (III) Разработка системы репликации - Моделирование рынка (Часть 22): ФОРЕКС (III)
Хотя это уже третья статья об этом, я должен объяснить для тех, кто еще не понял разницу между фондовым рынком и валютным рынком (ФОРЕКС): большая разница заключается в том, что в ФОРЕКС не существует, точнее, нам не дают информацию о некоторых моментах, которые действительно происходили в ходе торговли.