用于读取仓位特性的函数

根据特性类型,MQL 程序可以使用几个 PositionGet 函数来获取仓位特性。在所有函数中,被请求的特定特性在第一个参数中定义,该参数采用上一节中讨论的ENUM_POSITION_PROPERTY 枚举之一的 ID。

对于每种类型的特性,都存在函数的简短形式和完整形式:第一种形式直接返回特性的值,第二种形式将其写入第二个参数,通过引用传递。

整数型特性及其兼容类型(datetime 和枚举)特性可以通过 PositionGetInteger 函数获得。

long PositionGetInteger(ENUM_POSITION_PROPERTY_INTEGER property)

bool PositionGetInteger(ENUM_POSITION_PROPERTY_INTEGER property, long &value)

如果失败,该函数将返回 0 或 false

PositionGetDouble 函数用于获取实数型特性。

double PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE property)

bool PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE property, double &value)

最后,字符串型特性由 PositionGetString 函数返回。

string PositionGetString(ENUM_POSITION_PROPERTY_STRING property)

bool PositionGetString(ENUM_POSITION_PROPERTY_STRING property, string &value)

如果失败,函数的第一种形式将返回一个空字符串。

为了读取仓位特性,我们已经有了一个现成的摘要界面 MonitorInterface (TradeBaseMonitor.mqh),我们用其来编写订单监视器。现在很容易实现一个类似的仓位监视器。结果附在 PositionMonitor.mqh 文件中。

PositionMonitorInterface 类继承自 MonitorInterface,其用于为所考虑的 ENUM_POSITION_PROPERTY 枚举的模板类型 I、D 和 S 赋值,并且考虑到仓位特性的具体情况,重写了两个 stringify 方法。

