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

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

MetaTrader 5Тестер |
991 5
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)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (5)
Philip Tweens
Philip Tweens | 17 авг. 2023 в 19:39

Здравствуйте, уважаемый Даниил,

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

Я столкнулся с некоторыми проблемами при тестировании вашей системы, и мне нужна ваша помощь.

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

Второе - мне нужно изменить эту систему так, чтобы движение баров было как в Strategy Tester, т.е. положение пина отражало скорость отображения баров, или у меня была возможность двигаться бар за баром, как на сайте TradingView. Ваша система устроена таким образом, что ее можно изменить вот так????.

Я был бы благодарен, если бы вы могли меня направить.

Искренне Ваш,

Daniel Jose
Daniel Jose | 17 авг. 2023 в 21:32
Philip Tweens количестве 1-минутных баров (около 20 баров).

Второй момент - мне нужно изменить эту систему так, чтобы движение баров было как в Strategy Tester, т.е. положение пина отражало скорость отображения баров, или у меня была возможность двигаться бар за баром, как на сайте TradingView. Ваша система устроена таким образом, что ее можно изменить вот так????

Я был бы благодарен, если бы вы могли меня направить.

Искренне Ваш,

Ладно, давайте по частям, как сказал бы JACK...😁👍

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

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

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

Помните, что приложение разработано для использования во времени, эквивалентном реальному времени. Другими словами, короткие периоды. Чтобы ввести в исследование длинные периоды. Если вам нужно использовать среднее значение или индикатор, который требует построения большого количества баров. Вы НЕ ДОЛЖНЫ использовать эти данные в повторе или симуляторе. Вы должны использовать их в качестве предыдущих баров. Это первый момент, который вы должны постараться понять.

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

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

Philip Tweens
Philip Tweens | 18 авг. 2023 в 06:15
daniel jose # :

Ладно. Давайте разложим все по полочкам, как сказал бы Джек...😁👍

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

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

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

Помните, что приложение задумывалось для использования во времени, эквивалентном реальному времени. Другими словами, короткие периоды. Чтобы ввести в исследование длинные периоды. В случае, если вам нужно использовать какую-то среднюю или индикатор, для построения которого требуется много баров. Вы НЕ ДОЛЖНЫ использовать данные в реплее или симуляторе. Вы должны поместить их как пребары. Это первый пункт, который вы должны понять.

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

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

Я не думаю, что вы поняли, что я имел в виду, и, возможно, я плохо выразился.
Я понимаю функцию слайдера. Я поместил данные в Replay на месяц (около 20 дней). Однако я передвинул пин ближе к концу ползунка, но в первый день нарисовалось только несколько баров, тогда как до достижения нужной точки должно было пройти не менее 15 дней. Может, я что-то не так понял? Полагаю, это связано с тем, что вы сказали о неиспользовании данных более чем за один день.
Что касается скорости отображения баров, я бы хотел, чтобы вы посоветовали мне, как изменить систему таким образом.
Спасибо за ответ.
Daniel Jose
Daniel Jose | 18 авг. 2023 в 10:31
Philip Tweens #:
Я не думаю, что вы поняли, что я имел в виду, и, возможно, я плохо выразился.
Я понимаю функцию слайдера. Я поместил данные в Replay на месяц (около 20 дней). Однако я передвинул пин ближе к концу ползунка, но в первый день было нарисовано только несколько баров, тогда как до достижения нужной точки должно было пройти не менее 15 дней. Может, я что-то не так понял? Полагаю, это связано с тем, что вы сказали о том, что нельзя использовать данные более чем за один день.
Что касается скорости отображения баров, я бы хотел, чтобы вы посоветовали мне, как изменить систему таким образом.
Спасибо за ответ.

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

Philip Tweens
Philip Tweens | 18 авг. 2023 в 13:30
Daniel Jose #:

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

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