外汇EA编写教程:MetaTrader 5 中的订单、持仓和成交

交易术语

交易者的终极目标是通过交易操作从金融市场中盈利。本文介绍 MetaTrader 5 交易平台的术语和流程,这些知识是正确理解 MQL5 语言的交易函数的作用所必不可少的。

  • 订单 — 交易服务器接收到的交易操作请求,依据 MetaTrader 5 平台要求构成。如果请求不正确,则不会以订单的形式出现在交易平台中。订单可以在指定金融工具中按当前市场价立即执行,例如买入或卖出一定数量。另一类型的订单为挂单,包含承诺在满足一定的条件下进行交易操作的订单。挂单也可以包含对它们的操作的时间限制 – 订单到期日期。

    MetaTrader 5 客户端中的订单和持仓

    正在等待满足执行条件或取消的已下的(待办)订单,显示在客户端的 “Trade”(交易)选项卡中。可以修改或撤消这些订单。使用 OrderSend() 函数进行订单、撤消订单和修改订单。如果订单被撤消或到达订单到期日期,或者订单已被执行,则该订单移入订单历史记录。已执行或已撤消的订单显示在客户端的 “History”(历史记录)选项卡中。不能修改历史记录中的订单。

  • 成交 – 订单执行的结果(承诺成交的命令)。每笔成交都依据某个订单,但一个订单可生成一组成交。例如,一个买入10手的订单可能通过几笔连续进行的部分成交来执行。成交始终存储在交易历史记录中,不能被修改。在客户端中,成交显示在 “History”(历史记录)选项卡中。

    MetaTrader 5 客户端中的交易

  • 持仓是在金融工具中进行买入或卖出的合约。买入持仓 (Long) 是预期价格上涨而买入的结果,卖出持仓 (Short) 是预期未来价格下跌而卖出资产的结果。对于每个帐户,对于每一种金融工具,只能有一种持仓。对于每个交易品种,在任何给定时间,只能有一个未平持仓 – 买入持仓或卖出持仓。

    MetaTrader 5 客户端中的历史订单

    持仓数量可能因为相同方向的新交易操作而增加。这意味着,买入持仓的数量将在新的买入(Buy 成交)之后增大,在卖出(Sell 成交)之后减小。如果承诺的数量因为交易操作而变为零,则平仓。此类操作被称为平仓。

注:活动订单和持仓始终显示在 “Trade”(交易)选项卡中,并且成交和来自历史记录的订单始终反映在 “History”(历史记录)选项卡中。来自 “Trade”(交易)选项卡的活动订单不能与来自 “History”(历史记录)选项卡的历史订单相混淆。

 

客户端如何接收和存储来自服务器的交易信息?

客户端在一个特殊的资料库中存储交易历史记录,并且在每次连接到交易服务器时仅接收交易帐户中有关成交和已完成订单的缺失历史记录。这样做是为了节省流量。当关闭 MetaTrader 5 客户端或更改当前活动帐户时,整个历史记录被记录到硬盘上,并且在客户端下次启动时读取。

所有数据库都以加密方式记录到硬盘,并且加密密钥取决于客户端安装所在计算机。这将保护客户端用户,防止未经授权访问其数据,例如复制。

在连接帐户期间,客户端加载已保存的帐户资料库,其中含有帐户的历史记录,并且向交易服务器发送一个请求,要求用交易服务器上的帐户历史记录同步客户端上的历史记录数据库。此外,在成功连接帐户之后,交易服务器向客户端发送一个与此帐户有关的正在进行的交易事件的报告。

交易事件指以下帐户改变:

  • 出金和结余操作;
  • 手续费、库存费和税金的缴纳;
  • 进行订单、删除订单和更改订单;
  • 依据订单进行的成交的执行;
  • 建仓和平仓;
  • 持仓数量和方向的改变。

如果到交易服务器的连接中断,客户端会定期尝试重新连接。重新连接服务器之后,客户端请求交易历史记录中的所有最新变化,以维持其自己的历史记录数据库的完整性。

显示在客户端的”History”(历史记录)选项卡中的交易历史记录来自客户端历史记录的资料库,而显示在终端历史记录中的周期更改只能扩大存储在这个数据库的历史记录的范围。减小显示在历史记录中的周期并不能从客户端的资料库中实际上删除历史记录。

所显示交易历史记录的间隔的安装

这意味着安装较短的显示历史记录的间隔不会减少存储的交易历史记录的深度。但是,如果我们为 “History”(历史记录)选项卡中的显示指定更宽的间隔范围,则此类操作会导致向交易服务器请求更深的历史记录,如果客户端自己的资料库不包含该周期内的请求数据的话。

下图说明了客户端和 MetaTrader 5 交易服务器之间的一般互动方案:

客户端在启动期间、在连接失败后重新连接服务器期间、在从一个帐户切换到另一个帐户期间、以及在直接请求缺少的交易历史记录期间,向其自己的交易历史记录资料库发送一个同步请求。

