Взаимодействие между MetaTrader 4 и Matlab посредством CSV-файлов

Dmitriy | 6 июля, 2007

Введение

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

Однако взаимодействие между торговым терминалом и Matlab в реальном времени представляет собой нетривиальную задачу. В этой статье я предлагаю вариант организации обмена данными между MetaTrader 4 и Matlab посредством CSV-файлов.


1. Организация взаимодействия

Предположим, что с приходом каждого нового бара MetaTrader 4 должен отправить данные о последних 100 барах в Matlab и принять в ответ результат их обработки.

Для выполнения такой задачи нам потребуется создать индикатор для MetaTrader 4, который бы записывал данные в текстовый файл и читал результат обработки из другого текстового файла, созданного Matlab'ом.

MetaTrader 4 должен формировать свой файл с данными с приходом каждого нового бара. Чтение результата нужно пытаться осуществлять на каждом тике. Чтобы не прочитать результат раньше, чем его обновит Matlab, удалим файл с результатом ещё до того, как сформировали свой выходной файл. В этом случае попытка чтения удастся, только когда Matlab закончит свои вычисления и сформирует новый файл.

Matlab раз в секунду должен анализировать атрибуты файла, созданного MetaTrader 4, и при изменении времени его создания запускать процесс обработки. По окончании обработки пересоздаётся файл, удалённый MetaTrader 4 до начала записи данных. MetaTrader 4 его благополучно удаляет, выкладывает новые данные и ждёт ответа.


2. Формирование файла выходных данных

Сохранению данных в файл посвящено немало статей, поэтому не будем подробно останавливаться на этом. Скажем только, что данные будем записывать в 7 столбцов: “DATE”, “TIME”, “HI”, “LOW”, “CLOSE”, “OPEN”, “VOLUME”. Разделяющий символ -- “;”. Очерёдность баров -- от раннего к позднему, т.е. последней должна быть записана строка с характеристиками нулевого бара. Файл снабдим заголовком, содержащим наименования столбцов. Имя файла сформируем из имени инструмента и таймфрейма.

#property indicator_chart_window
extern int length = 100;   // Количество баров, отправляемых на обработку
double ExtMap[];           // Буфер графика
string nameData;
int init()
{
   nameData = Symbol()+".txt";         // имя отправляемого файла данных
   return(0);
}
int start()
{
   static int old_bars = 0;   // запомним число известных баров   
   if (old_bars != Bars)      // если получен новый бар 
   {
      write_data();                             // записать файл данных                              
   }      
   old_bars = Bars;              // запомним, сколько баров известно
   return(0);
}
//+------------------------------------------------------------------+
void write_data()
{
  int handle;
  handle = FileOpen(nameData, FILE_CSV|FILE_WRITE,';');
  if(handle < 1)
  {
    Comment("Не удалось создать "+nameData+". Ошибка #", GetLastError());
    return(0);
  }
  FileWrite(handle, ServerAddress(), Symbol(), Period());                  // заголовок
  FileWrite(handle, "DATE","TIME","HIGH","LOW","CLOSE","OPEN","VOLUME");   // заголовок
  int i;
  for (i=length-1; i>=0; i--)
  {
    FileWrite(handle, TimeToStr(Time[i], TIME_DATE), TimeToStr(Time[i], TIME_SECONDS),
                      High[i], Low[i], Close[i], Open[i], Volume[i]);
  }
  FileClose(handle);
  Comment("Файл "+nameData+" создан. "+TimeToStr(TimeCurrent(), TIME_SECONDS) );
  return(0);
}

Конечно, все эти данные нам не понадобятся, но всегда полезней иметь осмысленный файл, чем набор столбцов с непонятными цифрами.


3. Создание пользовательского интерфейса (GUI)

Итак, файл создан. Теперь запускаем Matlab.

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

Для создания пользовательского интерфейса запустим мастер “GUIDE Quick Start”, набрав команду “guide” в консоли или нажав кнопку на главной панели Matlab. В открывшемся диалоге выберем “Create New GUI” --> “Blank GUI (Default)”. Перед нами интерфейс создания GUI с пустым бланком. Разместим на пустом бланке следующие объекты: “Edit Text”, “Push Button”, “Static Text”, “Axes”, "Push Button". В результате должно получиться примерно как у меня.

Теперь мы должны вызвать редактор свойств каждого объекта двойным щелчком по объекту и установить свойства объектов следующим образом:

