按特性选择订单

在关于 交易品种特性一节中,我们引入了 SymbolFilter 类来选择具有特定特性的金融工具。现在,我们将对订单应用相同的方法。

由于我们不仅要分析订单,还要以类似的方式分析交易和仓位,我们将把筛选算法的一般部分分离到基础类 TradeFilter (TradeFilter.mqh) 中。该部分几乎完全重复了 SymbolFilter 的源代码。所以此处不再解释。

如果愿意的话,可以对 SymbolFilter.mqhTradeFilter.mqh 进行文件级上下文比较,看看它们相似程度,并对微小的编辑进行本地化。

主要区别在于 TradeFilter 类是一个模板,因为它必须处理不同对象的特性:订单、交易和仓位。

enum IS // supported comparison conditions in filters
{
   EQUAL,
   GREATER,
   NOT_EQUAL,
   LESS
};
   
enum ENUM_ANY // dummy enum to cast all enums to it
{
};
   
template<typename T,typename I,typename D,typename S>
class TradeFilter
{
protected:
   MapArray<ENUM_ANY,longlongs;
   MapArray<ENUM_ANY,doubledoubles;
   MapArray<ENUM_ANY,stringstrings;
   MapArray<ENUM_ANY,ISconditions;
   ...
   
   template<typename V>
   static bool equal(const V v1const V v2);
   
   template<typename V>
   static bool greater(const V v1const V v2);
   