在没有来自客户端的任何请求的情况下,交易服务器独立地向客户端发送有关在帐户中发生的交易事件的消息:订单和持仓的状态的改变、依据订单进行的成交的执行、手续费的缴纳、结余和出金等。

从 MQL5 程序访问交易历史记录

客户端可以同时操作若干个指标、脚本和 EA,并且所有这些程序可以请求它们需要的交易信息:订单、成交和持仓。考虑到整体稳定性、安全性和性能,不包括 MQL5 程序对客户端数据库的直接处理。

每个 MQL5 程序通过请求,在其缓存中接收有关其交易环境的工作“模型”。缓存是内存中的一个特殊区域,用于快速存取数据。例如,在开始处理订单之前,必须将需要的订单写入 MQL5 程序的缓存。当需要处理订单时,所有进一步的工作都将通过该订单的缓存副本进行。

以类似的方式处理来自历史记录的持仓、成交和订单。下图显示了从 MQL5 程序获取交易信息的一般方案:

在交易历史记录数据可供 MQL5 程序使用之前,必须从客户端数据库请求这些数据。在请求之后,获得的数据将被放入 MQL5 程序自己的缓存中。

注:缓存中的数据不会自动与客户端数据库进行同步,因此,必须经常更新以保持缓存中的数据具有适当的状态。

有可能出现错误使用缓存的情况。

  • 如果无法获得请求的数据,缓存将是空的并且无法获得必要的数据。
  • 如果缓存中的数据需要更新,但是未请求更新,则使用此类数据可能导致不可预期的结果。例如,尚未更新当前持仓的数据,并且程序不知道指定交易品种的未平持仓任何信息以及有关该仓位的不断增大的损失的任何信息。

 

处理缓存的函数

交易历史记录可能包含 MQL5 程序的当前工作不需要的数以千计的已执行订单和成交。因此,缓存工作建立在请求的基础上。缓存始终包含在上一次连接到客户端的数据库时加载的信息。如果您需要获得订单和成交的全部历史记录,您需要通过指定需要的间隔来显式请求这些信息。

对于每类信息形成一个独立的缓存。有关订单的数据存储在订单的缓存中,有关持仓的数据存储在仓位的缓存中,有关成交和订单的数据存储在相应的缓存历史记录的实例中。

在从缓存请求信息之前,需要填入信息。

注:无论请求的执行结果如何,任何填写缓存的请求都会事先清除缓存。

交易函数可分为两类:填写缓存的函数和从缓存读取信息的函数。

 

填写缓存的函数

要处理交易历史记录,必须首先获取交易历史记录并将其放入适当的缓存中。形成缓存的函数可分为两个小组。

填写交易缓存的函数(活动订单和持仓):

  • OrderSelect(ticket) – 按其单证(从客户端资料库)将活动订单复制到当前订单的缓存,以使用 OrderGetDouble()、OrderGetInteger() 和 OrderGetString() 函数进一步请求其属性;
  • OrderGetTicket(index) – 按订单客户端资料库的订单列表中的索引,从活动订单的客户端资料库复制到当前订单的缓存,以使用 OrderGetDouble()、OrderGetInteger() 和 OrderGetString() 函数进一步请求其属性。可以使用 OrdersTotal() 函数获得客户端资料库中的订单总数量;
  • PositionSelect(ticket) – 按其交易品种名称(从客户端资料库)将未平持仓复制到缓存,以使用 PositionGetDouble()、PositionGetInteger() 和 PositionGetString() 函数进一步请求其属性;
  • PositionGetSymbol(index) – 按其在客户端资料库的持仓列表中的索引(从客户端资料库)将未平持仓复制到缓存,以使用 PositionGetDouble()、PositionGetInteger() 和 PositionGetString() 函数进一步请求其属性。可以使用 PositionsTotal() 函数获得客户端资料库中的持仓总数量;

填写历史记录缓存的函数:

  • HistoryOrderSelect(ticket) – 按其单证将历史订单复制到历史订单缓存(从客户端的资料库),以使用 HistoryOrderGetDouble()、HistoryOrderGetInteger() 和 HistoryOrderGetString() 函数进一步调用其属性;
  • HistoryDealSelect(ticket) – 按其单证将成交复制到成交缓存(从客户端的资料库),以使用 HistoryDealGetDouble()、HistoryDealGetInteger() 和 HistoryDealGetString() 函数进一步调用其属性;

一般而言,我们需要单独考虑以下两个函数,这两个函数影响缓存中可用的交易历史记录

  • HistorySelect(start, end) – 用指定的服务器时间间隔内的成交和订单填写历史记录缓存。此函数的执行结果影响从 HistoryDealsTotal() 和 HistoryOrdersTotal() 返回的值;
  • HistorySelectByPosition (position_ID) – 用具有指定标识符持仓的成交和订单填写历史记录缓存。此函数的执行结果也会影响 HistoryDealsTotal() 和 HistoryOrdersTotal()。


OrderSelect 和 OrderGetTicket

