Как самостоятельно создать и протестировать в MetaTrader 5 инструменты Московской биржи

Dmitrii Troshin | 6 декабря, 2018

Введение

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

Но что если мы решим использовать имеющиеся наработки для торговле на бирже? Ряд биржевых терминалов имеет встроенные языки программирования. Например, в популярном терминале Transaq имеется язык программирования ATF (Advanced Trading Facility). Но, конечно, он не идёт ни в какое сравнение с MQL5, а тестер стратегий просто отсутствует. Поэтому в подобных случаях возникает желание получить биржевые данные и оптимизировать торговые алгоритмы в тестере стратегий MetaTrader.

Для решения этой задачи существует возможность создавать пользовательские (кастомные) символы. Как это делать, подробно описано в статье Cоздание и тестирование пользовательских символов в MetaTrader 5. Все что нам надо — это получить данные в виде файла CSV (TXT) и, руководствуясь статьёй, импортировать ценовую историю.

Все так просто и было бы, если бы не различие форматов данных. Возьмем для примера наиболее популярный биржевой ресурс finam.ru. Скачать котировки можно здесь:



Экспорт котировок московской биржи


Посмотрим для примера какие форматы данных предлагает финам:




Предлагаемые форматы даты "ггггммдд", "ггммдд", "ддммгг", "дд/мм/гг", "мм/дд/гг". Наш формат:



Нашего формата "гггг.мм.дд" среди них нет. То есть на finam.ru есть практически все форматы... кроме нашего. 

Кроме того, не стоит ограничиваться одним биржевым ресурсом. Форматы других ресурсов также могут оказаться неподходящими. Нам нужен определенный порядок данных, а котировки могут храниться в любом другом порядке, например Open, Close, High, Low. 

Итак, наша задача — преобразовать данные, расположенные в произвольном порядке и разных форматов, в наш формат. Это даст возможность получать данные для MetaTrader 5 из любых источников. Затем на основе полученных данных средствами MQL5 создать кастомный символ для возможности проводить тесты.

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

На бирже, конечно же, есть спред, цена Ask и Bid. Но всё это существует только в "моменте", в биржевом стакане. Дальше записывается только цена сделки. Независимо от того, по каким ценам была совершена сделка — по "бидам" или "оферам". Нам для терминала нужен спред. Здесь для этих целей просто добавляется фиксированный спред, так как восстановить спред в "стакане" невозможно. Тем, для кого это существенно, могут его как-то имитировать. Например, как описано в статье   Моделирование временных рядов с помощью пользовательских символов по заданным законам распределения. Или написать простую функцию зависимости спреда от волатильности Spread = f(High-Low).

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


Наш формат:



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

Статья плавно превращается из "импорта данных" в "моделирование данных", т.е. в статью, на которую дана ссылка чуть выше. В связи с этим, чтобы не вводить в заблуждение, я решил не выкладывать программу для импорта тиков, построенную по принципу ASK=BID(+spread)=LAST. На миллисекундах спред существенен и при тестировании для себя необходимо решить, как его моделировать.

После этого переписать код для импорта тиков — задача пары минут. Надо лишь заменить структуру MqlRates на MqlTick. А функцию CustomRatesUpdate() на CustomTicksAdd().

Следующий момент — невозможность рассмотреть всевозможные форматы данных. Например числа могут записываться с разделителями разрядов 1 000 000 или вместо десятичного разделителя — точки, может использоваться запятая 3,14. Или того хуже и разделитель данных и десятичный разделитель — точка или запятая (попробуйте понять, где что). Здесь рассмотрены только самые распространённые форматы. Если попадется какой-то нестандартный формат, то его разбор придется дописать самостоятельно.

Кроме того, на бирже нет тиковой истории, а есть только объём сделки. Поэтому в этой работе биржевой объём =VOL=TICKVOL.

Статья делится на две части. Часть первая — описание кода. Предназначена для ознакомления с кодом для возможности его редактирования в случае нестандартных форматов данных. Часть вторая — пошаговое руководство (руководство пользователя). Предназначена для тех, кого не интересует программирование, а интересует только функционал. Если имеющиеся данные (в частности данные с ресурса finam.ru) стандартных форматов,то можно сразу перейти к части 2.

