简介
利用 MQL 编写“EA 交易”的任何交易者,或早或晚都会面临报告其“EA 交易”如何起作用的必要性。也可能需要实现“EA 交易”行动相关的短信或电子邮件通知。不管哪种情况,我们都得“捕捉”市场中发生特定事件或某个“EA 交易”执行的行动,并通知用户。
我会用本文为您讲解可以如何实现交易事件的处理,并提供我的实现。
我们拟于本文中处理下述事件:
- 持仓
- Open(开仓)
- Add(增持)
- Modify (修改持仓)(更改“止损”与“获利”)
- Reverse(反向开仓)
- Close entire position(完全平仓)
- Close part of position(部分平仓)
- 挂单
- Place(下挂单)
- Modify(修改挂单)
1. 它是如何运作的呢?
在我们开始之前,我会笼统地描述一下交易事件的运行方式,而所有必要的详情则看到哪讲到哪。
MQL5 中有预定义与自定义两种事件。而我们感兴趣的是预定义事件,尤其是交易事件。
每当交易操作完成时,都会生成“交易”事件。每当“交易”事件生成之后,又都会调用 OnTrade() 函数。订单与持仓的处理将于 OnTrade() 函数内精确执行。
2. “EA 交易”模板
那么,我们来创建一个新的“EA 交易”。于 MetaEditor 中点击 File -> New 以启动 MQL5 向导。选择 Expert Advisor 并点击 Next。在 “General properties of the Expert Advisor” 对话框中输入“EA 交易”的 Name (名称),必要时还包括您自己的数据。我将自己的“EA 交易”命名为 “TradeControl”。您可以用这个名称或是自己选一个,这不重要。我们不会指定任何参数,因为参数都会在编写某“EA 交易”时动态创建。
好了!“EA 交易”模板已创建,我们得将 OnTrade() 函数添加进去。
如此一来,您就会得到下述代码:
//+------------------------------------------------------------------+ //| TradeControl_en.mq5 | //| Copyright KlimMalgin | //| | //+------------------------------------------------------------------+ #property copyright "KlimMalgin" #property link "" #property version "1.00" //+------------------------------------------------------------------+ //| EA初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(0); } //+------------------------------------------------------------------+ //| EA去初始化函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| OnTrade 函数 | //+------------------------------------------------------------------+ void OnTrade() { //--- //--- } //+------------------------------------------------------------------+ //| EA订单函数 | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+
3. 持仓操作
我们从最简单的交易事件 – 开仓平仓开始吧。首先,您要知道,按下 “Sell” 和 “Buy” 按钮后会有哪些进程发生。
如果我们在 OnTrade() 函数中置入一个调用:
Alert("Trade事件发生了");
之后我们就会看到,通过市场函数 OnTrade() 开仓后,我们的 Alert (警报)随同其执行了四次:
图 1. 警报
为什么会调用 OnTrade() 4 次,我们又该如何应对这些警报呢?为了解这些,我们来查询一下说明文档:
OnTrade
此函数会在交易事件发生时被调用。如果已下订单、新建持仓、订单历史和交易历史列表有变更时,也会发生此事件。
这里有一件事不得不提:
撰写本文并与开发人员们交流时,我发现历史方面的变更并不会导致 OnTrade() 调用!所以实际情况是,OnTrade() 函数只会在已下订单和新建持仓列表有变更时才被调用!开发交易事件句柄时,您可能会发现已执行订单与交易于历史中的显示会有延迟,而且如果 OnTrade() 函数正在运行,则您不能处理它们。
现在我们返回事件。正如我们所见 – 如果由市场开仓,则“交易”事件会发生 4 次:
- 创建由市场开仓的订单。
- 交易执行。
- 将完成的订单传递给历史。
- 开仓。
想要在终端中跟踪此过程,则要注意 MetaTrader 窗口 “Trade” 选项卡中的订单列表:
图 2. “Trade” 选项卡中的订单列表
一旦您建仓(比如 down),订单列表中就会出现一个带有 started 状态的订单(图 2)。这样会更改已下订单列表,并调用 Trade 事件。这是第一次激活 OnTrade() 函数。然后是由创建的订单执行交易。在此阶段,OnTrade() 函数被第二次执行。交易一执行,完成的订单及其执行的交易就会被发送到历史,而 OnTrade() 函数则会被第三次调用。最后阶段,由已执行交易开仓,而 OnTrade() 函数又被第四次调用。
想要“捕捉”开仓时机,您每次调用 OnTrade() 都要对订单列表、订单历史和交易历史进行分析。而这也正是我们马上要做的!
好了,OnTrade() 函数已被调用,我们需要看看 “Trade” 选项卡中的订单数量是否有变化。所以,我们必须对比上一次 OnTrade() 调用与当前调用列表中订单的数量。为知道列表中当前有多少订单,我们会使用 OrdersTotal() 函数。而为得知上一次调用列出了多少订单,我们会保留每一次 OnTrade() 调用的 OrdersTotal() 值。我们会为此创建一个专用变量:
int OrdersPrev = 0; // 上一次调用 OnTrade() 时订单的数量
于 OnTrade() 变量的结尾处,将 OrdersTotal() 的值赋给 OrdersPrev 变量。
您还要考虑到运行“EA 交易”时列表中已存在挂单的情况。“EA 交易”必须具备认出它们的能力,如此一来,OnInit() 函数中的 OrdersPrev 变量亦必须被指定为 OrdersTotal() 的值。我们刚刚在“EA 交易”中做出的改动如下:
int OrdersPrev = 0; // 上一次调用OnTrade()时订单的数量 //+------------------------------------------------------------------+ //| EA初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- OrdersPrev = OrdersTotal(); //--- return(0); } //+------------------------------------------------------------------+ //| OnTrade 函数 | //+------------------------------------------------------------------+ void OnTrade() { //--- OrdersPrev = OrdersTotal(); //--- }
我们现在知道了当前与上一次调用的订单的数量 – 我们可以看出订单于此列表中出现的时间,也能看出其因某种理由消失的时间。为达此目的,我们会采用下述条件:
if (OrdersPrev < OrdersTotal()) { // 出现订单 } else if(OrdersPrev > OrdersTotal()) { // 订单消失 }
结果显示:如果上一次调用时我们的订单数量少于当前的数量,则该订单会显示于列表中(多个订单不可同时显示);但如果是相反的情况,我们当前的订单数量多于上一次 OnTrade() 调用的数量,则此订单或被执行或基于某种原因取消。几乎所有持仓操作都从这两种条件开始。
只有“止损”与“获利”要求一种独立的持仓操作。我会向 OnTrade() 函数添加持仓操作的代码。我们一起来看一下:
void OnTrade() { //--- Alert("Trade 事件发生"); HistorySelect(start_date,TimeCurrent()); if (OrdersPrev < OrdersTotal()) { OrderGetTicket(OrdersTotal()-1);// 选择最后一笔订单 _GetLastError=GetLastError(); Print("错误 #",_GetLastError);ResetLastError(); //-- if (OrderGetInteger(ORDER_STATE) == ORDER_STATE_STARTED) { Alert(OrderGetTicket(OrdersTotal()-1),"订单到达有待处理"); LastOrderTicket = OrderGetTicket(OrdersTotal()-1); // 保存订单号用于未来工作 } } else if(OrdersPrev > OrdersTotal()) { state = HistoryOrderGetInteger(LastOrderTicket,ORDER_STATE); // 如果订单没有找到,产生一个错误 _GetLastError=GetLastError(); if (_GetLastError != 0){Alert("Error #",_GetLastError," 订单没有找到!");LastOrderTicket = 0;} Print("错误 #",_GetLastError," state: ",state);ResetLastError(); // 如果订单被全部执行 if (state == ORDER_STATE_FILLED) { // 分析最后一笔交易 // -- Alert(LastOrderTicket, "订单被执行, 进入交易"); switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_ENTRY)) { // 进入市场 case DEAL_ENTRY_IN: Alert(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_ORDER), " 订单发起交易 #",HistoryDealGetTicket(HistoryDealsTotal()-1)); switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE)) { case 0: // 如果仓位交易量和交易相等,说明仓位刚刚建立 if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) == HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME))) { Alert("建立买入仓位, 货币对 ", HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); } else // 如果仓位交易量和交易不相等,则仓位被增加 if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) > HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME))) { Alert("买入仓位增加, 货币对 ", HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); } break; case 1: // 如果仓位交易量和交易相等,则仓位刚刚建立 if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) == HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME))) { Alert("卖出仓位被建立,货币对 ", HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); } else // 如果仓位交易量和交易不相等,则仓位被增加 if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) > HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME))) { Alert("卖出仓位增加,货币对 ", HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); } break; default: Alert("未被处理的类型代码: ", HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE)); break; } break; // 退出市场 case DEAL_ENTRY_OUT: Alert(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_ORDER), " 订单发起交易 #",HistoryDealGetTicket(HistoryDealsTotal()-1)); switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE)) { case 0: // 如果我们试图关闭的仓位还有剩余,我们只关闭部分仓位 if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) == true) { Alert("卖出仓位部分平仓,货币对 ", HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL), " 利润 = ", HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT)); } else // 如果仓位没找到,说明是全部平仓 if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) == false) { Alert("卖出仓位平仓,货币对 ", HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL), " 利润 = ", HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT)); } break; case 1: // 如果我们尝试关闭的仓位依然存在,说明我们是部分平仓 if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) == true) { Alert("买入仓位部分平仓,货币对 ", HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL), " 利润 = ", HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT)); } else // 如果仓位没有找到,说明是完全平仓 if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) == false) { Alert("买入仓位平仓,货币对 ", HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL), " 利润 = ", HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT)); } break; default: Alert("没有处理的类型代码: ", HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE)); break; } break; // 反转 case DEAL_ENTRY_INOUT: Alert(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_ORDER), " 订单引发交易 #",HistoryDealGetTicket(HistoryDealsTotal()-1)); switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE)) { case 0: Alert("卖出被反转为买入,货币对 ", HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL), " 利润 = ", HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT)); break; case 1: Alert("买入被反转为卖出,货币对 ", HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL), " 利润 = ", HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_PROFIT)); break; default: Alert("未被处理的类型代码: ", HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE)); break; } break; // 状态记录 case DEAL_ENTRY_STATE: Alert("指出为状态记录. 未被处理的类型代码: ", HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE)); break; } // -- } } OrdersPrev = OrdersTotal(); //--- }
而且您一定要在程序的开头即声明下述变量:
datetime start_date = 0; // 我们开始读取历史的日期 int OrdersPrev = 0; // 上一次调用OnTrade()时订单的数量 int PositionsPrev = 0; // 上一次调用OnTrade()时仓位的数量 ulong LastOrderTicket = 0; // 最后一个处理订单的订单号 int _GetLastError=0; // 错误代码 long state=0; // 订单状态
我们返回到 OnTrade() 内容。
您可以注释掉开头的“警报”,但我会继续下一步前往 HistorySelect() 函数。它会针对指定时间段生成一个交易与订单历史列表,而该列表由此函数的两个参数定义。如果此函数未于前往交易及订单历史之前调用,则我们不会获取到任何信息,因为历史列表会是空的。条件会在调用 HistorySelect() 后被评估,与之前所述一样。
有新订单时,我们首先选择它,再检查有无错误:
OrderGetTicket(OrdersTotal()-1);// 选择最后一个订单 _GetLastError=GetLastError(); Print("Error #",_GetLastError);ResetLastError();
选好订单后,我们利用 GetLastError() 函数获取错误代码。然后再用 Print() 函数将代码记入日志,再利用 ResetLastError() 函数将错误代码重置为零,如此一来,下次再有其它情况有 GetLastError() 调用时,我们就不会再看到相同的错误代码了。
错误检查之后,如果已成功选定订单,则会检查其状态:
if (OrderGetInteger(ORDER_STATE) == ORDER_STATE_STARTED) { Alert(OrderGetTicket(OrdersTotal()-1),"订单到来,有待处理"); LastOrderTicket = OrderGetTicket(OrdersTotal()-1); // 保存订单编号以后使用 }
如果订单为 started 状态(即,处于接受错误检查状态),尚未被接受,则其有望于近期被执行,而我们也只需简单地给出一个 Alert(),通知订单正被处理,并将其标签保存于下一次 OnTrade() 调用。您还可以使用任何其它种类的通知来替代 Alert()。
上述代码中的
OrderGetTicket(OrdersTotal()-1)
会返回整个订单列表的最后一个订单标签。
OrdersTotal()-1 表明我们需要获取最新的订单。因为 OrdersTotal() 函数会返回订单的总数(比如说,如果列表中有 1 个订单,则 OrdersTotal() 会返回 1),而且订单索引编号从 0 开始计数,那么,想要获取上一次订单的索引编号,我们必须在订单总数的基础上减掉 1(如果 OrdersTotal() 返回 1,则此订单的索引编号为 0)。而 OrderGetTicket() 函数反过来又会返回订单标签,哪个编号会传递给它。
此为第一种条件,通常会在第一次 OnTrade() 调用时被触发。接下来是第二种条件,会在第二次 OnTrade() 调用时碰到 – 当订单被执行、被载入历史且应开仓时。
如果列表中未见订单,则其已被载入历史,肯定就在那里!因此,我们利用 HistoryOrderGetInteger() 函数诉诸订单历史获取订单状态。而且想要读取特定订单的历史数据,我们需要其标签。如是第一种情况,则新订单的标签会被存储到 LastOrderTicket 变量中。
我们由此获得了订单状态,将订单标签指定为 HistoryOrderGetInteger() 的第一个参数,而 needed property 类型则作为第二个参数。试着获取订单状态之后,我们会获取错误代码并将其写入日志。如果您需要操作的订单尚未进入历史,这样就是有必要的,而且我们也诉诸于它(经验表明这是可能的,而且非常有可能。我在本文开头已有论述)。
如果出现错误,则进程停止,因为已经没有要处理的数据,且不符合下述任何条件。以及,如果 HistoryOrderGetInteger() 调用成功且订单已声明 “Order is fully executed”(订单已完全执行):
// 如果订单被全部执行 if (state == ORDER_STATE_FILLED)
然后再给出另一个通知:
// 然后分析最后一笔交易 // -- Alert(LastOrderTicket, "订单被执行,转向交易");
接下来我们会处理由此订单调用的交易。首先,查明交易方向(DEAL_ENTRY 属性)。方向并不是指买入或卖出,而是进入市场、退出市场、反向或状态记录的指标。由此,利用 DEAL_ENTRY 属性我们可以清楚订单是否已被设置为开仓、平仓或反向开仓。
为对交易及其结果进行分析,我们还诉诸于采用下述构造的历史:
switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_ENTRY)) { ... }
其工作方式与订单相同:
HistoryDealsTotal() 会返回交易总数。想知道最新交易的编号,我们可以在 HistoryDealsTotal() 值的基础上减掉 1。作为结果的交易数量会被传递至 HistoryDealGetTicket() 函数,反过来它又会将选定交易的标签传递给 HistoryDealGetInteger() 函数。而按指定标签与属性类型分类的 HistoryDealGetInteger() 则会返回交易的方向。
我们来仔细研究研究进入市场的方向。其它的方向也会稍带谈到,因为它们的处理方式都大同小异:
由 HistoryDealGetInteger() 获取的表达式的值,与案例块的各个值进行对比,直至找到匹配。假设我们要进入市场,即,建立 Sell 订单。则第一个块会被执行:
// 进入市场 case DEAL_ENTRY_IN:
块的开头会通知您交易创建相关的信息。同时该通知还可确保一切正常,交易正被处理。
通知过后还有一个分析交易类型的交换块:
switch(HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE)) { case 0: // 如果持仓交易量和交易相同,说明仓位刚刚建立 if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) == HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME))) { Alert("建立买入仓位,货币对 ", HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); } else // 如果持仓交易量和交易不相等,说明仓位增加 if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) > HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME))) { Alert("买入仓位增加,货币对 ", HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); } break; case 1: // 如果持仓交易量与交易相等,说明仓位刚刚建立 if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) == HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME))) { Alert("卖出仓位建立,货币对 ", HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); } else // 如果持仓交易量与交易不相等,说明仓位被增加 if (PositionSelect(HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)) && (PositionGetDouble(POSITION_VOLUME) > HistoryDealGetDouble(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_VOLUME))) { Alert("卖出仓位增加,货币对 ", HistoryDealGetString(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_SYMBOL)); } break; default: Alert("未处理的类型代码: ", HistoryDealGetInteger(HistoryDealGetTicket(HistoryDealsTotal()-1),DEAL_TYPE)); break; }
从历史获取交易相关信息 – 除指定的属性外,方式皆与此前相同。此时您必须指定 DEAL_TYPE 以明确到底是执行 Buy 还是 Sell 交易。我只分析了 Buy 和 Sell 两种类型,除此之外还有四种类型。但剩余的四种交易类型不太常见,所以它们只采用一种默认块来替代四种案例块。它会给出一个带类型代码的 Alert()。
您在代码中可能已经注意到了,被处理的不仅是 Buy(买入)和 Sell(卖出)持仓的建立,还有其增量。想要确定持仓何时增加、何时被建 – 您需要对比执行的交易数量与持仓数量,由此形成本交易的结果。如果持仓数量等于执行交易的数量 – 此持仓已被建立;而如果持仓数量与交易数量不同 – 此持仓已被增加。这适用于 Buy持仓(于案例 ‘0’ 块中)和 Sell持仓(于案例 ‘1’ 块中)。最后是默认块,处理 Buy 与 Sell 以外的所有其它情况。整个进程由 HistoryDealGetInteger() 函数返回的类型代码相关的通知构成。
最后,有关操作持仓的最后一个顾虑。就是对于“止损”与“获利”值变动的处理。想知道哪个持仓参数被改动了,我们需要对比其参数的当前与之前的状态。持仓参数的当前值始终可以利用服务函数获取,但此前的值则要靠保存。
为此我们编写了一个专用函数,可将持仓参数保存到结构数组:
void GetPosition(_position &Array[]) { int _GetLastError=0,_PositionsTotal=PositionsTotal(); int temp_value=(int)MathMax(_PositionsTotal,1); ArrayResize(Array, temp_value); _ExpertPositionsTotal=0; for(int z=_PositionsTotal-1; z>=0; z--) { if(!PositionSelect(PositionGetSymbol(z))) { _GetLastError=GetLastError(); Print("OrderSelect() - Error #",_GetLastError); continue; } else { // 如果发现仓位,把它的信息放到数组中 Array[z].type = PositionGetInteger(POSITION_TYPE); Array[z].time = PositionGetInteger(POSITION_TIME); Array[z].magic = PositionGetInteger(POSITION_MAGIC); Array[z].volume = PositionGetDouble(POSITION_VOLUME); Array[z].priceopen = PositionGetDouble(POSITION_PRICE_OPEN); Array[z].sl = PositionGetDouble(POSITION_SL); Array[z].tp = PositionGetDouble(POSITION_TP); Array[z].pricecurrent = PositionGetDouble(POSITION_PRICE_CURRENT); Array[z].comission = PositionGetDouble(POSITION_COMMISSION); Array[z].swap = PositionGetDouble(POSITION_SWAP); Array[z].profit = PositionGetDouble(POSITION_PROFIT); Array[z].symbol = PositionGetString(POSITION_SYMBOL); Array[z].comment = PositionGetString(POSITION_COMMENT); _ExpertPositionsTotal++; } } temp_value=(int)MathMax(_ExpertPositionsTotal,1); ArrayResize(Array,temp_value); }
欲使用此功能,我们必须将下述代码添加到全局变量声明块中:
/* * * 保存仓位信息的结构 * */ struct _position { long type, // 仓位类型 magic; // 仓位幻数 datetime time; // 建仓时间 double volume, // 持仓交易量 priceopen, // 仓位价格 sl, // 仓位的止损价位 tp, // 仓位的获利价位 pricecurrent, // 交易品种的当前价位 comission, // 手续费 swap, // 累计库存费 profit; // 当前利润 string symbol, // 仓位的交易品种 comment; // 仓位注释 }; int _ExpertPositionsTotal = 0; _position PositionList[], // 保存仓位信息的数组 PrevPositionList[];
GetPosition() 函数原型是很长时间以前在 www.mql4.com 文章中找到的,但我现在找不到了,也不能指定源。我并不想详细地探讨该函数的作用。重点在于:作为引用传递 _position 类型数组(带有与持仓字段对应字段的结构)的一个参数,有关当前新建持仓及其参数值的所有信息都被传递至此。
为方便跟踪持仓参数中的变化,我们创建两个 _position 类型的数组。分别为 PositionList[] (持仓的当前状态)和 PrevPositionList[] (持仓的上一个状态)。
想开始操作持仓,我们必须向 OnInit() 和 OnTrade() 的末尾添加下一个调用:
GetPosition(PrevPositionList);
还必须在 Ontrade() 的开头添加此调用:
GetPosition(PositionList);
现在 PositionList[] 与 PrevPositionList[] 数组中将是我们自行支配的、分别与当前与此前 OnTrade() 调用持仓相关的信息。
现在,我们一起来看看跟踪 sl 与 tp 中变化的实际代码:
if ((PositionsPrev == PositionsTotal()) && (OrdersPrev == OrdersTotal())) { string _alerts = ""; bool modify = false; for (int i=0;i<_ExpertPositionsTotal;i++) { if (PrevPositionList[i].sl != PositionList[i].sl) { _alerts += "货币对 "+PositionList[i].symbol+" 止损价位从 "+ PrevPositionList[i].sl +" 改为 "+ PositionList[i].sl +"/n"; modify = true; } if (PrevPositionList[i].tp != PositionList[i].tp) { _alerts += "货币对 "+PositionList[i].symbol+" 获利价位从 "+ PrevPositionList[i].tp +" 改为 "+ PositionList[i].tp +"/n"; modify = true; } } if (modify == true) { Alert(_alerts); modify = false; } }
我们可以看到,代码量不算太大,但这只是海量准备工作的结果。我们深入研究一下。
此代码皆由条件开始:
if ((PositionsPrev == PositionsTotal()) && (OrdersPrev == OrdersTotal()))
我们由此看出,没有订单或持仓被下达或删除。如符合条件,则很可能某些持仓或订单的参数已被更改。
函数的开头声明了两个变量:
- _alerts – 存储变更相关的所有通知。
- modify – 允许您显示变更相关信息(只有真正存在变更时)。
接下来,我们在循环中检验每个持仓此前与当前调用 OnTrade() 的“获利”与“止损”值的匹配情况。所有不匹配的相关信息均存储于 _alerts 变量中,稍后则会通过 Alert() 函数显示。顺便提一下,挂单修改的处理亦会以同样的方式执行。
我们现在暂时结束持仓操作,继续学习挂单下达相关内容。
4. 操作订单
我们从下达挂单事件开始。
有新挂单出现时,只会生成 Trade 事件一次,但对于处理来讲已经足够了!将操作挂单的代码置入操作符主体中:
if (OrdersPrev < OrdersTotal())
就会得到下述内容:
if (OrdersPrev < OrdersTotal()) { OrderGetTicket(OrdersTotal()-1);// 选择最后一笔订单 _GetLastError=GetLastError(); Print("错误 #",_GetLastError);ResetLastError(); //-- if (OrderGetInteger(ORDER_STATE) == ORDER_STATE_STARTED) { Alert(OrderGetTicket(OrdersTotal()-1),"订单到达,有待处理"); LastOrderTicket = OrderGetTicket(OrdersTotal()-1); // 保存订单号,未来使用 } state = OrderGetInteger(ORDER_STATE); if (state == ORDER_STATE_PLACED) { switch(OrderGetInteger(ORDER_TYPE)) { case 2: Alert("挂限价买单 #", OrderGetTicket(OrdersTotal()-1)," 被接受!"); break; case 3: Alert("挂限价卖单 #", OrderGetTicket(OrdersTotal()-1)," 被接受!"); break; case 4: Alert("挂止损买单 #", OrderGetTicket(OrdersTotal()-1)," 被接受!"); break; case 5: Alert("挂止损卖单 #", OrderGetTicket(OrdersTotal()-1)," 被接受!"); break; case 6: Alert("挂限价止损买单 #", OrderGetTicket(OrdersTotal()-1)," 被接受!"); break; case 7: Alert("挂限价止损卖单 #", OrderGetTicket(OrdersTotal()-1)," 被接受!"); break; } } }
这个就是操作挂单的代码,以下述内容开头:
state = OrderGetInteger(ORDER_STATE); if (state == ORDER_STATE_PLACED) {
首先检查订单状态。订单必须处于 ORDER_STATE_PLACED 状态,即,被接受的状态。如满足此条件,则会出现 switch 操作符,该操作符可根据订单类型输出一个信息。
接下来我们将操作订单被修改时发生的事件。订单的修改与持仓修改类似。存储订单属性的结构也可依样创建:
/* * * 保存订单信息的结构 * */ struct _orders { datetime time_setup, // 下单时间 time_expiration, // 订单过期时间 time_done; // 订单执行或取消的时间 long type, // 订单类型 state, // 订单状态 type_filling, // 剩余的执行类型 type_time, // 订单生命周期 ticket; // 订单号 long magic, // 下单的EA幻数 // (用处是确保每个EA必须 // 有它自己的唯一编号) position_id; // 下单的仓位编号 // 当它被执行的时候. 每一个被执行的订单都引发 // 一个交易,会建立或者修改已有 // 仓位. 仓位的编号 // 当时执行订单. double volume_initial, // 下单的初始交易量 volume_current, // 未处理交易量 price_open, // 订单中指定的价位 sl, // 止损价位 tp, // 获利价位 price_current, // 订单中交易品种的当前价位 price_stoplimit; // 限价止损单被触发时的限价单价位 string symbol, // 下单针对的交易品种 comment; // 注释 }; int _ExpertOrdersTotal = 0; _orders OrderList[], // 保存订单信息的数组 PrevOrderList[];
此结构的每个字段都对应着某种订单属性。声明结构后,再声明 int 类型变量和两个 _orders 类型数组。_ExpertOrdersTotal 变量会存储订单的总数,而 OrderList[] 和 PrevOrderList[] 数组则会分别存储当前与此前 OnTrade() 调用订单相关的信息。
函数本身呈示如下:
void GetOrders(_orders &OrdersList[]) { int _GetLastError=0,_OrdersTotal=OrdersTotal(); int temp_value=(int)MathMax(_OrdersTotal,1); ArrayResize(OrdersList,temp_value); _ExpertOrdersTotal=0; for(int z=_OrdersTotal-1; z>=0; z--) { if(!OrderGetTicket(z)) { _GetLastError=GetLastError(); Print("GetOrders() - Error #",_GetLastError); continue; } else { OrdersList[z].ticket = OrderGetTicket(z); OrdersList[z].time_setup = OrderGetInteger(ORDER_TIME_SETUP); OrdersList[z].time_expiration = OrderGetInteger(ORDER_TIME_EXPIRATION); OrdersList[z].time_done = OrderGetInteger(ORDER_TIME_DONE); OrdersList[z].type = OrderGetInteger(ORDER_TYPE); OrdersList[z].state = OrderGetInteger(ORDER_STATE); OrdersList[z].type_filling = OrderGetInteger(ORDER_TYPE_FILLING); OrdersList[z].type_time = OrderGetInteger(ORDER_TYPE_TIME); OrdersList[z].magic = OrderGetInteger(ORDER_MAGIC); OrdersList[z].position_id = OrderGetInteger(ORDER_POSITION_ID); OrdersList[z].volume_initial = OrderGetDouble(ORDER_VOLUME_INITIAL); OrdersList[z].volume_current = OrderGetDouble(ORDER_VOLUME_CURRENT); OrdersList[z].price_open = OrderGetDouble(ORDER_PRICE_OPEN); OrdersList[z].sl = OrderGetDouble(ORDER_SL); OrdersList[z].tp = OrderGetDouble(ORDER_TP); OrdersList[z].price_current = OrderGetDouble(ORDER_PRICE_CURRENT); OrdersList[z].price_stoplimit = OrderGetDouble(ORDER_PRICE_STOPLIMIT); OrdersList[z].symbol = OrderGetString(ORDER_SYMBOL); OrdersList[z].comment = OrderGetString(ORDER_COMMENT); _ExpertOrdersTotal++; } } temp_value=(int)MathMax(_ExpertOrdersTotal,1); ArrayResize(OrdersList,temp_value); }
同 GetPosition() 函数类似,它会读取每个已下订单属性的相关信息,将其置入数组,并作为输入参数传递。函数代码必须置入您的“EA 交易”的末尾处,而其调用如下:
GetOrders(PrevOrderList);
置入 OnInit() 与 OnTrade() 的末尾处。
GetOrders(OrderList);
置于 OnTrade() 的开头。
现在来看处理订单修改的代码。它是一个循环,而且它会对持仓修改的代码进行补充:
for (int i = 0;i<_ExpertOrdersTotal;i++) { if (PrevOrderList[i].sl != OrderList[i].sl) { _alerts += "订单 "+OrderList[i].ticket+" 修改止损价位从 "+ PrevOrderList[i].sl +" 到 "+ OrderList[i].sl +"/n"; modify = true; } if (PrevOrderList[i].tp != OrderList[i].tp) { _alerts += "订单 "+OrderList[i].ticket+" 修改获利价位从 "+ PrevOrderList[i].tp +" 到 "+ OrderList[i].tp +"/n"; modify = true; } }
此循环会处理所有订单,并对比当前与此前 OnTrade() 调用的“止损”与“获利”值。如果不同,则将其保存到 _alerts 变量,而且会在循环结束时通过 Alert() 函数显示出来。
将此代码置入操作符主体:
if ((PositionsPrev == PositionsTotal()) && (OrdersPrev == OrdersTotal())) {
在操作持仓的循环之后立即执行。
到目前为止,交易事件操作尚未结束。本文只是讲到了操作 Trade 事件的主要原则。总而言之,此方法所实现的机遇非常之多,远远超出了本文的局限。
总结
操作交易事件的能力(作为 MQL5 语言的一部分)是一种潜在的强大工具,不仅允许相对快速地实现订单验证的算法并生成交易报告,还降低了系统资源的成本和源代码的体积,而这无疑会成为泽被广大开发人员的福利。
本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/40
(18.79 KB)
MyFxtop迈投-靠谱的外汇跟单社区,免费跟随高手做交易!
免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。