OrderSelect(ticket) 和 OrderGetTicket() 一般函数以同样的方式工作 – 它们用一个订单填写活动订单的缓存。OrderSelect(ticket) 用于在事先知道一个订单单证的情况。OrderGetTicket() 与 OrdersTotal() 配合使用,可用于在客户端的订单资料库中检查所有可用订单。

在调用这些函数中的任何一个之后,活动订单的缓存仅包含一个订单的信息,如果成功选择了订单的话。否则活动订单的缓存不含任何信息。函数 OrdersTotal() 的执行结果不会改变 – 无论缓存是否是满的,它始终返回客户端资料库中活动订单的实际数量。

 

PositionSelect 和 PositionGetSymbol

如同针对订单一样,针对持仓的这两个函数以同样的方式工作 – 它们用一个持仓填写仓位缓存。PositionGetSymbol(index)需要持仓资料库列表中的数量,作为一个参数,而 PositionSelect(symbol) 依据在其上建仓的交易品种的名称填写缓存。而交易品种的名称又可以通过 PositionGetSymbol(index) 函数获得。

在执行这些函数中的任何一个之后,仓位缓存仅包含一个持仓的数据,如果成功执行函数的话。否则仓位缓存不含任何数据。函数 PositionsTotal() 的执行结果不取决于缓存是否含有信息 – 它始终返回客户端资料库中所有交易品种的未平持仓的实际数量。

 

HistoryOrderSelect

HistoryOrderSelect(ticket) 按其单证从客户端的资料库选择放入缓存的历史订单。该函数在事先知道所需订单的单证时使用。

如果执行成功,则缓存将包含单一订单,并且 HistoryOrdersTotal() 函数返回 1。否则,历史订单的缓存将是空的,并且 HistoryOrdersTotal() 函数将返回 0。

HistoryDealSelect

HistoryDealSelect(ticket) 按其单证从客户端资料库选择成交。该函数在事先知道单证时使用。

如果执行成功,则缓存将包含单一成交,并且 HistoryDealsTotal() 函数将返回 1。否则,成交缓存将是空的,并且 HistoryDealsTotal() 函数将返回 0。

 

从缓存获取信息的函数

在请求有关持仓、成交或订单的属性之前,必须更新 MQL5 程序的相应缓存。这是因为,所请求的信息可能已经更新,这意味着存储在缓存中的副本已经过时了。

  • 订单
    为了获得有关活动订单的信息,必须首先用以下两个函数之一将活动订单复制到活动订单的缓存:OrderGetTicket() 或 OrderSelect()。当调用对应的函数时,是针对存储在缓存中的订单提供属性值:
    1. OrderGetDouble(type_property)
    2. OrderGetInteger(type_property)
    3. OrderGetString(type_property)

这些函数从缓存获取它们的所有数据,因此为了保证获取准确的订单数据,建议调用填写缓存的函数。

  • 持仓

    要获取持仓信息,必须首先选择持仓并使用以下两个函数之一将其复制到缓存:PositionGetSymbol 或 PositionSelect。当调用对应的函数时,是从这个缓存提供持仓的属性值:

    1. PositionGetDouble(type_property)
    2. PositionGetInteger(type_property)
    3. PositionGetString(type_property)

因为这些函数从缓存接收它们的所有数据,因此为了保证获取准确的持仓数据,建议调用填写持仓缓存的函数。

  • 历史订单

    为了从历史记录获取订单信息,需要首先使用以下三个函数之一创建历史订单的缓存:HistorySelect(start, end)、HistorySelectByPosition() 或 HistoryOrderSelect(ticket)。如果实施成功,则缓存将存储 HistoryOrdersTotal() 函数返回的订单数量。通过使用相应的函数,按单证上的每个元素实施对这些订单的属性的存取:

    1. HistoryOrderGetDouble(ticket_order, type_property)
    2. HistoryOrderGetInteger(ticket_order, type_property)
    3. HistoryOrderGetString(ticket_order, type_property)

可以使用 HistoryOrderGetTicket(index) 函数,按其在历史订单缓存中的索引找出历史订单的单证。为了保证收到正确的订单数据,建议调用填写历史订单缓存的函数。

  • 成交

    为了获取历史记录中具体成交的相关信息,需要首先使用以下三个函数之一创建成交缓存:HistorySelect(start, end)、HistorySelectByPosition()或HistoryDealSelect (ticket)。如果函数实施成功,则缓存将存储 HistoryDealsTotal() 函数返回的相应数量的成交。通过使用相应的函数,按单证实施对这些成交的属性的存取:

    1. HistoryDealGetDouble(ticket_deals, type_property)
    2. HistoryDealGetInteger(ticket_deals, type_property)
    3. HistoryDealGetString(ticket_deals, type_property)

可以使用 HistoryDealGetTicket(index) 函数,按其在成交缓存中的索引找出成交单证。为了保证收到正确的成交数据,建议调用填写成交缓存的函数。

从缓存历史记录获取单证的函数