Часть 1. Описание кода

Здесь приводится лишь часть кода. Полный код содержится в прикреплённых файлах.

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

input int SkipString        =1;                               // Количество пропускаемых строк
input string mark1          ="Позиция и формат времени";      // Время
input DATE indate           =yyyymmdd;                        // Исходный формат даты
input TIME intime           =hhdmmdss;                        // Исходный формат времени
input int DatePosition      =1;                               // Позиция даты
input int TimePosition      =2;                               // Позиция времени
//------------------------------------------------------------------+
input string mark2          ="Позиции ценовых данных";        // Цена 
input int OpenPosition      =3;                               // Позиция цены открытия
input int HighPosition      =4;                               // Позиция максимума цены
input int LowPosiotion      =5;                               // Позиция минимума цены   
input int ClosePosition     =6;                               // Позиция цены закрытия
input int VolumePosition    =7;                               // Позиция объёма
input string mark3          ="Параметры файла";               // Файл
//-------------------------------------------------------------------+
input string InFileName     ="sb";                            // Имя исходного файла
input DELIMITER Delimiter   =comma;                           // Разделитель
input CODE StrType          =ansi;                            // Тип строк
input string mark4          ="Другие параметры";              // Другое
//-------------------------------------------------------------------+
input string spread         ="2";                             // Фиксированный спред в пунктах
input string Name           ="SberFX";                        // Имя создаваемого символа


Для некоторых данных созданы перечисления. Например, для формата даты и время:

enum DATE
{
yyyycmmcdd, // yyyy.mm.dd
yyyymmdd,   // yyyymmdd
yymmdd,     // yymmdd
ddmmyy,     // ddmmyy   
ddslmmslyy, // dd/mm/yy
mmslddslyy  // mm/dd/yy
// Сюда добавляется дополнительные форматы
};

enum TIME
{
hhmmss,     // hhmmss
hhmm,       // hhmm
hhdmmdss,   // hh:mm:ss
hhdmm       // hh:mm
// Сюда добавляются дополнительные форматы
};


И если необходимый формат отсутствует, его нужно добавить.

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

// Open in file
    
  int out =FileOpen(InFileName,FILE_READ|StrType|FILE_TXT);
  if(out==INVALID_HANDLE)
  {
   Alert("Не удалось открыть файл для чтения");
   return; 
  }
// Open out file
  int in =FileOpen(Name+"(f).csv",FILE_WRITE|FILE_ANSI|FILE_CSV);
  if(in==INVALID_HANDLE)
  {
   Alert("Не удалось открыть файл для записи");
   return; 
  }
  //---Insert caption string  
  string Caption ="<DATE>\t<TIME>\t<OPEN>\t<HIGH>\t<LOW>\t<CLOSE>\t<TICKVOL>\t<VOL>\t<SPREAD>";
  FileWrite(in,Caption);
//-----------------------------------------------------------
string fdate="",ftime="",open="";
string high="",low="",close="",vol="";
int left=0,right=0;

string str="",temp="";
for(int i=0;i<SkipString;i++)
   {
   str =FileReadString(out);
   i++;
   }
MqlRates Rs[];
ArrayResize(Rs,43200,43200);  //43200 минут в месяце
datetime time =0;


Исходный файл должен находиться в каталоге MQL5/Files. Внешняя переменная SkipString — количество строк в заглавии файла, которые следует пропустить. Для возможности использования в качестве разделителей пробелов и табуляций файл открывается с флагом FILE_TXT. 

Дальше нам необходимо вычленить данные из строки. Расположение данных задаётся входными параметрами. Счёт начинается с единицы. Для примера возьмем котировки Сбербанка.


Здесь позиция даты=1, время=2 и т.д. SkipString=1.

Для разбора строки можно было использовать функцию StringSplit(), но для того чтобы отслеживать ошибки в исходом файле, удобнее написать свои функции. В них можно добавить анализ данных. Хотя, конечно, через StringSplit() код проще. Первая функция — функция нахождения границ данных — принимает строку, разделитель и позицию. Записывает границы в переменные a и b, передаваемые по ссылке.