Static Text : HorizontalAlignment – left, Tag – textInfo, String - Info.
Edit Text: HorizontalAlignment – left, Tag – editPath, String – Path select .
Push Button: Tag – pushBrowse, String – Browse.
Axes: Box – on, FontName – MS Sans Serif, FontSize – 8, Tag - axesChart.
Push Button: Tag – pushSrart, String – Start.

Изменением свойства Tag мы выбираем уникальное имя для каждого объекта. Изменением других – меняем внешний вид.

Когда всё готово, запустим интерфейс нажатием кнопки “Run”, утвердительно ответим на вопрос о сохранении файла интерфейса и M-файла, зададим имя (к примеру “FromTo”) и нажмём “Сохранить”. После этого GUI будет запущен и приобретёт тот вид, который будет иметь при работе. При этом Matlab генерирует M-файл, являющийся основой нашей будущей программы, и открывает его во встроенном текстовом редакторе.

Если внешний вид Вас не устраивает, закройте работающий GUI и скорректируйте положения объектов с помощью редактора. Мой дистрибутив, к примеру, некорректно отображал шрифт MS Sans Serif. Пришлось изменять на просто “Sans Serif”.


4. Программирование пользовательского интерфейса

Поведение интерфейса программируется в M-file Editor на языке Matlab. Сгенерированная Matlabом заготовка представляет собой список функций, вызываемых при работе пользователя с объектами интерфейса. Функции пустые, поэтому GUI пока ничего не делает. Наша задача - наполнить функции требуемым содержимым.


4.1 Программирование кнопки Browse

В первую очередь нам требуется получить доступ к файлу, сгенерированному MetaTrader 4, поэтому начнём с функции, вызываемой при нажатии кнопки “Browse”.

Имя функции, вызываемой при нажатии кнопки, состоит из имени кнопки (задается свойством “Tag”)и постфикса "_Callback". Найдём функцию “pushBrowse_Callback” в тексте файла или нажмём кнопку “Show Functions” на панели инструментов и выберем “pushBrowse_Callback” из списка.

Синтаксис языка программирования Matlab отличается от привычных правил написания на языке С и им подобных. В частности, не требуется обозначать тело функции фигурными скобками и указывать тип передаваемых в функцию данных, нумерация элементов массива (вектора) начинается с единицы, а символ комментирования – “%”. Так что, весь этот зелёный текст – не программа, а комментарий, оставленный разработчиками Matlab, чтобы нам было проще разобраться в ситуации.

Нам потребуется создать диалог для указания полного имени файла. Воспользуемся функцией “uigetfile” для этого:

% --- Executes on button press in pushBrowse.
function pushBrowse_Callback(hObject, eventdata, handles)
[fileName, filePath] = uigetfile('*.txt'); % получить имя и путь от юзера
if fileName==0          % если нажата отмена
    fileName='';        % создать пустое имя
    filePath='';        % создать пстой путь
end
fullname = [filePath fileName] % сформировать полное имя
set(handles.editPath,'String', fullname); % вписать в editPath

“handles” здесь – структура, хранящая дескрипторы всех объектов нашего GUI, в том числе и формы, на которой мы их разместили. Эта структура передаётся из функции в функцию и позволяет осуществлять доступ к объектам;
“hObject” – дескриптор объекта, вызвавшего функцию;
“set” – устанавливает свойство объекта в некоторое значение и имеет синтаксис: set(дескриптор_объекта, имя_свойства_объекта, значение_свойства).

Узнать значение свойств объекта можно, используя функцию: значение_свойства = get(дескриптор_объекта, имя_свойства_объекта).
Только не забывайте, что имя – значение типа string, поэтому заключается в одинарные кавычки.

И последнее об объектах и свойствах. Форма, на которой мы разместили элементы GUI, сама является объектом, размещённым в объекте “root” (является его потомком). Она также имеет набор свойств, доступных к изменению. Просмотреть свойства можно с помощью инструмента “Object Editor”, вызываемого с главной панели редактора интерфейса. Объект “root”, как следует из названия, является корнем иерархии графических объектов и предков не имеет.

Теперь проверим, что у нас получилось. Запустим наш GUI, нажав кнопку Run на главной панели М-file Editor, попробуем кликнуть на Browse и выбрать наш файл. Получилось? Тогда закроем работающий GUI и двинемся дальше.


4.2 Программирование кнопки Start, отрисовка графика


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

Сначала создадим саму функцию. В качестве входных данных нам понадобится структура дескрипторов объектов handles. Имея доступ к объектам, мы сможем читать и устанавливать их свойства.