HistoryOrderGetTicket (index) 按其索引从历史订单的缓存返回历史订单的单证(不是从客户端资料库!)。获得的单证可在 HistoryOrderSelect (ticket) 函数中使用,该函数清除缓存并用唯一一个订单重新填写缓存,如果成功的话。再一次声明,HistoryOrdersTotal() 返回的值取决于缓存中订单的数量。

HistoryDealGetTicket(index) 按其索引从成交缓存返回单证。成交单证可供 HistoryDealSelect(ticket) 函数使用,该函数清除缓存并用唯一一个成交重新填写缓存,如果成功的话。HistoryDealsTotal() 返回的值取决于缓存中成交的数量。

注:在调用 HistoryOrderGetTicket (index) 和 HistoryDealGetTicket (index) 函数之前,您需要用足够数量的历史订单和成交填写历史记录缓存。为此,使用以下函数之一:HistorySelect (start, end)、HistorySelectByPosition (position_ID)、HistoryOrderSelect (ticket) 和 HistoryDealSelect (ticket)。

 

使用活动订单获取信息

检查当前活动订单是一个标准过程。如果需要获得某些具体订单的信息,那么,在知道其单证的情况下,可通过使用 OrderSelect(ticket) 函数来进行。

bool selected=OrderSelect(ticket);
if(selected)
  {
   double price_open=OrderGetDouble(ORDER_PRICE_OPEN);
   datetime time_setup=OrderGetInteger(ORDER_TIME_SETUP);
   string symbol=OrderGetString(ORDER_SYMBOL);
   PrintFormat("订单 #%d 交易品种 %s 设置时间 %s",ticket,symbol,TimeToString(time_setup));
  }
else
  {
   PrintFormat("根据单号 %d 选择订单出错. 错误 %d",ticket, GetLastError());
  }

在以上例子中,假定事先知道订单的单证,例如,从全局变量中获得。但是在一般情况下缺少单证信息,因此我们需要求助于 OrderGetTicket(index) 函数,该函数也选择一个订单并将其放入缓存,但是仅需要作为参数指定当前订单列表中的订单编号。

处理订单的整体算法(与成交和持仓类似)如下:

  1. 使用 OrdersTotal() 函数获取订单的总数;
  2. 在一个循环中通过它们在列表中的索引搜索所有订单;
  3. 使用 OrderGetTicket() 函数将各个订单依次复制到缓存;
  4. 使用 OrderGetDouble()、OrderGetInteger() 和 OrderGetString() 函数从缓存获取正确的数据。如果需要,分析获得的数据并采取适当的措施。

以下是此类算法的一个简短例子:

input long my_magic=555;
//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 获取订单总数
   int orders=OrdersTotal();
//--- 扫描订单列表
   for(int i=0;i<orders;i++)
     {
      ResetLastError();
      //--- 把订单根据其在列表中的编号复制到缓存中
      ulong ticket=OrderGetTicket(i);
      if(ticket!=0)// 如果订单被成功复制到缓冲区,处理它
        {
         double price_open  =OrderGetDouble(ORDER_PRICE_OPEN);
         datetime time_setup=OrderGetInteger(ORDER_TIME_SETUP);
         string symbol      =OrderGetString(ORDER_SYMBOL);
         long magic_number  =OrderGetInteger(ORDER_MAGIC);
         if(magic_number==my_magic)
           {
            //  使用指定的 ORDER_MAGIC 处理订单
           }
         PrintFormat("订单 #%d 交易品种 %s 设置时间 %s, ORDER_MAGIC=%d",ticket,symbol,TimeToString(time_setup),magic_number);
        }
      else         // 调用 OrderGetTicket() 没有成功完成
        {
         PrintFormat("从列表复制订单到缓存时出错. 错误代码: %d",GetLastError());
        }
     }
  }

 

获取有关未平持仓的信息

不断监视未平持仓并不仅仅是一个标准过程,而且当然应该在每个实例中实施。要获取具体持仓的信息,知道建仓所在工具的名称就已经足够了。为此,使用 PositionSelect(symbol) 函数。对于 EA 仅处理一个交易品种的那些情形(其附加到的图表的交易品种),可以用 Symbol() 函数或从预定义变量 _Symbol 获得交易品种的名称。

//--- 我们将根据EA工作的图表的交易品种来查找仓位
   string symbol=Symbol();
//--- 尝试取得仓位
   bool selected=PositionSelect(symbol);
   if(selected) // 如果仓位被选择
     {
      long pos_id            =PositionGetInteger(POSITION_IDENTIFIER);
      double price           =PositionGetDouble(POSITION_PRICE_OPEN);
      ENUM_POSITION_TYPE type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      long pos_magic         =PositionGetInteger(POSITION_MAGIC);
      string comment         =PositionGetString(POSITION_COMMENT);
      PrintFormat("仓位 #%d 交易品种 %s: POSITION_MAGIC=%d, 价位=%G, 类型=%s, 注释=%s",
                 pos_id, symbol, pos_magic, price,EnumToString(type), comment);
     }

   else        // 如果选择仓位没有成功
     {
      PrintFormat("根据交易品种 %s 选择仓位没有成功. 错误",symbol,GetLastError());
     }
  }