//---Поиск границ позиции данных-------------------------------------+
bool SearchBorders(string str,int pos,int &a,int &b,DELIMITER delim)
{
// Вспомогательные переменные
int left=0,right=0;
int count=0;
int start=0;
string delimiter="";
//-------------------------------------------------------------------+

switch(delim)
{
case comma : delimiter =",";
   break; 
case tab : delimiter ="/t";
   break;
case space : delimiter =" ";
   break;
case semicolon : delimiter =";";
   break;
}

while(count!=pos||right!=-1)
   {
   right =StringFind(str,delimiter,start);
      
   if(right==-1&&count==0){Print("Wrong date");return false;} //Некорректные данные
   
   if(right==-1)
      {
      right =StringLen(str)-1;
      a =left;
      b =right;
      break;
      }
   
   count++;
      if(count==pos)
      {
      a =left;
      b =right-1;
      return true;
      }
   left =right+1;
   start =left;
   }

return true;
}


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

//---Форматирование даты---------------------------------------------+
//2017.01.02
string DateFormat(string str,DATE date)
{

string res="";
string yy="";
 
switch(date)
  { 
   case yyyycmmcdd :  //наш формат
      res =str;
      if(StringLen(res)!=10)res=""; // проверка формата даты
   case yyyymmdd :
      res =StringSubstr(str,0,4)+"."+StringSubstr(str,4,2)+"."+StringSubstr(str,6,2);
      if(StringLen(res)!=10)res=""; // проверка формата даты    
      break;
   case yymmdd :
      yy =StringSubstr(str,0,2);
      if(StringToInteger(yy)>=70)
         yy ="19"+yy;
      else
         yy ="20"+yy;
      res =yy+"."+StringSubstr(str,2,2)+"."+StringSubstr(str,4,2);
      if(StringLen(res)!=10)res=""; // проверка формата даты  
      break;
//---Другие форматы(Полностью код в файле)----------------
//При необходимости добавить разбор других форматов      
   default :
      break; 

  }

return res;
}


Если нужного формата нет (например, дата в виде 01 January 18), то его необходимо дописать. Здесь выполняется небольшая проверка на соответствие полученных данных требуемому формату  ( на случай ошибки в исходном файле)  if(StringLen(res)!=10) res="";Понимаю, что это довольно поверхностная проверка. Но анализ данных — дело непростое, и для их фильтра потребовалась бы отдельная программа. В случае ошибки функция выдаёт res =,"" и эта строка затем просто пропускается. 

Здесь для форматов типа ddmmyy, где год записан двумя цифрами, предусмотрено следующее преобразование. Года >=70 преобразуются в 19yy, меньше - в 20yy.

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

while(!FileIsEnding(out))
   {
   str =FileReadString(out);
   count++;
//---fdate-----------------------------
   if(SearchBorders(str,DatePosition,left,right,Delimiter))
   {
   temp =StringSubstr(str,left,right-left+1);
   fdate =DateFormat(temp,indate);
   if(fdate==""){Print("Ошибка в строке   ",count);continue;}
   }
   else {Print("Ошибка в строке   ",count);continue;}
//---Другие данные аналогично


Если в функциях SearchBorders, DateFormat или TimeFormat обнаружена ошибка, то строка пропускается, а её порядковый номер записывается функцией Print(). Все перечисления и функции преобразования форматов находятся в отдельном включаемом файле FormatFunctions.mqh 

Затем составляется и записывается итоговая строка. Данные присваиваются соответствующим элементам структуры MqlRates.

//-------------------------------------------------------------------+
   str =fdate+","+ftime+","+open+","+high+","+low+","+close+","+vol+","+vol+","+Spread;
   FileWrite(in,str);
//---Заполнение MqlRates --------------------------------------------+
   Rs[i].time               =time;
   Rs[i].open               =StringToDouble(open);
   Rs[i].high               =StringToDouble(high);
   Rs[i].low                =StringToDouble(low);
   Rs[i].close              =StringToDouble(close);
   Rs[i].real_volume        =StringToInteger(vol);
   Rs[i].tick_volume        =StringToInteger(vol);
   Rs[i].spread             =int(StringToInteger(Spread));
   i++;
//-------------------------------------------------------------------+   
   }


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

   ArrayResize(Rs,i);
   FileClose(out);
   FileClose(in);

