Добавление, замена и удаление котировок

Наполнение пользовательского символа котировками осуществляется двумя встроенными функциями: CustomRatesUpdate и CustomRatesReplace. Обе ожидают на входе, помимо названия символа, массив структур MqlRates для таймфрейма M1 (более старшие таймфреймы достраиваются из M1 автоматически). CustomRatesReplace имеет дополнительную пару параметров (from и to), задающих временной диапазон, которым ограничивается редактирование истории.

int CustomRatesUpdate(const string symbol, const MqlRates &rates[], uint count = WHOLE_ARRAY)

int CustomRatesReplace(const string symbol, datetime from, datetime to, const MqlRates &rates[], uint count = WHOLE_ARRAY)

CustomRatesUpdate добавляет в историю отсутствующие бары и заменяет существующие совпадающие бары данными из массива.

CustomRatesReplace полностью заменяет историю в указанном временном интервале данными из массива.

Разница между функциями обусловлена разными сценариями предполагаемого применения. Более детально отличия перечислены в следующей таблице.

CustomRatesUpdate

CustomRatesReplace

Применяет к истории элементы передаваемого массива MqlRates, вне зависимости от их временных меток

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

Оставляет нетронутыми в истории те бары M1, которые там уже были до вызова функции и не совпадают по времени с барами в массиве

Оставляет нетронутой всю историю вне диапазона

Изменяет существующие бары истории на бары в массиве при совпадении временных меток

Полностью удаляет существующие бары истории в указанном диапазоне

Вставляет элементы из массива как "новые" бары при отсутствии совпадений с прежними барами

Вставляет в указанный диапазон истории бары из массива, которые попадают в этот диапазон

Данные в массиве rates должны быть корректными четверками цен OHLC, а времена открытия баров — не содержать секунд.

Интервал в пределах from и to задается включительно: from равно времени первого бара, подлежащего обработке, а to — времени последнего.

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

История                        ABC-EFGHIJKLMN-PQRST------    Б
Массив                         -------hijk--nopqrstuvwxyz    А
Результат CustomRatesUpdate    ABC-EFGhijkLMnopqrstuvwxyz    Р
Результат CustomRatesReplace   ABC-E--hijk--nopqrstuvw---    Ы
                                    ^                ^
                                    |from          to|   ВРЕМЯ

Опциональный параметр count задает количество элементов массива rates, которые должны использоваться (остальные будут проигнорированы). Это позволяет обрабатывать переданный массив частично. Значение по умолчанию WHOLE_ARRAY означает весь массив.

Удалить историю котировок пользовательского символа — полностью или частично — позволяет функция CustomRatesDelete.

int CustomRatesDelete(const string symbol, datetime from, datetime to)

Здесь параметры from и to также задают временной диапазон удаляемых баров. Чтобы охватить всю историю, укажите 0 и LONG_MAX.

Все три функции возвращают количество обработанных баров: обновленных или удаленных. В случае ошибки результат равен -1.

Следует отметить, что котировки пользовательского символа можно формировать не только путем добавления готовых баров, но и массивов тиков или даже последовательности отдельных тиков — функции для этого будут представлены в следующем разделе. При добавлении тиков терминал сам рассчитает на их основе бары. Отличие между этими способами заключается в том, что история кастом-тиков позволяет тестировать MQL-программы в режиме "реальных" тиков, в то время как история только баров заставит либо ограничиться режимами OHLC M1 или цен открытия, либо положиться на эмуляцию тиков, реализованную тестером.

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

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

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

Все режимы собраны в перечисление RANDOMIZATION.

enum RANDOMIZATION
{
   ORIGINAL,
   RANDOM_WALK,
   FUZZY_WEAK,
   FUZZY_STRONG,
};

Зашумление котировок реализуем с двумя степенями интенсивности: слабой (weak) и сильной (strong).

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