在一般情形中,可以使用 PositionGetSymbol (index) 函数获取交易品种的信息,该函数选择一个持仓并将其放入缓存。作为参数,必须指定仓位在未平持仓列表中的索引。最好通过在一个循环中搜索所有持仓来进行。

处理持仓的整体算法:

  1. 使用 PositionsTotal() 函数获取持仓的总数;
  2. 在一个循环中通过它们在列表中的索引搜索所有持仓;
  3. 使用 PositionGetSymbol() 函数将各个持仓依次复制到缓存;
  4. 使用 PositionGetDouble()、PositionGetInteger() 和 PositionGetString() 函数从缓存获取需要的持仓数据。如果需要,分析获得的数据并采取适当的措施。

此类算法的一个例子:

#property script_show_inputs

input long my_magic=555;
//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 获取仓位总数
   int positions=PositionsTotal();
//--- 扫描仓位列表
   for(int i=0;i<positions;i++)
     {
      ResetLastError();
      //--- 根据仓位在列表中的编号把它复制到缓存中
      string symbol=PositionGetSymbol(i); //  获取所开启仓位对应的交易品种
      if(symbol!="") // 如果把仓位复制到缓存了,处理它
        {
         long pos_id            =PositionGetInteger(POSITION_IDENTIFIER);
         double price           =PositionGetDouble(POSITION_PRICE_OPEN);
         ENUM_POSITION_TYPE type=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
         long pos_magic         =PositionGetInteger(POSITION_MAGIC);
         string comment         =PositionGetString(POSITION_COMMENT);
         if(pos_magic==my_magic)
           {
           //  使用指定的 POSITION_MAGIC 处理仓位
           }
         PrintFormat("仓位 #%d 交易品种 %s: POSITION_MAGIC=%d, 价格=%G, 类型=%s, 注释=%s",
                     pos_id,symbol,pos_magic,price,EnumToString(type),comment);
        }
      else           // 调用 PositionGetSymbol() 没有成功
        {
         PrintFormat("根据索引 %d 复制仓位到缓存出错."+
                     " 错误代码: %d", i, GetLastError());
        }
     }
  }

处理历史记录缓存的原则

程序员通常是按照能够顺畅处理包含 5 至 10 个成交和订单的历史记录来编写处理历史记录缓存的代码的。一个典型的错误方法示例 – 将整个交易历史记录载入缓存,并且在一个循环中处理交易历史记录,搜索所有的订单和交易:

//---
   datetime start=0;           // 起始时间设为 1970
   datetime end=TimeCurrent();  // 结束时间设为当前服务器时间
//--- 在程序的缓存中请求交易历史的所有信息
   HistorySelect(start,end);
//--- 获取交易历史中的订单总数
   int history_orders=HistoryOrdersTotal();
//--- 现在扫描 全部订单
   for(int i=0;i<history_orders;i++)
     {
     //  处理历史中的每一个订单
     }   
 
    ...
       
//--- 获取历史中的交易总数
   int deals=HistoryDealsTotal();
//--- 现在扫描全部交易
   for(int i=0;i<deals;i++)
     {
     //  处理历史中的每一个交易
     }

在多数情况中,尝试处理所有交易历史记录是错误的。当所处理的成交/订单的数量达到成千上万时,程序的工作速度会显著下降。

注:务必慎重对待调用 HistorySelect() 函数的所有情形!考虑不周地、过度地将所有可用交易历史记录加载到 MQL5 程序的缓存会使程序的性能下降。

这对测试非常重要 – 用户发现测试程序突然变得周到,并开始在他们的客户端中寻找原因。因此,首先务必考虑优化 MQL5 程序的代码(EA 和从 EA 调用的指标)。不要依赖于计算机是铁做的并且有很多内核这一情况。

要使 EA 和指标能够在线正常工作,这点也一样重要。未经优化的程序代码甚至能够让最强大的计算机也瘫痪。

处理交易历史记录的正确算法

  1. 确定请求将交易历史记录载入到缓存的需要。如果没有此必要,则不要执行以下操作;
  2. 确定交易历史记录的最后日期(或许不必包含到目前为止的交易历史记录);
  3. 从结束日期开始计算交易历史记录的起始日期。通常 EA 需要的交易历史记录不超过一天或一周;
  4. 获取成交和历史订单的单证以按已知单证获取属性:
    • HistoryOrderGetDouble()
    • HistoryOrderGetInteger()
    • HistoryOrderGetString()
    • HistoryDealGetDouble()
    • HistoryDealGetInteger()
    • HistoryDealGetString()
  5. 如果单证未知,并且如果有必要,则组织一个循环进行排序;
  6. 在循环中,按索引从交易历史记录缓存获取每个成交/订单的单证(HistoryOrderGetTicket(Index) 和 HistoryDealGetTicket(Index));
  7. 通过已知单证获取订单和成交的必要属性(参见第 4 点)。

