用于从历史中读取交易特性的函数

为了读取交易特性,函数组按 特性类型组织,具体分为整数型、实数型和字符串型。在调用函数之前,你需要选择所需的 历史时期 ,从而确保所有函数的第一个参数 (ticket) 中传递的订单号交易的可用性。

每种类型的特性都有两种形式:直接返回值和通过引用写入变量。第二种形式返回 true 表示成功。第一种形式仅在出错时返回 0。错误代码在 _LastError 变量中。

整数型及其兼容的特性类型(datetime 和枚举)可以使用 HistoryDealGetInteger 函数获得。

long HistoryDealGetInteger(ulong ticket, ENUM_DEAL_PROPERTY_INTEGER property)

bool HistoryDealGetInteger(ulong ticket, ENUM_DEAL_PROPERTY_INTEGER property,
long &value)

实数型特性由 HistoryDealGetDouble 函数读取。

double HistoryDealGetDouble(ulong ticket, ENUM_DEAL_PROPERTY_DOUBLE property)

bool HistoryDealGetDouble(ulong ticket, ENUM_DEAL_PROPERTY_DOUBLE property,
double &value)

对于字符串型特性,提供了 HistoryDealGetString 函数。

string HistoryDealGetString(ulong ticket, ENUM_DEAL_PROPERTY_STRING property)

bool HistoryDealGetString(ulong ticket, ENUM_DEAL_PROPERTY_STRING property,
string &value)

DealMonitor 类 (DealMonitor.mqh) 可提供交易特性的统一读数,其组织方式与OrderMonitorPositionMonitor 完全相同。基础类为 DealMonitorInterface,继承自模板 MonitorInterface(我们在 读取活动订单特性的函数一节中进行说明)。正是在这个水平,ENUM_DEAL_PROPERTY 枚举的特定类型被指定为模板参数和stringify 方法的特定实现。

#include <MQL5Book/TradeBaseMonitor.mqh>
   