   template<typename V>
   bool match(const T &mconst MapArray<ENUM_ANY,V> &dataconst;
   
public:
   // methods for adding conditions to the filter
   TradeFilter *let(const I propertyconst long valueconst IS cmp = EQUAL);
   TradeFilter *let(const D propertyconst double valueconst IS cmp = EQUAL);
   TradeFilter *let(const S propertyconst string valueconst IS cmp = EQUAL);
   // methods for getting into arrays of records matching the filter
   template<typename E,typename V>
   bool select(const E propertyulong &tickets[], V &data[],
      const bool sort = falseconst;
   template<typename E,typename V>
   bool select(const E &property[], ulong &tickets[], V &data[][],
      const bool sort = falseconst
   bool select(ulong &tickets[]) const;
   ...
}

模板参数 I、D、S 为三种主要类型(整数型、实数型、字符串型)特性组的枚举:对于订单,在前面的小节中已经说明过了,所以为了清晰起见,可以想象 I = ENUM_ORDER_PROPERTY_INTEGER,D =ENUM_ORDER_PROPERTY_DOUBLE,S = ENUM_ORDER_PROPERTY_STRING

T 类型是为指定监视器类而设计的。目前,我们只有一个准备好的监视器 OrderMonitor。稍后我们将实现 DealMonitorPositionMonitor

之前,在 SymbolFilter 类中,我们没有使用模板参数,因为对于交易品种,所有类型的特性枚举都是已知的,并且只有一个类 SymbolMonitor

回想一下筛选器类的结构体。一组 let 方法允许你在筛选器中注册 "property=value" 对的组合,该组合将用于在 select 方法中选择对象。ID 特性在 property 参数中指定,值在 value 参数中指定。

还有几种 select 方法。这些方法允许调用代码用选定订单号填充数组,如果需要,还可以用请求的对象特性值填充其他数组。所请求特性的特定标识符在 select 方法的第一个参数中设置;可以是一个或几个特性。因此,接收数组必须是一维的或二维的。

特性和值的组合不仅可以检查相等性 (EQUAL),还可以检查大于/小于运算 (GREATER/LESS)。对于字符串型特性,可以指定一个搜索模式,用字符 "*" 表示任何字符序列(例如,ORDER_COMMENT 特性的 "*[tp]*" 将匹配任何地方出现 "[tp]" 的所有注释,尽管这只是可能性的演示,而要搜索由触发 Take Profit 产生的订单,就应分析 ORDER_REASON)。

由于该算法要求实现一个循环,尽管所有对象和对象可以是不同类型(迄今为止,这些都是订单,但随后将出现对交易和仓位的支持),我们需要说明 TradeFilter 类中的两个抽象方法: totalget

   virtual int total() const = 0;
   virtual ulong get(const int iconst = 0;

第一个方法返回对象的数量,第二个方法按其编号返回订单号。这样可以让你想起一对函数 OrdersTotalOrderGetTicket。实际上,它们被用于筛选订单方法的特定实现中。

以下是 OrderFilter 类 (OrderFilter.mqh) 的完整版。

#include <MQL5Book/OrderMonitor.mqh>
#include <MQL5Book/TradeFilter.mqh>
   
class OrderFilterpublic TradeFilter<OrderMonitor,
   ENUM_ORDER_PROPERTY_INTEGER,
   ENUM_ORDER_PROPERTY_DOUBLE,
   ENUM_ORDER_PROPERTY_STRING>
{
protected:
   virtual int total() const override
   {
      return OrdersTotal();
   }
   virtual ulong get(const int iconst override
   {
      return OrderGetTicket(i);
   }
};

这种简单性尤其重要,因为可以轻松地为交易和仓位创建类似的筛选器。

借助这个新类,我们可以更容易地检查属于我们的 EA 交易订单的存在,即替换 GetMyOrder 函数的任何自己编写版本(在示例 PendingOrderModify.mq5 中使用)。

   OrderFilter filter;
   ulong tickets[];
   
   // set a condition for orders for the current symbol and our "magic" number
   filter.let(ORDER_SYMBOL_Symbol).let(ORDER_MAGICMagic);
   // select suitable tickets in an array
   if(filter.select(tickets))
   {
      ArrayPrint(tickets);
   }

此处的“任何版本”是指,由于有了 filter 类,我们可以创建任意条件来选择订单并“随时”更改它们(例如,根据用户的命令,而不是根据程序员的命令)。

作为如何使用筛选器的一个示例,我们使用一个 EA 交易,为从某个价格范围内水平反弹时的交易创建一个挂单净额结算网格,也就是说,针对波动的市场而设计。从这一节开始,在接下来的几节中,我们将在所研究材料的上下文中修改 EA 交易。

EA 交易的第一个版本 PendingOrderGrid1.mq5 根据限价订单和止损订单构建了一个给定大小的净额结算网格。这些参数将是价格水平的数量及其之间的步长(点数)。运算方案如下图所示。

步长为 200 点的 4 个水平的挂单网格

步长为 200 点的 4 个水平的挂单网格

在某个初始时间(该时间可以由一天内的时间表确定,并且可以对应于“夜盘”),当前价格被向上舍入到净额结算网格步长的大小,并且从该水平上下设置指定数量的水平。

在每一个较高的水平,我们可下达限价卖出订单和 stoplimit 买入订单(期货限价订单的价格要低一个水平)。在每一个较低的水平,我们可下达限制买入订单和 stoplimit 卖出订单(期货限价订单的价格要高一个水平)。

当价格触及其中一个水平时,该水平的限价订单就变成了买入或卖出(仓位)。同时,同一水平的止损限价订单被系统自动转换为下一水平的反向限价订单。

例如,如果价格在向上移动的同时突破了该水平,我们将获得一个空头仓位,并在其下方的一步距离处创建一个买入限价订单。

EA 交易将监控每个水平是否有与限价订单配对的止损限价订单。因此,在检测到新的限价买入订单后,程序将在同一水平为其添加止损限价卖出订单,期货限价订单的目标价格为顶部的下一个水平,即开仓水平。

假设价格下跌,并激活限价订单至下方水平,我们将获得多头仓位。同时,止损限价订单转换为下一个上方水平的限价卖单。现在,EA 交易将再次检测到一个“裸”限价订单,并创建一个止损限价订单与其配对买入,比期货限价订单低一个水平。

如果有相反的仓位,我们会平仓。我们还将提供交易系统启用时的日内时段设置,在其余时间,所有订单和仓位都将被删除。这对于“夜盘”尤其有用,因为此时市场的回报波动尤其明显。

当然,这只是网格策略的许多潜在实现之一,缺少了网格的很多定制项,但是我们不会使这个示例过于复杂。

EA 交易将分析每根柱线的情况(大概为 H1 时间或更短)。理论上,这个 EA 交易的运算逻辑需要通过及时响应 交易事件 来改进,但我们还没有探讨过这方面。因此,我们没有持续跟踪和即时“手动”恢复空缺网格水平的限价订单,而是通过使用止损限价订单将这项工作委托给服务器。但是,此处有一个细微的差别。

事实上,每个水平的限价订单和止损订单为相反的类型 (buy/sell),因此由不同类型的价格激活。

造成的结果是,如果市价在网格的上半部分向上移动到下一个水平,Ask 价格可能会触及该水平并激活止损限价买入订单,Bid 价格不会达到该水平,卖出限价订单将保持原样(不会变为持仓)。在网格的下半部分,当市价向下移动时,情况是镜像的。任何水平首先被 Bid 价格触及,并激活止损限价订单卖出,只有在进一步下跌时,该水平才会被 Ask 价格触及。如果没有变动,买入限价订单将保持不变。

随着点差的增加,这个问题变得很关键。因此,EA 交易需要对“额外”限价订单进行额外控制。换言之,如果在其假定的目标价格(相邻水平)已经有限价订单,EA 交易将不会生成在该水平丢失的止损限价订单。

源代码包含在 PendingOrderGrid1.mq5 文件中。在输入参数中,你可以设置每次交易的 Volume(默认情况下,如果保留为 0,则采用图表交易品种的最小手数)、网格水平数 GridSize(必须是偶数)以及水平之间的步长 GridStep(点数)。在 StartTimeStopTime 参数中指定允许策略起作用的日内时段的开始和结束时间:在这两个参数中,只有时间是重要的。

#include <MQL5Book/MqlTradeSync.mqh>
#include <MQL5Book/OrderFilter.mqh>
#include <MQL5Book/MapArray.mqh>
   
input double Volume;                                       // Volume (0 = minimal lot)
input uint GridSize = 6;                                   // GridSize (even number of price levels)
input uint GridStep = 200;                                 // GridStep (points)
input ENUM_ORDER_TYPE_TIME Expiration = ORDER_TIME_GTC;
input ENUM_ORDER_TYPE_FILLING Filling = ORDER_FILLING_FOK;
input datetime StartTime = D'1970.01.01 00:00:00';         // StartTime (hh:mm:ss)
input datetime StopTime = D'1970.01.01 09:00:00';          // StopTime (hh:mm:ss)
input ulong Magic = 1234567890;

工作时段可以在一天之内 (StartTime < StopTime),也可以跨越一天的边界 (StartTime > StopTime),例如,从 22:00 到 09:00。如果两个时间相等,则假定为全天交易。

在继续实现交易想法之前,我们简化设置查询和将诊断信息输出到日志的任务。为此,我们介绍了自己的结构体 MqlTradeRequestSyncLog,其为 MqlTradeRequestSync 的派生结构体。

const ulong DAYLONG = 60 * 60 * 24// length of the day in seconds
   
struct MqlTradeRequestSyncLogpublic MqlTradeRequestSync
{
   MqlTradeRequestSyncLog()
   {
      magic = Magic;
      type_filling = Filling;
      type_time = Expiration;
      if(Expiration == ORDER_TIME_SPECIFIED)
      {
         expiration = (datetime)(TimeCurrent() / DAYLONG * DAYLONG
            + StopTime % DAYLONG);
         if(StartTime > StopTime)
         {
            expiration = (datetime)(expiration + DAYLONG);
         }
      }
   }
   ~MqlTradeRequestSyncLog()
   {
      Print(TU::StringOf(this));
      Print(TU::StringOf(this.result));
   }
};

在构造函数中,我们用不变的值填充所有字段。在析构函数中,我们记录有意义的查询和结果字段。显然,自动对象的析构函数始终会在从形成和发送订单的代码块中退出时被调用,也就是发送和接收的数据会被打印出来。

OnInit 中,我们对输入变量的正确性进行一些检查,特别是对于偶数网格大小。

int OnInit()
{
   if(GridSize < 2 || !!(GridSize % 2))
   {
      Alert("GridSize should be 2, 4, 6+ (even number)");
      return INIT_FAILED;
   }
   return INIT_SUCCEEDED;
}

该算法的主要入口点是 OnTick 处理程序。为了简洁起见,我们将省略示例 PendingOrderModify.mq5 中基于 TRADE_RETCODE_SEVERITY 的错误处理机制。

对于逐柱线工作,该函数有一个静态变量 lastBar,其中存储了最后一个成功处理柱线的时间。同一根柱线上的所有后续分时报价都会被跳过。

void OnTick()
{
   static datetime lastBar = 0;
   if(iTime(_Symbol_Period0) == lastBarreturn;
   uint retcode = 0;
   
   ... // main algorithm (see further)
   
   const TRADE_RETCODE_SEVERITY severity = TradeCodeSeverity(retcode);
   if(severity < SEVERITY_RETRY)
   {
      lastBar = iTime(_Symbol_Period0);
   }
}

为了系统化的目的,主算法将被分成几个辅助函数,而不是省略号。首先,我们来确定是否设置了当天的工作时段,如果设置了,当前是否启用了该策略。该属性存储在 tradeScheduled 变量中。

   ...
   bool tradeScheduled = true;
   
   if(StartTime != StopTime)
   {
      const ulong now = TimeCurrent() % DAYLONG;
      
      if(StartTime < StopTime)
      {
         tradeScheduled = now >= StartTime && now < StopTime;
      }
      else
      {
         tradeScheduled = now >= StartTime || now < StopTime;
      }
   }
   ...

启用交易后,首先使用 CheckGrid 函数检查是否已经有一个订单网络。如果没有网络,函数将返回 GRID_EMPTY 常量,我们应通过调用 Setup Grid 来创建网络。如果网络已经建立,检查是否有要平仓的相反仓位是有意义的:这是由 CompactPositions 函数完成的。

   if(tradeScheduled)
   {
      retcode = CheckGrid();
      
      if(retcode == GRID_EMPTY)
      {
         retcode = SetupGrid();
      }
      else
      {
         retcode = CompactPositions();
      }
   }
   ...

交易期一结束,就必须删除订单并全部平仓(如果有)。这项工作分别由 RemoveOrdersCompactPositions function 函数来完成,这些函数带有一个布尔标志 (true):这个单一可选自变量指示在相反平仓之后对剩余仓位进行简单平仓。

   else
   {
      retcode = CompactPositions(true);
      if(!retcoderetcode = RemoveOrders();
   }

所有函数都返回一个服务器代码,用 TradeCodeSeverity 分析该代码以确认操作成功还是失败。根据 TRADE_RETCODE_SEVERITY,特殊应用代码 GRID_EMPTY和GRID_OK 也视为标准代码。

#define GRID_OK    +1
#define GRID_EMPTY  0

现在我们来逐个看下这些函数。

CheckGrid 函数使用本节开始时提供的 OrderFilter 类。筛选器请求当前交易品种和带有“我们的”标识号的所有挂单,找到的订单订单号存储在数组中。

uint CheckGrid()
{
   OrderFilter filter;
   ulong tickets[];
   
   filter.let(ORDER_SYMBOL_Symbol).let(ORDER_MAGICMagic)
      .let(ORDER_TYPEORDER_TYPE_SELLIS::GREATER)
      .select(tickets);
   const int n = ArraySize(tickets);
   if(!nreturn GRID_EMPTY;
   ...

该网格的完整性是使用我们已经熟悉的 MapArray 类来分析的,该类存储 "key=value" 对。在这种情况下,key 表示水平(价格转换为点数),value 表示给定水平的订单类型的位掩码(叠加)。此外,限价和止损限价订单分别计入 limitsstops 变量。

   // price levels => masks of types of orders existing there
   MapArray<ulong,uintlevels;
 
   const double point = SymbolInfoDouble(_SymbolSYMBOL_POINT);
   int limits = 0;
   int stops = 0;
   
   for(int i = 0i < n; ++i)
   {
      if(OrderSelect(tickets[i]))
      {
         const ulong level = (ulong)MathRound(OrderGetDouble(ORDER_PRICE_OPEN) / point);
         const ulong type = OrderGetInteger(ORDER_TYPE);
         if(type == ORDER_TYPE_BUY_LIMIT || type == ORDER_TYPE_SELL_LIMIT)
         {
            ++limits;
            levels.put(levellevels[level] | (1 << type));
         }
         else if(type == ORDER_TYPE_BUY_STOP_LIMIT 
            || type == ORDER_TYPE_SELL_STOP_LIMIT)
         {
            ++stops;
            levels.put(levellevels[level] | (1 << type));
         }
      }
   }
   ...

如果每种类型的订单数匹配并等于指定的网格大小,则一切正常。

   if(limits == stops)
   {
      if(limits == GridSizereturn GRID_OK// complete grid
      
      Alert("Error: Order number does not match requested");
      return TRADE_RETCODE_ERROR;
   }
   ...

限价订单数量大于止损限价订单数量的情况是正常的:这意味着由于价格变动,一个或多个止损订单变成了限价订单。然后,该程序应在不足的水平上增加止损限价订单。特定水平的特定类型的单独订单可以通过 RepairGridLevel 函数进行设置。

   if(limits > stops)
   {
      const uint stopmask = 
         (1 << ORDER_TYPE_BUY_STOP_LIMIT) | (1 << ORDER_TYPE_SELL_STOP_LIMIT);
      for(int i = 0i < levels.getSize(); ++i)
      {
         if((levels[i] & stopmask) == 0// there is no stop-limit order at this level
         {
            // the direction of the limit is required to set the reverse stop limit
            const bool buyLimit = (levels[i] & (1 << ORDER_TYPE_BUY_LIMIT));
            // checks for "extra" orders due to the spread are omitted here (see the source code)
            ...
            // create a stop-limit order in the desired direction
            const uint retcode = RepairGridLevel(levels.getKey(i), pointbuyLimit);
            if(TradeCodeSeverity(retcode) > SEVERITY_NORMAL)
            {
               return retcode;
            }
         }
      }
      return GRID_OK;
   }
   ...

止损限价订单数量大于限价订单数量的情况视为错误(可能是服务器因为某种原因跳过了该价格)。

   Alert("Error: Orphaned Stop-Limit orders found");
   return TRADE_RETCODE_ERROR;
}

RepairGridLevel 函数执行以下操作。

uint RepairGridLevel(const ulong levelconst double pointconst bool buyLimit)
{
   const double price = level * point;
   const double volume = Volume == 0 ?
      SymbolInfoDouble(_SymbolSYMBOL_VOLUME_MIN) : Volume;
   
   MqlTradeRequestSyncLog request;
   
   request.comment = "repair";
   
   // if there is an unpaired buy-limit, set the sell-stop-limit to it
   // if there is an unpaired sell-limit, set buy-stop-limit to it
   const ulong order = (buyLimit ?
      request.sellStopLimit(volumepriceprice + GridStep * point) :
      request.buyStopLimit(volumepriceprice - GridStep * point));
   const bool result = (order != 0) && request.completed();
   if(!resultAlert("RepairGridLevel failed");
   return request.result.retcode;
}

请注意,我们不需要实际填写该结构体(除了在必要时可以提供更多信息的注释之外),因为有些字段是由构造函数自动填写的,并且我们将交易量和价格直接传递给 sellStopLimitbuyStopLimit 方法。

SetupGrid 函数中使用了类似的方法,其创建了一个新的完整订单网络。在该函数的开始,我们准备了用于计算的变量,并说明了该结构体的 MqlTradeRequestSyncLog 数组。

uint SetupGrid()
{
   const double current = SymbolInfoDouble(_SymbolSYMBOL_BID);
   const double point = SymbolInfoDouble(_SymbolSYMBOL_POINT);
   const double volume = Volume == 0 ?
      SymbolInfoDouble(_SymbolSYMBOL_VOLUME_MIN) : Volume;
   // central price of the range rounded to the nearest step,
   // from it up and down we identify the levels  
   const double base = ((ulong)MathRound(current / point / GridStep) * GridStep)
      * point;
   const string comment = "G[" + DoubleToString(base,
      (int)SymbolInfoInteger(_SymbolSYMBOL_DIGITS)) + "]";
   const static string message = "SetupGrid failed: ";
   MqlTradeRequestSyncLog request[][2]; // limit and stop-limit - one pair
   ArrayResize(requestGridSize);      // 2 pending orders per level

接下来,我们为该网格的下半部分和上半部分生成从中心向两侧发散的订单。

   for(int i = 0i < (int)GridSize / 2; ++i)
   {
      const int k = i + 1;
      
      // bottom half of the grid
      request[i][0].comment = comment;
      request[i][1].comment = comment;
      
      if(!(request[i][0].buyLimit(volumebase - k * GridStep * point)))
      {
         Alert(message + (string)i + "/BL");
         return request[i][0].result.retcode;
      }
      if(!(request[i][1].sellStopLimit(volumebase - k * GridStep * point,
         base - (k - 1) * GridStep * point)))
      {
         Alert(message + (string)i + "/SSL");
         return request[i][1].result.retcode;
      }
      
      // top half of the grid
      const int m = i + (int)GridSize / 2;
      
      request[m][0].comment = comment;
      request[m][1].comment = comment;
      
      if(!(request[m][0].sellLimit(volumebase + k * GridStep * point)))
      {
         Alert(message + (string)m + "/SL");
         return request[m][0].result.retcode;
      }
      if(!(request[m][1].buyStopLimit(volumebase + k * GridStep * point,
         base + (k - 1) * GridStep * point)))
      {
         Alert(message + (string)m + "/BSL");
         return request[m][1].result.retcode;
      }
   }

然后我们检查准备情况。

   for(int i = 0i < (int)GridSize; ++i)
   {
      for(int j = 0j < 2; ++j)
      {
         if(!request[i][j].completed())
         {
            Alert(message + (string)i + "/" + (string)j + " post-check");
            return request[i][j].result.retcode;
         }
      }
   }
   return GRID_OK;
}

尽管检查(调用 completed)与发送订单间隔开来,我们的结构体仍然在内部使用了同步形式 OrderSend。事实上,为了加快发送一批订单(就像在我们的网格 EA 交易中一样),最好使用异步版本 OrderSendAsync。但随后订单执行状态应从 OnTradeTransaction 的事件处理程序中启动。我们稍后将研究该程序。

发送任何订单时的错误都会导致提前退出循环并从服务器返回代码。如果出现错误,这个测试 EA 交易就会直接停止后续工作。对于一个真实的机器人,最好能提供对错误含义的智能分析,如有必要,删除所有订单并平仓。

挂单产生的仓位由函数 CompactPositions 平仓。

uint CompactPositions(const bool cleanup = false)

默认情况下,cleanup 参数等于 false 意味着在交易周期内定期“清理”仓位,即对反向仓位(如果有)进行平仓。cleanup=true 的值用于在交易期末强制平仓所有仓位。

该函数通过一个辅助函数 GetMyPositions,用多头和空头订单号填充 ticketsLongticketsShort 数组。我们已经在示例 TradeCloseBy.mq5 中使用了后者,该示例在 反向平仓:全部和部分一节中说明。该示例中的 CloseByPosition 函数在新的 EA 交易中经历了最小的变化:它从服务器返回一个代码,而不是成功或错误的逻辑指示符。

uint CompactPositions(const bool cleanup = false)
{
   uint retcode = 0;
   ulong ticketsLong[], ticketsShort[];
   const int n = GetMyPositions(_SymbolMagicticketsLongticketsShort);
   if(n > 0)
   {
      Print("CompactPositions, pairs: "n);
      for(int i = 0i < n; ++i)
      {
         retcode = CloseByPosition(ticketsShort[i], ticketsLong[i]);
         if(retcodereturn retcode;
      }
   }
   ...

CompactPositions 的第二部分仅在 cleanup=true 时有效。该函数远不完美,很快我们会重新编写。

   if(cleanup)
   {
      if(ArraySize(ticketsLong) > ArraySize(ticketsShort))
      {
         retcode = CloseAllPositions(ticketsLong, ArraySize(ticketsShort));
      }
      else if(ArraySize(ticketsLong) < ArraySize(ticketsShort))
      {
         retcode = CloseAllPositions(ticketsShort, ArraySize(ticketsLong));
      }
   }
   
   return retcode;
}

对于所有找到的剩余仓位,通常通过调用 CloseAllPositions 来执行平仓。

uint CloseAllPositions(const ulong &tickets[], const int start = 0)
{
   const int n = ArraySize(tickets);
   Print("CloseAllPositions "n);
   for(int i = start; i < n; ++i)
   {
      MqlTradeRequestSyncLog request;
      request.comment = "close down " + (string)(i + 1 - start)
         + " of " + (string)(n - start);
      if(!(request.close(tickets[i]) && request.completed()))
      {
         Print("Error: position is not closed "tickets[i]);
         return request.result.retcode;
      }
   }
   return 0// success
}

现在我们仅需考虑 RemoveOrders 函数。该函数还使用订单筛选器获取其列表,然后在循环中调用 remove 方法。

uint RemoveOrders()
{
   OrderFilter filter;
   ulong tickets[];
   filter.let(ORDER_SYMBOL_Symbol).let(ORDER_MAGICMagic)
      .select(tickets);
   const int n = ArraySize(tickets);
   for(int i = 0i < n; ++i)
   {
      MqlTradeRequestSyncLog request;
      request.comment = "removal " + (string)(i + 1) + " of " + (string)n;
      if(!(request.remove(tickets[i]) && request.completed()))
      {
         Print("Error: order is not removed "tickets[i]);
         return request.result.retcode;
      }
   }
   return 0;
}

我们检查一下在默认设置下(交易时段从 00:00 到 09:00),EA 交易在测试程序中是如何工作的。以下是在 EURUSD,H1 上发布的屏幕截图。

测试程序中的网格策略 PendingOrderGrid1.mq5

测试程序中的网格策略 PendingOrderGrid1.mq5

在日志中,除了关于批量创建的几个订单(在一天的开始)及其在早上移除的周期性条目之外,我们还会定期看到网络的恢复(添加订单而不是触发的订单)和平仓。

buy stop limit 0.01 EURUSD at 1.14200 (1.14000) (1.13923 / 1.13923)
TRADE_ACTION_PENDING, EURUSD, ORDER_TYPE_BUY_STOP_LIMIT, V=0.01, ORDER_FILLING_FOK, »
   » @ 1.14200, X=1.14000, ORDER_TIME_GTC, M=1234567890, repair
DONE, #=159, V=0.01, Bid=1.13923, Ask=1.13923, Request executed, Req=287
CompactPositions, pairs: 1
close position #152 sell 0.01 EURUSD by position #153 buy 0.01 EURUSD (1.13923 / 1.13923)
deal #18 buy 0.01 EURUSD at 1.13996 done (based on order #160)
deal #19 sell 0.01 EURUSD at 1.14202 done (based on order #160)
Positions collapse initiated
OK CloseBy Order/Deal/Position
TRADE_ACTION_CLOSE_BY, EURUSD, ORDER_TYPE_BUY, ORDER_FILLING_FOK, P=152, b=153, »
   » M=1234567890, compacting
DONE, D=18, #=160, Request executed, Req=288

现在,我们来研究用于处理仓位的 MQL5 函数,并在我们的 EA 交易中改进其选择和分析。下面几节将讨论这个问题。