Взаимодействие между MetaTrader 4 и Matlab посредством DDE

Dmitriy | 30 июня, 2008



Введение

Мной уже размещена здесь статья об обмене данными между MetaTrader 4 и Matlab посредством CSV-файлов (MT4 <-CSV->Matlab). Однако подход, описанный в статье, во многих случаях нерационален, а во многих - просто неприменим.

Поддерживаемый в MT4 механизм DDE (Dynamic data exchange) позволяет передавать данные из приложения в приложение непосредственно через RAM компьютера. Matlab обладает всей полнотой функций для реализации как клиентской, так и серверной части DDE, и нам хотелось бы воспользоваться этой возможностью.

DDE-сервер MT4 предоставляет только тиковые и только последние данные, но даже с такими ограничениями DDE-обмен предпочтительнее, например, при работе с котировками внутри баров.

Как и в статье “MT4 <-CSV->Matlab” я буду описывать последовательность создания инструмента для организации обмена.

Не забудьте разрешить передачу по DDE на вкладке “ Сервис -> Настройки -> Сервер” в вашем терминале MT4 и начнём.


Сначала о DDE

Итак, в организации обмена данными по DDE существуют две стороны, между которыми устанавливается связь – сервер и клиент. Клиент – это приложение, запрашивающее данные (в нашем случае - Matlab), сервер – приложение, обладающее этими данными (MT4).

Передача данных от сервера к клиенту в DDE возможна трёмя способами:
- по запросу клиента,
- по запросу клиента, после получения от сервера уведомления о готовности данных,
- по готовности данных.

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

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

Создание GUI

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

Подробнее построение GUI описано в разделе 3 статьи “MT4 <-CSV->Matlab”, поэтому здесь я лишь упомяну консольную команду “guide”, запускающую мастер создания GUI, и приведу список графических объектов, которые нам понадобятся.

Итак, нам необходимы:
- поле ввода “Edit Text” для ввода имени валютной пары;
- оси “Axes” для вывода графика;
- два поля вывода текста “Static Text” для вывода точного значения последней котировки и чего-нибудь ещё.

Вот как я разместил эти объекты на бланке GUI:


Установите свойства графических объектов следующим образом:

Для Axes:
Tag = axesChart (сюда будем выводить график);
Box = on (обводит поле графика полным прямоугольником, off – только слева и снизу);
FontSize = 7 (размер, установленный по умолчанию – просто огромный);
Units = pixels (понадобится при построении графика для установки масштаба 1:1).

Для EditText:
Tag = editPair (в это поле будем вводить название валютной пары).

Для StaticText под полем EditText:
Tag = textBid (сюда мы будем выводить точное значение последней котировки);
HorizontalAlignment = left (не принципиально, можете оставить 'center').

Для StaticText в самом низу бланка:
Tag = textInfo;
HorizontalAlignment = left.

Теперь можно нажать RUN.
Я назвал свой проект "DDEs", и если вы хотите, чтобы у вашей версии не было расхождения с моей версией, - назовите свой проект также.
Если внешний вид GUI вас устраивает, и m-файл готов к редактированию, - приступим к созданию DDE-клиента.

Инициализация соединения

В первую очередь необходимо организовать канал связи с сервером при запуске нашего GUI и озаботиться разрывом соединения при закрытии интерфейса.
В Matlab инициализация DDE- соединения осуществляется функцией: channel = ddeinit('service','topic');
‘service’ здесь – имя DDE-сервера (‘MT4’)
'topic’ – имя раздела данных. В нашем случае может принимать значения 'BID', ‘ASK’, ‘QUOTE’ и т.п.
Функция возвращает дескриптор проинициализированного канала, используемый для дальнейших обращений(conversation) к DDE-серверу.

Необходимо также задать способ обмена. В Matlab способ обмена, поддерживаемый MT4, называется “Advisory link” и инициализируется функцией: rc = ddeadv(channel,'item','callback','upmtx',format);,
Где channel – дескриптор проинициализированного канала,
‘item’ – данные, которые нас интересуют, т.е. символьное имя валютной пары,
'callback' – строка для выполнения при приходе данных от сервера,
'upmtx' – символьное имя переменной, в которую занесутся данные от сервера,
format – массив из двух флагов, определяющий формат отправленных данных.
Функция ddeadv возвращает “1” в случае успеха и “0” в другом случае.

