Interacción entre MetaTrader 4 y Matlab mediante DDE

Dmitriy | 11 mayo, 2016



Introducción

Ya he publicado un artículo sobre el intercambio de datos entre MetaTrader 4 y Matlab mediante archivos CSV (MT 4 <-CSV-> Matlab). Sin embargo, el enfoque descrito en el artículo es, en muchos casos, imposible, e, incluso, a menudo inaceptable.

El procedimiento DDE (Intercambio dinámico de datos) soportado por MT 4 nos permite transferir datos de una aplicación a otra directamente a través de la memoria RAM del ordenador. Matlab tiene la funcionalidad completa para realizar, tanto la interfaz como el procesado de DDE, por lo que nos gustaría aprovechar esta oportunidad.

El servidor DDE de MetaTrader 4 sólo proporciona los datos tick más recientes. Sin embargo, aun teniendo en cuenta estas limitaciones, el DDE es el más adecuado cuando, por ejemplo, trabajamos con cotizaciones dentro de las barras.

Al igual que en el artículo de "MT 4 <-CSV-> Matlab", voy a describir la secuencia de creación de una herramienta de organización del intercambio.

No se olvide de habilitar el servidor DDE en Herramientas -> Opciones -> pestaña Servidor, en su Terminal de cliente MetaTrader 4, y podemos empezar.


Acerca de DDE

Así, en la organización del intercambio de datos mediante DDE, hay dos extremos (parte delantera y parte final) entre los cuales se establecerá la conexión. La parte delantera es una aplicación que solicita datos (Matlab, en nuestro caso), la parte final es una aplicación que tiene esta información a su disposición (MT 4).

Los datos pueden ser transferidos desde el servidor al cliente a través de DDE de tres modos:
- mediante petición del cliente,
- mediante petición del cliente y después de que el servidor haya notificado que los datos están preparados para la transferencia, o
- en cuanto los datos estén listos para su transferencia.

El servidor DDE de MetaTrader 4 sólo trabaja en un modo (el tercero) y envía los datos listos al cliente sin esperar peticiones, confirmaciones, ni otras cosas por el estilo. =) Así que la tarea de Matlab es notificar a MT 4 que tiene un cliente, informar sobre qué datos se necesitan y esperar hasta que lleguen estos datos.

Tan pronto como lleguen los datos, nos limitaremos a mostrarlos en un gráfico.

Creando una GUI

En el entorno de Matlab, hay una posibilidad de crear una interfaz gráfica de usuario (GUI). Una vez se haya creado la GUI, combinaremos en ella todos los controles, los gráficos de precios y la información textual que estimemos necesaria mostrar.

La creación de una GUI se describe con más detalle en el apartado 3 del artículo "MT4 <-CSV-> Matlab", por lo que sólo voy a mencionar aquí el comando de consola denominado "guide" que ejecuta el Asistente de Creación de GUI, así como la lista de objetos gráficos que necesitaremos.

Así pues, necesitamos lo siguiente:
- campo de entrada "Edit Text" para introducir el nombre del par de divisas;
- "Axes" para visualizar el gráfico;
- dos campos de salida de texto "Static Text" para visualizar el valor exacto de la última cotización, u otra cosa.

A continuación se ilustra la forma en que he colocado los objetos en una pantalla de GUI:


Debe establecer las propiedades del objeto gráfico de la siguiente manera:

Para Axes:
Tag = axesChart (vamos a mostrar el gráfico aquí);
Box = on - encierra el área del gráfico en un rectángulo completo, off - encierra el área del gráfico con una línea a la izquierda y una línea abajo;
FontSize = 7 (el tamaño por defecto es simplemente enorme);
Units = pixels (necesario para trazar el gráfico con escala 1:1).

Para EditText:
Tag = editPair (vamos a introducir el nombre del par de divisas en este campo).

Para StaticText debajo del campo EditText:
Tag = textBid (aquí introduciremos el valor exacto de la última cotización);
HorizontalAlignment = left (esto no es muy importante, puede dejarlo como "center").

Para StaticText en la parte inferior de la pantalla:
Tag = TextInfo;
HorizontalAlignment = left.

Ahora podemos pulsar RUN.
He llamado a mi proyecto "DDEs", por lo que si quiere que su versión no tenga ninguna discrepancia con la mía, por favor, llámelo con el mismo nombre.
Si el aspecto de su GUI le parece bien y el archivo .m está listo para su edición, vamos a empezar a crear un cliente DDE.

Iniciando la conexión

