外汇EA编写教程:EA 交易中采用OnTrade() 函数处理交易事件

简介

利用 MQL 编写“EA 交易”的任何交易者,或早或晚都会面临报告其“EA 交易”如何起作用的必要性。也可能需要实现“EA 交易”行动相关的短信或电子邮件通知。不管哪种情况,我们都得“捕捉”市场中发生特定事件或某个“EA 交易”执行的行动,并通知用户。

我会用本文为您讲解可以如何实现交易事件的处理,并提供我的实现。

我们拟于本文中处理下述事件:

  • 持仓
    1. Open(开仓)
    2. Add(增持)
    3. Modify (修改持仓)(更改“止损”与“获利”)
    4. Reverse(反向开仓)
    5. Close entire position(完全平仓)
    6. Close part of position(部分平仓)
  • 挂单
    1. Place(下挂单)
    2. 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. 警报

图 1. 警报

为什么会调用 OnTrade() 4 次,我们又该如何应对这些警报呢?为了解这些,我们来查询一下说明文档:

 OnTrade

此函数会在交易事件发生时被调用。如果已下订单、新建持仓、订单历史和交易历史列表有变更时,也会发生此事件。

这里有一件事不得不提:

撰写本文并与开发人员们交流时,我发现历史方面的变更并不会导致 OnTrade() 调用!所以实际情况是,OnTrade() 函数只会在已下订单和新建持仓列表有变更时才被调用!开发交易事件句柄时,您可能会发现已执行订单与交易于历史中的显示会有延迟,而且如果 OnTrade() 函数正在运行,则您不能处理它们。

现在我们返回事件。正如我们所见 – 如果由市场开仓,则“交易”事件会发生 4 次:

  1. 创建由市场开仓的订单。
  2. 交易执行。
  3. 将完成的订单传递给历史。
  4. 开仓。

 
想要在终端中跟踪此过程,则要注意 MetaTrader 窗口 “Trade” 选项卡中的订单列表:

图 2. "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

附加的文件 |

下载ZIP
tradecontrol.mq5
(18.79 KB)

 

 


MyFxtop迈投-靠谱的外汇跟单社区,免费跟随高手做交易!

 

免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。

著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。

風險提示

MyFxtops邁投所列信息僅供參考,不構成投資建議,也不代表任何形式的推薦或者誘導行為。MyFxtops邁投非外匯經紀商,不接觸妳的任何資金。 MYFXTOPS不保證客戶盈利,不承擔任何責任。從事外彙和差價合約等金融產品的槓桿交易具有高風險,損失有可能超過本金,請量力而行,入市前需充分了解潛在的風險。過去的交易成績並不代表以後的交易成績。依據各地區法律法規,MyFxtops邁投不向中國大陸、美國、加拿大、朝鮮居民提供服務。

邁投公眾號

聯繫我們

客服QQ:981617007
Email: service@myfxtop.com

MyFxtops 邁投