
MQL5 简介:如何编写简单的EA 交易和自定义指标
简介
相比 MQL4,MetaTrader 5 客户端的 MetaQuotes 编程语言 5 (MQL5) 具有许多新的发展潜力和更高的性能。本文将帮助您熟悉这一新的编程语言。文中给出了编写“EA 交易”和自定义指标的简单示例。我们还会涉及到 MQL5 语言的一些细节,这些细节对于理解示例是必要的。
文章细节和 MQL5 语言的完整说明可在 MetaTrader 5 中包含的 MQL5 参考中找到。MQL 5 内置的“帮助”内容足以帮助您学习本语言。本文对于那些熟悉 MQL4 以及那些刚刚涉足交易系统和指标编程的初学者而言均可提供帮助。
MQL5 入门
MetaTrader 5 交易平台允许您以手动或自动模式对金融工具和交易进行技术分析。MetaTrader 5 与之前的版本 MetaTrader 4 有所不同,尤其是交易、持仓和订单概念得到改进。
- 持仓 - 一种市场承诺,是买入或卖出金融工具数量的合同。
- 订单 - 在一定条件下买入或卖出一定量的金融工具的订单。
- 交易- 经纪人执行订单而引起开仓、持仓修改或平仓的事实。
客户端具有内置编程语言 MQL5,可用于编写具有不同目的的多种类型的程序:
- EA 交易 - 一种根据指定算法进行交易的程序。“EA
交易”允许您在交易系统上实施自动交易(无需交易人员即可执行交易操作)。“EA
交易”可执行交易操作,进行开仓和平仓,以及管理挂单。
- 指标 - 一种以图表形式呈现数据的程序,便于分析。
- 脚本 - 一种可一次执行某些操作序列的程序。
“EA 交易”、“指标”和“脚本”可调用 MQL5 标准库的函数及 DLL 函数,包括操作系统库。位于其他文件中的代码可包含于以 MQL5 编写的程序文本中。
要编写程序(“EA
交易”、“指标”或“脚本”),您可以启动
MetaTrader 5 客户端,从 Tools(工具)菜单选择 MetaQuotes
Language Editor(MetaQuotes 语言编辑器),或按 F4 键。
图 1. 启动 MetaEditor。
在 MetaEditor 5 窗口中,从 File(文件)菜单选择 New(新 建),或按 Ctrl+N。
图 2. 创建新程序。
在 MQL5 Wizard(MQL5 向导)窗口中选择您想要创建的程序类型:
图 3. MQL5 向导。
接下来您可以指定程序名称、作者信息,以及在启动程序后向用户要求的参数。
图 4. “EA 交易”的一般属性。
随后,系统将创建程序模板(“EA 交易”、“指标”或“脚本”),您可以对其进行 编辑或填入代码:
图 5. 新程序模板。
程序就绪后,必须对其进行编译。要编译程序,从 File(文件)菜单选择 Compile(编 译),或按 F7 键:
图 6. 程序编译。
如果程序代码没有错误,系统将创建扩展名为 .ex5 的文件。之后,您可以将此新的“EA 交易”、“指标”或“脚本”附加至 MetaTrader 5 客户端的图表进行执行。
MQL5 程序是一个运算符序 列。每个运算符以分号 ";" 结束。为您方便起见,您可以为代码添加注 释,注释位于符号 "/*" 和 "*/" 之中,或在代码行末尾的 "//" 后。MQL5 是“面向事件”的编程语言。这表示当特定事件(程 序启动或终止、新的报价到来等)发生时,客户端启动用户编写的相应函 数(子程序),以执行指定的操作。客户端具有以下预定义 事件:
- Start
事件在“脚本”运行时发生(仅用于“脚本”)。它将会引起 OnStart
函数的执行。MQL4 对应物 -“脚本”中的 start 函
数。
- Init 事件在“EA 交易”或“指标”启动时发生。它将会引起 OnInit 函数的执行。MQL4 等价物 - init 函数。
- Deinit 事件在“EA
交易”或“指标”终止时发生(例如,从图表分离后、关闭客户端等)。它将会引起 OnDeinit
函数的执行。MQL4 等价物 - deinit 函数。
- NewTick 事件在当前金融工具有新的报价到来时发生(仅用于“EA 交易”)。它将会引起 OnTick 函数的执行。MQL4 对应物 -“EA 交易”中的 start 函数。
- Calculate 事件在指标启动(在 OnInit 函数执行后)以及当前金融工具有新报价到来时发生(仅用于“指标”)。它将会引起 OnCalculate
函数的执行。MQL4 对应物 -“指标”中的 start 函数。
- Trade
事件在订单执行、修改或删除,以及在开仓、持仓修改或平仓时发生(仅用于“EA 交易”)。它将会引起 OnTrade
函数的执行。MQL4 中没有该事件和函数的对应物。
- BookEvent
事件在“市场深度”改变时发生(仅用于“EA
交易”)。它将会引起 OnBookEvent 函数的执行。MQL4
中没有该事件和函数以及“市场深度”的对应物。
- ChartEvent
事件在用户使用图表时发生:在图表窗口处于焦点状态时点击鼠标和按下按键。该事件也会在创建、移动或删除图形对象等时发生(用于“
EA 交易”和“指标”)。它将会引起 OnChartEvent 函数的执行。
MQL4 中没有该事件和函数的对应物。
- Timer 事件在计时器触发时定 期发生,如果计时器已使用 EventSetTimer 函数激活的话。它将会引起 OnTimer 函数的执行。MQL4 中没有该事件和函数以及计时器的对 应物。
使用变 量前,必须指定每个变量的数据类型。相比 MQL4,MQL 5 支持更多的数据类型:
- bool 用于存储逻辑值(true 或 false)。该数据类型占用 1 字节内存。
- char 用于存储从 -128 到 127 的整数值。该数据类型占用 1 字节内存。
- uchar 用于存储从 0 到 255 的无符号整数值。该数据类型占用 1 字节内存。
- short 用于存储从 -32,768 到 32,767 的整数值。该数据类型占用 2 字节内存。
- ushort 用于存储从 0 到 65,535 的无符号整数值。该数据类型占用 2 字节内存。
- int 用于存储从 -2,147,483,648 到 2,147,483,647 的整数值。该数据类型占用 4 字节内存。
- uint 用于存储从 0 到 4,294,967,295 的无符号整数值。该数据类型占用 4 字节内存。
- long 用于存储从 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 的整数值。该数据类型占用 8 字节内存。
- ulong 用于存储从 0 到 18,446,744,073,709,551,615 的无符号整数值。该数据类型占用 8 字节内存。
- float 用于存储浮点值。该数据类型占用 4 字节内存。
- double 用于存储浮点值,通常为价格数据。该数据类型占用 8 字节内存。
- datetime 用于存储日期和时间值,它是从 01.01.1970 00:00:00 开始流逝的秒数。该数据类型占用 8 字节内存。
- color 用于存储颜色信息,它包含三种颜色分量的特性 - 红色、绿色和蓝色。该数据类型占用 4 字节内存。
- enum 代表枚举。它允许指定某种限制类型的数据集。该数据类型占用 4 字节内存。
- string 用于存储文本字符串。它的内部表示为 8 字节结构,包括含字符串的缓冲区大小以及指向该缓冲区的指针。
选择合适的数据类型对于获得最佳性能及合理的内存使用十分 必要。在 MQL5 中有个称之为结构的新概念。结构将逻辑相关的数据结合在一起。
交易系统
本文用作示例的交易系统基于这样一种假设,即欧洲金融机构
在早晨开盘,随后美国发表引出欧元兑美元走势的经济事态。图表时间周期无关紧要,但推荐使用分钟柱,以使全天(或其部分)情况一次可见,从而便于观察。
图 7. 交易系统。
上午 7 时(服务器时间)买入止损和卖出止损挂单下达在超出当前日期价格范围一个点的位置。对于买入止损挂单,将点差纳入考虑。止损水平设在范围的相对面。执行 后,止损订单移至简单移动平均线,但仅在其盈利时。
对比经典“追踪止损”,此种类型的追踪有如下益处:避免在带有修正的价格尖峰情形下过早平 仓。另一方面,在趋势结束和平移开始时导致平仓。简单移动平均线使用分钟图表数据计算,平均周期为 240。
利润水平取决于当前的市场波动。要确定市场波动,使用 “真实波动幅度均值”(Average True Range, ATR) 指标(时间周期为 5,应用至日图)。因此,它显示过去一周的日均波幅。要确定买入订单的“获利水平”值,我们需要将 ATR 指标值添加至当前日期的最低价位。这同样适用于卖出订单:我们将从当前日期的最高价位减去 ATR 指标值。如果订单价格值低于止损和获利水平,将不会下达订单。下午 7 时后(服务器时间),所有挂单删除且不会在当天下达(仍然追踪开仓直至其平仓)。
编写指标
接下来,我们将编写上文提及的显示交易系统利润水平的指 标。
如果代码行的第一个符号为 "#", 则表示该字符串是一条预处理程序指令。指令用于指定额外的程序属 性,以声明常量、包 含头文件以及导入函数。请注意,预处理程序指令末尾没有分号 (;)。
#property copyright "2010, MetaQuotes Software Corp." #property link "http://www.mql5.com" #property description "本指标使用平均市场波动" #property description "计算获利水平. 它使用的数据是" #property description "平均真实波动范围 (ATR) 指标, 根据" #property description "每日价格数据计算. 指标数值的计算是" #property description "使用每日价格的 最大值和最小值." #property version "1.00"
有关作者和其网页信息可在 copyright(版 权)和 link(链接)属性中指定,您还可以在 description(说 明)属性中添加简短的说明,在 version(版本)属性中指定程序版本。 指标运行时,该信息如下所示:
图
8. 指标信息。
必须指定指标所在的位置:位于图表上或在单独的窗口中。这 可以通过指定以下的属性之一来实现:indicator_chart_window 或 indicator_separate_window:
#property indicator_chart_window
此外,您需要指定将要使用的指标的缓冲区数量以及图形序列 数量。对于我们而言,存在两根线条,每条具有自己的缓冲区 - 含有将要绘制数据的数组。
#property indicator_buffers 2 #property indicator_plots 2
对于每条指标线,我们指定以下属性:类型(indicator_type 属性)、颜色(indicator_color 属性)、图形样式(indicator_style 属性)以及文本标签(indicator_label 属性):
#property indicator_type1 DRAW_LINE #property indicator_color1 C'127,191,127' #property indicator_style1 STYLE_SOLID #property indicator_label1 "Buy TP" #property indicator_type2 DRAW_LINE #property indicator_color2 C'191,127,127' #property indicator_style2 STYLE_SOLID #property indicator_label2 "Sell TP"
基准线类型为:DRAW_LINE 用于线条、DRAW_SECTION 用于区间、DRAW_HISTORAM 用于直方图。还有许多其他的图形样式。可以通过指定 RGB 三种分量的亮度定义颜色,或使用预定义颜色,例如,红色、绿色、蓝色、白色等。线条样式为: STYLE_SOLID - 实心线、STYLE_DASH - 短划线、STYLE_DOT - 虚线、STYLE_DASHDOT - 点实线、STYLE_DASHDOTDOT - 双点线。
图 9. 指标线说明。
使用 input 修饰符,指定外部变量(您可以在启动指标后指定它们的值)、它们的类型以及默认值:
input int ATRper = 5; //ATR 周期数 input ENUM_TIMEFRAMES ATRtimeframe = PERIOD_D1; //指标周期类型
参数的名称可以在注释中指定 - 它们将会代替变量的名称出现:
图 10. 指标的输入参数。
我们将在全 局层面(对所有函数可见)指定变量,以用于指标的不同函数。
double bu[],bd[]; int hATR;
bu[] 和 bd[] 数组将用于指标的上轨线和下轨线。我们将使用动态数组(即数组未指定元素数量),因为我们不知道要使用的元素的确切数量(数组大小将自动分配)。内置技术 指标的处理函数将存储于 hATR 变量中。指标处理函数对于指标的使用是必需的。
函数 OnInit 在 指标运行后调用(在其附加到图表后)。
void OnInit() { SetIndexBuffer(0,bu,INDICATOR_DATA); SetIndexBuffer(1,bd,INDICATOR_DATA); hATR=iATR(NULL,ATRtimeframe,ATRper); }
使用函数 SetIndexBuffer 指出 bu[] 和 bd[] 数组是指标缓冲区的事实是必要的,指标缓冲区将用于存储作为指标线绘制的指标值。第一个参数定义指标缓冲区的索引,排序从 0 开始。第二个参数指定一个分配给指标缓冲区的数组。第三个参数指定存储于指标缓冲区中的数据的类型:INDICATOR_DATA - 绘图数据、INDICATOR_COLOR_INDEX - 图形颜色、INDICATOR_CALCULATIONS - 用于中间计算的辅助缓冲区。
iATR 函数返回的指标的处理函数存储于 hATR 变量中。iATR 函数的第一个参数为交易信号,NULL - 是当前图表的符号。第二个参数指定图表时间周期,用于指标计算。第三个参数是 ATR 指 标的平均周期。
OnCalculate 函数紧接 OnInit 函数执行结束后以及当前交易品种每次有新的报价到来后调用。有两种方法调用该函数。第一种方法是使用我们的指标,如下所示:
int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], const double& high[], const double& low[], const double& close[], const long& tick_volume[], const long& volume[], const int& spread[])
在调用 OnCalculate 函数后,客户端传递以下函数: rates_total - 当前图表上的柱数量,prev_calculated - 指标已计算的柱数量,time[]、open[]、high[]、low[]、close[]、tick_volume[]、volume[]、spread[] - 数组,分别包含各个柱的时间、开盘价、最高价、最低价、收盘价、跳动量、交易量和点差值。为减少计算时间,重新计算指标值是不必要的,因为这些值已经计算 且未经更改。在调用 OnCalculate 函数后,返回已计算的柱数量。
OnCalculate 函数的代码位于括号中。代码起始于在函数中使用的局 部变量 - 变量的类型和名称。
{ int i,day_n,day_t; double atr[],h_day,l_day;
i 用作循环计数器,day_n 和 day_t 变量用于存储天数,并在计算一天当中最大和最小价格值时用于临时存储天数。atr[] 数组用于存储 ATR 指标值,h_day 和 l_day 变量用于存储一天当中最大和最小价格值。
首先,我们必须使用 CopyBuffer 函数将 ATR 指标的值复制到 atr[] 数 组中。我们将使用 ATR 指标的处理函数作为该函数的第一个参数。第二个参数是指标缓冲区 的编号(编号从 0 开始),ATR 指标仅有一个缓冲区。第三个参数指定第一个元素的起始编号,索引从现在向过去执行,第零个元素对应当前(未完成)柱。第四个参数指定应复制的元素数量。
我们复制了两个元素,因为我们仅对倒数第二的元素感兴趣, 该元素对应于最后一个(已完成)柱。最后一个参数是要复制数据的目标数组。
CopyBuffer(hATR,0,0,2,atr);
数组索引方向取决于 AS_SERIES 标志。如果标志设定(即,等于 true),数组为时序型,元素索引从最新数据向最旧数据 执行。如果标志未设定(即,等于 false),则越旧的数据具有越小的索引,越新的数据具有越大的索引。
ArraySetAsSeries(atr,true);
对于 atr[] 数组,我们使用 ArraySetAsSeries 函数(该函数的第一个参数为数组,应为该数据更改标志值,第二个参数为新标志值)将 AS_SERIES 标志设置为 "true"。现在,当前(未完成)柱的索引等于 0,倒数第二个(已完成)柱的索引等于 1。
for 运算符可用于创建循环。
for(i=prev_calculated;i<rates_total;i++) { day_t=time[i]/PeriodSeconds(ATRtimeframe); if(day_n<day_t) { day_n=day_t; h_day=high[i]; l_day=low[i]; } else { if(high[i]>h_day) h_day=high[i]; if(low[i]<l_day) l_day=low[i]; } bu[i]=l_day+atr[1]; bd[i]=h_day-atr[1]; }
for 运算符后面的括号中的第一个运算符是一个语句:i=prev_calculated。接下来是一个表达式,在我们 的示例中为:i<rates_total。这是一个循环条件 - 表达式为真时循环执行。第三部分是在每次执行循环后执行的语句。在我们的示例中,为 i++(等同于 i=i+1, 意思是将变量 i 的值增加 1)。
在循环中,i 变量的值从 prev_calculated 值起以 1 的增量变为 rates_total-1。 历史数据数组(time[]、high[] 和 low[]) 并非是默认为时序型,第零个索引对应历史数据中最旧的柱,最后一个索引对应于当前未完成的柱。在循环中,从第一个未计算的柱 (prev_calculated) 开始到包括最后一个柱 (rates_total-1) 在内的所有柱均进行了处理。对于其中的每个柱我们都计算了指标的值。
time[] 数组中的时间值是作为从 01.01.1970 00:00:00 开始流逝的秒数存储。如果将该值除以一天当中的秒数(或其他时间周期),则结果的整数部分将为从 01.01.1970(或其他时间周期)开始的天数。PeriodSeconds 函数返回作为参数定义的时间周期的秒数。day_t 变量是天数,对应于索引为 i 的柱。day_n 变量是为其计算最高和最低价格值的天数。
我们考虑 if 运算符。若该运算符的括号中的表达式为真,则 if 关键字后的运算符将执行。若表达式为假,则 else 关键字后的运算符将执行。每个运算符都可以是复合的,即可以由多个运算符组成,在我们的示例中它们包含于括号中。
处理日的最高和最低价格值分别存储在 h_day 和 l_day 变量中。在我们的示例中,我们检查以下条件:如果所分析的柱对应于新的一天,我们将再次计算最大和最小价格值,否则我们继续。我们为每个指标线计算以下 值:上轨线 - 我们使用最小每日价格,下轨线 - 我们使用价格的最大值。
在 OnCalculate 函数的末尾,return 运算符返回已计算柱的数量。
return(rates_total);
}
在 DeInit 函数中(指标从图表移除时或客户端关闭时操作),ATR 指标保留的内存使用 IndicatorRelease 函数释放。该函数仅有一个参数 - 指标的手柄。
void OnDeinit(const int reason) { IndicatorRelease(hATR); }
现在我们的指标完成。要编译指标,从 File(文 件)菜单选择 Compile(编译),或按 F7 键。如果没有错误,代码将成功编译。编译结果在 Toolbox(工具箱)窗口的 Errors(错 误)选项卡中列示。在您的情况下,编译器可能为下列字符串显示“转换可能损失数据”警告:
day_t=time[i]/PeriodSeconds(ATRtimeframe);
在上述代码行中我们有意去掉小数部分,所以该数据损失不是 错误。
在指标完成并编译后,指标可附加至 MetaTrader 5 客户端的图表或用于其他的“指标”、“EA 交易”或“脚本”。该指标的源代码请见本文附件。
编写“EA 交易”
现在是时候编写上述实施交易系统的“ EA 交易”了。我们假设该“EA 交易”仅交易一种金融工具。要使多个“EA 交易”在一种工具上交易,有必要仔细分析每个“EA 交易”对于整体持仓的影响,但这超出了本文的论述范围。
#property copyright "2010, MetaQuotes Software Corp." #property version "1.00" #property description "这个EA交易程序在每天的StartHour和EndHour" #property description "之间挂单.每个订单的止损价格是价格区间的" #property description "相反方向的值."当订单被执行后, 获利值" #property description "被设定为 'indicator_TP' 计算的水平. 止损移动到" #property description "SMA 的值,只针对获利订单."
这些 预处理 程序指令的目的已在“编写指标”一节中论述。它们以同样的方式为“ EA 交易”工作。
让我们指定输 入参数的值(可在启动“EA 交易”后由用户定义)、它们的类型和默认值。
input int StartHour = 7; input int EndHour = 19; input int MAper = 240; input double Lots = 0.1;
StartHour 和 EndHour 参数定义挂单的时间周期(起始时数和结束时数)。MAper 参数定义简单移动平均线的平均周期,用于开仓在其追踪过程中的止损水平。Lots 参数定义金融工具的交易量,用于交易。
让我们指定将用于不同交易函数的全 局变量:
int hMA,hCI;
hMA 变量将用于存储 MA 指标的处理函数,hCI 变量将用于存储自定义指标的处理函数(已在上文中编写的指标)。
“EA 交易”启动时,OnInit 函数执行。
void OnInit() { hMA=iMA(NULL,0,MAper,0,MODE_SMA,PRICE_CLOSE); hCI=iCustom(NULL,0,"indicator_TP"); }
在该函数中,我们获得 MA 指标和自定义指标的处理函数。iMA 函数及其参数的使用与上文所述的 iATR 函数一致。
iCustom 函数的第一个参数是工具的符号名称,NULL - 表示当前图表的工具。第二个参数是 chart timeframe,其数据用于计算指标,0 - 表示当前图表的时间周期。第三个参数是指标的文件名(不包含扩展名)。文件路径是相对于 MQL5\Indicators\ 文件夹。
让我们创建 OnTick 函数,每当新报价到来后该函数执行:
void OnTick() {
函数的代码位于括号中。
让我们指定将用于“EA 交易”的预定义数据结构:
MqlTradeRequest request; MqlTradeResult result; MqlDateTime dt;MqlTradeRequest 预定义结构具有订单和 持仓参数,这些参数在交易操作中传递至 OrderSend 函数。MqlTradeResult 结构的目的是用于存储由 OrderSend 函数返回的有关交易结果的信息。MqlDateTime 预定义结构的目的是用于存储日期和时间信息。
让我们指定将用于 OnTick 函数的局 部变量(及其类型):
bool bord=false, sord=false; int i; ulong ticket; datetime t[]; double h[], l[], ma[], atr_h[], atr_l[], lev_h, lev_l, StopLoss, StopLevel=_Point*SymbolInfoInteger(Symbol(),SYMBOL_TRADE_STOPS_LEVEL), Spread =NormalizeDouble(SymbolInfoDouble(Symbol(),SYMBOL_ASK) - SymbolInfoDouble(Symbol(),SYMBOL_BID),_Digits);bord 和 sord 布尔变量用作标志,以标示是否存在买入止损和卖出止损挂单。若存在,相应变量值为 "true",反之为 "false"。i 变量用作循环运算符中的计数器并用于存储中间数据。挂单的订单号存储于 ticket 变量中。
t[]、h[] 和 l[] 数组用于在历史数据中存储各个柱的时间、最大和最小价格值。ma[] 数组用于存储 MA 指标的值,atr_h[] 和 atr_l[] 数组用于存储我们创建的 indicator_TP 自定义指标的上轨线和下轨线的值。
lev_h 和 lev_l 变量用于存储当前日期的最大和最小价格值以及挂单的开盘价。StopLoss 变量用于临时存储开仓的止损价格。
StopLevel 变量用于存储 STOP_LEVEL 的值 - 现行价格和挂单价格的最小距离(以价格单位表示)。我们通过点中定义的 STOP_LEVEL 变量的值计算该值作为点价格(_Point 预定义变量)的结果。 STOP_LEVEL 的值由 SymbolInfoInterger 函数返回。该函数的第一个参数为符号名称,第二个参数是要求属性的标识符。符号(金融工具名称)可使用 Symbol 函数(该函数无参数)获得。
Spread 值用于存储点差的值(以价格单位表示)。其值作为当前买/卖值的差额计算,使用 NormalizeDouble 函数进行规范化。该函数的第一个参数是待规范化的双精度型值,第二个参数是我们从 Digits 预定义变量获得的小数点后的位数。当前买/卖值可使用 SymbolInfoDouble 函数获得。该函数的 第一个参数为符号名称,第二个参数是属性标识符。
让我们在结构 request 中填入值,这些值对于大多数 OrderSend 函数调用而言十分常见:
request.symbol =Symbol(); request.volume =Lots; request.tp =0; request.deviation =0; request.type_filling=ORDER_FILLING_FOK;request.symbol 元素包含交易工具的符号名称,request.volume 元素 - 金融工具的交易量(合约规模),request.tp - TakeProfit(获利)的数字值(在某些情况下我们不使用它而是填入 0),request.deviation - 允许在交易操作执行时偏离价格,request.type_filling - 订单类型,可以是以下类型之一:
- ORDER_FILLING_FOK - 仅在交易量等于或大于订 单中的指定量时执行交易。如果没有足够的交易量,将不会执行订单。
- ORDER_FILLING_IOC - 没有
足够的交易量,将按照最大可用市
场容量执行订单。
- ORDER_FILLING_RETURN - 与 ORDER_FILING_IOC 无异,但在此 情况下将针对缺失的交易量下达额外的订单。
我们可使用 TimeCurrent 函数获取当前的服务器时间(最后一次报价的时间)。该函数具有的唯一参数是指向含结果结构的指针。
TimeCurrent(dt);
对于所有的计算,我们仅需要将历史价格数据用于当前日期。 柱的数量是必要的(和一些保留),可使用下述公式进行计算:i = (dt.hour + 1)*60,其中 dt.hour - 是结构元素,包含当前时数。时间值、最大和最小价格分别使用 CopyTime、CopyHigh 和 CopyLow 函数复制到 t[]、h[] 和 l[] 中:
i=(dt.hour+1)*60; if(CopyTime(Symbol(),0,0,i,t)<i || CopyHigh(Symbol(),0,0,i,h)<i || CopyLow(Symbol(),0,0,i,l)<i) { Print("Can't copy timeseries!"); return; }
CopyTime、CopyHigh 和 CopyLow 函数的第一个参数是符号名称,第二个参数是 chart timeframe,第三个参数是要复制的起始元素,第四个参 数是要复制的元素个数,第五个参数是数据的目标数组。所有这些函数均返回复制的元素数量,或在发生错误的情形下返回负值 -1。
if 运算符用于检查为所有三个数组复制的元素数量。如果复制的元素数量小于计算所需量(即使是对于数组的其中之一而言)或是发生错误,该运算符会将 "Can't copy timeseries!"(无法复制时序!)消息打印至 Experts(专家)日志,并使用 return 运算符终止 OnTick 函数的执行。消息通过 Print 函数打印。该函数可打印由逗号分隔的任意类型的数据。
当价格数据复制到 t[]、h[] 和 l[] 数组时,我们使用上文提及的 ArraySetAsSeries 函数将 AS_SERIES 标志设置为 "true"。将数组索引设置为时序是必要的(从当前价格到较早的价格):
ArraySetAsSeries(t,true); ArraySetAsSeries(h,true); ArraySetAsSeries(l,true);
将当前日期的最大和最小价格值放入 lev_h 和 lev_l 变量:
lev_h=h[0]; lev_l=l[0]; for(i=1;i<ArraySize(t) && MathFloor(t[i]/86400)==MathFloor(t[0]/86400);i++) { if(h[i]>lev_h) lev_h=h[i]; if(l[i]<lev_l) lev_l=l[i]; }
循环仅在 MathFloor(t[i]/86400)
==
MathFloor(t[0]/86400) 条件为真时执行,以通过属于当前日期的
柱限制搜索。表达式的左边是当前柱日期的数量,表达式的右边是当前日期的数量(86400 是一天中的秒数)。MathFloor
函数对数值进行四舍五入,即它仅适用正数值的整数部分。
该函数的唯一参数是待四舍五入的表达式。在
MQL5 和 MQL4 中,等式使用 "==" 符号定
义(请参见 关系运算)。
订单价格的计算如下所示:对于买入止损类型的挂单,我们添 加一个点(_Point 预定义变量等于以价格单位表示的点大小)和Spread(点差)至 lev_h 变 量(lev_h+=Spread+_Point 或 lev_h=lev_h+Spread+_Point)。 对于卖出止损类型的的挂单,我们从 lev_l 变量值(lev_l-=_Point 或 lev_l=lev_l-_Point)减去一个点。
lev_h+=Spread+_Point; lev_l-=_Point;
接下来,我们使用 CopyBuffer 函数从指标的缓冲区复制值到数组。MA 值复制到 ma[] 数组,自定义指标的上轨线值复制到 atr_h[] 数组,指标的下轨线值复制到 atr_l[] 数组。CopyBuffer 函数已在我们考虑该指标的细节时在上文作出说明。
if(CopyBuffer(hMA,0,0,2,ma)<2 || CopyBuffer(hCI,0,0,1,atr_h)<1 || CopyBuffer(hCI,1,0,1,atr_l)<1) { Print("Can't copy indicator buffer!"); return; }
我们需要对应于倒数第二个(最后一个完成)柱的 MA 指标值,以及对应于最后一个柱的我们的指标的值,因此我们是复制这两个元素至 ma[] 数组和一个元素至 atr_h[] 个 atr_l[] 数组。如果在复制时发生错误,或如果复制的值的数量少于所需量(对于这些数组中的任意数 组而言),消息打印至 Experts(专家)日志,并且系统将使用 return 运算符终止 OnTick 函数。
对于 ma[] 数组,我们设置 AS_SERIES 标志,以指示数组的时序索引。
ArraySetAsSeries(ma,true);
atr_[] 和 atr_l[] 数组仅有一个元素,因此时序索引无法紧要。由于 atr_l[0] 值将进一步用于确定获利水平,卖出订单将于卖价平仓,但我们将点差添加至 atr_l[0] 的值,因为买价用于价格图表中。
atr_l[0]+=Spread;
PositionsTotal 函数返回持仓的数量(无参数)。持仓索引起始于 0。让我们创建一个循环,用于搜索所有的持仓:
// 在这个循环中,我们检查所有的持仓 for(i=0;i<PositionsTotal();i++) { // 只处理我们自己的交易品种的订单 if(Symbol()==PositionGetSymbol(i)) { // 我们将修改止损和获利值 request.action=TRADE_ACTION_SLTP; // 处理买入订单 if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) { // 让我们确定止损 if(ma[1]>PositionGetDouble(POSITION_PRICE_OPEN)) StopLoss=ma[1]; else StopLoss=lev_l; // 如果止损没有被定义或者比所需低 if((PositionGetDouble(POSITION_SL)==0 || NormalizeDouble(StopLoss-PositionGetDouble(POSITION_SL),_Digits)>0 // 如果获利没有被定义或者比所需高 || PositionGetDouble(POSITION_TP)==0 || NormalizeDouble(PositionGetDouble(POSITION_TP)-atr_h[0],_Digits)>0) // 新的止损接近当前价格吗? && NormalizeDouble(SymbolInfoDouble(Symbol(),SYMBOL_BID)-StopLoss-StopLevel,_Digits)>0 // 新的获利接近当前价格吗? && NormalizeDouble(atr_h[0]-SymbolInfoDouble(Symbol(),SYMBOL_BID)-StopLevel,_Digits)>0) { // 设置结构中止损的新值 request.sl=NormalizeDouble(StopLoss,_Digits); // 设置结构中获利的新值 request.tp=NormalizeDouble(atr_h[0],_Digits); // 发请求到交易服务器 OrderSend(request,result); } } // 处理卖出订单 if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) { // 让我们确定止损的值 if(ma[1]+Spread<PositionGetDouble(POSITION_PRICE_OPEN)) StopLoss=ma[1]+Spread; else StopLoss=lev_h; // 如果止损没有被定义或者比所需低 if((PositionGetDouble(POSITION_SL)==0 || NormalizeDouble(PositionGetDouble(POSITION_SL)-StopLoss,_Digits)>0 // 如果获利没有被定义或者比所需低 || PositionGetDouble(POSITION_TP)==0 || NormalizeDouble(atr_l[0]-PositionGetDouble(POSITION_TP),_Digits)>0) // 新的止损接近当前价格吗? && NormalizeDouble(StopLoss-SymbolInfoDouble(Symbol(),SYMBOL_ASK)-StopLevel,_Digits)>0 // 新的获利接近当前价格吗? && NormalizeDouble(SymbolInfoDouble(Symbol(),SYMBOL_ASK)-atr_l[0]-StopLevel,_Digits)>0) { // 设置结构中止损的新值 request.sl=NormalizeDouble(StopLoss,_Digits); // 设置结构中获利的新值 request.tp=NormalizeDouble(atr_l[0],_Digits); // 发请求到交易服务器 OrderSend(request,result); } } // 如果有持仓,从这里退出... return; } }通过在循环中使用 if 运算符,我们选择已为当前图表的交易品种开仓的持仓。PositionGetSymbol 函数返回工具的符号,此外还自动选择待处理的持仓。该函数仅有一个参数 - 开仓列表中的持仓索引。函数将必须更改开仓的止损值和获利值,这是很有可能的,因此,我们将TRADE_ACTION_SLTP 值放入 request.action 元素。接下来,取决于其方向,持仓被分为买入订单和卖出订单。
对于买入订单,止损水平由以下因素确定:如果 MA 指标值大于持仓的开盘价,则止损值假定等于 MA 指标值,否则止损值假定等于 lev_l 变量值。开仓的 止损当前值使用 PositionGetDouble 函数确定,该函数仅有一个参数 - 持仓属性的标识符。如果没有为开仓定义止损值(等于 0),或止损值大于其应有值 - 我们将修改该开仓的止损值和获利值。如果没有定义获利值(等于 0),或获利值大于其应有值(大于我们的指标的上轨线)- 我们将修改该开仓的止损值和获利值。
我们必须检查更改止损值和获利值的可能性。止损的新值应至 少小于 STOP_LEVEL 值的当前买价,获利的新值应至少大于 STOP_LEVEL 值的当前买价。我们使用标准化差值进行比较,因为比较的值会因从双精度型浮点二进制数到浮点十进制数的转换带来的误差而导致最后一个数位的值有所不同。
如果必须更改开仓的止损值和获利值,且新值对于交易规则有 效,我们将止损和获利的新值放入结构的相应元素中,并调用 OrderSend 函数以将数据发送至交易服务器。
对于卖出订单,止损值和获利值的更改过程并无二致。相比买 入订单,卖出订单将于卖价平仓,所以卖价值将用于对比。如果当前图表存在开仓 - 我们使用 return 运算符结束 OnTick 函数的执行。
OrdersTotal 函数(无参数)返回挂单的数量。索引起始于 0。让我们创建一个循环,用于处理所有挂单:
// 在这个循环中我们将检查所有的挂单 for(i=0;i<OrdersTotal();i++) { // 选择每一个订单,取得订单号 ticket=OrderGetTicket(i); // 只处理我们交易品种的订单 if(OrderGetString(ORDER_SYMBOL)==Symbol()) { // 处理止损买单 if(OrderGetInteger(ORDER_TYPE)==ORDER_TYPE_BUY_STOP) { // 检查是否是交易时间,价格能否移动 if(dt.hour>=StartHour && dt.hour<EndHour && lev_h<atr_h[0]) { // 如果开仓价位比所需低 if((NormalizeDouble(lev_h-OrderGetDouble(ORDER_PRICE_OPEN),_Digits)>0 // 如果止损价位没有定义或者比所需高 || OrderGetDouble(ORDER_SL)==0 || NormalizeDouble(OrderGetDouble(ORDER_SL)-lev_l,_Digits)!=0) // 开仓价位是否接近当前价位? && NormalizeDouble(lev_h-SymbolInfoDouble(Symbol(),SYMBOL_ASK)-StopLevel,_Digits)>0) { // 将要修改挂单的参数 request.action=TRADE_ACTION_MODIFY; // 填写结构中的订单编号 request.order=ticket; // 填写结构中新的开仓价位 request.price=NormalizeDouble(lev_h,_Digits); // 填写结构中新的止损价位 request.sl=NormalizeDouble(lev_l,_Digits); // 给交易服务器发请求 OrderSend(request,result); // 退出OnTick()函数 return; } } // 如果不是交易时间或者已经过了平均交易范围 else { // 我们会删除此挂单 request.action=TRADE_ACTION_REMOVE; // 填写结构中的订单编号 request.order=ticket; // 给交易服务器发请求 OrderSend(request,result); // 退出OnTick()函数 return; } // 设置标志,指出存在止损买单 bord=true; } // 处理所有止损卖单 if(OrderGetInteger(ORDER_TYPE)==ORDER_TYPE_SELL_STOP) { // 检查是否是交易时间,价格能否移动 if(dt.hour>=StartHour && dt.hour<EndHour && lev_l>atr_l[0]) { // 如果开仓价位比所需高 if((NormalizeDouble(OrderGetDouble(ORDER_PRICE_OPEN)-lev_l,_Digits)>0 // 如果止损没有被定义或者比所需低 || OrderGetDouble(ORDER_SL)==0 || NormalizeDouble(lev_h-OrderGetDouble(ORDER_SL),_Digits)>0) // 开仓价位接近当前价位吗? && NormalizeDouble(SymbolInfoDouble(Symbol(),SYMBOL_BID)-lev_l-StopLevel,_Digits)>0) { // 挂单参数将要被修改 request.action=TRADE_ACTION_MODIFY; // 填写结构中的订单编号 request.order=ticket; // 填写结构中新的开仓价位 request.price=NormalizeDouble(lev_l,_Digits); // 填写结构中新的止损价位 request.sl=NormalizeDouble(lev_h,_Digits); // 给交易服务器发请求 OrderSend(request,result); // 退出OnTick()函数 return; } } // 如果不是交易时间或者已经过了平均交易范围 else { // 我们会删除此挂单 request.action=TRADE_ACTION_REMOVE; // 填写结构中的订单编号 request.order=ticket; // 给交易服务器发请求 OrderSend(request,result); // exiting from the OnTick() function return; } // 设置标志,指出已经存在止损卖单 sord=true; } } }我们使用 OrderGetTicket 函数选择订单以进行进一步的处理,并将订单号保存至 ticket 变量中。该函数仅有一个参数 - 未结订单列表中订单的索引。OrderGetString 函数用于获取符号的名称。该函数仅有一个参数 - 订单属性标识符。我们将符号名称与当前图表的名称进行对比,以仅通过“EA 交易”处理的工具选择订单。订单类型由 OrderGetInteger 函数和相应订单类型标识符确定。我们将分别处理买入止损和卖出止损订单。
如果当前时数位于起始时数到结束时数的范围内,且买入止损 订单的开盘价未超过指标的上轨线,我们将修改开盘价和止损水平的值(如必要),否则我们删除订单。
接下来,我们确定是否有必要修改挂单的开盘价或止损水平。 如果买入止损订单的开盘价低于其应有值,或如果未定义止损或止损过高,我们将 TRADE_ACTION_MODIFY 值放入 request.action 元素中 - 它表示应更改挂单参数。同样地,我们将订单号放入 request.ticket 元素并使用 OrderSend 函数将交易请求发送至交易服务器。在开盘价低于其应有值时,我们可以通过对比确定,因为点差值可以变化,但我们没有在每个点差更改后修改订单, 它将被设置为对应于最大点差值的最高水平。
当止损值高于其应有值时,同样地,我们仅可通过对比确定, 因为在一天当中价格范围可能扩大,并且在新的最低价后下移止损订单的值是必要的。发送请求至交易服务器后,系统使用 return 运算符终止 OnTick 函数的执行。如果买入止损订单出现,则 bord 变量值设为 true。
卖出止损订单的处理与买入止损订单并无二致。
现在,让我们在买入止损和卖出止损挂单缺失时下达买入止损 和卖出止损挂单。我们将 TRADE_ACTION_PENDING 值放入 request.action 元素中(它表示挂单已下达)。
request.action=TRADE_ACTION_PENDING;
如果当前时数的值在订单下达时间范围内,我们下达订单:
if(dt.hour>=StartHour && dt.hour<EndHour) { if(bord==false && lev_h<atr_h[0]) { request.price=NormalizeDouble(lev_h,_Digits); request.sl=NormalizeDouble(lev_l,_Digits); request.type=ORDER_TYPE_BUY_STOP; OrderSend(request,result); } if(sord==false && lev_l>atr_l[0]) { request.price=NormalizeDouble(lev_l,_Digits); request.sl=NormalizeDouble(lev_h,_Digits); request.type=ORDER_TYPE_SELL_STOP; OrderSend(request,result); } } }在下达买入止损和卖出止损订单时,我们通过分析 bord 和 sord 变量的值检查是否存在相同订单。我们也检查以下条件:订单价格应在我们的指标的值的范围内。我们将订单的标准化价格放入 request.price 元素,将止损的标准化值放入 request.sl 变量,将订单类型 (ORDER_BUY_STOP 或 ORDER_SELL_STOP)放入 request.type 变 量。之后我们发送请求至交易服务器。OnTick 函数的代码以分号结束。
指标分配的资源使用 IndicatorRelease 函数在 OnDeinit 函数中释放,这在上文中已有论述。
void OnDeinit(const int reason) { IndicatorRelease(hCI); IndicatorRelease(hMA); }
“EA 交易”完成,如果没有任何错误则会成功编译。现在我们可以将其附加至图表来运行“EA 交易”。源代码可在本文的附件中下载。
启动和调试
当“EA 交易”和指标就绪,接下来我们要考虑如何启动以及如何使用内置的 MetaEditor 调试程序对其进行调试。
要启动“EA 交易”,必须在 Navigator(导 航器)窗口的 Expert Advisors(EA 交易)组中将其找到。之后,单击鼠标右键,在出现的上下文菜单中选择 Attach to chart(附加至 图表):
图 11. 启动“EA 交易”。
带“EA
交易”输入参数的窗口将会出现,如必要,您可以更改这些参数。在按下 OK(确定)后,图
标 将在图表的右上角出现,该图标表示“EA
交易”正在工作。要显示或关闭 Navigator(导航器)窗口,您可以从 View(视
图)菜单中选择 Navigator(导航器)或按 Ctrl+N。
启动“EA 交易”的另一种方式是在 Insert(插入)菜单的
Experts(专家)子菜单中将其选择。
为使“EA 交易”能够进行交易,应在客户端选项中允许自动交易:Tools(工具)菜单 -> Options(选项)窗口 -> Exprert Advisors(EA 交易)选项卡 -> 启用 Allow AutoTrading(允 许自动交易)选项。要使“EA 交易”能够从 DLL 调用函数,应启用 Allow DLL imports(允许 DLL 导入)选项。
图 12. 终端选项 - 允许自动交易。
此外,您可以通过勾选相应选项来分别对每个“EA 交易”授权交易或禁止交易、导入外部 DLL 库。
尽管我们的“EA 交易”使用指标,但指标的线条不会绘制到图表上。如必要,您可以手动附加指标。
启动指标的过程和启动“EA 交易”一样:如果您想要启动内置指标,在 Navigator(导 航器)窗口中展开 Indicators(指标)树(对于自定义指标,必须展开 Custom Indicators (自定义指标) 树),单击右键以显示弹出菜单,然后从上下文菜单中选择 Attach to Chart(附加至图表)。另一方式是从 Insert(插入)菜单中选择 Indicators(指 标),选择组(或对于自定义指标选择 Custom (自定义))和指标本身。
“脚本”启动方式与“EA 交易”及“指标”一样。
有关客户端事件的信息(连接至交易服务器/从交易服务器断开连接、自动更新、持仓和订单更改、“EA 交易”和“脚本”运行、错误消息)可在 Toolbox(工 具箱)窗口的 Journal(日志)选项卡中找到。“EA 交易”、“指标”和“脚本”打印的消息位于 Experts(专 家)选项卡中。
MetaEditor 具有内置调试程序。它允许您调试程序 - 逐步执行“EA 交易”、“指标”和“脚本”。调试可帮助寻找程 序代码的错误和观察“EA 交易”、“指标”和“脚本”的执行过程。要以调 试模式运行程序,必须从 Debug(调试)菜单中选择 Start(启 动)或按 F5 键。程序将在单独图表中以调试模式编译和运行,其时间周期和交易品种可在 MetaEditor Options(选项)窗口的 Debugging(调 试)选项卡中指定。
图 13. 编辑器选项 - 调试。
您可以按 F9 键设置断点,或通过在线条左侧双击鼠标或从 Debug(调试)窗口选择 Toggle Breakpoint(切换断点)来设置断点。在调试模式中,程序将在含断点的运算符前停止执行。程序停止后,Debug(调 试)选项卡将在 Toolbox(工具箱)窗口中显示(请参见图 14)。左侧有一个调用栈面板 - 文件、函数和代码行的编号在此显示。右侧是查看面板 - 查看的变量的值在此显示。要添加变量至查看列表,右键单击面板然后选择 Add(添 加)或按 Insert(插入)键。
图 14. 程序调试。
程序的逐步执行可通过按 F11、F10 或 Shift+F11 键实现。按下 F11 键或从 Debug(调 试)菜单选择 Step Into(单步执行)后,它将通过程序执行的一步且进入所有调用的函数。按下 F10 键或从 Debug(调试)菜单选择 Step Over(跳过) 后,它将通过程序执行的一步且不进入调用的函数。按下 Shift+F11 键或从 Debug(调 试)菜单选择 Step Into(跳出)后,它将运行更高一级的程序执行步骤。代码左侧的绿色箭头指示将要执 行的代码行。
总结
本文给出了编写简单“EA 交易”和“指标”的示例,并对 MQL5 编程语言作出了基本说明。本文论及的交易系统仅作示例之用,作者不对其在真实交易中的使用承担责任。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/35
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.




向人们解释 mql5 并不容易。(就像编程书籍一样,90% 的人都不知道如何开始。例如,他们打开这本关于 C 语言编程的书,第一个 "简单示例 "就用很小的字体写了整整一页。在我的一生中,我只读过一本好的编程书,它可以教任何人编程。究其原因,这些书的作者可能都是优秀的程序员,但不幸的是,他们都是非常糟糕的老师。编程其实可以很简单,当我有更多时间的时候,我就会为MQL4 编程语言编写一个简单的教程来证明这一点。它适合所有人,甚至是完全的初学者。最大的错误是教人们语言的细节,他们的大部分工作应该是ctrl+C和ctrl+v,使用谷歌搜索命令,最重要的是--保持一切都非常简单。我懂 10 多种编程语言(我从 7 岁开始编程),但我仍然被这个 11KB 的 "简易示例"(!!!)的解释方式吓坏了。我想知道有没有真正的编程初学者通过这个例子学会了 MQL5。我很怀疑,如果有的话,一只手的手指就能数过来。
Ibrahim Melssen:
I have copy paste the Expert Advisor and try to test it with Strategytester. But it doesn't make any trades. I am new to MQL5 and programming so maybe I just made a stupid mistake. It compiled without any errors. I'd really like the strategy! Anyone ideas why it doesn't run on strategytester..?
男人们也一样,我似乎找不到原因
我是mql5 编程 方面的新手。
我想通过这个示例来学习,但我对建立指标末尾的循环有点迷茫。他到底在哪里给 day_n 变量赋值?
因为循环会检查day_n<day_t。程序如何知道 day_n 的值?
它又是如何计算出来的?让我们假设 rate_total = 10,并且还没有计算过的条形图。因此 prev_calculated = 0
day_t=time[0] (今天!因为是倒数)/PeriodSeconds......因为是从 1970 年开始计算,所以假设是从 10 天前开始计算。
所以 day_t=10.现在检查 dayt 是否大于 dayn。我不知道 dayn,但我知道 dayt=10。因为没有值,所以我会假设 dayn 为零。
那么 dayn 也就变成了 10。好的。
prev_calculated + 1= 1。
DayT=time[1](昨天)/period......记住,是从 10 天前开始计算......但现在只计算到昨天,应该是 9,对吗?
但现在 dayN < dayT 是假的。然后开始执行 else 表达式。好吧,我明白了。我明白了。
然后计算所有的 bu[] 和 bd[]。好的。当 prev < 总比率为假时,循环结束。
但是,当新的条形图出现,并且它再次变为 true 时,我会再次从零开始计算吗?还是从 10 开始,直接进入 else 部分?
谢谢!!!!
您需要将其分为两部分:
1 在第一个时间指标应用于图表时: prev_calculated =0 , i = 0, i++ 直到 i = rates_total,它退出循环(time[0] 是过去的,而不是现在的。)
2 新条形图开始:prev_calculated 将小于 rates_total,因此条件为真,循环将仅在该新条形图上运行
你好@Guin、
我认为您的问题还没有得到正确的回答。如果您使用了示例中的代码,您可能会得到一个不可见的指标,它没有任何意义,您也无法在图表上看到它。这是因为代码从未经过此代码块:
if(day_n < day_t){ day_n = day_t; h_day = high[i]; l_day = low[i] }
原因是没有手动将 day_n 设置为任何默认值,而且比较 day_n < day_t 的结果总是 false。调试器显示,day_n 在未明确设置时的值是 "2076449103"。
只需将 day_n 的定义改为类似的值即可:
希望能帮到你。
我是mql5 编程 新手。
我想通过这个示例来学习,但我对建立指标末尾的循环有点迷茫。他到底在哪里给 day_n 变量赋值?
因为循环会检查day_n<day_t。程序如何知道 day_n 的值?
很棒的文章、
感谢分享