此算法的一个代码示例:

//--- 只有在交易历史改变过程中才被设为true的变量
   bool TradeHistoryChanged=false;
//--- 在此我们检查历史中的改变,如有必要把 TradeHistoryChanged 设为 true
//... 所需代码

//--- 检查交易历史是否有改变
   if(!TradeHistoryChanged) return;

//--- 如果历史改变了,那么顺理成章把它载入缓存
//--- 结束时间设为当前服务器时间
   datetime end=TimeCurrent();
//--- 起始时间设为3天前
   datetime start=end-3*PeriodSeconds(PERIOD_D1);
//--- 在程序缓存中请求近3天的交易历史
   HistorySelect(start,end);
//--- 获取历史缓存中的订单总数
   int history_orders=HistoryOrdersTotal();
//--- 扫描订单
   for(int i=0;i<history_orders;i++)
     {
      //--- 获得历史订单编号
      ulong ticket=HistoryOrderGetTicket(i);
      //--- 处理订单 - 获取其幻数
      long order_magic=HistoryOrderGetInteger(ticket,ORDER_MAGIC);
      // 根据单号获取其余的订单属性
      // ...
     }

本示例表达的基本想法是首先您必须验证在交易历史记录中发生的变化。一个选项是在 OnTrade() 函数内,将全局变量 TradeHistoryChanged 的值设置为 true,因为对于任何类型的交易事件,交易事件始终返回 true。

如果交易历史记录没有变化,则无需将交易历史记录重新载入缓存,并且浪费 CPU 资源。这是符合逻辑的,不需要任何解释。如果交易历史记录已经改变,则我们仅添加有必要的部分,并且仅处理每个成交/订单一次。避免不必要的重复循环。

注:每次通过 HistorySelect() 函数请求整个交易历史记录的缓存,以及处理来自历史记录的成交和订单的每个循环都必须着地(即使用完后释放资源)。否则,您的计算机资源使用效率将非常低下。

本文附带了正确地和不正确地处理交易历史记录的例子,分别见文件 WrongWorkWithHistory.mq5 和 RightWorkWithHistory.mq5。

 

从历史记录按订单获取信息

除了一点以外,处理历史订单与处理活动订单几乎没有差别。如果 MQL5 程序的缓存中的活动订单的数量不能大于 1,则 HistoryOrdersTotal() 的结果,以及缓存中历史订单的数量取决于 HistorySelect(start, end)、HistorySelectByPosition() 或 HistoryOrderSelection() 函数加载了多少交易历史记录。

注:如果交易历史记录尚未通过 HistorySelect()、HistorySelectByPosition() 或 HistoryOrderSelect() 函数之一加载到 MQL5 程序的缓存,则不可能处理历史订单和成交。确保在接收交易历史记录的相关数据之前请求必需的成交和订单历史记录。

例如,我们提供一个脚本,该脚本搜索最后一天的最后一个订单,并且显示该订单的信息。

// --- 确定所需交易历史的时间间隔
   datetime end=TimeCurrent();                // 当前服务器时间
   datetime start=end-PeriodSeconds(PERIOD_D1);// 起始时间为24小时之前
//--- 请求程序缓存中1天的交易历史
   HistorySelect(start,end);
//--- 接收历史中的订单总数
   int history_orders=HistoryOrdersTotal();
//--- 根据历史中列表最后的索引获取订单编号
   ulong order_ticket=HistoryOrderGetTicket(history_orders-1);
   if(order_ticket>0) // 在历史订单缓存中得到了,处理它
     {
      //--- 订单状态
      ENUM_ORDER_STATE state=(ENUM_ORDER_STATE)HistoryOrderGetInteger(order_ticket,ORDER_STATE);
      long order_magic      =HistoryOrderGetInteger(order_ticket,ORDER_MAGIC);
      long pos_ID           =HistoryOrderGetInteger(order_ticket,ORDER_POSITION_ID);
      PrintFormat("订单 #%d: ORDER_MAGIC=#%d, ORDER_STATE=%d, ORDER_POSITION_ID=%d",
                  order_ticket,order_magic,EnumToString(state),pos_ID);
     }
   else              // 未能成功获得订单
     {
      PrintFormat("总之,在 %d 个订单历史中, 我们未能"+
                  "根据索引%d选择订单.错误%d",history_orders,history_orders-1,GetLastError());
     }
 

在大多数一般情形中,需要在循环中从缓存搜索订单,然后分析订单。一般算法如下所示:

  1. 如果历史记录是通过 HistorySelect() 函数加载的,确定足够多的历史记录的时间范围 – 不建议将整个交易历史记录载入缓存;
  2. 使用 HistorySelect()、HistorySelectByPosition() 或 HistoryOrderSelect (ticket) 函数将交易历史记录载入程序的缓存
  3. 使用 HistoryOrdersTotal() 获取缓存中订单的总数;
  4. 在一个循环中通过它们在列表中的索引搜索所有订单;
  5. 使用 HistoryOrderGetTicket() 函数获取缓存中订单的单证;
  6. 使用 HistoryOrderGetDouble()、HistoryOrderGetInteger() 和 HistoryOrderGetString() 函数从缓存获取订单数据。如果需要,分析获得的数据并采取适当的措施。

