通过 DDE 在 MetaTrader 4 与 Matlab 之间进行交互

Dmitriy | 18 三月, 2016

简介

我已经在这里发布了一篇有关通过 CSV 文件在 MetaTrader 4 与 Matlab 之间交换数据 (MT 4 <-CSV->Matlab) 的文章。但是,这篇文章中介绍的方法在很多情况下都不能实行,而且往往不受认可。

MT 4 中支持的 DDE(动态数据交换)机制让我们能够通过计算机的 RAM 将数据直接从一个应用程序传输到另一个应用程序。Matlab 拥有实现 DDE 的前端和后端的完整功能,因此,我们想充分利用这次机会。

MetaTrader 4 DDE 服务器仅提供最新的价格变动数据。然而,考虑到这些限制,例如在处理柱内报价时,DDE 更为可取。

如同在“MT 4 <-CSV->Matlab”文章中,我将介绍创建交换组织工具的顺序。

不要忘记在 MetaTrader 4 客户终端的“工具” ->“选项”->“服务器”选项卡中启用 DDE 服务器,然后我们才可以已启动。


关于 DDE

在使用 DDE 组织数据交换的过程中,有两个端(前端和后端),将在两者之间建立连接。前端是用于请求数据的应用程序(在我们的例子中为 Matlab),后端是使这些数据可供其使用的应用程序 (MT 4)。

可使用三种方式通过 DDE 将数据从服务器传输到客户端:
- 通过客户端的请求;
- 通过客户端的请求并且在服务器通知已准备好传输数据之后;
- 已准备好传输数据时。

MetaTrader 4 DDE 服务器仅在一种(第三种)模式下运行,将准备就绪的数据发送到客户端,而无需等待请求、确认和其他类似材料。=) 因此,Matlab 的任务是通知 MT 4 它有一个客户端,告知需要哪些数据,然后等待直至数据到达。

一旦数据到达,我们将只需在图上显示数据。

创建 GUI

在 Matlab 环境中,有可能创建一个图形用户接口 (GUI)。一旦创建了 GUI,我们将在其中组合所有控件、价格图表以及我们认为需要显示的文本信息。

文章“MT4 <-CSV->Matlab”的第 3 部分详细介绍了 GUI 的创建,因此我在这里只介绍用于启动 GUI Creation Wizard 的称为“guide”的控制台命令,另外我还将提供我们需要的图形对象列表。

因此,我们需要以下项目:
- 输入框“编辑文本”用于输入货币对名称;
- “轴”用于显示图表;
- 两个文本输出字段“静态文本”用于显示上一个报价的精确值,或者用于其他内容。

下面显示了我如何在 GUI 表格中放置对象:

你应按如下方式设置图形对象属性:

对于Axes:
Tag = axesChart(我们将在此处显示图表);
Box = on – 用完整的矩形围绕图表区域,off – 用左线和底线围绕图表区域;
FontSize = 7(默认大小很大)
Units = pixels(在绘制表格时,我们需要使用此项来设置 1:1 的比例)。

对于 EditText:
Tag = editPair(我们将在此字段中输入货币对名称)。

对于 EditText 字段下面的 StaticText
Tag = textBid(我们将在此处输入上一报价的精确值);
HorizontalAlignment = left(此项不是很重要,你可以将其保留为“center”)。

对于表格最底部的 StaticText
Tag = textInfo;
HorizontalAlignment = left。

现在我们可以按“运行”。
我将我的项目命名为“DDEs”,因此,如果你希望你的版本与我的版本没有任何不同之处,请用同一名称命名你的项目。
如果你的 GUI 外观适合你,且 m-file 已准备进行编辑,我们来开始创建 DDE 客户端。

连接初始化

首先,你应在启动 GUI 时安排通道连接到服务器,同时在关闭此接口时应注意断开连接。
在 Matlab 中,DDE 连接由以下函数进行初始化:channel = ddeinit('service','topic'),
其中:
‘service’ – DDE 服务器名称 (‘MT4’)
‘topic’ – 数据段的名称。在我们的例子中,它可以是 ‘BID’、‘ASK’、‘QUOTE’ 等的值。
此函数返回已初始化通道的描述符。此描述符将用于与 DDE 服务器进行进一步对话。

你还应指定交换方法。在 Matlab 中,MT4 支持的交换方法称为“Advisory link(咨询链接)”,由以下函数进行初始化:rc = ddeadv(channel,’item’,’callback’,’upmtx’,format);,,
其中:
channel – 已初始化通道的描述符;
‘item’ – 我们感兴趣的数据,即货币对的交易品种名称;
‘callback’ – 数据从服务器到达后要执行的行;
‘upmtx’ – 用于放置从服务器收到的数据的变量的符号名称;
format – 两个标记的数组,用于定义发送的数据的格式。
如果成功,函数 ddeadv 返回“1”;否则,返回“0”。