Теперь всё готово для создания пользовательского символа. Кроме того, у нас есть файл типа CSV, который удобно редактировать прямо в MetaEditor. Имея CSV-файл, можно создавать кастомные символы стандартными методами через терминал MetaTrader 5.



Создание кастомного символа средствами MQL5

Теперь, когда все данные подготовлены, сделать остаётся совсем немного — добавить пользовательский символ.

   CustomSymbolCreate(Name);
   CustomRatesUpdate(Name,Rs);

Для импорта котировок используется функция CustomRatesUpdate() — это значит, что программу можно использовать не только для создания символа, но и для добавления новых данных. В случае если символ уже существует функция CustomSymbolCreate() просто вернёт -1 (минус один) и выполнение программы продолжится — котировки просто обновятся функцией CustomRatesUpdate(). Символ появляется в окне MarketWatch и подсвечивается зелёным.



Теперь можно открыть график и убедиться, что всё работает как надо:


График EURUSD


Установка спецификаций (свойств символа)

При тестировании инструмента может понадобится установка различных его характеристик (спецификаций). Для удобства редактирования свойств написан включаемый файл Specification. В нем в функции SetSpecifications() устанавливаются свойства символа. Здесь собраны все имеющиеся у символа свойства из перечислений  ENUM_SYMBOL_INFO_INTEGER, ENUM_SYMBOL_INFO_DOUBLE , ENUM_SYMBOL_INFO_STRING.

void SetSpecifications(string Name)
   {  
//---Integer Properties-------------------------------------
//   CustomSymbolSetInteger(Name,SYMBOL_CUSTOM,true);                       // bool Признак того, что символ является пользовательским
//   CustomSymbolSetInteger(Name,SYMBOL_BACKGROUND_COLOR,clrGreen);         // color Цвет фона, которым подсвечивается символ в Market Watch
// Другие свойства Integer
//---Double Properties ---------------------------------------------------   
//   CustomSymbolSetDouble(Name,SYMBOL_BID,0);                              // Bid - лучшее предложение на продажу 
//  CustomSymbolSetDouble(Name,SYMBOL_BIDHIGH,0);                           // Максимальный Bid за день
//   CustomSymbolSetDouble(Name,SYMBOL_BIDLOW,0);                           // Минимальный Bid за день
// Другие свойства Double
//---String Properties-----------------------------------------------+
//   CustomSymbolSetString(Name,SYMBOL_BASIS,"");                           // Имя базового актива для производного инструмента
//   CustomSymbolSetString(Name,SYMBOL_CURRENCY_BASE,"");                   // Базовая валюта инструмента
//   CustomSymbolSetString(Name,SYMBOL_CURRENCY_PROFIT,"");                 // Валюта прибыли
// Другие свойства String
}


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

   CustomSymbolSetInteger(Name,SYMBOL_CUSTOM,true);                       // bool Признак того, что символ является пользовательским
   CustomSymbolSetInteger(Name,SYMBOL_BACKGROUND_COLOR,clrGreen);         // color Цвет фона, которым подсвечивается символ в Market Watch
   CustomSymbolSetInteger(Name,SYMBOL_SELECT,true);                       // bool Признак того, что символ выбран в Market Watch 
   CustomSymbolSetInteger(Name,SYMBOL_VISIBLE,true);                      // bool Признак того, что выбранный символ отображается в Market Watch


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

   CustomSymbolSetDouble(name,SYMBOL_POINT,0.01);               // Значение одного пункта
   CustomSymbolSetDouble(name,SYMBOL_VOLUME_MIN,1);             // Минимальный объем для заключения сделки
   CustomSymbolSetDouble(name,SYMBOL_VOLUME_STEP,1);            // Минимальный шаг изменения объема
   CustomSymbolSetInteger(name,SYMBOL_DIGITS,2);                // int Количество знаков после запятой 
   CustomSymbolSetInteger(name,SYMBOL_SPREAD,2);                // int Размер спреда в пунктах 
   CustomSymbolSetInteger(name,SYMBOL_SPREAD_FLOAT,false);      // bool Признак плавающего спреда
   CustomSymbolSetDouble(name,SYMBOL_TRADE_TICK_SIZE,0.01);	// Минимальное изменение цены