En primer lugar, debe organizar el canal para conectar con el servidor al lanzar la GUI, y procurar que la conexión se interrumpa cuando se cierra la interfaz.
En Matlab, se inicializa la conexión DDE mediante la función: channel = ddeinit('service','topic'),
dónde:
‘service’ – DDE server name (‘MT4’)
'topic’ – nombre de una sección de datos. En nuestro caso, puede tomar los valores de 'BID', ‘ASK’, ‘QUOTE’, etc.
La función devuelve el descriptor del canal inicializado. Este descriptor se utilizará para más conversaciones con el servidor DDE.

También debe especificar el método de intercambio. En Matlab, el método de intercambio compatible con MT4 se llama "Advisory link" y se inicializa mediante la función: rc = ddeadv(channel,'item','callback','upmtx',format);,
dónde:
channel - descriptor del canal inicializado,
'item' - los datos que nos interesan, es decir, el nombre del símbolo de un par de divisas,
'callback' - una línea que se ejecuta a la llegada de los datos del servidor,
'upmtx' - nombre de símbolo de la variable para colocar los datos recibidos desde el servidor,
format - conjunto de dos señales, que define el formato de los datos enviados.
La función ddeadv devuelve "1" si todo es correcto; en caso contrario, devuelve "0".

Tenga en cuenta que el parámetro 'callback' se proporciona como una expresión símbolo, no un descriptor de función. De hecho, vamos a ejecutar la función "eval" que ejecuta la línea como si hubiera sido escrita en una consola. Esta característica genera la siguiente dificultad: A la llegada de un nueva cotización, tenemos que ejecutar una función para recibir la nueva cotización. Al mismo tiempo, nos gustaría pasar a esta función la estructura de descriptores "handles" que será utilizada para obtener acceso a los objetos gráficos de la GUI. Sin embargo, no he encontrado ningún método para pasar el descriptor de la estructura "handles" en una línea ejecutable, ni la forma de llamar a la función que describe la GUI ubicada en el archivo .m.
Todo esto me llevó a colocar la función de recepción de la nueva cotización en un archivo .m separado y llamarlo al igual que una función normal de Matlab. Sin embargo, el inconveniente resultó ser una ventaja tras el descubrimiento de que podía editar la función de procesamiento sin interrumpir las operaciones de cliente de DDE.

Por lo tanto, en primer lugar, vamos a crear una función de procesamiento separada que mostrara únicamente los datos recibidos en la consola.


function newTick(simbols)
% new tick processing
disp(simbols); % display the argument in console
song = wavread('C:\WINDOWS\Media\Windows XP - launch.wav'); % read the sound
wavplay(song,40000); % play the sound with the sampling rate of 40 kHz

La función del ejemplo anterior también lanzará el archivo 'C: WINDOWS Media Windows XP - launch.wav' tan pronto como llegue una nueva cotización. Guarde la función texto como newTick.m en el directorio de trabajo de MATLAB.