请注意,交易品种描述被指定作为 ‘callback’ 参数,而不是函数描述符。事实上,我们将执行 “eval” 函数,此函数将执行此代码行(就像已在控制台中输入了该代码行一样)。此功能将带来以下难处:当新报价到达时,我们需要执行接收新报价的大函数。同时,我们希望将“句柄”描述符结构传递给此函数,用于获取对 GUI 图形对象的访问。然而,我没有找到任何将句柄结构描述符传递给可执行行的方法,也没有找到调用位于描述 GUI 的 m-file 中的函数的方式。
所有这些导致我需要将新报价接收函数放入单独的 m-file 中,并且将此函数作为正常的 Matlab 函数调用。然而,当我发现我可以在不中断 DDE 客户端操作的情况下编辑处理函数之后,这种不便之处变成了一种优势。

因此,首先,我们来创建一个仅将收到的数据显示到控制台的独立处理函数。

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

一旦新报价到达之后,上述示例函数还将立即播放 ‘C:\WINDOWS\Media\Windows XP - launch.wav’文件。在 MATLAB 的工作目录中将函数文本另存为 newTick.m。

现在,我们来编辑描述 GUI 行为的 m-file。将连接初始化添加到 DDEs_OpeningFcn 函数中,同时去初始化将被添加到 figure1_CloseRequestFcn 函数中。
(要将 CloseRequestFcn 函数添加到 m-file,你应在 GUI 编辑器中执行以下操作:“查看”->“查看回调”-> 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


我在上面提供了修改函数的全部文本,以及为 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’); % 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’
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

接收价格变动

考虑到已建立连接,在出现新的价格变动时,将调用“newTick(x)”函数,以将从 MT 4 收到的参数“密封”到控制台中。首先,我们来在我们的 GUI 的相应行中显示上一个收到的报价。

为此,我们必须拥有一个 GUI 图形对象描述符的结构 - 句柄由 “newTick” 函数进行处理。我们来使用将数据保存到应用程序域的 setappdata(h,name,value) 函数。将 “0” 指定为应用程序 ID。它是 Matlab 对象 ‘root’的描述符,它保持不变,因此我们始终知道它。

紧随 “DDEs_OpeningFcn” 函数的标题之后添加 “setappdata(0,’hndls’,handles);” 行:

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

现在,在函数 “newTick” 中,我们可以通过函数 value = getappdata(h,name) 提取句柄,将 “0” 指定为 “h” 的参数。然后,我们将能够通过 “newTick” 函数管理 GUI 对象。

接着,我们转换从 DDE 服务器传递到此函数的字符串参数,并在 GUI 中显示 Bid 的值。此外,我们还检测接收报价的本地时间,但在 GUI 状态栏中显示此时间。需要本地时间,因为 DDE 服务器传递精度为分钟的时间,这对于处理价格变动而言是不能接受的。‘now’ 函数返回精度不到一毫秒的本地时间,因此我们无须担心不同的价格变动会保持相同的时间不变。我们还将从 DDE 服务器接收的行中提取服务器时间,并将此时间转换为 Matlab 时间格式。

下面是 “newTick”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



绘制价格变动图表

这里是上面启动的 “newTick” 函数的延伸。代码提供了详细的注释,因此我认为,你要彻底了解此函数应该不算麻烦。
我将介绍 Bid 报价数组(类似于句柄数组)存储在 ‘root’ 对象区域,但被保存为 “data”。存储的数据显示了一个包含两个字段的结构:
data.name - 货币对的交易品种名称;
data.array - 报价数组本身。

在 “newTick” 函数中,此数据出现在 “ticks” 的名称下方,结构的字段的名称分别为 ticks.name 和 ticks.array。

Ticks.array 表示包含以下三列的数组:
- Matlab 时间格式的本地时间(使用 Matlab 支持的精确度 [毫秒]);
- Matlab 时间格式的
服务器时间(精确到分钟);
- 买入价。

如果 “editPair”字段中的交易品种名称已更改,并且另一个交易品种的报价即将到达,”newTick” 函数将清空报价数组。如果未发生更改,则行将被添加到现有数组中。

用于处理图表的块定义 axesChart 窗口的参数(大小和位置),并从中提取窗口宽度(单位:像素)。程序需要设置显示的水平比例 - 一个报价对一个像素。
如果在窗口宽度中报价数比像素数少,将从总体上绘制图表。如果报价数多于像素数,则将仅显示适合图表的最新数据。

% --- 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



将数据保存在文件中

要介绍的最后一个函数是根据用户的请求将价格变动数据保存到文件中。
我们将通过按下按钮来保存数据,因此使用编辑器将“按下按钮”对象添加到 GUI 表格中。

设置以下对象属性:Tag = pushSave,String = Save。

一旦按下 “M-file Editor” 按钮,将在 “DDEs.m” 的结尾处自动添加 pushSave_Callback 函数的模板。

下面是用于保存数据的函数的完整文本:

% --- 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

此函数准备文件名,其中包含证券的日期、时间和交易品种名称。
保存时,将初步制定交易品种矩阵:
- timesStr -- 报价对应的本地时间和日期;
- delimStr -- 定界符;
- bidStr -- BID 列。
然后,将它们全部合并在一个矩阵中。

delimStr 表示一个由空格组成的行;此行长度等于 BID 列的长度。在合并时,delimStr 行变换为一列,并将报价的列与时间隔开。

总结

我希望,通过上面介绍的方法,你能够使用 Matlab 中丰富的数学函数来开发和测试你的自动交易策略。