input string CustomPath = "MQL5Book\\Part7";    // Custom Symbol Folder
input RANDOMIZATION RandomFactor = RANDOM_WALK;
input datetime _From;                           // From (умолчание: 120 дней назад)
input datetime _To;                             // To (умолчание: текущее время)
input uint RandomSeed = 0;

По умолчанию, когда даты не указаны, скрипт генерирует котировки для последних 120 дней. Значение 0 в параметре RandomSeed означает случайную инициализацию.

Название символа создается на основе символа текущего чарта и выбранных настроек.

const string CustomSymbol = _Symbol + "." + EnumToString(RandomFactor)
   + (RandomSeed ? "_" + (string)RandomSeed : "");

В начале OnStart произведем подготовку и проверку данных.

datetime From;
datetime To;
   
void OnStart()
{
   From = _From == 0 ? TimeCurrent() - 60 * 60 * 24 * 120 : _From;
   To = _To == 0 ? TimeCurrent() / 60 * 60 : _To;
   if(From > To)
   {
      Alert("Date range must include From <= To");
      return;
   }
   
   if(RandomSeed != 0MathSrand(RandomSeed);
   ...

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

   bool custom = false;
   if(PRTF(SymbolExist(CustomSymbolcustom)) && custom)
   {
      if(IDYES == MessageBox(StringFormat("Delete custom symbol '%s'?"CustomSymbol),
         "Please, confirm"MB_YESNO))
      {
         if(CloseChartsForSymbol(CustomSymbol))
         {
            Sleep(500); // ждем пока изменения вступят в силу (навскидку)
            PRTF(CustomRatesDelete(CustomSymbol0LONG_MAX));
            PRTF(SymbolSelect(CustomSymbolfalse));
            PRTF(CustomSymbolDelete(CustomSymbol));
         }
      }
   }
   ...

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

Более важно обратить внимание на вызов CustomRatesDelete с указанием полного диапазона дат. Если его не сделать, на диске на некоторое время останутся данные прежнего пользовательского символа в базе истории (папка bases/Custom/history/<имя-символа>). Иными словами, вызов CustomSymbolDelete, который показан выше последней строкой, недостаточен для того, чтобы фактически вычистить пользовательский символ из терминала.

Если пользователь решит тут же создать символ с тем же именем заново (а у нас в коде далее такая возможность предусмотрена), то старые котировки могут подмешаться в новые.

Далее, опять же с запросом подтверждения пользователем, запускается процесс генерации котировок: за него отвечает функция GenerateQuotes (см. далее).

   if(IDYES == MessageBox(StringFormat("Create new custom symbol '%s'?"CustomSymbol),
      "Please, confirm"MB_YESNO))
   {
      if(PRTF(CustomSymbolCreate(CustomSymbolCustomPath_Symbol)))
      {
         if(RandomFactor == RANDOM_WALK)
         {
            CustomSymbolSetInteger(CustomSymbolSYMBOL_DIGITS8);
         }
         
         CustomSymbolSetString(CustomSymbolSYMBOL_DESCRIPTION"Randomized quotes");
      
         const int n = GenerateQuotes();
         Print("Bars M1 generated: "n);
         if(n > 0)
         {
            SymbolSelect(CustomSymboltrue);
            ChartOpen(CustomSymbolPERIOD_M1);
         }
      }
   }

В случае успеха вновь созданный символ выбирается в Обзор рынка и для него открывается график. Попутно здесь демонстрируется установка пары свойств: SYMBOL_DIGITS и SYMBOL_DESCRIPTION.

В функции GenerateQuotes для всех режимов кроме RANDOM_WALK требуется запросить котировки оригинального символа.

int GenerateQuotes()
{
   MqlRates rates[];
   MqlRates zero = {};
   datetime start;     // время текущего бара
   double price;       // последняя цена закрытия
   
   if(RandomFactor != RANDOM_WALK)
   {
      if(PRTF(CopyRates(_SymbolPERIOD_M1FromTorates)) <= 0)
      {
         return 0// ошибка
      }
      if(RandomFactor == ORIGINAL)
      {
         return PRTF(CustomRatesReplace(CustomSymbolFromTorates));
      }
      ...

Важно напомнить, что на CopyRates влияет ограничение на количество баров на графике, которое стоит в настройках терминала.

В случае режима ORIGINAL мы просто пересылаем полученный массив rates в функцию CustomRatesReplace. Для режимов зашумления устанавливаем специально выделенные переменные price и start в начальные значения цены и времени из первого бара.

      price = rates[0].open;
      start = rates[0].time;
   }
   ...

В режиме случайного блуждания котировки не нужны, поэтом мы просто выделяем массив rates под будущие случайные бары M1.

   else
   {
      ArrayResize(rates, (int)((To - From) / 60) + 1);
      price = 1.0;
      start = From;
   }
   ...

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

   const int size = ArraySize(rates);
   
   double hlc[3]; // будущие High Low Close (в неизвестном порядке)
   for(int i = 0i < size; ++i)
   {
      if(RandomFactor == RANDOM_WALK)
      {
         rates[i] = zero;             // обнуление структуры
         rates[i].time = start += 60// плюс минута к прошлому бару
         rates[i].open = price;       // начинаем с прошлой цены
         hlc[0] = RandomWalk(price);
         hlc[1] = RandomWalk(price);
         hlc[2] = RandomWalk(price);
      }
      else
      {
         double delta = 0;
         if(i > 0)
         {
            delta = rates[i].open - price// кумулятивная коррекция
         }
         rates[i].open = price;
         hlc[0] = RandomWalk(rates[i].high - delta);
         hlc[1] = RandomWalk(rates[i].low - delta);
         hlc[2] = RandomWalk(rates[i].close - delta);
      }
      ArraySort(hlc);
      
      rates[i].high = fmax(hlc[2], rates[i].open);
      rates[i].low = fmin(hlc[0], rates[i].open);
      rates[i].close = price = hlc[1];
      rates[i].tick_volume = 4;
   }
   ...

На основе цены закрытия прошлого бара генерируются 3 случайных значения (с помощью функции RandomWalk). Максимальное и минимальное из них становятся, соответственно, ценами High и Low нового бара. Среднее значение — это цена Close.

По завершении цикла остается только передать массив в CustomRatesReplace.

   return PRTF(CustomRatesReplace(CustomSymbolFromTorates));
}

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

double RandomWalk(const double p)
{
   const static double factor[] = {0.00.10.010.05};
   const static double f = factor[RandomFactor] / 100;
   const double r = (rand() - 16383.0) / 16384.0// [-1,+1]
   const int sign = r >= 0 ? +1 : -1;
   if(r != 0)
   {
      return p + p * sign * f * sqrt(-log(sqrt(fabs(r))));
   }
   return p;
}

Коэффициенты разброса случайных величин зависят от режима. Например, слабое зашумление добавляет (или отнимает) максимум 1 сотую процента, а сильное — 5 сотых процента от цены.

В процессе работы скрипт выводит подробный лог вроде этого:

Create new custom symbol 'GBPUSD.RANDOM_WALK'?
CustomSymbolCreate(CustomSymbol,CustomPath,_Symbol)=true / ok
CustomRatesReplace(CustomSymbol,From,To,rates)=171416 / ok
Bars M1 generated: 171416

Давайте посмотрим, что получилось в результате.

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

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

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

А вот как выглядят зашумленные котировки GBPUSD (черным цветом оригинал, цветные с шумом). Сначала в слабом варианте.

Котировки GBPUSD со слабым зашумлением

Котировки GBPUSD со слабым зашумлением

А затем — в сильном.

Котировки GBPUSD с сильным зашумлением

Котировки GBPUSD с сильным зашумлением

Налицо большие расхождения, но с сохранением локальных особенностей.