class DealMonitorInterface:
   public MonitorInterface<ENUM_DEAL_PROPERTY_INTEGER,
   ENUM_DEAL_PROPERTY_DOUBLE,ENUM_DEAL_PROPERTY_STRING>
{
public:
   // property descriptions taking into account integer subtypes
   virtual string stringify(const long v,
      const ENUM_DEAL_PROPERTY_INTEGER propertyconst override
   {
      switch(property)
      {
         case DEAL_TYPE:
            return enumstr<ENUM_DEAL_TYPE>(v);
         case DEAL_ENTRY:
            return enumstr<ENUM_DEAL_ENTRY>(v);
         case DEAL_REASON:
            return enumstr<ENUM_DEAL_REASON>(v);
         
         case DEAL_TIME:
            return TimeToString(vTIME_DATE TIME_SECONDS);
         
         case DEAL_TIME_MSC:
            return STR_TIME_MSC(v);
      }
      
      return (string)v;
   }
};

下面的 DealMonitor 类有点类似于最近为了处理历史数据而修改的 OrderMonitor 类。除了应用 HistoryDeal 函数而不是 HistoryOrder 函数之外,应注意的是,对于交易,不需要在在线环境中检查订单号,因为交易只存在于历史中。

class DealMonitorpublic DealMonitorInterface
{
   bool historyDealSelectWeak(const ulong tconst
   {
      return ((HistoryDealGetInteger(tDEAL_TICKET) == t) ||
         (HistorySelect(0LONG_MAX) && (HistoryDealGetInteger(tDEAL_TICKET) == t)));
   }
public:
   const ulong ticket;
   DealMonitor(const long t): ticket(t)
   {
      if(!historyDealSelectWeak(ticket))
      {
         PrintFormat("Error: HistoryDealSelect(%lld) failed"ticket);
      }
      else
      {
         ready = true;
      }
   }
   
   virtual long get(const ENUM_DEAL_PROPERTY_INTEGER propertyconst override
   {
      return HistoryDealGetInteger(ticketproperty);
   }
   
   virtual double get(const ENUM_DEAL_PROPERTY_DOUBLE propertyconst override
   {
      return HistoryDealGetDouble(ticketproperty);
   }
   
   virtual string get(const ENUM_DEAL_PROPERTY_STRING propertyconst override
   {
      return HistoryDealGetString(ticketproperty);
   }
   ...
};

基于 DealMonitorTradeFilter,很容易创建交易筛选器 (DealFilter.mqh)。注意,TradeFilter 作为许多实体的基础类,在 按特性选择订单一节中进行了说明。

#include <MQL5Book/DealMonitor.mqh>
#include <MQL5Book/TradeFilter.mqh>
   
class DealFilterpublic TradeFilter<DealMonitor,
   ENUM_DEAL_PROPERTY_INTEGER,
   ENUM_DEAL_PROPERTY_DOUBLE,
   ENUM_DEAL_PROPERTY_STRING>
{
protected:
   virtual int total() const override
   {
      return HistoryDealsTotal();
   }
   virtual ulong get(const int iconst override
   {
      return HistoryDealGetTicket(i);
   }
};

作为使用历史的通用示例,我们来考虑仓位历史恢复脚本 TradeHistoryPrint.mq5

TradeHistoryPrint

该脚本可为当前图表交易品种构建历史。

我们首先需要交易和订单的筛选器。

#include <MQL5Book/OrderFilter.mqh>
#include <MQL5Book/DealFilter.mqh>

我们将从交易中提取仓位 ID,并基于仓位 ID 来请求订单的详细信息。

可以查看整个历史,也可以查看特定仓位的历史,为此,我们将提供一个模式选择和一个输入字段,用作输入变量中的标识符。

enum SELECTOR_TYPE
{
   TOTAL,    // Whole history
   POSITION// Position ID
};
   
input SELECTOR_TYPE Type = TOTAL;
input ulong PositionID = 0// Position ID

必须要记住,对久远的账户历史进行采样可能是一种开销,因此我们希望在处理的 EA 交易中提供对所获得的历史处理结果以及最后处理时间戳的缓存。后续每次分析历史订单时,可以不从头开始,而是从一个记忆中的时刻开始这个过程。

为了以视觉上吸引人的方式显示以列对齐的历史信息,将其表示为一个结构数组是有意义的。但是,我们的筛选器已经支持查询存储在特殊结构中的数据,也就是元组。因此,我们可应用一个技巧:描述我们的应用程序结构体,并观察元组的规则:

  • 第一个字段的名称必须为 _1;该名称可在排序算法中选择性使用。
  • 返回字段数量的 size 函数必须在结构体中说明。
  • 该结构体应具有一个模板方法 assign,用于根据 MonitorInterface 派生的已传递监视器对象的特性填充字段。

在标准元组中,assign 方法的说明如下:

   template<typename M
   void assign(const int &properties[], M &m);

作为第一个参数,可接收一个数组,其特性 ID 对应于我们关注的字段。事实上,这是由调用代码传递给筛选器的 select 方法 (TradeFilter::select) 的数组,然后通过引用传递给 assign。但是,由于我们现在创建的不是一些标准元组,而是我们自己的结构体,这些结构“知道”其字段的应用特性,我们可以将带有特性标识符的数组留在结构本身内,而不是在筛选器与同一结构体的 assign 方法之间来回传递。

具体来说,为了请求交易,我们用 8 个字段对 DealTuple 结构体进行了说明。其标识符将在 fields 静态数组中指定。

struct DealTuple
{
   datetime _1;   // deal time
   ulong deal;    // deal ticket
   ulong order;   // order ticket
   string type;   // ENUM_DEAL_TYPE as string 
   string in_out// ENUM_DEAL_ENTRY as string 
   double volume;
   double price;
   double profit;
   
   static int size() { return 8; }; // number of properties 
   static const int fields[]; // identifiers of the requested deal properties
   ...
};
   
static const int DealTuple::fields[] =
{
   DEAL_TIMEDEAL_TICKETDEAL_ORDERDEAL_TYPE,
   DEAL_ENTRYDEAL_VOLUMEDEAL_PRICEDEAL_PROFIT
};

这种方法将标识符和字段放在一起,将相应的值存储在一个地方,这样更容易理解和维护源代码。

用特性值填充字段需要稍微修改(简化)一下 assign 方法,该方法用于从 fields 数组(而不是从输入参数)获取 ID。

struct DealTuple
{
   ...
   template<typename M// M is derived from MonitorInterface<>
   void assign(M &m)
   {
      static const int DEAL_TYPE_ = StringLen("DEAL_TYPE_");
      static const int DEAL_ENTRY_ = StringLen("DEAL_ENTRY_");
      static const ulong L = 0// default type declaration (dummy)
      
      _1 = (datetime)m.get(fields[0], L);
      deal = m.get(fields[1], deal);
      order = m.get(fields[2], order);
      const ENUM_DEAL_TYPE t = (ENUM_DEAL_TYPE)m.get(fields[3], L);
      type = StringSubstr(EnumToString(t), DEAL_TYPE_);
      const ENUM_DEAL_ENTRY e = (ENUM_DEAL_ENTRY)m.get(fields[4], L);
      in_out = StringSubstr(EnumToString(e), DEAL_ENTRY_);
      volume = m.get(fields[5], volume);
      price = m.get(fields[6], price);
      profit = m.get(fields[7], profit);
   }
};

同时,我们将 ENUM_DEAL_TYPE 和 ENUM_DEAL_ENTRY 枚举的数字元素转换为用户友好的字符串。当然,这只在日志记录时需要。对于编程分析,这些类型应保持原样。

因为我们已经在其元组中开发了一个新版本的 assign 方法,所以你需要在 TradeFilter 类中为其添加一个新版本的 select 方法。这项创新肯定会对其他程序有用,因此我们可将其直接引入 TradeFilter,而不是引入某个新的派生类。

template<typename T,typename I,typename D,typename S>
class TradeFilter
{
   ...
   template<typename U// U must have first field _1 and method assign(T)
   bool select(U &data[], const bool sort = falseconst
   {
      const int n = total();
      // loop through the elements
      for(int i = 0i < n; ++i)
      {
         const ulong t = get(i);
         // read properties through the monitor object
         T m(t);
         // check all filtering conditions
         if(match(mlongs)
         && match(mdoubles)
         && match(mstrings))
         {
            // for a suitable object, add its properties to an array
            const int k = EXPAND(data);
            data[k].assign(m);
         }
      }
      
      if(sort)
      {
         static const U u;
         sortTuple(datau._1);
      }
      
      return true;
   }

注意,在用特定类型的代码调用模板方法之前,编译器并不会实现所有模板方法。因此,TradeFilter 中存在这种模式并不意味着必须包含任何元组头文件或必须描述类似的结构(除非你要使用)。

因此,如果在更早的时候,要使用标准元组选择交易,我们必须这样编写:

#include <MQL5Book/Tuples.mqh>
...
DealFilter filter;
int properties[] =
{
   DEAL_TIMEDEAL_TICKETDEAL_ORDERDEAL_TYPE,
   DEAL_ENTRYDEAL_VOLUMEDEAL_PRICEDEAL_PROFIT
};
Tuple8<ulong,ulong,ulong,ulong,ulong,double,double,doubletuples[];
filter.let(DEAL_SYMBOL_Symbol).select(propertiestuples);

有了定制的结构体,一切都简单多了:

DealFilter filter;
DealTuple tuples[];
filter.let(DEAL_SYMBOL_Symbol).select(tuples);

类似于 DealTuple 结构体,我们来描述一下 OrderTuple 的 10 字段结构体。

struct OrderTuple
{
   ulong _1;       // ticket (also used as 'ulong' prototype)
   datetime setup;
   datetime done;
   string type;
   double volume;
   double open;
   double current;
   double sl;
   double tp;
   string comment;
   
   static int size() { return 10; }; // number of properties
   static const int fields[]; // identifiers of requested order properties
   
   template<typename M// M is derived from MonitorInterface<>
   void assign(M &m)
   {
      static const int ORDER_TYPE_ = StringLen("ORDER_TYPE_");
      
      _1 = m.get(fields[0], _1);
      setup = (datetime)m.get(fields[1], _1);
      done = (datetime)m.get(fields[2], _1);
      const ENUM_ORDER_TYPE t = (ENUM_ORDER_TYPE)m.get(fields[3], _1);
      type = StringSubstr(EnumToString(t), ORDER_TYPE_);
      volume = m.get(fields[4], volume);
      open = m.get(fields[5], open);
      current = m.get(fields[6], current);
      sl = m.get(fields[7], sl);
      tp = m.get(fields[8], tp);
      comment = m.get(fields[9], comment);
   }
};
   
static const int OrderTuple::fields[] =
{
   ORDER_TICKETORDER_TIME_SETUPORDER_TIME_DONEORDER_TYPEORDER_VOLUME_INITIAL,
   ORDER_PRICE_OPENORDER_PRICE_CURRENTORDER_SLORDER_TPORDER_COMMENT
};

现在,实现脚本的主要函数 – OnStart 准备就绪。首先,我们将描述交易和订单筛选器的对象。

void OnStart()
{
   DealFilter filter;
   HistoryOrderFilter subfilter;
   ...

根据输入变量,我们可选择整个历史或特定仓位。

   if(PositionID == 0 || Type == TOTAL)
   {
      HistorySelect(0LONG_MAX);
   }
   else if(Type == POSITION)
   {
      HistorySelectByPosition(PositionID);
   }
   ...

接下来,我们将在一个数组中收集所有仓位标识符,或者保留一个由用户指定的标识符。

   ulong positions[];
   if(PositionID == 0)
   {
      ulong tickets[];
      filter.let(DEAL_SYMBOL_Symbol)
         .select(DEAL_POSITION_IDticketspositionstrue); // true - sorting
      ArrayUnique(positions);
   }
   else
   {
      PUSH(positionsPositionID);
   }
   
   const int n = ArraySize(positions);
   Print("Positions total: "n);
   if(n == 0return;
   ...

辅助函数 ArrayUnique 可在数组中留下不重复的元素。前提是对源数组进行排序。

此外,在遍历仓位过程中,我们可请求与每个仓位相关的交易和订单。交易按 DealTuple 结构体的第一个字段进行排序,即按时间排序。也许最有趣的是计算仓位的盈利/亏损。为此,我们对所有交易的 profit 字段的值进行求和。

   for(int i = 0i < n; ++i)
   {
      DealTuple deals[];
      filter.let(DEAL_POSITION_IDpositions[i]).select(dealstrue);
      const int m = ArraySize(deals);
      if(m == 0)
      {
         Print("Wrong position ID: "positions[i]);
         break// invalid id set by user
      }
      double profit = 0// TODO: need to take into account commissions, swaps and fees
      for(int j = 0j < m; ++jprofit += deals[j].profit;
      PrintFormat("Position: % 8d %16lld Profit:%f"i + 1positions[i], (profit));
      ArrayPrint(deals);
      
      Print("Order details:");
      OrderTuple orders[];
      subfilter.let(ORDER_POSITION_IDpositions[i], IS::OR_EQUAL)
         .let(ORDER_POSITION_BY_IDpositions[i], IS::OR_EQUAL)
         .select(orders);
      ArrayPrint(orders);
   }
}

此代码不会分析交易特性中的佣金 (DEAL_COMMISSION)、掉期 (DEAL_SWAP) 和费用 (DEAL_FEE)。在真正的 EA 交易中,可能需要进行这些分析(取决于策略的需求)。我们将在 测试多币种 EA 交易一节中看到交易历史分析的另一个示例,到时我们会考虑这一要求。

你可以将脚本的结果与终端中的历史选项卡上的表进行比较:其“盈利”列显示了每个仓位的净利润(掉期、佣金和费用在相邻的列内,但需要手动将其包括在内)。

务必要注意的是,仅当在设置中选择了整个历史时,ORDER_TYPE_CLOSE_BY 类型的订单才会在两个仓位中显示。如果选择了一个特定的仓位,系统只会将这样的订单计入其中一个仓位(在交易请求中首先指定的仓位,在 position 字段内),而不会计入第二个仓位(在 position_by 中指定)。

以下是一个历史较短的交易品种的脚本结果示例。

Positions total: 3

Position: 1 1253500309 Profit:238.150000

[_1] [deal] [order] [type] [in_out] [volume] [price] [profit]

[0] 2022.02.04 17:34:57 1236049891 1253500309 "BUY" "IN" 1.00000 76.23900 0.00000

[1] 2022.02.14 16:28:41 1242295527 1259788704 "SELL" "OUT" 1.00000 76.42100 238.15000

Order details:

[_1] [setup] [done] [type] [volume] [open] [current] »

» [sl] [tp] [comment]

[0] 1253500309 2022.02.04 17:34:57 2022.02.04 17:34:57 "BUY" 1.00000 76.23900 76.23900 »

» 0.00 0.00 ""

[1] 1259788704 2022.02.14 16:28:41 2022.02.14 16:28:41 "SELL" 1.00000 76.42100 76.42100 »

» 0.00 0.00 ""

Position: 2 1253526613 Profit:878.030000

[_1] [deal] [order] [type] [in_out] [volume] [price] [profit]

[0] 2022.02.07 10:00:00 1236611994 1253526613 "BUY" "IN" 1.00000 75.75000 0.00000

[1] 2022.02.14 16:28:40 1242295517 1259788693 "SELL" "OUT" 1.00000 76.42100 878.03000

Order details:

[_1] [setup] [done] [type] [volume] [open] [current] »

» [sl] [tp] [comment]

[0] 1253526613 2022.02.04 17:55:18 2022.02.07 10:00:00 "BUY_LIMIT" 1.00000 75.75000 75.67000 »

» 0.00 0.00 ""

[1] 1259788693 2022.02.14 16:28:40 2022.02.14 16:28:40 "SELL" 1.00000 76.42100 76.42100 »

» 0.00 0.00 ""

Position: 3 1256280710 Profit:4449.040000

[_1] [deal] [order] [type] [in_out] [volume] [price] [profit]

[0] 2022.02.09 13:17:52 1238797056 1256280710 "BUY" "IN" 2.00000 74.72100 0.00000

[1] 2022.02.14 16:28:39 1242295509 1259788685 "SELL" "OUT" 2.00000 76.42100 4449.04000

Order details:

[_1] [setup] [done] [type] [volume] [open] [current] »

» [sl] [tp] [comment]

[0] 1256280710 2022.02.09 13:17:52 2022.02.09 13:17:52 "BUY" 2.00000 74.72100 74.72100 »

» 0.00 0.00 ""

[1] 1259788685 2022.02.14 16:28:39 2022.02.14 16:28:39 "SELL" 2.00000 76.42100 76.42100 »

» 0.00 0.00 ""

在净额结算账户上增加仓位(两笔“IN”交易)及其冲销(一笔较大数量的“INOUT”交易)的情况如以下片段所示。

Position: 5 219087383 Profit:0.170000

[_1] [deal] [order] [type] [in_out] [volume] [price] [profit]

[0] 2022.03.29 08:03:33 215612450 219087383 "BUY" "IN" 0.01000 1.10011 0.00000

[1] 2022.03.29 8:04:05 215612451 219087393 "BUY" "IN" 0.01000 1.10009 0.00000

[2] 2022.03.29 08:04:29 215612457 219087400 "SELL" "INOUT" 0.03000 1.10018 0.16000

[3] 2022.03.29 08:04:34 215612460 219087403 "BUY" "OUT" 0.01000 1.10017 0.01000

Order details:

[_1] [setup] [done] [type] [volume] [open] [current] »

» [sl] [tp] [comment]

[0] 219087383 2022.03.29 08:03:33 2022.03.29 08:03:33 "BUY" 0.01000 0.0000 1.10011 »

» 0.00 0.00 ""

[1] 219087393 2022.03.29 8:04:05 2022.03.29 8:04:05 "BUY" 0.01000 0.0000 1.10009 »

» 0.00 0.00 ""

[2] 219087400 2022.03.29 08:04:29 2022.03.29 08:04:29 "SELL" 0.03000 0.0000 1.10018 »

» 0.00 0.00 ""

[3] 219087403 2022.03.29 8:04:34 2022.03.29 8:04:34 "BUY" 0.01000 0.0000 1.10017 »

» 0.00 0.00 ""

我们将考虑部分历史,以对冲账户反向平仓的特定仓位为例。首先可以单独查看第一个仓位:PositionID=1276109280。无论 Type 输入参数如何,该仓位都将完整显示。

Positions total: 1

Position: 1 1276109280 Profit:-0.040000

[_1] [deal] [order] [type] [in_out] [volume] [price] [profit]

[0] 2022.03.07 12:20:53 1258725455 1276109280 "BUY" "IN" 0.01000 1.08344 0.00000

[1] 2022.03.07 12:20:58 1258725503 1276109328 "SELL" "OUT_BY" 0.01000 1.08340 -0.04000

Order details:

[_1] [setup] [done] [type] [volume] [open] [current] »

» [sl] [tp] [comment]

[0] 1276109280 2022.03.07 12:20:53 2022.03.07 12:20:53 "BUY" 0.01000 1.08344 1.08344 »

» 0.00 0.00 ""

[1] 1276109328 2022.03.07 12:20:58 2022.03.07 12:20:58 "CLOSE_BY" 0.01000 1.08340 1.08340 »

» 0.00 0.00 "#1276109280 by #1276109283"

还可以看到第二个仓位:PositionID=1276109283。但是,如果 Type 等于 "position",为了选择历史的片段,可使用 HistorySelectByPosition 函数,结果是只有一个退出订单(尽管事实上有两个交易)。

Positions total: 1

Position: 1 1276109283 Profit:0.000000

[_1] [deal] [order] [type] [in_out] [volume] [price] [profit]

[0] 2022.03.07 12:20:53 1258725458 1276109283 "SELL" "IN" 0.01000 1.08340 0.00000

[1] 2022.03.07 12:20:58 1258725504 1276109328 "BUY" "OUT_BY" 0.01000 1.08344 0.00000

Order details:

[_1] [setup] [done] [type] [volume] [open] [current] »

» [sl] [tp] [comment]

[0] 1276109283 2022.03.07 12:20:53 2022.03.07 12:20:53 "SELL" 0.01000 1.08340 1.08340 »

» 0.00 0.00 ""

如果我们将 Type 设置为“整个历史”,将会出现一个 "CLOSE_BY" 订单。

Positions total: 1

Position: 1 1276109283 Profit:0.000000

[_1] [deal] [order] [type] [in_out] [volume] [price] [profit]

[0] 2022.03.07 12:20:53 1258725458 1276109283 "SELL" "IN" 0.01000 1.08340 0.00000

[1] 2022.03.07 12:20:58 1258725504 1276109328 "BUY" "OUT_BY" 0.01000 1.08344 0.00000

Order details:

[_1] [setup] [done] [type] [volume] [open] [current] »

» [sl] [tp] [comment]

[0] 1276109283 2022.03.07 12:20:53 2022.03.07 12:20:53 "SELL" 0.01000 1.08340 1.08340 »

» 0.00 0.00 ""

[1] 1276109328 2022.03.07 12:20:58 2022.03.07 12:20:58 "CLOSE_BY" 0.01000 1.08340 1.08340 »

» 0.00 0.00 "#1276109280 by #1276109283"

使用这样的设置,可完整选择历史,但是筛选器只留下那些在 ORDER_POSITION_ID 或 ORDER_POSITION_BY_ID 特性中找到指定仓位标识符的订单。为了用逻辑 OR 构成条件,在 TradeFilter 类中添加了 IS::OR_EQUAL 元素。你可以另外研究一下。