Всё бы было хорошо, если бы для установки нужных свойств не нужно было бы каждый раз перекомпилировать код. Хотелось иметь возможность всю работу выполнять, лишь вводя параметры. Поэтому пришлось немножко изменить подход. Свойства символа будут содержаться в обычном текстовом файле Specifications.txt, который редактируется вручную для каждого нового символа. Перекомпиляция кода при этом не требуется.

Редактировать текстовый файл также удобнее в MetaEditor. В первую очередь из-за привычной подсветки параметров и данных. Свойства записаны в следующем формате:



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

   while(!FileIsEnding(handle))
     {
     str =FileReadString(handle);
//--- Пропуск строк ------------------------+     
     if(str=="") continue;
     if(StringFind(str,"//")<10) continue;
//------------------------------------------+     
     sub =StringSplit(str,u_sep,split);
     if(sub<2) continue;
     SetProperties(SName,split[0],split[1]);
     }


Строка пропускается, если она пустая или символ комментирования "//" находится в начале (позиция<10). Затем строка разбивается на подстроки функцией StringSplit(), и эти строки передаются в функцию SetProperties() для установки свойств символа. Структура кода функции:

void SetProperties(string name,string str1,string str2)
   {
   int n =StringTrimLeft(str1);
       n =StringTrimRight(str1);
       n =StringTrimLeft(str2);
       n =StringTrimRight(str2);
       
   if(str1=="SYMBOL_CUSTOM")
      {
      if(str2=="0"||str2=="false"){CustomSymbolSetInteger(name,SYMBOL_CUSTOM,false);}
      else {CustomSymbolSetInteger(name,SYMBOL_CUSTOM,true);}
      return;
      }
   if(str1=="SYMBOL_BACKGROUND_COLOR")
      {
      CustomSymbolSetInteger(name,SYMBOL_BACKGROUND_COLOR,StringToInteger(str2));
      return;
      }
   if(str1=="SYMBOL_CHART_MODE")
      {
      if(str2=="SYMBOL_CHART_MODE_BID"){CustomSymbolSetInteger(name,SYMBOL_CHART_MODE,SYMBOL_CHART_MODE_BID);}
      if(str2=="SYMBOL_CHART_MODE_LAST"){CustomSymbolSetInteger(name,SYMBOL_CHART_MODE,SYMBOL_CHART_MODE_LAST);}
      return;
      }
//--- Другие свойства символа
}


На тот случай, если пользователь при редактировании оставит пробелы или знаки табуляции, добавлены функции:  StringTrimLeft() и StringTrimRight().

Полный код находится во включаемом файле PropertiesSet.mqh

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


Интерфейс

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


Коротко о коде панели. Здесь использован стандартный набор элементов управления из подключаемых файлов:

#include <Controls\Dialog.mqh>
#include <Controls\Label.mqh>
#include <Controls\Button.mqh>
#include <Controls\ComboBox.mqh>

Для кнопки "ОК" создан обработчик событий.

//+------------------------------------------------------------------+ 
//| Event Handling                                                   | 
//+------------------------------------------------------------------+ 
EVENT_MAP_BEGIN(CFormatPanel) 
ON_EVENT(ON_CLICK,BOK,OnClickButton) 
EVENT_MAP_END(CAppDialog)

void CFormatPanel::OnClickButton(void) 
  { 
// Описанная выше программа
  } 

Теперь практически весь код описанной выше программы переносится в этот обработчик событий. Внешние параметры становятся локальными переменными.