此类算法的一个例子:

#property script_show_inputs

input long my_magic=999;
//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
// --- 设置请求交易历史的时间间隔
   datetime end=TimeCurrent();                // 当前服务器时间
   datetime start=end-PeriodSeconds(PERIOD_D1);// 起始时间设为24小时之前
//--- 从程序缓存中请求交易历史的所需时间间隔
   HistorySelect(start,end);
//--- 获取历史中的订单总数
   int history_orders=HistoryOrdersTotal();
//--- 现在扫描所有订单
   for(int i=0;i<history_orders;i++)
     {
      //--- 根据订单在列表中的编号获取单号
      ulong order_ticket=HistoryOrderGetTicket(i);
      if(order_ticket>0) //  在缓存中得到历史订单,并处理它
        {
         //--- 执行时间
         datetime time_done=HistoryOrderGetInteger(order_ticket,ORDER_TIME_DONE);
         long order_magic  =HistoryOrderGetInteger(order_ticket,ORDER_MAGIC);
         long pos_ID       =HistoryOrderGetInteger(order_ticket,ORDER_POSITION_ID);
         if(order_magic==my_magic)
           {
           //  处理设为 ORDER_MAGIC 的订单
           }
         PrintFormat("订单 #%d: ORDER_MAGIC=#%d, time_done %s, ORDER_POSITION_ID=%d",
                     order_ticket,order_magic,TimeToString(time_done),pos_ID);
        }
      else               // 从历史中获取订单没有成功
        {
         PrintFormat("我们无法根据索引 %d 选择订单. 错误 %d",
                     i,GetLastError());
        }
     }
  }
注:务必慎重对待调用 HistorySelect() 函数的所有情形!考虑不周地、过度地将所有可用交易历史记录加载到 MQL5 程序的缓存会使程序的性能下降。

 

从历史记录获取成交信息

处理成交与处理历史订单具有一样的特点。交易历史记录中的成交数量与 HistoryDealsTotal() 的执行结果取决于 HistorySelect(start, end) 或 HistorySelectByPosition() 函数将多少交易历史记录载入缓存。

要通过其单证仅用一个成交填写缓存,请使用 HistoryDealSelect(ticket) 函数。

// --- 决定请求交易历史的时间间隔
   datetime end=TimeCurrent();                // 当前服务器时间
   datetime start=end-PeriodSeconds(PERIOD_D1);// 把起始时间设为24小时之前
//--- 在程序缓存中根据所需时间间隔请求交易历史
   HistorySelect(start,end);
//--- 获得历史中的交易总数
   int deals=HistoryDealsTotal();
//--- 获取列表中最后一个索引的交易单号
   ulong deal_ticket=HistoryDealGetTicket(deals-1);
   if(deal_ticket>0) // 我们在缓存中得到交易了,处理它
     {
      //--- 所达成交易的订单编号
      ulong order     =HistoryDealGetInteger(deal_ticket,DEAL_ORDER);
      long order_magic=HistoryDealGetInteger(deal_ticket,DEAL_MAGIC);
      long pos_ID     =HistoryDealGetInteger(deal_ticket,DEAL_POSITION_ID);
      PrintFormat("交易 #%d 订单 #%d ORDER_MAGIC=%d  仓位编号",
                  deals-1,order,order_magic,pos_ID);
     }
   else              //尝试获取交易没有成功
     {
      PrintFormat("总之,%d 个交易历史中, 我们无法根据"+
                  "索引 %d 选择交易. 错误 %d",deals,deals-1,GetLastError());
     }

在大多数一般情形中,需要在循环中从缓存搜索成交,然后分析成交。一般算法如下所示:

  1. 如果历史记录是通过 HistorySelect(start, end) 函数加载的,确定足够多的历史记录的范围 – 不建议将整个交易历史记录载入缓存;
  2. 使用 HistorySelect() 或 HistorySelectByPosition() 函数将交易历史记录载入程序的缓存
  3. 使用 HistoryDealsTotal() 函数获取历史记录中成交的总数;
  4. 在一个循环中通过它们在列表中的数量搜索所有成交;
  5. 使用 HistoryDealGetTicket() 确定缓存中下一成交的单证;
  6. 使用 HistoryDealGetDouble()、HistoryDealGetInteger() 和 HistoryDealGetString() 函数从缓存获取成交信息。如果需要,分析获得的数据并采取适当的措施。

计算盈利和损失的此类算法的一个例子:

input long my_magic=111;
//+------------------------------------------------------------------+
//| 脚本程序起始函数                                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
// --- 决定请求交易历史的时间间隔
   datetime end=TimeCurrent();                 // 当前服务器时间
   datetime start=end-PeriodSeconds(PERIOD_D1);// 起始时间设为24小时之前