Обратите внимание, что в качестве параметра ‘callback’ указывается не дескриптор функции, а символьное выражение. Фактически будет выполнена функция “eval”, исполняющая строку, словно она набрана в консоли. С этой особенностью связано следующее затруднение:
по приходу новой котировки нам необходимо будет выполнить большую функцию приёма новой котировки. Причём хотелось бы передать в эту функцию структуру дескрипторов “handles”, с помощью которой мы намерены получать доступ к графическим объектам GUI. Но я не нашёл ни способов передать дескриптор структуры handles в исполняемую строку, ни просто вызвать функцию, расположенную в m-файле, описывающем GUI.
Всё это привело к тому, что я был вынужден вынести функцию принятия новой котировки в отдельный m-файл и вызывать уже её, как обычную функцию Matlab. Правда, неудобство оказалось достоинством, когда я обнаружил, что могу редактировать функцию-обработчик не прерывая работы DDE-клиента.

Итак, первым делом создадим отдельную функцию-обработчик, просто выводящую полученные ею данные в консоль.

function newTick(simbols)
% обработка нового тика
disp(simbols); % вывести аргумент в консоль
song = wavread('C:\WINDOWS\Media\Windows XP - пуск.wav'); % прочитать звук
wavplay(song,40000); % играть звук с частотой дискретизации 40kHz

Приведённый пример функции проигрывает ещё файл 'C:\WINDOWS\Media\Windows XP - пуск.wav' при поступлении каждой новой котировки. Сохраним текст функции в рабочую папку MATLAB под именем "newTick.m".

Теперь редактируем m-файл, описывающий поведение нашего GUI. В функцию DDEs_OpeningFcn добавим инициализацию соединения, а в функцию figure1_CloseRequestFcn - деинициализацию.
(Для добавления функции CloseRequestFcn в m-файл, - в GUI-эдиторе необходимо выполнить: View -> View Callbacks -> CloseRequestFcn).

% --- Executes just before DDEs is made visible.
function DDEs_OpeningFcn(hObject, eventdata, handles, varargin)
% This function has no output args, see OutputFcn.
% hObject handle to figure
% eventdata reserved - to be defined in a future version of MATLAB
% handles structure with handles and user data (see GUIDATA)
% varargin command line arguments to DDEs (see VARARGIN)

channel = ddeinit('MT4','QUOTE'); % инициализация
pair = get(handles.editPair,'UserData'); % прочитать имя инструмента
rc = ddeadv(channel, pair,'newTick(x)','x',[1 1]); % установить связь
if (rc==1) % если связь установлена
disp('Connected'); % сообщить в консоль
end
handles.chann = channel; % сохранить ID канала в handles

% Choose default command line output for DDEs
handles.output = hObject;
% Update handles structure
guidata(hObject, handles);
% UIWAIT makes DDEs wait for user response (see UIRESUME)
% uiwait(handles.figure1);

% --- Executes when user attempts to close figure1.
function figure1_CloseRequestFcn(hObject, eventdata, handles)
% hObject handle to figure1 (see GCBO)
% eventdata reserved - to be defined in a future version of MATLAB
% handles structure with handles and user data (see GUIDATA)

channel = handles.chann; % получить ID канала из handles
pair = get(handles.editPair,'UserData'); % прочитать имя инструмента
ddeunadv(channel,pair); % разорвать связь
rc = ddeterm(channel); % деинициализация
if (rc==1) % если всё ОК
disp('Disconnected'); % сообщить в консоль
end


% Hint: delete(hObject) closes the figure
delete(hObject);

% --- Executes during object creation, after setting all properties.
function editPair_CreateFcn(hObject, eventdata, handles)
% hObject handle to editPair (see GCBO)
% eventdata reserved - to be defined in a future version of MATLAB
% handles empty - handles not created until after all CreateFcns called

set(hObject, 'String', 'EURUSD'); % Записать имя инструмента в поле ввода
set(hObject, 'UserData', 'EURUSD'); % И в UserData поля ввода - записать


% Hint: edit controls usually have a white background on Windows.
% See ISPC and COMPUTER.
if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor'))
set(hObject,'BackgroundColor','white');
end


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

Последний блок реализует запись имени инструмента в соответствующее поле до запуска GUI. Запись дублируется в свойстве 'UserData'. Копию в 'UserData' мы будем использовать всегда, а отображаемое в поле имя ('String') - только при попытке пользователя сменить инструмент. Если, пользователь ошибся при наборе и в 'String' записано неверное имя, - мы будем возращаться к имени, хранящемся 'UserData'.

Следующий код реализует функцию смены имени инструмента пользователем:

function editPair_Callback(hObject, eventdata, handles)
% hObject handle to editPair (see GCBO)
% eventdata reserved - to be defined in a future version of MATLAB
% handles structure with handles and user data (see GUIDATA)