Ahora vamos a editar el archivo .m que describe el comportamiento de nuestra GUI. Se añade la inicialización de la conexión a la función DDEs_OpeningFcn, y la desinicialización será añadida a la función figure1_CloseRequestFcn.
(Para añadir la función CloseRequestFcn al archivo .m, se debe ejecutar lo siguiente en el editor de la 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'); % initialization
pair = get(handles.editPair,'UserData'); % read the symbol name
rc = ddeadv(channel, pair,'newTick(x)','x',[1 1]); % establish connection
if (rc==1) % if the connection has been established,
disp('Connected'); % inform the console
end
handles.chann = channel; % save the channel ID in 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; % get the channel ID from handles
pair = get(handles.editPair,'UserData'); % read the symbol name
ddeunadv(channel,pair); % disconnect
rc = ddeterm(channel); % deinitialization
if (rc==1) % if everything is OK
disp('Disconnected'); % inform the console
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'); % Enter the symbol name in the input field
set(hObject, 'UserData', 'EURUSD'); % In the UserData of the input field - save


% 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


He proporcionado antes los textos completos de modificación de las funciones junto con el texto preparado para las funciones esqueleto vacías por los desarrolladores de Matlab.

El último bloque introduce el nombre del símbolo en el campo correspondiente antes de que se inicie la GUI. La entrada se copia en la propiedad 'UserData'. Siempre vamos a utilizar la copia en 'UserData', mientras que sólo usaremos el nombre que aparece en el campo ('String') si el usuario intenta cambiar el instrumento financiero. Si el usuario se ha confundido al escribir y se ha escrito un nombre equivocado en 'String', volveremos al nombre almacenado en 'UserData'.

El código siguiente realiza la función de cambio de nombre del símbolo del usuario:

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'); % previous symbol name
newPair = get(hObject,'String'); % new symbol name
channel = handles.chann; % get the channel ID

disconn = ddeunadv(channel,oldPair); % disconnect
if (disconn==0) % if you fail to
disconnect
set(hObject,'String',oldPair); % restore the old symbol name in the input field
else % if diconnected
conn = ddeadv(channel, newPair,'newTick(x)','x',[1 1]); % establish a new connection
if (conn==1) % if the connection is established
set(hObject,'UserData',newPair); % memorize what symbol is used
else % if you fail to
establish a new connection
ddeadv(channel, oldPair,'newTick(x)','x',[1 1]); % restore the old one
set(hObject,'String',oldPair); %
restore the old symbol name in the input field
end
end

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



Recepción de Ticks

Supongamos que se establece la conexión y, a la llegada de un nuevo tick, se llama a la función "newTick (x)" que "pega" el argumento recibido de MT 4 en una consola. En primer lugar, vamos a mostrar la última cotización recibida en la línea correspondiente de nuestra GUI.

Para ello, debemos tener una estructura de descriptores de objetos gráficos de la GUI; "handles" a disposición de la función "newTick". Vamos a utilizar la función setappdata(h,name,value) que guarda los datos en el dominio de aplicación. Especifique "0" como el ID de la aplicación. Este es el descriptor del objeto Matlab 'root', que es invariante, por lo que siempre puede conocerse.

Añadir la línea de "setappdata(0,'hndls',handles);" inmediatamente después de la cabecera de la función "DDEs_OpeningFcn":

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

Ahora, en la función "newTick", podemos extraer los valores de "handles" mediante la función de value = getappdata(h,name), habiendo especificado "0" como argumento de "h". A continuación, vamos a poder gestionar los objetos de la GUI desde la función "newTick".

Luego transformamos el argumento de cadena que se pasa a la función desde el servidor DDE y mostramos el valor de BID en la GUI. Además, detectamos la hora local de recepción de la cotización y la mostramos también, pero en la barra de estado de la GUI. La hora local es necesaria, ya que para el servidor DDE el tiempo tiene una precisión de hasta minutos, lo cual es inaceptable para trabajar con ticks. La función "now" devuelve la hora local con una precisión de hasta fracciones de milisegundo, por lo que no nos vamos a preocupar por el hecho de que diferentes ticks tengan el mismo tiempo asignado. También vamos a extraer la hora del servidor desde la línea recibida del servidor DDE y la transformamos en el formato de hora de Matlab.

A continuación se muestra un ejemplo más de la función "newTick":


function newTick(simbols)
% NEW TICK PROCESSING

timeLocal = now; % Detect the exact local time
handles = getappdata(0,'hndls'); % Receive handles from root

% disp(simbols); % put the argument into console (commented)
song = wavread('C:\WINDOWS\Media\Windows XP - launch.wav'); %read the sound
wavplay(song,40000); % play the sound with the sampling rate of 40 kHz

set(handles.textInfo,'String', datestr(timeLocal)); % show the local time in GUI

% --- transforming the line received from MT 4 ---
parts = sscanf(simbols, '%i/%i/%i %i:%i %f %f' ); % studying the line according
%to the format: int/int/int int:int float float
timeServerVect = parts(1:5); % extract the time
timeServerVect = timeServerVect'; % transpose (column into line)
timeServerVect = [timeServerVect 00]; % add seconds
timeServer = datenum(timeServerVect); % transform into the Matlab time format
Bid = parts(6); % extract Bid
Ask = parts(7); % extract Ask
% --- end of transforming ---

set(handles.textBid,'String',['Bid: ' num2str(Bid)]); % Show Bid in GUI



Dibujando un gráfico de ticks

Esta es la continuación de la función "newTick" que iniciamos anteriormente. El código se proporciona con comentarios detallados, por lo que, supongo, no será ningún problema comprenderlo.
Sólo voy a explicar que la matriz de cotizaciones BID, como la de handles, se almacena en la zona de objetos 'root'
, pero guardada como "datos". Los datos almacenados representan una estructura que consta de dos campos:
data.name - nombre del símbolo de un par de divisas;
data.array - la propia matriz de cotizaciones.

En la función "newTick", estos datos aparecen bajo el número de "ticks", y los campos de la estructura tienen los nombres de ticks.name y ticks.array, respectivamente.

ticks.array representa una matriz que contiene tres columnas:
hora local en el formato de hora Matlab (con la exactitud facilitada por Matlab [microsegundos]);
- hora del servidor
en el formato de la hora de Matlab (con una precisión de minutos);
- Bid.

La función "newTick" vacía la matriz de cotizaciones, si el nombre del símbolo en el campo "editPair" ha cambiado y las cotizaciones para otro símbolo han comenzado a llegar. Si eso NO ha cambiado, las líneas se añaden a la matriz existente.

El bloque de las operaciones con el gráfico define los parámetros (tamaño y posición) de la ventana axesChart y extrae de ellos el ancho de la ventana en píxeles. Esto es necesario para que el programa establezca la escala horizontal de la muestra - una cotización por un píxel.
Si hay menos cotizaciones que píxeles en el ancho de la ventana, el gráfico será dibujado completamente. Si hay más cotizaciones que píxeles, se mostrarán sólo los datos más recientes que quepan en el gráfico.

% --- working with quotes array ---
GUIpairName = get(handles.editPair, 'UserData'); % symbol name
if (~isappdata(0,'data')) % if no data
ticks.name = GUIpairName; % form the name field
ticks.array = []; % form a field - an empty array
setappdata(0,'data',ticks); % write the data into root
end
ticks = getappdata(0,'data'); % extract data
if ~strcmp(ticks.name,GUIpairName) % if the name has changed
ticks.name = GUIpairName; % form the name field
ticks.array = []; %
form a field - an empty array
setappdata(0,'data',ticks); % write the data into root
end
ticks.array = [ticks.array; timeLocal timeServer Bid]; % add a line
% containing the new data to the existing data array
setappdata(0,'data',ticks); %
write the data into root
% --- end of working with the array ---

% --- working with chart ---
chartSize = get(handles.axesChart,'Position');% get the chart window size
chartSize = chartSize(3); % extract the chart window width
lenArray = size(ticks.array); % get the size of the data array
lenArray = lenArray(1); % extract the amount of lines in the data array

set(handles.axesChart, 'NextPlot','replace'); % drawing mode - replace
% the old chart with a new one

if (chartSize >= lenArray)
stairs(handles.axesChart,ticks.array(:,3)); % draw the whole chart
else
stairs(handles.axesChart,ticks.array(lenArray-chartSize+1:lenArray,3));
% display the latest data fitting into the chart
end
set(handles.axesChart,'XLim',[1 chartSize]); % set the scale - one count
% in one width pixel
set(handles.axesChart, 'NextPlot','add'); % drawing mode - adding
plot(handles.axesChart,[1 chartSize], [Bid Bid],'m');% draw the Bid horizontal



Almacenamiento de datos en el archivo

La última función que se describirá es el almacenamiento de datos tick en un archivo a petición del usuario.
Vamos a guardar los datos pulsando un botón, por lo que añadiremos el objeto "Push Button" a la GUI usando el editor.

Establecer las siguientes propiedades de los objetos: Tag = pushSave, String = Save.

Al presionar el botón "M-file Editor", se añadirá automáticamente la plantilla de la función pushSave_Callback al final de "DDEs.m".

A continuación está el texto completo de la función que guarda los datos:

% --- 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'); % get to know the date (string)
time = datestr(now,'HH-MM-SS') % get to know the time (string)
name = get(handles.editPair,'UserData');% get to know the symbol name (string)
template = [name '@' date '@' time]; % form the file name
[userName, userPath] = uiputfile([template '.txt']); % get the name and the path from the user
if userName~=0 % if "Cancel" is not pressed
ticks = getappdata(0,'data'); % get the data from root

timesStr = datestr(ticks.array(:,1)); % form a string array
% of time and date
bidStr = num2str(ticks.array(:,3)); % form string array named BID
delimStr(1:length(bidStr)) =' ' ; % form a "column" separator
% more exactly, form a line that will be transposed into a column
matrix=[timesStr delimStr' bidStr]; % gather write all Str into one matrix
dlmwrite([userPath userName], matrix, '');% save the matrix in a file
end


La función prepara el nombre del archivo que consiste en la fecha, la hora y el nombre del símbolo de del instrumento financiero.
Al guardar, se preparan primero las matrices:
-timesStr -- hora local y fecha que corresponden con las cotizaciones;
- delimStr-- delimitadores;
- bidStr-- la columna de BID.
A partir de ahí, todos están unidos en una matriz.

delimStr representa una línea que consta de espacios; la longitud de la línea es igual a la longitud de la columna de BID. Al mezclarse, la línea delimStr se transpone en una columna y separa la columna de cotizaciones de las del tiempo.


Conclusión

Espero que el método descrito anteriormente le permitirá utilizar todo el abanico de funciones matemáticas de Matlab para el desarrollo y la prueba de sus estrategias de trading automatizado.