//--- 在程序缓存中请求交易历史所需的时间间隔
   HistorySelect(start,end);
//--- 获得历史中的交易数量
   int deals=HistoryDealsTotal();

   int returns=0;
   double profit=0;
   double loss=0;
//--- 扫描历史中的所有交易
   for(int i=0;i<deals;i++)
     {
      //--- 根据其在列表中的索引获取交易单号
      ulong deal_ticket=HistoryDealGetTicket(i);
      if(deal_ticket>0) // 在缓存中获取交易,处理它
        {
         string symbol             =HistoryDealGetString(deal_ticket,DEAL_SYMBOL);
         datetime time             =HistoryDealGetInteger(deal_ticket,DEAL_TIME);
         ulong order               =HistoryDealGetInteger(deal_ticket,DEAL_ORDER);
         long order_magic          =HistoryDealGetInteger(deal_ticket,DEAL_MAGIC);
         long pos_ID               =HistoryDealGetInteger(deal_ticket,DEAL_POSITION_ID);
         ENUM_DEAL_ENTRY entry_type=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal_ticket,DEAL_ENTRY);

         //--- 根据指定的 DEAL_MAGIC 处理交易
         if(order_magic==my_magic)
           {
            //... 所需行为
           }

         //--- 根据交易的结果计算利润和亏损
         if(entry_type==DEAL_ENTRY_OUT)
          {
            //--- 增加交易数 
            returns++;
            //--- 交易结果
            double result=HistoryDealGetDouble(deal_ticket,DEAL_PROFIT);
            //--- 把正的结果输入到累计利润中
            if(result>0) profit+=result;
            //--- 把负的结果输入到累计亏损中
            if(result<0) loss+=result;
           }
        }
      else // 未能成功尝试获取交易
        {
         PrintFormat("我们无法根据索引 %d 选择交易. 错误 %d",
                     i,GetLastError());
        }
     }
   //--- 输出计算的结果
   PrintFormat("财务结果中共有 %d 个交易. 利润=%.2f , 亏损= %.2f",
               returns,profit,loss);
  }
注:务必慎重对待调用 HistorySelect() 函数的所有情形!考虑不周地、过度地将所有可用交易历史记录加载到 MQL5 程序的缓存会使程序的性能下降。

 

通过持仓的标识符 (POSITION_IDENTIFIER) 从历史记录的缓存获取

HistorySelectByPosition (position_ID) 函数如同 HistorySelect (start, end) 函数,用来自历史记录的成交和订单填写缓存,但是必须满足一个条件 – 它们必须要有指定的持仓标识符 (POSITION_IDENTIFIER)。持仓的标识符是一个唯一的数字,自动赋予每个重新建仓的仓位,并且在其生命周期内不会改变。同时,必须记住,持仓的改变(持仓类型从 POSITION_TYPE_BUY 变为 POSITION_TYPE_SELL)并不会改变持仓的标识符。

每个未平持仓都是该工具中一个或多个成交的结果。因此,要分析在其寿命周期内的持仓变化,每笔成交和成交所依据的订单都被赋予成交所属的持仓的标识符。因此,知道当前未平持仓的标识符,我们可以重构整个历史 – 查找所有对它进行更改的订单和成交。

HistorySelectByPosition(position_ID) 函数用于让程序员不必在搜索此类信息时编写他们自己的代码来遍历整个交易历史记录。使用此函数的一个典型算法:

  1. 获取正确的持仓标识符;
  2. 使用 HistorySelectByPosition() 函数将所有标识符等于当前持仓的标识符的订单和成交写入交易历史记录缓存;
  3. 依据算法处理交易历史记录。

 

总结

整个交易子系统平台 MetaTrader 5 构思完美且使用方便。此外,丰富的交易函数允许我们以最有效的方式解决每一个具体的问题。

但是,尽管来自标准库的专业交易类让我们不用担心太多细微差别,并且以很高的水平编写程序,而不用进入实施,但对基础知识的理解也会让我们创建更可靠的、更有效率的交易 EA。

可以在本文附带的文件中找到所有给出的例子。

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/211

附加的文件 |

下载ZIP
demo_historydealselectbyindex.mq5
(2.67 KB)
demo_historydealselectbyticket.mq5
(1.77 KB)
demo_historyorderselectbyindex.mq5
(2.07 KB)
demo_historyorderselectbyticket.mq5
(1.87 KB)
demo_ordergetticketbyindex.mq5
(1.75 KB)
demo_orderselectbyticket.mq5
(1.29 KB)
demo_positiongetsymbolbyindex.mq5
(1.97 KB)
demo_positionselectbysymbol.mq5
(1.57 KB)
rightworkwithhistory.mq5
(3.59 KB)
wrongworkwithhistory.mq5
(1.33 KB)

 

 


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

 

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

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

風險提示

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

邁投公眾號

聯繫我們

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

MyFxtops 邁投