oldPair = get(hObject,'UserData'); % имя бывшего инструмента
newPair = get(hObject,'String'); % имя нового инструмента
channel = handles.chann; % получить ID канала

disconn = ddeunadv(channel,oldPair); % разорвать связь
if (disconn==0) % если не удалось разорвать связь
set(hObject,'String',oldPair); % вернуть имя старого инструмента в поле ввода
else % если связь разорвана
conn = ddeadv(channel, newPair,'newTick(x)','x',[1 1]); % установить новую связь
if (conn==1) % если связь установлена
set(hObject,'UserData',newPair); % запомнить какой инструмент используется
else % если не удалось установить новую связь
ddeadv(channel, oldPair,'newTick(x)','x',[1 1]); % вернуть старую
set(hObject,'String',oldPair); % вернуть имя старого инструмента в поле вввода
end
end

% Hints: get(hObject,'String') returns contents of editPair as text
% str2double(get(hObject,'String')) returns contents of editPair as a double



Приём тиков

Будем считать, что связь установлена и при при приходе нового тика вызывается функция "newTick(x)", отпечатывающая полученный от MT4 аргумент в консоль. Для начала отобразим последнюю принятую котировку в соответствующей строке нашего GUI.

Для этого нам необходимо иметь структуру дескрипторов графических объектов GUI - handles в распоряжении функции "newTick". Воспользуемся функцией setappdata(h,name,value), сохраняющей данные в область приложения. В качестве ID приложения укажем "0". Это дескриптор объекта root Matlab, он неизменен и мы всегда можем его знать.

Добавим строку "setappdata(0,'hndls',handles);" сразу после заголовка функции "DDEs_OpeningFcn":

function DDEs_OpeningFcn(hObject, eventdata, handles, varargin)
setappdata(0,'hndls',handles); %

Теперь в функции "newTick" мы сможем извлечь handles функцией value = getappdata(h,name), указав "0" в качестве аргумента "h". Тогда из функции "newTick" мы сможем управлять объектами GUI.

Далее мы преобразуем строковый аргумент, переданный в функцию от DDE-сервера и выведем значение Bid в GUI. Кроме того, мы определим локальное время получения котировки и тоже отобразим его, но в информационной строке GUI. Локальное время необходимо потому, что DDE-сервер передаёт время с точностью до минут, что неприемлемо для работы с тиками. Функция now возвращает локальное время с точностью до долей миллисекунд, поэтому беспокоиться от том, что разные тики будут иметь одно и то же время - не будем. Время сервера тоже вычленим из строки, полученной от DDE-сервера, и преобразуем в формат времени Matlab.

Вот пример функции "newTick":

function newTick(simbols)
% ОБРАБОТКА НОВОГО ТИКА

timeLocal = now; % Узнать точное локальное время
handles = getappdata(0,'hndls'); % Получить handles из root

% disp(simbols); % вывести аргумент в консоль (закоментировано)
song = wavread('C:\WINDOWS\Media\Windows XP - пуск.wav'); %прочитать звук
wavplay(song,40000); % играть звук с частотой дискретизации 40kHz

set(handles.textInfo,'String', datestr(timeLocal)); % вывести локальное время в GUI

% --- преобразование полученной от MT4 строки ---
parts = sscanf(simbols, '%i/%i/%i %i:%i %f %f' ); % разбор строки в соответствии
%с форматом: int/int/int int:int float float
timeServerVect = parts(1:5); % вычленить время
timeServerVect = timeServerVect'; % транспонировать (столбец в строку)
timeServerVect = [timeServerVect 00]; % добавить секунды
timeServer = datenum(timeServerVect); % перевести в формат времени Matlab
Bid = parts(6); % вычленить Bid
Ask = parts(7); % вычленить Ask
% --- конец преобразования ---

set(handles.textBid,'String',['Bid: ' num2str(Bid)]); % Вывести Bid в GUI



Построение тикового графика

Вот продолжение функции "newTick", начатой чуть выше. Код снабжён подробными комментариями и, я думаю, вам не составит труда в нём разобраться.
Поясню только, что массив котировок Bid,
также как и handles, хранится в пространстве объекта root, но под именем "data". Хранимые данные представляют собой структуру, состоящую из двух полей:
data.name - символьное имя валютной пары;
data.array - собственно массив котировок.

В функции "newTick" эти данные фигурируют под именем "ticks", и поля структуры имеют имена ticks.name и ticks.array соответственно.