long SkipString        =1;                              // Количество пропускаемых строк
DATE indate           =yyyymmdd;                        // Исходный формат даты
TIME intime           =hhdmmdss;                        // Исходный формат времени
int DatePosition      =1;                               // Позиция даты
int TimePosition      =2;                               // Позиция времени
// другие параметры

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

//-----------ComboBox Date Format------------------------------------+     
    if(!CreateComboBox(CDateFormat,"ComDateFormat",x0,y0+h+1,x0+w,y0+2*h+1))
     {
      return false;
     }
   CDateFormat.ListViewItems(6);
   CDateFormat.AddItem(" yyyy.mm.dd",0);
   CDateFormat.AddItem(" yyyymmdd",1);
   CDateFormat.AddItem(" yymmdd",2);
   CDateFormat.AddItem(" ddmmyy",3);
   CDateFormat.AddItem(" dd/mm/yy",4);
   CDateFormat.AddItem(" mm/dd/yy",5);
   CDateFormat.Select(1);
     }

Затем эти значения возвращаются из полей ввода в соответствующие переменные:

long sw;  
SkipString =StringToInteger(ESkip.Text());

sw =CDateFormat.Value();
switch(int(sw))
{
   case 0 :indate =yyyycmmcdd;
      break;
   case 1 :indate =yyyymmdd;
      break;
   case 2 :indate =yymmdd;
      break;
   case 3 :indate =ddmmyy;
      break;
   case 4 :indate =ddslmmslyy;
      break;
   case 5 :indate =mmslddslyy;
      break;                
}
//Другие переменные

 

Понятно, что данная версия значительно объёмнее и в случае необходимости редактирования кода лучше работать с input-версией.


Часть 2. Пошаговое руководство

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

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

В статье описаны два варианта импорта данных. Через скрипт CreateCustomSymbol и эксперт CreateSymbolPanel, у которого есть панель ввода параметров. Оба советника выполняют совершенно одинаковую работу. Рассмотрим, например, работу с панелью ввода данных. В демонстрационных примерах используются котировки акций Сбербанка на Московской бирже. Они находятся в приложенном файле sb.csv.

1. Размещение файлов

Прежде всего нам нужно поместить наш файл с котировками в каталог MQL5/Files. Это с связано с концепцией программирования на языке MQL5, где из соображений безопасности строго контролируется работа с файлами. Проще всего найти нужный каталог, открыв его в MetaTrader. Для этого нужно в окне навигатора щелкнуть правой кнопкой мыши по папке "Files" и затем в контекстном меню выбрать пункт "Открыть папку".


В эту папку и должен быть помещен исходный файл с данными (куда размещать файлы программы, описано в самом конце в разделе "Файлы"). Теперь его можно открыть в MetaEditor.



В эту же папку необходимо поместить текстовый файл Specifications.txt. Он задаёт свойства символа.

2. Ввод параметров

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


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



Имя файла пишется полностью, вместе с расширением.

Теперь, прежде чем нажать на кнопку "ОК", необходимо задать необходимые спецификации символа. Они находятся в текстовом файле Specifications.txt, который мы поместили в каталог MQL5/Files.

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



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

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

Эти характеристики (в исходном файле) соответствуют акциям сбербанка на Московской бирже. Для других инструментов эти характеристики будут другие и их необходимо отредактировать. 

Набор минимально необходимых свойств расположен в самом начале файла.

Обычно для акций количество знаков после запятой (SYMBOL_DIGITS) равно 2, стоимость пункта 0.01 руб. Для фьючерсов на акции количество знаков — 0, а пункт равен 1 руб. Смотрите спецификации на сайте moex.com.

После установки необходимых свойств жмём кнопку "ОК". В окне навигатора появляется созданный символ. У меня он выделяется зелёным.


Для проверки откроем график:



Всё в порядке, теперь по символу можно проводить тестирование стратегий.

Тестирование пользовательского символа ничем не отличается от тестирования обычного символа. Главное правильно установить его спецификации.

Для примера возьмём любой стандартный советник из поставки (Moving Average) и прогоним его на наших данных:

 


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


Файлы

В приложенном архиве файлы расположены в директориях, в которых их следует расположить у себя на компьютере: