本文的目标受众是想要学习如何以全新的 MQL5 语言编写简单“EA 交易”的初学者。首先,我们将定义我们的 EA(EA 交易)的功能,接下来是 EA 如何实现这些功能。
EA 的功能:
以上被称之为交易策略。您必须首先开发您想要 EA 自动执行的策略,然后您才能编写 EA。所以在这种情况下,让我们修改上述语句以使其反映我们想要在 EA 中开发的策略。
我们将使用时间周期为 8、名为“移动平均线”(Moving Average) 的指标(您可以选择任意时间周期,但出于策略的考虑我们将使用 8)。
我们完成了策略的开发,现在是时候开始编写代码。
2.1 MQL5 向导
启动 MetaQuotes Language Editor 5(MetaQuotes 语言编辑器 5)开始。然后按下 Ctrl+N 或单击菜单栏上的 New(新建)按钮。
图 1. 启动新的 MQL5 文档
如图 2 所示,在 MQL5 Wizard(MQL5 向导)窗口中选择 Expert Advisor(EA 交易),然后单击 "Next"(下一步):
图 2. 选择文档类型
在下一窗口中,将您想要为 EA 指定的名称键入 Name(名称)框中。在这里,我输入的是 My_First_EA。然后您可以在 Author(作者)框中输入您的名字,并在 Link(链接)框中输入您的网址或电子邮件地址(如有)。
图 3.“EA 交易”的一般属性
由于我们要能够更改 EA 的某些参数以找出哪些值可以带来最佳结果,我们应使用 "Add"(添加)按钮将这些参数添加进来。
图 4. 设置 EA 输入参数
在 EA 中,我们需要能够尝试我们的“止损”、“获利”、“ADX 时间周期”以及“移动平均线时间周期”设置,因此我们将在此定义这些设置。
在 Name(名称)部分下双击并输入参数的名称,然后在 Type(类型)部分下双击以选择参数的数据类型,最后在 Initial value(初始值)部分下双击并输入参数的初始值。
完成上述步骤后,看到的画面应如下所示:
图 5. EA 输入参数的数据类型
如您在上图中所见,我为所有参数选择了整数 (int) 数据类型。接下来让我们就数据类型稍作论述。
从上述各种数据类型的说明我们可以得知,无符号整数类型不是为存储负值而设计,任何设置负值的尝试可能导致意想不到的后果。例如,若要存储负值,不能将其存储于无符号类型中(即 uchar、uint、ushort、ulong)。
回到有关 EA 的讨论。考察这些数据类型,您会同意我们使用 char 或 uchar 数据类型,因为我们要在这些参数中存储的数据分别小于 127 或 255。要获得良好的内存管理,这么做是最合适的。但为了便于我们讨论,我们将继续使用 int 类型。
完成所有必要参数的设置后,单击 Finished(完成)按钮,“MetaQuotes 语言编辑器”将为您创建如下图所示的代码框架。
我们将代码分段,以便于您更好理解。
代码的顶部(头)是定义 EA 属性的所在。在这里您可以看到您在图 3 中填入“MQL5 向导”的值。
在该部分代码中,您可以定义诸如 description(说明)(EA 中的简短文字说明)的其他参数、声明常量、包含其他文件或导入函数。
当语句以 # 符号开头时,它是调用一条预处理程序指令,且该语句不以分号 ';' 结束。预处理程序指令的其他示例包括:
#define:
#define 指令用于声明常量。它的书写形式为
#define 标识符 token_string
其作用是在代码中每次出现标识符时用值 token_string 进行替换。
示例:
#define ABC 100
#define COMPANY_NAME "MetaQuotes Software Corp."
它会在代码中每次出现 COMPANY_NAME 时用字符串 "MetaQuotes Software Corp." 进行替换,或在每次出现 ABC 时用字符(或整数) 100 进行替换。
您可以在 MQL5 手册中找到更多有关预处理程序指令的信息。现在我们继续我们的讨论。
代码头的第二部分是输入参数部分:
我们在该部分指定将用于我们的 EA 的所有参数。这些参数包括所有我们将写入 EA 的函数将会用到的所有变量。
在该层面声明的变量称为全局变量,因为 EA 中的每一个可能需要它们的函数都能访问它们。 输入参数 是只能在我们的 EA 外部进行更改的参数。我们还可以在该部分声明我们将在 EA 中使用而在 EA 外部不可用的其他变量。
接下来是 EA 初始化函数。这是在 EA 启动或附加至图表时调用的第一个函数,且仅调用一次。
为确保我们的 EA 工作出色,该部分是进行某些重要检查的最佳所在。
我们可以了解图表是否有足够的柱用于 EA 的工作等。
这同样是获取我们将用于指标(ADX 和“平均移动线”指标)的句柄的最佳所在。
在 EA 从图表移除时,OnDeinit 函数调用。
我们将为 EA 释放在初始化过程中在此部分为指标创建的句柄。
该函数处理 NewTick 事件,该事件在接收到交易品种的新报价时生成。
注意,如果在客户端中不允许使用“EA 交易”(按钮 "Auto Trading" (自动交易)),该“EA 交易”无法执行交易操作。
图 6. 自动交易已启用
大部分实施我们先前开发的交易策略的代码将在该部分中编写。
现在看看我们 EA 代码的各个部分,让我们开始在“骨架”上添加“血肉”。
2.2 输入参数部分
//--- 输入参数 input int StopLoss=30; // 止损 input int TakeProfit=100; // 获利 input int ADX_Period=8; // ADX 周期 input int MA_Period=8; // 移动平均周期 input int EA_Magic=12345; // EA 幻数 input double Adx_Min=22.0; // ADX 最小值 input double Lot=0.1; // 交易手数 //--- 其他参数 int adxHandle; // 我们 ADX 指标的句柄 int maHandle; // 我们移动平均指标的句柄 double plsDI[],minDI[],adxVal[]; // 保存每个柱 +DI, -DI 和 ADX 数值的动态数组 double maVal[]; // 保存每个柱移动平均值的动态数组 double p_close; // 保存一个柱收盘价的变量 int STP, TKP; // 将用于计算止损和获利值
如您所见,我们添加了更多的参数。在开始讨论新参数前,我们先讨论您现在可以看到的一些内容。我们可以使用正斜杠 '//' 在我们的代码中添加注释。通过注释,我们能够了解变量的意义,或此时在代码的该处我们在做什么。注释还使得我们的代码更易于理解。撰写注释有两种基本方式:
// 其他参数 …
这是单行注释
/*
这是一个多行注释
*/
这是多行注释。多行注释的起始和结束以符号对 /* 和 */ 标示。
在编译代码时,编译程序会忽略所有注释。
将单行注释用于输入参数是一个很好的方式,使 EA 用户可以理解那些参数的意义。 在 EA 输入属性上,我们的用户将不会看到参数本身,而是看到如下所示的注释:
图 7.“EA 交易”输入参数
现在,回到我们的代码本身...
我们决定为我们的 EA 添加更多参数。EA_Magic 是出自 EA 的所有订单的幻数。 最小 ADX 值 (Adx_Min) 将声明为 double 数据类型。double 用于存储浮点常数,浮点常数包含整数部分、小数点和小数部分。
示例:
double mysum = 123.5678;
double b7 = 0.09876;
交易手数 (Lot) 表示我们想要交易的金融工具的量。接下来我们声明将要使用的其他参数:
adxHandle 用于存储 ADX 指标句柄,而 maHandle 将用于存储“平均移动线”指标的句柄。plsDI[]、minDI[]、adxVal[] 是动态数组,用于保存图表上每个柱的 +DI、-DI 和(ADX 指标的)主 ADX 的值。maVal[] 是动态数组,用于保存图表上每个柱的“移动平均线”指标的值。
在这里顺便说明一下什么是动态数组。动态数组是在声明时没有确定数组大小的数组。换言之,方括号对中没有指定任何值。与之相对,静态数组在声明时定义了数组的大小。
示例:
double allbars[20]; // 该数组可存储 20 个元素
p_close 是用于存储我们将要监视以检查我们的买入/卖出交易的柱的收盘价。
STP 和 TKP 将用于在 EA 中存储“止损”值和“获利”值。
2.3. EA 初始化部分
int OnInit() { //--- 得到ADX指标句柄 adxHandle=iADX(NULL,0,ADX_Period); //--- 得到移动平均指标的句柄 maHandle=iMA(_Symbol,_Period,MA_Period,0,MODE_EMA,PRICE_CLOSE); //--- 如果句柄返回了无效句柄 if(adxHandle<0 || maHandle<0) { Alert("创建指标句柄出错 - 错误: ",GetLastError(),"!!"); }
在这里,我们使用相应的指标函数获取指标的句柄。
ADX 指标函数通过 iADX 函数获得。它将图表交易品种(NULL 也意味着当前图表的当前交易品种)、图表时间周期/时间表(0 也意味着当前图表的当前时间表)和 ADX 平均周期用于作为参数或实参的索引(之前我们在输入参数部分已定义)的计算。
int iADX(
string symbol, // 交易品种名称
ENUM_TIMEFRAMES period, // 周期
int adx_period // 平均周期
);
“平均移动线”指标句柄通过 iMA 函数获得。它具有以下实参:
int iMA( |
请阅读 MQL5 手册以获得有关这些指标函数的更多细节。它将帮助您更好地理解如何使用每个指标。
如果函数未成功返回句柄,我们将得到一个 INVALID_HANDLE 错误,此时我们需要再次检查是否存在错误。我们使用 GetlastError 函数通过警示函数来显示错误。
//--- 让我们用5位或3位小数的价格而不是4位小数的价格处理货币对 STP = StopLoss; TKP = TakeProfit; if(_Digits==5 || _Digits==3) { STP = STP*10; TKP = TKP*10; }
我们决定将“止损”值和“获利”值存储在我们先前声明的 STP 和 TKP 变量中。我们这样做的原因是什么?
这是因为存储在输入参数中的值是只读的,无法对其进行更改。因此,我们希望在此确保所有经纪人的 EA 都运转良好。Digits 或 Digits() 返回决定当前图表交易品种的价格精度的小数位数。对于 5 位或 3 位价格图表,我们将“止损”和“获利”均乘以 10。
2.4. EA 取消初始化部分
由于该函数是在将 EA 从图表中移除或禁用 EA 时调用,我们将在此释放我们在初始化过程中创建的所有指标句柄。我们创建了两个句柄,一个用于 ADX 指标,另外一个用于“平均移动线”指标。
我们将使用 IndicatorRelease() 函数来完成释放工作。该函数仅有一个实参(指标句柄)
bool IndicatorRelease(
int indicator_handle, // 指标句柄
);
该函数删除指标句柄并释放指标的运算块,如果未在使用的话。
2.5 EA ONTICK 部分
在这里我们首先要做的是检查当前图表是否有足够数量的柱。我们可以使用 Bars 函数在任意图表的历史数据中获取总柱数。该函数有两个参数,symbol(可使用 _Symbol 或 Symbol() 获取,它们将返回附加 EA 的当前图表的当前交易品种)和当前图表的 period 或 timeframe (可使用 Period 或 Period() 获取,它们将返回附加 EA 的当前图表的时间表)。
如果总可用柱数少于 60,我们希望 EA 不做任何操作直至我们在图表上有足够的柱可用。 Alert 函数在单独窗口中显示消息。它将任何以逗号分隔的值作为参数/实参。在本例中,我们只有一个字符串值。返回退出我们的 EA 初始化。
//+------------------------------------------------------------------+ //| EA订单函数 | //+------------------------------------------------------------------+ void OnTick() { // 我们是否有足够用于操作的柱 if(Bars(_Symbol,_Period)<60) // 如果总柱数少于60 { Alert("我们只有不到60个柱, EA 将要退出!!"); return; } // 我们将会使用 Old_Time 静态变量来保存柱时间. // 在每一次执行 OnTick 函数的时候我们都将检查当前柱的时间和保存的时间. // 如果柱时间不等于保存的时间,说明我们有了一个新柱. static datetime Old_Time; datetime New_Time[1]; bool IsNewBar=false; // 把最新的柱时间复制到 New_Time[0] int copied=CopyTime(_Symbol,_Period,0,1,New_Time); if(copied>0) // 数据已经被成功复制 { if(Old_Time!=New_Time[0]) // 如果旧的时间不等于新柱的时间 { IsNewBar=true; // 如果不是第一次调用,新柱已经出现 if(MQL5InfoInteger(MQL5_DEBUGGING)) Print("我们在此时有了新柱 ",New_Time[0]," 旧的时间是 ",Old_Time); Old_Time=New_Time[0]; // 保存柱时间 } } else { Alert("复制历史时间数据出错, 错误 =",GetLastError()); ResetLastError(); return; } //--- EA 应该在有新柱的时候只检查新的交易 if(IsNewBar==false) { return; } //--- 我们是否有足够用于处理的柱 int Mybars=Bars(_Symbol,_Period); if(Mybars<60) // 如果总柱数少于60 { Alert("我们只有不到60个柱, EA 将要退出!!"); return; } //--- 定义一些我们将用于交易的MQL5结构 MqlTick latest_price; // 将用于取得最近/最新报价 MqlTradeRequest mrequest; // 将用于发送我们的交易请求 MqlTradeResult mresult; // 将用于取得我们的交易结果 MqlRates mrate[]; // 将用于保存每个柱的价格,交易量和差价 ZeroMemory(mrequest); // 初始化mrequest 结构
“EA 交易”将在新柱开始时执行交易操作,所以有关新柱识别的问题必须解决。换言之,我们希望确定我们的 EA 不会针对每个订单号检查买入/卖出设置,我们仅希望 EA 在有新柱时检查买入/卖出持仓。
我们首先声明一个 datetime 时间变量 Old_Time,用于存储柱时间。我们将其声明为静态,是因为我们希望将值保留在内存中直至下次调用 OnTick 函数。然后我们就可以将它的值和 New_Time 变量(同样为 datetime 数据类型)进行比较,后者是用于保存新(当前)柱时间的包含一个元素的数组。我们还声明了一个 bool 数据类型变量 IsNewBar,并将其值设为 false。这是因为我们希望它的值仅在我们具有新柱时为 TRUE。
我们使用 CopyTime 函数来获取当前柱的时间。函数将柱时间复制到仅有一个元素的数组 New_Time;如果成功,我们将新柱的时间与之前的柱时间进行比较。如果时间不等,即表示我们有了一个新柱,则我们将变量 IsNewBar 设为 TRUE 并将当前柱时间的值保存至变量 Old_Time。
IsNewBar 变量指示我们有了新柱。如果其值为 FALSE,我们结束 OnTick 函数的执行。
看一下代码
if(MQL5InfoInteger(MQL5_DEBUGGING)) Print("我们在此时有了新柱 ",New_Time[0]," 旧的时间是 ",Old_Time);
该代码用于调试模式执行,在调试模式时将打印有关柱时间的消息;我们稍后还将进一步讨论。
在这里我们接下来要做的是检查是否有足够数量的柱用于处理。为什么要重复检查?我们只是希望确定 EA 工作正常。应当要注意的是,OnInit 函数仅在将 EA 附加至图表时调用一次,而 OnTick 函数在每次有新的订单号(报价)时调用。
您可以观察到,在这里我们重新检查了一次,但和之前有所不同。我们决定将我们从以下表达式
int Mybars=Bars(_Symbol,_Period);
中获得的历史数据中的总柱数存储到新变量 Mybars 中,该变量在 OnTick 函数中声明。该类型的变量是局部变量,和我们在代码的输入参数部分声明的变量不一样。在代码的输入参数部分声明的变量对于代码中所有需要它们的函数均可用,而在单一函数中声明的变量则受到限制,仅对该函数可用。这类变量无法在相应函数的外部使用。
接下来,我们声明一个 MQL5 结构类型的新变量,新变量将用于我们 EA 的此部分。MQL5 具有大量的内置结构,为 EA 开发人员提供了极大的方便。接下来让我们逐个认识这些结构。
MqlTick
该结构用于存储交易品种的最新价格。
struct MqlTick
{
datetime time; // 上次更新价格的时间
double bid; // 当前卖价
double ask; // 当前买价
double last; // 上次交易价格(最后价格)
ulong volume; // 当前最后价格的交易量
};
如果调用 SymbolInfoTick() 函数,我们可以使用任何声明为 MqlTick 类型的变量很容易地获得 买价、卖价、最后价格以及交易量的当前值。
因此我们将 latest_price 声明为 MqlTick 类型,以便我们将其用于获取买价和卖价。
MqlTradeRequest
该结构用于执行交易操作的所有交易请求。其结构包含执行交易所需的所有字段。
struct MqlTradeRequest
{
ENUM_TRADE_REQUEST_ACTIONS action; // 交易操作类型
ulong magic; // “EA 交易”ID(幻数)
ulong order; // 订单号
string symbol; // 交易品种
double volume; // 以手数表示的交易的请求交易量
double price; // 价格
double stoplimit; // 订单的止损限价水平
double sl; // 订单的止损水平
double tp; // 订单的获利水平
ulong deviation; // 相对于请求价格的最大可能偏移
ENUM_ORDER_TYPE type; // 订单类型
ENUM_ORDER_TYPE_FILLING type_filling; // 订单执行类型
ENUM_ORDER_TYPE_TIME type_time; // 订单执行时间
datetime expiration; // 订单到期时间(针对 ORDER_TIME_SPECIFIED 类型的订单)
string comment; // 订单注释
};
任何声明为 MqlTradeRequest 类型的变量可用于为我们的交易操作发送订单。在这里,我们将 mrequest 声明为 MqlTradeRequest 类型。
MqlTradeResult
任何交易操作的结果均作为 MqlTradeResult 类型的特殊预定义结构返回。任何声明为 MqlTradeResult 类型的变量都能够访问交易请求结果。
struct MqlTradeResult
{
uint retcode; // 操作返回代码
ulong deal; // 交易订单号,如果已执行的话
ulong order; // 订单号,如果已下达的话
double volume; // 交易量,由经纪人批准
double price; // 交易价格,由经纪人批准
double bid; // 当前卖价
double ask; // 当前买价
string comment; // 经纪人对操作的注释(默认情况下此处填写操作说明)
};
在这里,我们将 mresult 声明为 MqlTradeResult 类型。
MqlRates
交易品种的价格(开盘价、收盘价、最高价、最低价)、时间、每个柱的交易量以及点差均存储于该结构中。 任何声明为 MqlRates 类型的数组可用于存储交易品种的历史价格、交易量和点差数据。
struct MqlRates
{
datetime time; // 时间周期开始时间
double open; // 开盘价
double high; // 时间周期内的最高价
double low; // 时间周期内的最低价
double close; // 收盘价
long tick_volume; // 交易量
int spread; // 点差
long real_volume; // 实际交易量
};
在这里,我们声明一个数组 mrate[] 用来存储这些信息。
/* 让我们确认用于保存报价, ADX 值 和 MA 值的数组都以 时间序列数组相同的形式存放 */ // 报价数组 ArraySetAsSeries(mrate,true); // ADX DI+值数组 ArraySetAsSeries(plsDI,true); // ADX DI-值数组 ArraySetAsSeries(minDI,true); // ADX 值数组 ArraySetAsSeries(adxVal,true); // MA-8 值数组 ArraySetAsSeries(maVal,true);
接下来,我们决定设置所有我们将用于存储柱细节序列的数组。这样做是为了确保复制到数组的值如时序一样编入索引,即 0、1、2、3(以对应柱索引。所以我们使用 ArraySetAsSeries() 函数。
bool ArraySetAsSeries(
void array[], // 引用数组
bool set // true 表示倒序索引
);
应该注意的是,这同样可以在代码的初始化部分执行一次。然而,为便于阐述,我决定在此处进行说明。
//--- 使用MQL5的MqlTick结构取得最后报价 if(!SymbolInfoTick(_Symbol,latest_price)) { Alert("Error getting the latest price quote - error:",GetLastError(),"!!"); return; }
现在,我们使用 SymbolInfoTick 函数以获得最新的报价。该函数有两个实参 – 图表交易品种和 MqlTick 结构变量 (latest_price)。同样地,如果有错误,报告错误。
//--- 取得最新三个柱的详细信息 if(CopyRates(_Symbol,_Period,0,3,mrate)<0) { Alert("复制报价/历史数据出错 - 错误:",GetLastError(),"!!"); return; }
接下来,我们使用 CopyRates 函数将有关最新的三个柱的信息复制到 Mqlrates 类型数组中。CopyRates 用于获取指定数量的指定交易品种-时间周期的 MqlRates 结构的历史数据至 MqlRates 型数组中。
int CopyRates(
string symbol_name, // 交易品种名称
ENUM_TIMEFRAMES timeframe, // 时间周期
int start_pos, // 起始位置
int count, // 要复制的数据计数
MqlRates rates_array[] // 要复制的目标数组
);
交易品种名称使用 '_symbol' 获取,当前时间周期/时间表使用 '_period' 获取。对于起始位置,我们将从当前柱柱 0 开始,并且我们仅计数三个柱,即柱 0、1 和 2。结果将存储在数组 mrate[] 中。
mrate[] 数组现在包含柱 0、1 和 2 的所有价格、时间、交易量和点差信息。 因此,要获取任意柱的详细信息,我们将使用:
mrate[bar_number].bar_property
例如,我们可获得有关每个柱的以下信息:
mrate[1].time // 柱 1 开始时间
mrate[1].open // 柱 1 开盘价
mrate[0].high // 柱 0(当前柱)最高价,等等
接下来,我们将所有指标值复制到我们使用 CopyBuffer 函数声明的动态数组中。
int CopyBuffer(
int indicator_handle, // 指标句柄
int buffer_num, // 指标缓冲区数量
int start_pos, // 起始位置
int count, // 复制数量
double buffer[] // 复制目标数组
);
指标句柄是我们在 OnInit 部分中创建的句柄。关于缓冲区数量,ADX 指标具有三 (3) 个缓冲区:
“移动平均线”指标只有一个 (1) 个缓冲区:
我们将当前柱 (0) 复制到较早的两个柱。所以要复制的记录量为 3(柱 0、1 和 2)。buffer[] 是我们之前声明的目标动态数组 – adxVal、plsDI、minDI 和 maVal。
正如您在这里再次看到的一样,我们尝试捕获在复制过程中可能发生的任何错误。如果存在错误,无需执行进一步操作。
务必要注意,如果复制成功,CopyBuffer() 和 CopyRates() 函数返回复制的记录总数,如果在此过程中出现错误则返回 -1。这就是我们在此处的错误检查函数中检查小于 0(零)的值的原因。
//--- 使用句柄把我们指标的新值复制到缓冲区(数组)中 if(CopyBuffer(adxHandle,0,0,3,adxVal)<0 || CopyBuffer(adxHandle,1,0,3,plsDI)<0 || CopyBuffer(adxHandle,2,0,3,minDI)<0) { Alert("复制 ADX 指标缓冲区出错 - 错误:",GetLastError(),"!!"); return; } if(CopyBuffer(maHandle,0,0,3,maVal)<0) { Alert("复制移动平均指标缓冲区出错 - 错误:",GetLastError()); return; }
此时,我们希望检查我们是否已有未平仓买入或卖出头寸,换言之,我们希望确认我们一次仅有一个卖出或买入交易未平仓。如果我们已有一个未平仓买入或卖出,我们不希望建立一个新的买入或卖出。
为此,我们首先应声明两个 bool 数据类型变量(Buy_opened 和 Sell_opened),用于在我们已有未平仓买入或卖出头寸时保存 TRUE 值。
//--- 我们没有遇到错误,所以继续 //--- 我们是否已经有持仓? bool Buy_opened=false; // 保存建买仓结果的变量 bool Sell_opened=false; // 保存建卖仓结果的变量 if (PositionSelect(_Symbol) ==true) // 我们还有未平仓头寸 { if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { Buy_opened = true; //是买入 } else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL ) { Sell_opened = true; // 是卖出 } }
我们使用交易函数 PositionSelect 来获知我们是否有未平仓头寸。如果我们已有未平仓头寸,该函数返回 TRUE;如果没有则返回 FALSE。
bool PositionSelect( string symbol // 交易品种名称 );
函数将我们希望检查的交易品种(货币对)作为实参/参数。在这里我们使用 _symbol,因为我们检查的是当前交易品种(货币对)。
如果该表达式返回 TRUE,则我们希望检查未平仓头寸是买入还是卖出。我们使用 PositionGetInteger 函数来进行检查。我们将该函数配合 POSITION_TYPE 修饰符使用时,函数给出未平仓头寸的类型。该函数将返回头寸类型标识符,这可以是 POSITION_TYPE_BUY 也可以是 POSITION_TYPE_SELL。
long PositionGetInteger( ENUM_POSITION_PROPERTY property_id // 属性标识号 );
在我们的示例中,我们用它来确定我们未平仓的头寸。如果该头寸为卖出,我们将 TRUE 值存储在 Sell_opened 中,如果是买入,我们则将 TRUE 值存储在 Buy_opened 中。当我们稍后在代码中检查卖出或买入条件时,我们将能够使用这两个变量。
现在是时候存储我们将用于我们的买入/卖出设置的柱的收盘价。我们之前已为此声明了一个变量。
// 复制当前柱的前一个柱的收盘价, 也就是柱 1 p_close=mrate[1].close; // 柱1收盘价
为此,现在我们需要进行下一步骤。
/* 1. 检查买入设置 : MA-8 向上增加, 前一收盘价比它高, ADX > 22, +DI > -DI */ //--- 声明bool类型变量用于保存我们的买入条件 bool Buy_Condition_1 = (maVal[0]>maVal[1]) && (maVal[1]>maVal[2]); // MA-8 向上增加 bool Buy_Condition_2 = (p_close > maVal[1]); // 前一收盘价高于 MA-8 bool Buy_Condition_3 = (adxVal[0]>Adx_Min); // 当前 ADX 值比最小值大 (22) bool Buy_Condition_4 = (plsDI[0]>minDI[0]); // +DI 大于 -DI //--- 放到一起 if(Buy_Condition_1 && Buy_Condition_2) { if(Buy_Condition_3 && Buy_Condition_4) { // 有未平买入仓位? if (Buy_opened) { Alert("我们已经有了买入仓位!!!"); return; // 不建新的买入仓位 } mrequest.action = TRADE_ACTION_DEAL; // 立即执行订单 mrequest.price = NormalizeDouble(latest_price.ask,_Digits); // 最新卖家报价 mrequest.sl = NormalizeDouble(latest_price.ask - STP*_Point,_Digits); // 止损 mrequest.tp = NormalizeDouble(latest_price.ask + TKP*_Point,_Digits); // 获利 mrequest.symbol = _Symbol; // 货币对 mrequest.volume = Lot; // 交易手数 mrequest.magic = EA_Magic; // 订单幻数 mrequest.type = ORDER_TYPE_BUY; // 买入订单 mrequest.type_filling = ORDER_FILLING_FOK; // 订单执行类型 mrequest.deviation=100; // 当前价格偏移 //--- 发送订单 OrderSend(mrequest,mresult);
现在可以开始检查买入机会了。
让我们来分析上述表达式,该表达式体现了我们此前设计的策略。我们声明一个 bool 类型的变量,用于在下达订单之前必须满足的各个条件。bool 类型的变量只能包含 TRUE 或 FALSE 值。因此,我们的买入策略可细分为四个条件。 如果任一条件满足,则 TRUE 值将存储在我们的 bool 类型变量中,反之将存储 FALSE 值。让我们逐一进行阐述。
bool Buy_Condition_1 = (maVal[0]>maVal[1]) && (maVal[1]>maVal[2]);
在这里我们看到的是柱 0、1 和 2 上的 MA-8 值。如果当前柱上的 MA-8 值大于较早的柱 1 上的 MA-8 值,并且柱 1 上的 MA-8 值大于柱 2 上的对应值,这意味着 MA-8 向上增加。这便满足了我们买入设置的四个条件的其中一个。
bool Buy_Condition_2 = (p_close > maVal[1]);
该表达式旨在检查在同一时间周期内(柱 1 时间周期),柱 1 的收盘价是否高于 MA-8 的值。如果高于,则我们的第二个条件也同样得到满足,接下来我们将检查其他条件的满足情况。然而,如果我们刚才讨论的两个条件并未满足,则没有必要检查其他条件的满足情况。这就是我们决定将下述表达式包含在这两个初始条件(表达式)中的原因。
bool Buy_Condition_3 = (adxVal[0]>Adx_Min);
现在,我们希望检查 ADX 的当前值(ADX 值或柱 0)是否大于在输入参数中声明的最小 ADX 值。如果表达式为 true,即 ADX 的当前值大于最小所需值;我们还希望确认 plusDI 值大于 minusDI 值。这是我们在下一表达式中获得的结果
bool Buy_Condition_4 = (plsDI[0]>minDI[0]);
如果所有这些条件均得到满足,即全部返回 true,则我们希望确认在我们已有买入头寸时不会建立新的买入头寸。现在是检查我们之前在代码中声明的 Buy_opened 变量的值的时候了。
// 有买入持仓吗? if (Buy_opened) { Alert("我们已经有了买入仓位!!!"); return; // 不建新的买入仓位 }
如果 Buy_opened 返回 TRUE,我们不希望建立其他的买入头寸,因此我们显示一则警示消息来通知我们然后返回,因此我们的 EA 将等待下一订单号的到来。然而,如果 Buy_opened 返回 FALSE,则我们使用之前声明用于发送订单的 MqlTradeRequest 类型变量 (mrequest) 来准备我们的记录。
OrderSend() 函数有两个实参,一个是 MqlTradeRequest 类型变量,另一个是 MqlTradeResult 类型变量。
bool OrderSend( MqlTradeRequest& request // 查询结构 MqlTradeResult& result // 回复结构 );
如您所见,我们在使用 OrderSend 下达订单时使用 MqlTradeRequest 类型变量和 MqlTradeResult 类型变量。
// 取得返回代码 if(mresult.retcode==10009 || mresult.retcode==10008) //请求完成或者已下订单 { Alert("买入订单已经成功下单, 订单#:",mresult.order,"!!"); } else { Alert("买入订单请求无法完成 -错误:",GetLastError()); ResetLastError(); return; }
在发送订单后,现在我们将使用 MqlTradeResult 类型变量来检查订单的结果。如果订单成功执行,我们希望得到通知;如果未成功,我们同样希望知情。通过 MqlTradeResult 类型变量 'mresult',我们可访问操作返回码和订单号,如果订单已下达的话。
返回码 10009 表示 OrderSend 请求已成功执行,而 10008 表示订单已下达。这就是我们需要检查这两个返回码的原因。如果我们得到两个返回码的任意一个,我们可以确认我们的订单已完成或已下达。
要检查卖出机会,除了我们的 ADX 必须大于指定的最小值外,我们应检查我们为买入机会所做的对应部分。
/* 2. 检查卖出设定 : MA-8 向下降低, 前一收盘价低于它, ADX > 22, -DI > +DI */ //--- 声明bool类型变量用于保存我们的卖出条件 bool Sell_Condition_1 = (maVal[0]<maVal[1]) && (maVal[1]<maVal[2]); // MA-8 向下降低 bool Sell_Condition_2 = (p_close <maVal[1]); // 前一收盘价低于 MA-8 bool Sell_Condition_3 = (adxVal[0]>Adx_Min); // 当前 ADX 值大于最小值 (22) bool Sell_Condition_4 = (plsDI[0]<minDI[0]); // -DI 大于 +DI //--- 放到一起 if(Sell_Condition_1 && Sell_Condition_2) { if(Sell_Condition_3 && Sell_Condition_4) { // 还有未平卖出仓位? if (Sell_opened) { Alert("我们已经有了卖出仓位!!!"); return; // 不建新的卖出仓位 } mrequest.action = TRADE_ACTION_DEAL; // 立即执行订单 mrequest.price = NormalizeDouble(latest_price.bid,_Digits); // 最新买家报价 mrequest.sl = NormalizeDouble(latest_price.bid + STP*_Point,_Digits); // 止损 mrequest.tp = NormalizeDouble(latest_price.bid - TKP*_Point,_Digits); // 获利 mrequest.symbol = _Symbol; // 货币对 mrequest.volume = Lot; // 交易手数 mrequest.magic = EA_Magic; // 订单幻数 mrequest.type= ORDER_TYPE_SELL; // 卖出订单 mrequest.type_filling = ORDER_FILLING_FOK; // 订单执行类型 mrequest.deviation=100; // 当前价格偏移 //--- 发送订单 OrderSend(mrequest,mresult);
正如同我们在买入部分所做的那样,我们声明一个 bool 类型的变量,用于在下达订单之前必须满足的各个条件。bool 类型的变量只能包含 TRUE 或 FALSE 值。因此,我们的卖出策略可细分为四个条件。 如果任一条件满足,则 TRUE 值将存储在我们的 bool 类型变量中,反之将存储 FALSE 值。如同我们在买入部分所做的那样,让我们逐一进行阐述。。
bool Sell_Condition_1 = (maVal[0]<maVal[1]) && (maVal[1]<maVal[2]);
在这里我们看到的是柱 0、1 和 2 上的 MA-8 值。如果当前柱上的 MA-8 值小于较早的柱 1 上的 MA-8 值,并且柱 1 上的 MA-8 值小于柱 2 上的对应值,这意味着 MA-8 向下减少。这便满足了我们卖出设置的四个条件的其中一个。
bool Sell_Condition_2 = (p_close <maVal[1]);
该表达式旨在检查在同一时间周期内(柱 1 时间周期),柱 1 的收盘价是否低于 MA-8 的值。如果低于,则我们的第二个条件也同样得到满足,接下来我们将检查其他条件的满足情况。然而,如果我们刚才讨论的两个条件并未满足,则没有必要检查其他条件的满足情况。这就是我们决定将下述表达式包含在这两个初始条件(表达式)中的原因。
bool Sell_Condition_3 = (adxVal[0]>Adx_Min);
现在,我们希望检查 ADX 的当前值(ADX 值或柱 0)是否大于在输入参数中声明的最小 ADX 值。如果表达式为真,即 ADX 的当前值大于最小所需值;我们还希望确认 MinusDI 值大于 plusDI 值。这是我们在下一表达式中获得的结果
bool Sell_Condition_4 = (plsDI[0]<minDI[0]);
如果这些条件得到满足,即全部返回 true,则我们希望确认在我们已有买入头寸时不会建立新的买入头寸。现在是检查我们之前在代码中声明的 Buy_opened 变量的值的时候了。
// 是否有卖出仓位? if (Sell_opened) { Alert("我们已经有了卖出仓位!!!"); return; // 不建新的卖出仓位 }
如果 Sell_opened 返回 true,我们不希望建立其他的卖出头寸,因此我们显示一则警示消息来通知我们然后返回,因此我们的 EA 将等待下一订单号的到来。然而,如果 Sell_opened 返回 FALSE,则我们按照在买入订单中所做的设置卖出交易请求。
此处主要的区别在于我们计算止损价格和获利价格的方式。同样地,由于我们卖出,我们以卖价卖出;这就是我们使用 MqlTick 类型的变量 latest_price 来获取最新卖价的原因。在这里,其他类型为 ORDER_TYPE_SELL,此前已给出了说明。
同样是在这里,我们将 NormalizeDouble 函数用于卖价、StopLoss 和 TakeProfit 值,这是一种良好的习惯做法,即在将价格发送至交易服务器前始终先将这些价格标准化为货币对的位数。
正如同我们对买入订单所做的那样,我们也必须检查卖出订单成功与否。因此我们使用在买入订单中使用的同一表达式。
if(mresult.retcode==10009 || mresult.retcode==10008) //请求完成或者已下订单 { Alert("卖出订单已经成功下单,订单#:",mresult.order,"!!"); } else { Alert("卖出订单请求无法完成 -错误:",GetLastError()); ResetLastError(); return; } }
3. 调试和测试“EA 交易”
到这一步,我们需要测试我们的 EA,以获悉我们的策略工作与否。同样地,在我们的 EA 代码中可能存在个别错误。这些错误将在接下来的步骤中找出。
3.1 调试
通过代码调试,我们可以逐行(如果设置了断点)查看代码的执行情况,找出代码中存在的任何错误或缺陷,然后在将代码用于真实交易前快速对其进行必要的更改。
在这里,我们将在设置断点和接下来不设置断点的两种情形中逐步遍历“EA 交易”的调试过程。为此,确保我们没有关闭“编辑器”。首先,我们应选择希望用于测试 EA 的图表。在编辑器菜单栏上,点击 Tools(工具),然后点击 Options(选项),如下图所示:
图 8. 设置调试选项
Options(选项)窗口出现后,选择要使用的货币对、时间周期/时间表,然后单击 OK(确定)按钮。
启动调试程序前,我们先设定断点。断点使我们可以监视某个选定位置或代码行的代码的行为/性能。当调试程序遇到断点时,它将停止运行以等待您的操作,而非一次运行完所有的代码。通过这种方法,我们便能够在代码到达每个断点时对其进行分析并监视其行为。我们还将能够计算某些变量的值,以查看事情是否按照我们设想的方式发展。
要设置断点,前往您想要设置断点的代码行。在左侧靠近代码行边界的灰色区域内双击,您将看到一个蓝色的圆形小按钮,按钮内部有一个白色的实心方框。或者,将鼠标的光标悬停在代码行中您想要设置断点的任意位置,然后按 F9。要移除断点,再次按 F9 或双击断点。
图 10. 设置断点
我们将在五行不同的代码行中设置断点。
为便于阐述,我将使用从 1 到 5 的五个数字来标示断点。
如下图所示,我们继续在七行代码行中设置断点。断点 1 已在上文中创建。
图 11. 设置其他断点
完成断点的设置后,我们便可以开始调试代码。
要启动调试程序,按 F5 或单击 MetaEditor 工具栏上的绿色按钮:
图 12. 启动调试程序
编辑器首先将编译代码,如果代码中存在错误,编译器将显示错误,反之编译器将通知您代码成功编译。
图 13. 编译报告
请注意,代码编译成功并不意味着代码中没有错误。取决于代码的编写方式,可能会存在运行时错误。例如,如果因为任何小的疏忽而导致表达式未能正确求值,代码将成功编译但可能不会正确运行。少说多做,让我们来实际操作一下...
调试程序在完成代码的编译后,将跳转至交易端,并将 EA 附加至您在 MetaEditor 选项设置中指定的图表。同时,调试程序将显示 EA 的输入参数部分。由于我们未作任何调整,单击 OK(确定)按钮即可。
图 14.“EA 交易”输入参数调试
现在,您将在图表的右上角清楚地看到 EA。
启动 OnTick() 后,它将在断点 1 处停止运行。
图 15. 调试程序在第一个断点处停止运行
您将注意到,在代码行的左边有一个绿色的箭头出现。该箭头表示已执行了前面的代码行;现在我们已准备好执行当前的代码行。
在继续前,请允许我做一些必要的说明。观察编辑器的工具栏,您会发现之前呈现灰色的三个带弯曲箭头的按钮现已激活。这是因为我们现在正在运行调试程序。这些按钮/命令用于单步调试我们的代码(单步执行、跳过或跳出)。
图 16. 单步执行命令
Step Into(单步执行)用于从程序执行的一步进入下一步,将进入该代码行中任何调用的函数。点击按钮或按 F11 调用命令。(我们将该命令用于代码的逐步调试。)
图 17. 跳过命令
另一方面,Step over(跳过)不会进入该代码行内调用的任何函数。点击按钮或按 F10 调用命令。
图 18. 跳出命令
要执行更高一级的程序步骤,点击此按钮或按 Shift+F11。
同样地,在“编辑器”的底部,您将看到 Toolbox window(工具箱)。窗口中的 Debug(调试)选项卡具有以下标题:
回到我们调试过程的讨论上...
接下来要做的是输入我们希望在代码中监视的变量/表达式。确保仅监视代码中真正重要的变量/表达式。对于我们的示例,我们将监视以下内容:
您可以添加诸如 ADX 值、MA-8 值等等
要添加表达式/变量,双击 Expressions(表达式)区域或右键单击该区域然后选择 Add (添加),如上图所示。
输入要监视或查看的表达式/变量。
图 19. 表达式查看窗口
输入所有必要的变量/表达式...
图 20. 添加要查看的表达式或变量
如果变量尚未声明,其类型为 "Unknown identifier"(未知标识符)(静态变量除外)。
图 21. 运行单步执行命令
点击 Step into(单步执行)按钮或按 F11,观察发生的情况。如下所示,按住该按钮或 F11 不放,直至到达断点 2,继续直至到达断点 4,然后观察 表达式查看窗口。
图 22. 查看表达式或变量
图 23. 查看表达式或变量
图 24. 查看表达式或变量
一旦有新订单号,它将返回 OnTick() 函数的第一行代码。由于是新的订单号,除非已声明为静态变量,否则所有变量/表达式的值现在将重置。在我们的示例中,我们有一个静态变量 Old_Time。
图 25. NewTick 事件的变量值
要重复该过程,继续按 F11 键并在表达式查看窗口中持续监视变量。您可以停止调试程序,然后移除所有断点。
在“调试”模式下,我们将看到显示的消息 "We have new bar here..."(此处有新柱...)。
图 26.“EA 交易”在“调试”模式下打印消息
再次启动调试过程;但此次未设置断点。持续关注每个订单号,如果有任何买入/卖出条件得到满足 EA 将下达订单,并且通过编写的代码,我们将获悉订单是否成功下达或是看到一则警示消息。
图 27.“EA 交易”在调试过程中下达订单
我认为您可以离开几分钟去喝杯咖啡,使 EA 在无人值守的状态下工作。回来时,您会发现自己已有所进账(开个玩笑),然后单击 MetaEditor 上的 STOP(停止)(红色)按钮停止调试。
图 28. 停止调试程序
实际上,我们在这里完成的工作是查看我们的 EA 仅在建立新柱时检查交易,并且 EA 确实起到了作用。我们的 EA 代码仍有很大的改进空间。
有一点我要明确一下,交易端必须连接网络,否则调试会因为终端无法进行交易而不起作用。
3.2 测试 EA 策略
现在,我们将使用交易端内置的策略测试程序来测试我们的 EA。 要启动策略测试程序,按 CONTROL+R 或点击终端菜单栏上的 View(视图)菜单,然后单击 Strategy Tester(策略测试程序),如下图所示。
图 26. 启动策略测试
测试程序(策略测试程序)在终端的下部显示。要查看测试程序的所有设置,需要将其展开或调整大小。为此,将鼠标指针移至红色箭头标示的位置(如下所示)。
图 27. 策略测试程序窗口
鼠标指针将变为双向箭头,按住鼠标并向上拖动线条。当您可以看到设置选项卡上的所有内容时,停止拖动鼠标。
图 28. 策略测试程序设置选项卡
在我们点击 Start(开始)按钮前,我们先熟悉一下测试程序上的其他选项卡。
Agents(代理)选项卡
测试程序在测试中使用的处理器。基于您的电脑的处理器类型。我的仅仅是单 (1) 核处理器。
图 29. 策略测试程序代理选项卡
一旦进行代理,您将看到和下图相似的画面
图 30. 测试时的策略测试程序代理选项卡
Journal(日志)选项卡
这是在测试时间周期内所有运行事件显示的所在
图 31. 显示交易活动的策略测试程序日志选项卡
Inputs(输入)选项卡
这是您可以为 EA 指定输入参数的所在。
图 32. 策略测试程序输入选项卡
如果我们要优化 EA,我们需要在圈出的区域中设置值。
然而,在我们的示例中并未要求优化 EA,所以我们现在还无需使用这部分功能。
当一切就绪,我们将回到 Settings(设置)选项卡并点击 Start(开始)按钮。然后测试程序开始运转。如果您愿意,您现在要做的是再次离开去喝杯咖啡,或者像我一样,可能希望监视所有一切,然后转到 Journal(日志)选项卡。
Graph(图形)选项卡
一旦您开始在 Journal(日志)选项卡上查看已发送订单的相关消息,您可能会想要转到刚刚创建的名为 Graph(图形)的新选项卡。切换至 Graph(图形)选项卡,您将会看到图形持续上升或下降,具体情形可能取决于交易的结果。
图 33.“EA 交易”测试的图形结果
Results(结果)选项卡
测试完成后,您将会看到名为 Results(结果)的选项卡。切换至 Results(结果)选项卡,您将看到我们刚才所执行测试的摘要。
图 34. 显示测试结果摘要的策略测试程序结果选项卡
在这里,您可以查看总毛利、净利、总交易数、亏损交易总数以及更多内容。看到我们在为测试选择的时间周期内拥有约 USD 1,450.0 是一件有趣的事情。至少我们获得了利润。
在这里,我要着重强调一件事情。您会发现,您在策略测试程序中看到的 EA 参数的设置和 EA 输入参数中的初始设置不同。我刚刚向您指出,您可以更改那些输入参数中的任意参数以获得最优的 EA 性能。我可以将“移动平均线”和 ADX 为 8 的时间周期分别改为 10 和 14。我还可以将止损从 30 改为 35。最后一点也很重要,我可以使用 2 小时时间表。记住,这是策略测试程序。
如果想要查看测试的完整报告,在 Results(结果)选项卡中使用右键单击任意处,然后您将会看到一个菜单。在该菜单上选择'Save as Report'(另存为报告)。
图 35. 保存测试结果
保存对话框窗口将显示,如果您愿意,为报表输入名称,或是保留默认名称,然后单击保存按钮。完整的报告将以 HTML 格式保存。
要查看所执行测试的图表,单击Open Chart(打开图表),然后您将看到显示的图表。
图 36. 显示测试的图表
就这样,我们成功编写和测试了我们的 EA,现在我们有了一个可使用的结果。现在您可以返回策略测试程序 Settings(设置)选项卡,使用其他的时间表/时间周期来进行测试。
作业
我希望您可以使用不同的货币对、不同的时间表、不同的止损值、不同的获利值来进行测试,看看 EA 是如何工作的。您甚至可以试试新的“移动平均线”值和 ADX 值。如我之前所言,这正是策略测试程序的精髓所在。同样,我也希望您可以将结果与我分享。
在本分步指南中,我们可以找到基于开发的交易策略编写简单的“EA 交易”所需的基本步骤。指南中也包含了如何使用调试程序检查 EA 是否存在错误的方法。我们还讨论了使用策略测试程序测试 EA 性能的方法。为此,我们能够体会到全新的 MQL5 语言的强大性和可靠性。我们的 EA 尚不完美或完善,在用于真实交易前我们还必须对其进行大量改进。
还有很多知识亟待我们学习,我希望您能配合 MQL5 手册重读此文,并将本文中所学的一切一一印证,我可以保证,在不久的将来您一定会是一名优秀的 EA 开发人员。
编程愉快。
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程