ticks.array - представляет собой массив, состоящий из трёх столбцов:
- локальное время в формате времени Matlab (с точностью, обеспечиваемой форматом времени Matlab [микросекунды]);
- время сервера
в формате времени Matlab (с точностью до минут);
- Bid.

Функция "newTick" очищает массив котировок, если имя рабочего инструмента в поле "editPair" изменилось и поступают котировки другого инструмента. Если НЕ изменилось - дописываются строки к существующему массиву.

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

% --- работа с массивом котировок ---
GUIpairName = get(handles.editPair, 'UserData'); % имя инструмента
if (~isappdata(0,'data')) % если данных не было
ticks.name = GUIpairName; % сформировать поле имени
ticks.array = []; % сформировать поле - пустой массив
setappdata(0,'data',ticks); % записать данные в root
end
ticks = getappdata(0,'data'); % извлечь данные
if ~strcmp(ticks.name,GUIpairName) % если имя изменилось
ticks.name = GUIpairName; % сформировать поле имени
ticks.array = []; % сформировать поле - пустой массив
setappdata(0,'data',ticks); % записать данные в root
end
ticks.array = [ticks.array; timeLocal timeServer Bid]; % добавить строку
% с новыми данными к существующему массиву данных
setappdata(0,'data',ticks); % записать данные в root
% --- конец работы с массивом ---

% --- работа с графиком ---
chartSize = get(handles.axesChart,'Position');% получить размеры окна графика
chartSize = chartSize(3); % вычленить ширину окна графика
lenArray = size(ticks.array); % получить размеры массива данных
lenArray = lenArray(1); % вычленить количество строк в массиве данных

set(handles.axesChart, 'NextPlot','replace'); % режим отрисовки - замена
% старого графика новым

if (chartSize >= lenArray)
stairs(handles.axesChart,ticks.array(:,3)); % нарисовать весь график
else
stairs(handles.axesChart,ticks.array(lenArray-chartSize+1:lenArray,3));
% отобразить последние данные, умещающиеся на графике
end
set(handles.axesChart,'XLim',[1 chartSize]); % установить масштаб - один отсчёт
% в одном пикселе ширины
set(handles.axesChart, 'NextPlot','add'); % режим отрисовки - добавление
plot(handles.axesChart,[1 chartSize], [Bid Bid],'m');% нарисовать горизонталь Bid



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

Последняя функция, которую я опишу - это сохранение тиковых данных в файл по запросу пользователя.
Сохранение будем выполнять при нажатии кнопки, поэтому добавьте объект "Push Button" на бланк GUI с помощью редактора.

Установите свойства объекта: Tag = pushSave, String = Save.

При нажатии кнопки "M-file Editor" заготовка функции pushSave_Callback автоматически будет добавлена в конец файла "DDEs.m".

Вот полный текст функции, выполняющей сохранение:

% --- Executes on button press in pushSave.
function pushSave_Callback(hObject, eventdata, handles)
% hObject handle to pushSave (see GCBO)
% eventdata reserved - to be defined in a future version of MATLAB
% handles structure with handles and user data (see GUIDATA)
date = datestr(now,'yyyy-mm-dd'); % узнать дату (string)
time = datestr(now,'HH-MM-SS') % узнать время (string)
name = get(handles.editPair,'UserData');% узнать имя инструмента(string)
template = [name '@' date '@' time]; % сформировать имя файла
[userName, userPath] = uiputfile([template '.txt']); % получить имя и путь от юзера
if userName~=0 % если не нажали отмена
ticks = getappdata(0,'data'); % полчить данные из root

timesStr = datestr(ticks.array(:,1)); % сформировать string-массив
% времени и даты
bidStr = num2str(ticks.array(:,3)); % сформировать string-массив BID
delimStr(1:length(bidStr)) =' ' ; % сформировать "столбец" разделитель
% точнее строку, которая будет транспонироваться в столбец
matrix=[timesStr delimStr' bidStr]; % слить все Str в одну матрицу
dlmwrite([userPath userName], matrix, '');% записать матрицу в файл
end


Функция подготавливает имя файла, состоящее из локального времени, даты и символьного имени инструмента.
При осуществлении записи предварительно подготавливаются три матрицы символов:
- timesStr -- локальные дата и время, соответствующие котировкам;
- delimStr -- разделители;
- bidStr -- столбец BID.
Затем всё объединяется в единую матрицу.

delimStr - представляет собой строку из пробелов, длина которой равна длине столбца BID. При объединении - строка delimStr транспонируется в столбец, и отделяет столбец котировок от времени.


Заключение

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