% чтение данных, построение графиков, обработка, сохранение
function process(handles)
fullname = get(handles.editPath, 'String'); % прочитать имя из editPath
data = dlmread(fullname, ';', 2, 2);    % прочитать матрицу из файла
info = ['Last update: ' datestr(now)];  % сформировать информационное сообщение
set(handles.textInfo, 'String',info);   % вписать info в информационную строку
 
high = data(:,1);   % в high теперь первый столбец матрицы data
low = data(:,2);    % d low -- второй
close = data(:,3);  % --/--
open = data(:,4);   %
len = length(open); % количество элементов в open
 
axes(handles.axesChart); % сделать оси текущими
hold off; % очищать оси перед добалением нового графика
candle(high, low, close, open); % нарисовать "свечной" график (в текущих осях)
set(handles.axesChart,'XLim',[1 len]); % установить пределы отображения на графике

В качестве пояснения:

“dlmread” - читает данные из текстового файла с разделителями и имеет синтаксис: dlmread(полное_имя_файла, разделитель, пропустить_строк, пропустить_столбцов);
“length(qqq)” – наибольший размер матрицы qqq.
”now” –текущие время и дата
“datestr(now)” – преобразует время и дату в текстовый вид.
А вообще, в Matlab просто огромный help с теорией и примерами.

Разместим нашу функцию в самом конце программы (там её будет легче найти), а в “pushStart_Callback” добавим её вызов:

% --- Executes on button press in pushStart.
function pushStart_Callback(hObject, eventdata, handles)
process(handles);

Запускаем кнопкой “Run”, выбираем файл, нажимаем "Start" и любуемся результатом.


4.3 Сохранение пути к файлу

Всё бы хорошо, только щёлкать мышкой, выбирая файл после нажатия “Browse”, надоело. Попробуем сохранить однажды выбранный путь в файл.
Начнём с чтения. Имя файла, хранящего путь, будет состоять из имени GUI и постфикса “_saveparam” и иметь расширение “.mat”.
Функция “FromTo_OpeningFcn” выполняется непосредственно после создания формы с GUI. Добавим в неё попытку чтения пути из файла. Если попытка не удастся, используем значениe “по умолчанию”.

% --- Executes just before FromTo is made visible.
function FromTo_OpeningFcn(hObject, eventdata, handles, varargin)
guiName = get(handles.figure1, 'Name'); % получить имя нашего GUI
name = [guiName '_saveparam.mat']       % определить имя файла
h=fopen(name);                          % попытаться открыть файл
if h==-1                                % если файл не открывается
    path='D:\';                         % значение по умолчанию
else
    load(name);                         % прочитать файл    
    fclose(h);                          % закрыть файл
end
set(handles.editPath,'String', path);   % вписать имя в объект "editPath"

Остальные строки функции “FromTo_OpeningFcn” оставим без изменений.


Функцию “pushBrowse_Callback” изменим следующим образом:

% --- Executes on button press in pushBrowse.
function pushBrowse_Callback(hObject, eventdata, handles)
path = get(handles.editPath,'String'); % прочитать путь из объекта editPath 
[partPath, partName, partExt] = fileparts(path);    % разбить путь на части
template = [partPath '\*.txt'];                     % создать шаблон из частей
[userName, userPath] = uigetfile(template);         % получить имя и путь от юзера
if userName~=0                                      % если не нажали отмена        
    path = [userPath userName];                     % собрать путь
end
set(handles.editPath,'String', path);               % вписать путь в объект "editPath"
guiName = get(handles.figure1, 'Name');             % узнать имя нашего GUI
save([guiName '_saveparam.mat'], 'path');           % сохранить путь

4.4 Обработка данных

В качестве примера обработки выполним интерполяцию столбца “OPEN” полиномиальной функцией 4-ого порядка.
Добавим в конец нашей функции “process” следующий код:

fitPoly2 = fit((1:len)',open,'poly4'); % получить формулу полинома
fresult = fitPoly2(1:len); % вычислить значения Y для X=(от 1 до len)
hold on; % новый график добавится к старому
stairs(fresult,'r'); % plot ступеньками цветом - 'r'- red


Попробуем запустить и нажать “Start”.


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


4.5 Сохранение данных в файл

Сохранение данных выполняется не сложнее, чем чтение. Единственная тонкость в том, что отсчёты вектора “fresult” нужно записать в обратном порядке. Т.е. от последнего к первому. Это делается для того, чтобы было проще прочитать файл из MetaTrader 4 – начиная с нулевого бара и пока не кончится файл.

Дополним функцию “process” следующим кодом:

[pathstr,name,ext,versn] = fileparts(fullname); % разобрать полное имя
                                                % файла на части
newName = [pathstr '\' name '_result' ext];     % собрать новое имя файла
fresult = flipud(fresult);  % перевернуть вектор fresult
dlmwrite(newName, fresult);    % записать в файл

Теперь убедитесь, что файл с результатом создан, располагается там же, где исходный и имеет то же имя, но дополненное постфиксом “_result”.


4.6 Управление таймером

Это самая сложная часть работы. Нам потребуется создать таймер, который раз в секунду проверял бы время создания файла, сформированного MT4. Если время изменилось, должна быть запущена функция “process”. Пуск-останов таймера будем осуществлять кнопкой “Start”. При открытии GUI все ранее созданные таймеры удалим.

Создадим таймер, разместив внутри функции “FromTo_OpeningFcn” следующий код:

timers = timerfind; % найти таймеры
if ~isempty(timers) % если есть таймеры
    delete(timers); % удалить все  таймеры
end
handles.t = timer('TimerFcn',{@checktime, handles},'ExecutionMode','fixedRate','Period',1.0,'UserData', 'NONE');

Код нужно разместить сразу за нашей предыдущей вставкой в эту функцию. Т.е. перед строками “handles.output = hObject;” и “guidata(hObject, handles);”

Выполняя этот код, Matlab сразу после создания GUI проверит наличие таймеров, удалит существующие и создаст новый таймер. Каждую секунду таймер будет вызывать функцию “checktime” и передавать ей список дескрипторов “handles”. Кроме “handles” таймер передаст в функцию свой дескриптор и структуру, которая содержит время и причину вызова. На это мы повлиять не можем, но должны учесть, при написании вызываемой таймером функции.

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

% функция, вызаваемая таймером
function checktime(obj, event, handles)
set(handles.textInfo,'String',datestr(now));

При создании таймер находится в остановленном состоянии и теперь его нужно запустить. Найдём функцию “pushStart_Callback” . Закомментируем размещённый в ней вызов 'process(handles)” и впишем управление таймером:

% --- Executes on button press in pushStart.
function pushStart_Callback(hObject, eventdata, handles)
% process(handles);
statusT = get(handles.t, 'Running'); % Узнать состояние таймера
if strcmp(statusT,'on')     % Если включен - 
    stop(handles.t);        % выключить
    set(hObject,'ForegroundColor','b'); % изменить цвет надписи на pushStart
    set(hObject,'String',['Start' datestr(now)]); % изменить надпись на кнопке
end     
if strcmp(statusT,'off')    % Если выключен - 
    start(handles.t);       % включить
    set(hObject,'ForegroundColor','r');% изменить цвет надписи на pushStart
    set(hObject,'String',['Stop' datestr(now)]); % изменить надпись на кнопке
end 

Теперь проверим, как всё работает. Кнопкой “Start” попробуем запустить и выключить таймер. При включенном таймере часы над полем ввода пути должны идти.

Вообще-то, правильнее будет удалять таймер при закрытии GUI кнопкой “Х”. Если вы хотите так сделать, добавьте

stop(handles.t) ; % остановить таймер
delete(handles.t); % удалить таймер

в начало функции “figure1_CloseRequestFcn”. Эта функция будет вызвана при закрытии GUI. Доступ к ней можно получить из редактора GUI:

Но учтите, теперь, если вы нажмёте кнопку “Run” редактора, не закрыв работающий GUI, старый таймер не будет удалён, но новый будет создан. Следующий раз – ещё один. Бороться с неупокоенными таймерами можно командой “delete(timerfind)” с консоли Matlab.


Теперь, если всё работает, пишем функцию проверки времени последнего изменения файла от MetaTrader 4:

% функция, вызываемая таймером
function checktime(obj, event, handles)
filename = get(handles.editPath, 'String'); % узнать имя файла 
fileInfo = dir(filename);        % получить информацию о файле
oldTime = get(obj, 'UserData');  % вспомнить время
if ~strcmp(fileInfo.date, oldTime) % если время изменилось
    process(handles);
end
set(obj,'UserData',fileInfo.date); % запомнить время
set(handles.pushStart,'String',['Stop ' datestr(now)]); % переписать время

Функция "dir(полное_имя_файла)" возвращает структуру, содержащую информацию о файле (name, date, bytes, isdir). Информацию о прошлом времени создания файла будем хранить в свойстве "Userdata" объекта таймер. Его дескриптор передаётся в функцию "checktime" под именем obj.

Теперь при изменении файла, созданного MetaTrader 4, наша программа будет переписывать результат. Проверить это можно, изменяя файл вручную (например, удаляя последние строки) и отслеживая изменения на графике или в файле результата. Кнопка "Start" при этом, естественно, должна быть нажата.

Если при работе программы создаётся ещё одно окно с копией графика, впишите в самое начало функции "process" следующую строку:

set(handles.figure1,'HandleVisibility','on');


5. Отрисовка результата в MetaTrader 4

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

1. Если получен новый бар: Удалить старый файл результата, Стереть график, Записать файл данных.
2. Если файл результата читается: Прочитать файл, Нарисовать график, Удалить файл результата.

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

Ошибки чтения возникают в двух случаях:
1. Сразу после прихода нового бара, поскольку файл результата ещё не сформирован.
2. Сразу после прочтения результата и отрисовки графика, поскольку файл удалён, чтобы не перечитывать одни и те же данные.
Таким образом, программа находится в состоянии ошибки чтения практически всегда. :)

#property indicator_chart_window
#property indicator_buffers 1
#property indicator_width1 2
#property indicator_color1 Tomato
extern int length = 100;   // Количество баров, отправляемых на обработку
double ExtMap[];           // Буфер графика
string nameData;
string nameResult;

int init()
{
   nameData = Symbol()+".txt";         // имя отправляемого файла данных
   nameResult = Symbol()+"_result.txt";// имя принимаемого файла  с результатом
   SetIndexStyle(0, DRAW_LINE);
   SetIndexBuffer(0, ExtMap);
   return(0);
}
int deinit()
  {
   Comment("");
   return(0);
  }
int start()
{
   static int attempt = 0;    // число попыток чтения результата
   static int old_bars = 0;   // запомним число известных баров
   
   if (old_bars != Bars)      // если получен новый бар 
   {
      FileDelete(nameResult);                   // удалить файл результата
      ArrayInitialize( ExtMap, EMPTY_VALUE);    // очистить график
      write_data();                             // записать файл данных
      
      old_bars = Bars; return(0);               // больше ничего в этот раз не делать                           
   }
   //
   int handle_read = FileOpen(nameResult,
                              FILE_CSV|FILE_READ,
                              ';'); // попытаться открыть файл с результатом
   attempt++;                       // посчитать попытку открытия
   
   if(handle_read >= 0)             // если файл окрылся для чтения
   { 
      Comment(nameResult+". Открылся с попытки #"+ attempt); // отчёт об открытии
      read_n_draw(handle_read);  // прочитать результат и нарисовать график
      FileClose(handle_read);    // закрыть файл
      FileDelete(nameResult);    // удалить файл результата
      attempt=0;                 // обнулить количество попыток чтения
   }
   else                          // если не можем открыть файл результата
   {
      Comment( "Не удалось прочитать "+nameResult+
               ". Попыток: "+attempt+
               ". Ошибка #"+GetLastError()); //Отчёт о невозможности прочитать
   }
   old_bars = Bars;              // запомним, сколько баров известно
   return(0);
}
//+------------------------------------------------------------------+
void read_n_draw(int handle_read)
{
   int i=0;
   while ( !FileIsEnding(handle_read)) 
   {
      ExtMap[i] = FileReadNumber(handle_read);
      i++;     
   }
   ExtMap[i-1] = EMPTY_VALUE;
}

 
void write_data()
{
  int handle;
  handle = FileOpen(nameData, FILE_CSV|FILE_WRITE,';');
  if(handle < 1)
  {
    Comment("Не удалось создать "+nameData+". Ошибка #", GetLastError());
    return(0);
  }
  FileWrite(handle, ServerAddress(), Symbol(), Period());                  // заголовок
  FileWrite(handle, "DATE","TIME","HIGH","LOW","CLOSE","OPEN","VOLUME");   // заголовок
  int i;
  for (i=length-1; i>=0; i--)
  {
    FileWrite(handle, TimeToStr(Time[i], TIME_DATE), TimeToStr(Time[i], TIME_SECONDS),
                      High[i], Low[i], Close[i], Open[i], Volume[i]);
  }
  FileClose(handle);
  Comment("Файл "+nameData+" создан. "+TimeToStr(TimeCurrent(), TIME_SECONDS) );
  return(0);
}

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





Заключение

Мы рассмотрели способ организации взаимодействия между MetaTrader 4 и Matlab посредством CSV-файлов. Этот способ не является ни единственным, ни оптимальным. Ценность такого подхода лишь в том, что он позволяет обмениваться массивами данных, не требуя навыков владения программными инструментами, отличными от МТ4 и Matlab.