Организация доступа к данным

В этом разделе рассматриваются вопросы, связанные с получением, хранением и запросами ценовых данных (таймсерий).

Получение данных от торгового сервера

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

Механизм обращения к серверу за данными не зависит от того, каким образом был инициирован запрос — пользователем при навигации по графику или программным способом на языке MQL5.

Хранение промежуточных данных

Полученные с сервера данные автоматически распаковываются и сохраняются в специальном промежуточном формате HCC. Данные по каждому символу пишутся в отдельную папку каталог_терминала\bases\имя_сервера\history\имя_символа. Например, данные по символу EURUSD  с торгового сервера MetaQuotes-Demo будут находиться в папке каталаг_терминала\bases\MetaQuotes-Demo\history\EURUSD\.

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

Получение данных нужного таймфрейма из промежуточных данных

Служебные файлы в формате HCC исполняют роль источника данных для построения ценовых данных по запрошенным таймфреймам в формате HC. Данные в формате HC являются таймсериями, максимально подготовленными для быстрого доступа. Они создаются только по запросу графика или mql5-программы в объеме, не превышающем значения параметра "Max bars in charts", и сохраняются для дальнейшего использования в файлах с расширением hc.

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

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

Параметр "Max bars in chart"

Параметр "Max bars in charts" ограничивает доступное для графиков, индикаторов и mql5-программ количество баров в формате HC. Это ограничение действует для данных всех таймфреймов, и предназначено в первую очередь для экономии ресурсов.

Устанавливая большие значения данного параметра, следует помнить, что при наличии достаточно глубокой истории ценовых данных для младших таймфреймов расход памяти на хранение таймсерий и буферов индикаторов может составить сотни мегабайт и достигнуть ограничения оперативной памяти для программы клиентского терминала (2Гб для 32-битных приложений MS Windows).

Изменение параметра "Max bars in charts" вступает в силу после перезапуска клиентского терминала. Само по себе изменение данного параметра не вызывает ни автоматического обращения к серверу за дополнительными данными, ни формирования дополнительных баров таймсерий. Запрос дополнительных ценовых данных у сервера и обновление таймсерий с учетом нового ограничения произойдет либо в случае прокрутки графика в область недостающих данных, либо в случае запроса недостающих данных из mql5-программы.

Объем запрашиваемых у сервера данных соответствует требуемому количеству баров данного таймфрейма с учетом значения параметра "Max bars in charts". Ограничение, задаваемое параметром, не является жестким, и в некоторых случаях количество доступных баров по таймфрейму может быть незначительно больше текущего значения параметра.

Доступность данных

Наличие данных в формате HCC или даже в готовом для использования формате HC не всегда означает безусловную доступность этих данных для отображения на графике или для использования в mql5-программах.

При доступе к ценовым данным или к значениям индикаторов из mql5-программ следует помнить, что не гарантируется их доступность в определенный момент времени, либо с определенного момента времени. Это связано с тем, что в целях экономии ресурсов в MetaTrader 5 не хранится полная копия требуемых данных для mql5-программы, а дается прямой доступ к базе данных терминала.

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

Синхронизация данных терминала и данных сервера #

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

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

Для экспертов и пользовательских индикаторов лучше использовать событийную модель обработки. Если при обработке события OnTick() или OnCalculate() не удалось получить все необходимые данные требуемой таймсерии, то следует выйти из обработчика события, рассчитывая на появление доступа к данным при следующем вызове обработчика.

Пример скрипта для закачки истории

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

Оформим все действия по получению данных в виде отдельной функции CheckLoadHistory(symbol, timeframe, start_date):

int CheckLoadHistory(string symbol,ENUM_TIMEFRAMES period,datetime start_date)
  {
  }

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

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

   if(symbol==NULL || symbol==""symbol=Symbol();
   if(period==PERIOD_CURRENT)     period=Period();

Далее — убедимся, что указанный символ доступен в окне MarketWatch, то есть, история по данному символу будет доступна при запросе к торговому серверу. Если его там нет - добавим символ в окно самостоятельно с помощью функции SymbolSelect().

   if(!SymbolInfoInteger(symbol,SYMBOL_SELECT))
     {
      if(GetLastError()==ERR_MARKET_UNKNOWN_SYMBOLreturn(-1);
      SymbolSelect(symbol,true);
     }

Теперь необходимо получить начальную дату по уже имеющейся истории для указанной пары символ/период. Возможно, что значение входного параметра startdate, переданного функции CheckLoadHistory() попадает в интервал уже доступной истории, и тогда никакого запроса к торговому серверу не потребуется. Для получения самой первой даты по символу-периоду на данный момент предназначена функция SeriesInfoInteger() с модификатором SERIES_FIRSTDATE.

   SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date);
   if(first_date>0 && first_date<=start_date) return(1);

Следующая важная проверка — проверка типа программы, из которой вызывается функция. Напомним, что отправка запроса на обновление таймсерии с тем же периодом, что и у индикатора, вызывающего обновление, крайне нежелательна. Нежелательность запроса данных по тому же символу-периоду, что и у индикатора обусловлена тем, что обновление исторических данных производится в том же потоке, в котором работает индикатор. Поэтому велика вероятность клинча. Для проверки используем функцию MQL5InfoInteger() с модификатором MQL5_PROGRAM_TYPE.

   if(MQL5InfoInteger(MQL5_PROGRAM_TYPE)==PROGRAM_INDICATOR && Period()==period && Symbol()==symbol)
      return(-4);

Если мы успешно прошли все проверки, то сделаем последнюю попытку обойтись без обращения к торговому серверу. Сначала узнаем начальную дату, для которой доступны минутные данные в формате HCC. Запросим это значение функцией SeriesInfoInteger() с модификатором SERIES_TERMINAL_FIRSTDATE и опять сравним со значением параметра start_date.

   if(SeriesInfoInteger(symbol,PERIOD_M1,SERIES_TERMINAL_FIRSTDATE,first_date))
     {
      //--- there is loaded data to build timeseries
      if(first_date>0)
        {
         //--- force timeseries build
         CopyTime(symbol,period,first_date+PeriodSeconds(period),1,times);
         //--- check date
         if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
            if(first_date>0 && first_date<=start_date) return(2);
        }
     }

Если после всех проверок поток выполнения по-прежнему находится в теле функции CheckLoadHistory(), то значит, есть необходимость запросить недостающие ценовые данные с торгового сервера. Для начала мы узнаем значение "Max bars in chart" с помощью функции TerminalInfoInteger():

  int max_bars=TerminalInfoInteger(TERMINAL_MAXBARS);

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

   datetime first_server_date=0;
   while(!SeriesInfoInteger(symbol,PERIOD_M1,SERIES_SERVER_FIRSTDATE,first_server_date) && !IsStopped())
      Sleep(5);

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

   if(first_server_date>start_date) start_date=first_server_date;
   if(first_date>0 && first_date<first_server_date)
      Print("Warning: first server date ",first_server_date," for ",symbol,
            " does not match to first series date ",first_date);

Если вдруг начальная дата first_server_date на сервере окажется меньше, чем начальная дата first_date по символу в формате HCC , то в журнал будет выведено соответствующее сообщение.

Теперь мы готовы сделать запрос к торговому серверу за недостающими ценовыми данными. Запрос сделаем в виде цикла и начнем заполнять его тело:

   while(!IsStopped())
     {
      //1. дождаться синхронизации между перестроенной таймсерией и промежуточной историей в формате HCC
      //2. получить текущее количество баров bars на данной таймсерии
      //    если bars больше, чем Max_bars_in_chart, то можем выходить, работа окончена
      //3. Получим начальную дату first_date в перестроенной таймсерии и сравним со значением start_date
      //    если first_date меньше чем start_date, то можем выходить, работа окончена
      //4. Запросим новую порцию истории в 100 баров у торгового сервера от последнего доступного бара под номером bars
     }

Первые три пункта реализуются уже знакомыми приемами.

   while(!IsStopped())
     {
      //--- 1.дождаться окончания процесса перестройки таймсерии
      while(!SeriesInfoInteger(symbol,period,SERIES_SYNCHRONIZED) && !IsStopped())
         Sleep(5);
      //--- 2.запросим сколько баров мы теперь имеем
      int bars=Bars(symbol,period);
      if(bars>0)
        {
         //--- баров больше, чем можно отобразить на графике, выходим
         if(bars>=max_bars) return(-2); 
         //--- 3. узнаем текущую начальную дату в таймсерии
         if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
            // начальная дата более ранняя, чем запрашивалось, задача выполнена
            if(first_date>0 && first_date<=start_date) return(0);
        }
      //4. Запросим новую порцию истории в 100 баров у торгового сервера от последнего доступного бара под номером bars
     }

Остался последний четвертый пункт — непосредственный запрос истории. Мы не можем прямо обратиться к серверу, но любая Copy-функция при нехватке истории в формате HCC терминал автоматически инициирует посылку такого запроса от терминала к торговому серверу. Так как время самой первой начальной даты в переменной first_date является самым простым и естественным критерием для оценки степени выполнения запроса, то самым простым будет использовать функцию CopyTime().

При вызове функций, осуществляющих копирование любых данных из таймсерий, необходимо иметь в виду то, что параметр start (номер бара, с которого начинать копирование ценовых данных) всегда должен быть в пределах доступной истории терминала. Если у нас имеется только 100 баров, то не имеет смысла пытаться скопировать 300 баров, начиная с бара с индексом 500. Такой запрос будет воспринят как ошибочный и не будет обработан, т.е. никакая история с торгового сервера подгружена не будет.

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

   int copied=CopyTime(symbol,period,bars,100,times);

После операции копирования необходимо проанализировать количество скопированных элементов, если попытка оказалась неудачной, то значение переменной copied будет равно нулю и значение счетчика fail_cnt будет увеличено на 1. После 100 неудачных попыток работа функции будет прервана.

int fail_cnt=0;
...
   int copied=CopyTime(symbol,period,bars,100,times);
   if(copied>0)
     {
      //--- проверим данные
      if(times[0]<=start_date)  return(0);  // скопированное значение меньше, готово
      if(bars+copied>=max_bars) return(-2); // баров стало больше, чем помещается на график, готово
      fail_cnt=0;
     }
   else
     {
      //--- не более 100 неудачных попыток подряд
      fail_cnt++;
      if(fail_cnt>=100) return(-5);
      Sleep(10);
     }
 

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

   int res=CheckLoadHistory(InpLoadedSymbol,InpLoadedPeriod,InpStartDate);
   switch(res)
     {
      case -1 : Print("Неизвестный символ",InpLoadedSymbol);                        break;
      case -2 : Print("Запрошенных баров больше, чем можно отобразить на графике"); break;
      case -3 : Print("Выполнение было прервано пользователем");                    break;
      case -4 : Print("Индикатор не должен загружать собственные данные");          break;
      case -5 : Print("Загрузка окончилась неудачей");                              break;
      case  0 : Print("Все данные загружены");                                      break;
      case  1 : Print("Уже имеющихся данных в таймсерии достаточно");               break;
      case  2 : Print("Таймсерия построена из имеющихся данных терминала");         break;
      default : Print("Результат выполнения не определен");
     }

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

Код:

//+------------------------------------------------------------------+
//|                                              TestLoadHistory.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2009, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.02"
#property script_show_inputs
//--- input parameters
input string          InpLoadedSymbol="NZDUSD";   // Symbol to be load
input ENUM_TIMEFRAMES InpLoadedPeriod=PERIOD_H1;  // Period to be load
input datetime        InpStartDate=D'2006.01.01'; // Start date
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   Print("Start load",InpLoadedSymbol+","+GetPeriodName(InpLoadedPeriod),"from",InpStartDate);
//---
   int res=CheckLoadHistory(InpLoadedSymbol,InpLoadedPeriod,InpStartDate);
   switch(res)
     {
      case -1 : Print("Unknown symbol ",InpLoadedSymbol);             break;
      case -2 : Print("Requested bars more than max bars in chart "); break;
      case -3 : Print("Program was stopped ");                        break;
      case -4 : Print("Indicator shouldn't load its own data ");      break;
      case -5 : Print("Load failed ");                                break;
      case  0 : Print("Loaded OK ");                                  break;
      case  1 : Print("Loaded previously ");                          break;
      case  2 : Print("Loaded previously and built ");                break;
      default : Print("Unknown result ");
     }
//---
   datetime first_date;
   SeriesInfoInteger(InpLoadedSymbol,InpLoadedPeriod,SERIES_FIRSTDATE,first_date);
   int bars=Bars(InpLoadedSymbol,InpLoadedPeriod);
   Print("First date ",first_date," - ",bars," bars");
//---
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int CheckLoadHistory(string symbol,ENUM_TIMEFRAMES period,datetime start_date)
  {
   datetime first_date=0;
   datetime times[100];
//--- check symbol & period
   if(symbol==NULL || symbol==""symbol=Symbol();
   if(period==PERIOD_CURRENT)     period=Period();
//--- check if symbol is selected in the MarketWatch
   if(!SymbolInfoInteger(symbol,SYMBOL_SELECT))
     {
      if(GetLastError()==ERR_MARKET_UNKNOWN_SYMBOLreturn(-1);
      SymbolSelect(symbol,true);
     }
//--- check if data is present
   SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date);
   if(first_date>0 && first_date<=start_date) return(1);
//--- don't ask for load of its own data if it is an indicator
   if(MQL5InfoInteger(MQL5_PROGRAM_TYPE)==PROGRAM_INDICATOR && Period()==period && Symbol()==symbol)
      return(-4);
//--- second attempt
   if(SeriesInfoInteger(symbol,PERIOD_M1,SERIES_TERMINAL_FIRSTDATE,first_date))
     {
      //--- there is loaded data to build timeseries
      if(first_date>0)
        {
         //--- force timeseries build
         CopyTime(symbol,period,first_date+PeriodSeconds(period),1,times);
         //--- check date
         if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
            if(first_date>0 && first_date<=start_date) return(2);
        }
     }
//--- max bars in chart from terminal options
   int max_bars=TerminalInfoInteger(TERMINAL_MAXBARS);
//--- load symbol history info
   datetime first_server_date=0;
   while(!SeriesInfoInteger(symbol,PERIOD_M1,SERIES_SERVER_FIRSTDATE,first_server_date) && !IsStopped())
      Sleep(5);
//--- fix start date for loading
   if(first_server_date>start_date) start_date=first_server_date;
   if(first_date>0 && first_date<first_server_date)
      Print("Warning: first server date ",first_server_date," for ",symbol,
            " does not match to first series date ",first_date);
//--- load data step by step
   int fail_cnt=0;
   while(!IsStopped())
     {
      //--- wait for timeseries build
      while(!SeriesInfoInteger(symbol,period,SERIES_SYNCHRONIZED) && !IsStopped())
         Sleep(5);
      //--- ask for built bars
      int bars=Bars(symbol,period);
      if(bars>0)
        {
         if(bars>=max_bars) return(-2);
         //--- ask for first date
         if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
            if(first_date>0 && first_date<=start_date) return(0);
        }
      //--- copying of next part forces data loading
      int copied=CopyTime(symbol,period,bars,100,times);
      if(copied>0)
        {
         //--- check for data
         if(times[0]<=start_date)  return(0);
         if(bars+copied>=max_bars) return(-2);
         fail_cnt=0;
        }
      else
        {
         //--- no more than 100 failed attempts
         fail_cnt++;
         if(fail_cnt>=100) return(-5);
         Sleep(10);
        }
     }
//--- stopped
   return(-3);
  }
//+------------------------------------------------------------------+
//| Возвращает строкое значение периода                              |
//+------------------------------------------------------------------+
string GetPeriodName(ENUM_TIMEFRAMES period)
  {
   if(period==PERIOD_CURRENTperiod=Period();
//---
   switch(period)
     {
      case PERIOD_M1:  return("M1");
      case PERIOD_M2:  return("M2");
      case PERIOD_M3:  return("M3");
      case PERIOD_M4:  return("M4");
      case PERIOD_M5:  return("M5");
      case PERIOD_M6:  return("M6");
      case PERIOD_M10return("M10");
      case PERIOD_M12return("M12");
      case PERIOD_M15return("M15");
      case PERIOD_M20return("M20");
      case PERIOD_M30return("M30");
      case PERIOD_H1:  return("H1");
      case PERIOD_H2:  return("H2");
      case PERIOD_H3:  return("H3");
      case PERIOD_H4:  return("H4");
      case PERIOD_H6:  return("H6");
      case PERIOD_H8:  return("H8");
      case PERIOD_H12return("H12");
      case PERIOD_D1:  return("Daily");
      case PERIOD_W1:  return("Weekly");
      case PERIOD_MN1return("Monthly");
     }
//---
   return("unknown period");
  }