class PositionMonitorInterface:
   public MonitorInterface<ENUM_POSITION_PROPERTY_INTEGER,
   ENUM_POSITION_PROPERTY_DOUBLE,ENUM_POSITION_PROPERTY_STRING>
{
public:
   virtual string stringify(const long v,
      const ENUM_POSITION_PROPERTY_INTEGER propertyconst override
   {
      switch(property)
      {
         case POSITION_TYPE:
            return enumstr<ENUM_POSITION_TYPE>(v);
         case POSITION_REASON:
            return enumstr<ENUM_POSITION_REASON>(v);
         
         case POSITION_TIME:
         case POSITION_TIME_UPDATE:
            return TimeToString(vTIME_DATE TIME_SECONDS);
         
         case POSITION_TIME_MSC:
         case POSITION_TIME_UPDATE_MSC:
            return STR_TIME_MSC(v);
      }
      
      return (string)v;
   }
   
   virtual string stringify(const ENUM_POSITION_PROPERTY_DOUBLE property,
      const string format = NULLconst override
   {
      if(format == NULL &&
         (property == POSITION_PRICE_OPEN || property == POSITION_PRICE_CURRENT
         || property == POSITION_SL || property == POSITION_TP))
      {
         const int digits = (int)SymbolInfoInteger(PositionGetString(POSITION_SYMBOL),
            SYMBOL_DIGITS);
         return DoubleToString(PositionGetDouble(property), digits);
      }
      return MonitorInterface<ENUM_POSITION_PROPERTY_INTEGER,
         ENUM_POSITION_PROPERTY_DOUBLE,ENUM_POSITION_PROPERTY_STRING>
         ::stringify(propertyformat);
   }

准备好查看仓位的特定监视器类是继承链中的下一个类,其基于 PositionGet 函数。通过订单号选择仓位是在构造函数中完成的。

class PositionMonitorpublic PositionMonitorInterface
{
public:
   const ulong ticket;
   PositionMonitor(const ulong t): ticket(t)
   {
      if(!PositionSelectByTicket(ticket))
      {
         PrintFormat("Error: PositionSelectByTicket(%lld) failed: %s",
            ticketE2S(_LastError));
      }
      else
      {
         ready = true;
      }
   }
   
   virtual long get(const ENUM_POSITION_PROPERTY_INTEGER propertyconst override
   {
      return PositionGetInteger(property);
   }
   
   virtual double get(const ENUM_POSITION_PROPERTY_DOUBLE propertyconst override
   {
      return PositionGetDouble(property);
   }
   
   virtual string get(const ENUM_POSITION_PROPERTY_STRING propertyconst override
   {
      return PositionGetString(property);
   }
   ...
};

一个简单脚本将允许你记录第一个仓位的所有特性(如果至少有一个可用)。

void OnStart()
{
   PositionMonitor pm(PositionGetTicket(0));
   pm.print();
}

在日志中,我们应会看到此类内容。

MonitorInterface<ENUM_POSITION_PROPERTY_INTEGER, »
   » ENUM_POSITION_PROPERTY_DOUBLE,ENUM_POSITION_PROPERTY_STRING>
ENUM_POSITION_PROPERTY_INTEGER Count=9
  0 POSITION_TIME=2022.03.24 23:09:45
  1 POSITION_TYPE=POSITION_TYPE_BUY
  2 POSITION_MAGIC=0
  3 POSITION_IDENTIFIER=1291755067
  4 POSITION_TIME_MSC=2022.03.24 23:09:45'261
  5 POSITION_TIME_UPDATE=2022.03.24 23:09:45
  6 POSITION_TIME_UPDATE_MSC=2022.03.24 23:09:45'261
  7 POSITION_TICKET=1291755067
  8 POSITION_REASON=POSITION_REASON_EXPERT
ENUM_POSITION_PROPERTY_DOUBLE Count=8
  0 POSITION_VOLUME=0.01
  1 POSITION_PRICE_OPEN=1.09977
  2 POSITION_PRICE_CURRENT=1.09965
  3 POSITION_SL=0.00000
  4 POSITION_TP=1.10500
  5 POSITION_COMMISSION=0.0
  6 POSITION_SWAP=0.0
  7 POSITION_PROFIT=-0.12
ENUM_POSITION_PROPERTY_STRING Count=3
  0 POSITION_SYMBOL=EURUSD
  1 POSITION_COMMENT=
  2 POSITION_EXTERNAL_ID=

如果目前没有未结仓位,我们会看到一条错误消息。

Error: PositionSelectByTicket(0) failed: TRADE_POSITION_NOT_FOUND

但是,监视器的用处不仅仅在于将特性输出到日志中。基于 PositionMonitor,我们创建了一个按条件选择仓位的类,类似于我们对订单 (OrderFilter) 所创建的。最终目标是为了改进我们的网格 EA 交易。

有了 OOP,创建新的筛选器类就变得非常简单。以下是完整的源代码(文件 PositionFilter.mqh)。

class PositionFilterpublic TradeFilter<PositionMonitor,
   ENUM_POSITION_PROPERTY_INTEGER,
   ENUM_POSITION_PROPERTY_DOUBLE,
   ENUM_POSITION_PROPERTY_STRING>
{
protected:
   virtual int total() const override
   {
      return PositionsTotal();
   }
   virtual ulong get(const int iconst override
   {
      return PositionGetTicket(i);
   }
};

例如,现在我们可以编写这样一个脚本,在给定魔术编号的仓位上获得特定的盈利。

input ulong Magic;
   
void OnStart()
{
   PositionFilter filter;
   
   ENUM_POSITION_PROPERTY_DOUBLE properties[] =
      {POSITION_PROFITPOSITION_VOLUME};
   
   double profits[][2];
   ulong tickets[];
   string symbols[];
   
   filter.let(POSITION_MAGICMagic).select(propertiesticketsprofits);
   filter.select(POSITION_SYMBOLticketssymbols);
   
   for(int i = 0i < ArraySize(symbols); ++i)
   {
      PrintFormat("%s[%lld]=%f",
         symbols[i], tickets[i], profits[i][0] / profits[i][1]);
   }
}

在这种情况下,我们必须调用 select 方法两次,因为我们感兴趣的特性类型是不同的:真实盈利和手数以及工具的字符串名称。在本章开头的其中一节中,当我们开发交易品种的筛选器类时,对 元组的概念进行了说明。在 MQL5 中,我们可以将其实现为具有任意类型字段的结构体模板。这种元组对于最终确定筛选器类的层次结构体非常方便,因为这样就能说明用任何类型的字段填充元组数组的 select 方法。

元组在 Tuples.mqh 文件中说明。其中的所有结构体都具有名称 TupleN<T1,...>,其中 N 为 2 到 8 之间的数字,并且对应于模板参数(Ti 类型)的数量。例如,Tuple2

template<typename T1,typename T2>
struct Tuple2
{
   T1 _1;
   T2 _2;
   
   static int size() { return 2; };
   
   // M – order, position, deal monitor class, any MonitorInterface<>
   template<typename M>
   void assign(const int &properties[], M &m)
   {
      if(ArraySize(properties) != size()) return;
      _1 = m.get(properties[0], _1);
      _2 = m.get(properties[1], _2);
   }
};

TradeFilter (TradeFilter.mqh) 类中,我们添加一个带有元组的 select 函数版本。

template<typename T,typename I,typename D,typename S>
class TradeFilter
{
   ...
 template<typename U// type U must be Tuple<>, e.g. Tuple3<T1,T2,T3>
   bool select(const int &property[], U &data[], const bool sort = falseconst
   {
      const int q = ArraySize(property);
      static const U u;                 // PRB: U::size() does not compile
      if(q != u.size()) return false;   // required condition
      
      const int n = total();
      // cycle through orders/positions/deals
      for(int i = 0i < n; ++i)
      {
         const ulong t = get(i);
         // access to properties via monitor T
         T m(t);
         // check all filter conditions for different types of properties
         if(match(mlongs)
         && match(mdoubles)
         && match(mstrings))
         {
            // for a suitable object, store the properties in an array of tuples
            const int k = EXPAND(data);
            data[k].assign(propertym);
         }
      }
      
      if(sort)
      {
         sortTuple(datau._1);
      }
      
      return true;
   }

元组数组可以根据第一个 field_1 进行排序,因此你可以另外研究 sortTuple 辅助方法。

使用元组,你可以在一次 select 调用中查询一个筛选对象的三种不同类型的特性。

下面有一些显示 Magic 编号的仓位,对于每个额外获得一个交易品种和订单号,按盈利排序。

 input ulong Magic;
   
   void OnStart()
   {
      int props[] = {POSITION_PROFITPOSITION_SYMBOLPOSITION_TICKET};
      Tuple3<double,string,ulongtuples[];
      PositionFilter filter;
      filter.let(POSITION_MAGICMagic).select(propstuplestrue);
      ArrayPrint(tuples);
   }

当然,元组数组(本例中为 Tuple3<double,string,ulong>)说明中的参数类型必须与请求的特性枚举类型 (POSITION_PROFIT, POSITION_SYMBOL, POSITION_TICKET) 相匹配。

现在我们可以稍微简化一下网格 EA 交易(这不仅意味着代码更短,而且更容易理解)。新版本名为 PendingOrderGrid2.mq5。这些变化将会影响与仓位管理相关的所有函数。

GetMyPositions 函数用于填充通过引用传递的元组的 types4tickets 数组。在每个 Tuple2 元组中,应存储仓位的类型和标签。在这种特殊情况下,我们可以只用一个二维数组 ulong 来代替元组,因为两个特性都是相同的基础类型。但是,我们使用元组来演示如何在调用代码中使用它们。

#include <MQL5Book/Tuples.mqh>
#include <MQL5Book/PositionFilter.mqh>
   
int GetMyPositions(const string sconst ulong m,
   Tuple2<ulong,ulong> &types4tickets[])
{
   int props[] = {POSITION_TYPEPOSITION_TICKET};
   PositionFilter filter;
   filter.let(POSITION_SYMBOLs).let(POSITION_MAGICm)
      .select(propstypes4ticketstrue);
   return ArraySize(types4tickets);
}

注意,select 方法的最后一个参数(第三个)等于 true,它指示按照第一个字段(即仓位类型)对数组进行排序。因此,我们将在开始时买入,在结束时卖出。这是对冲平仓所必需的。

CompactPositions 方法的循环如下。

uint CompactPositions(const bool cleanup = false)
{
   uint retcode = 0;
   Tuple2<ulong,ulongtypes4tickets[];
   int i = 0j = 0;
   int n = GetMyPositions(_SymbolMagictypes4tickets);
   if(n > 0)
   {
      Print("CompactPositions: "n);
      for(i = 0j = n - 1i < j; ++i, --j)
      {
         if(types4tickets[i]._1 != types4tickets[j]._1// as long as the types are different
         {
            retcode = CloseByPosition(types4tickets[i]._2types4tickets[j]._2);
            if(retcodereturn retcode// error
         }
         else
         {
            break;
         }
      }
   }
   
   if(cleanup && j < n)
   {
      retcode = CloseAllPositions(types4ticketsij + 1);
   }
   
   return retcode;
}

CloseAllPositions 函数几乎相同:

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

你可以在测试程序中比较 EA 交易 PendingOrderGrid1.mq5PendingOrderGrid2.mq5 的效果。

报告将略有不同,因为如果有几个仓位,它们会以相反的组合平仓,因此其他不成对的仓位平仓是相对于它们各自的点差进行的。