监测交易环境变化

在前面关于 OnTrade 事件的章节中,我们提到一些交易策略编程方法可能需要你生成环境的快照,并随着时间的推移将它们相互比较。这是使用 OnTrade 时的一种常见做法,但也可以按时间表、每根柱线甚至分时报价激活。我们的监视器类虽然可以读取订单、交易和仓位的特性,但不具备保存状态的能力。在本节中,我们将介绍一种交易环境缓存选项。

所有交易对象的特性按类型分为三组:整数型、实数型和字符串型。每个对象类都有自己的组(例如,对于订单,整数型特性在 ENUM_ORDER_PROPERTY_INTEGER 枚举中描述,对于仓位,其特性在 ENUM_POSITION_PROPERTY_INTEGER 中描述),但分组的本质是相同的。因此,我们将引入 PROP_TYPE 枚举,借助该枚举,可以描述一个对象特性属于哪种类型。由于存储和处理相同类型特性的机制应是相同的,无论特性是订单的、仓位的还是交易的,所以这种通用性是自然而然出现的。

enum PROP_TYPE
{
   PROP_TYPE_INTEGER,
   PROP_TYPE_DOUBLE,
   PROP_TYPE_STRING,
};

数组是存储特性值的最简单方式。显然,由于存在三种基础类型,我们将需要三个不同的数组。让我们在 MonitorInterface (TradeBaseMonitor.mqh) 中嵌套的新类 TradeState 中进行说明。

基础模板 MonitorInterface<I,D,S> 构成了所有应用监视器类(OrderMonitorDealMonitorPositionMonitor)的基础。此处的类型 I、D 和 S 对应于整数、实数和字符串型特性的具体枚举。

在基础监视器中包含存储机制是非常合理的,特别是因为创建的特性缓存应通过从监视器对象读取特性来填充数据。

template<typename I,typename D,typename S>
class MonitorInterface
{
   ...
   class TradeState
   {
   public:
      ...
      long ulongs[];
      double doubles[];
      string strings[];
      const MonitorInterface *owner;
      
      TradeState(const MonitorInterface *ptr) : owner(ptr)
      {
         ...
      }
   };

整个 TradeState 类已被公开,因为其字段需要从父监视器对象(作为指针传递给构造函数)中访问,而且 TradeState 仅在监视器的受保护部分使用(不能从外部访问)。

为了用三种不同类型的特性值填充三个数组,必须首先找出每个特定数组中按类型和索引的特性分布。

对于每个交易对象类型(订单、交易和仓位),具有不同类型特性的 3 个对应枚举标识符不相交,并形成连续编号。我们来演示一下。

枚举 一章中,我们考虑了脚本 ConversionEnum.mq5,其实现了 process 函数来记录特定枚举的所有元素。该脚本检查了 ENUM_APPLIED_PRICE 枚举。现在我们可以创建该脚本的副本,并分析其他三个枚举。例如,如下所示:

void OnStart()
{
   process((ENUM_POSITION_PROPERTY_INTEGER)0);
   process((ENUM_POSITION_PROPERTY_DOUBLE)0);
   process((ENUM_POSITION_PROPERTY_STRING)0);
}

执行该脚本后,我们可得到以下日志。左边的列包含枚举内部的编号,右边的值(在 “=” 号之后)为元素的内置常量(标识符)。

ENUM_POSITION_PROPERTY_INTEGER Count=9
0 POSITION_TIME=1
1 POSITION_TYPE=2
2 POSITION_MAGIC=12
3 POSITION_IDENTIFIER=13
4 POSITION_TIME_MSC=14
5 POSITION_TIME_UPDATE=15
6 POSITION_TIME_UPDATE_MSC=16
7 POSITION_TICKET=17
8 POSITION_REASON=18
ENUM_POSITION_PROPERTY_DOUBLE Count=8
0 POSITION_VOLUME=3
1 POSITION_PRICE_OPEN=4
2 POSITION_PRICE_CURRENT=5
3 POSITION_SL=6
4 POSITION_TP=7
5 POSITION_COMMISSION=8
6 POSITION_SWAP=9
7 POSITION_PROFIT=10
ENUM_POSITION_PROPERTY_STRING Count=3
0 POSITION_SYMBOL=0
1 POSITION_COMMENT=11
2 POSITION_EXTERNAL_ID=19

例如,常量为 0 的特性为字符串型 POSITION_SYMBOL,常量为 1 和 2 的特性为整数型 POSITION_TIME 和 POSITION_TYPE,常量为 3 的特性为实数型 POSITION_VOLUME 等。

因此,常量是所有类型特性的端到端索引系统,我们可以使用相同的算法(基于 EnumToArray.mqh)来获取它们。

对于每个特性,需要记住其类型(这决定了三个数组中的哪一个应存储该值)和同一类型特性中的序列号(这是相应数组中元素的索引)。例如,我们看到仓位只有 3 个字符串型特性,因此一个仓位快照中的 strings 数组必须具有相同的大小,POSITION_SYMBOL (0)、POSITION_COMMENT (11) 和 POSITION_EXTERNAL_ID (19) 将被写入其索引 0、1 和 2。

特性的端到端索引到其类型(PROP_TYPE 之一)以及到相应类型数组中序数的转换可以在程序开始时完成一次,因为具有特性的枚举为常量(内置于系统中)。我们可将得到的间接寻址表写入一个静态二维 indices 数组。其第一维度的大小动态确定为特性总数(所有 3 种类型)。我们把其大小写入 limit 静态变量。为第二维度分配了几个单元:indices[i][0] – 类型为 PROP_TYPE、indices[i][1]ulongsdoublesstrings 数组之一的索引 (具体取决于 indices[i][0])。

   class TradeState
   {
      ...
      static int indices[][2];
      static int jds;
   public:
      const static int limit;
      
      static PROP_TYPE type(const int i)
      {
         return (PROP_TYPE)indices[i][0];
      }
      
      static int offset(const int i)
      {
         return indices[i][1];
      }
      ...

变量 jds 用于按顺序对 3 种不同类型的特性建立索引。以下是其在静态方法 calcIndices 中完成的方式。

      static int calcIndices()
      {
         const int size = fmax(boundary<I>(),
            fmax(boundary<D>(), boundary<S>())) + 1;
         ArrayResize(indicessize);
         j = d = s = 0;
         for(int i = 0i < size; ++i)
         {
            if(detect<I>(i))
            {
               indices[i][0] = PROP_TYPE_INTEGER;
               indices[i][1] = j++;
            }
            else if(detect<D>(i))
            {
               indices[i][0] = PROP_TYPE_DOUBLE;
               indices[i][1] = d++;
            }
            else if(detect<S>(i))
            {
               indices[i][0] = PROP_TYPE_STRING;
               indices[i][1] = s++;
            }
            else
            {
               Print("Unresolved int value as enum: "i" "typename(TradeState));
            }
         }
         return size;
      }

boundary 方法可返回给定枚举 E 的所有元素中的最大常量。

   template<typename E>
   static int boundary(const E dummy = (E)NULL)
   {
      int values[];
      const int n = EnumToArray(dummyvalues01000);
      ArraySort(values);
      return values[n - 1];
   }

所有三种类型枚举的最大值确定了应根据其所属特性类型排序的整数范围。

此处我们使用 detect 方法,如果该整数是枚举的一个元素,则该方法返回 true

   template<typename E>
   static bool detect(const int v)
   {
      ResetLastError();
      const string s = EnumToString((E)v); // result is not used 
      if(_LastError == 0// only the absence of an error is important
      {
         return true;
      }
      return false;
   }

最后一个问题是程序启动时如何运行该计算。这是通过利用变量和方法的静态特性来实现的。

template<typename I,typename D,typename S>
static int MonitorInterface::TradeState::indices[][2];
template<typename I,typename D,typename S>
static int MonitorInterface::TradeState::j,
   MonitorInterface::TradeState::d,
   MonitorInterface::TradeState::s;
template<typename I,typename D,typename S>
const static int MonitorInterface::TradeState::limit =
   MonitorInterface::TradeState::calcIndices();

注意,limit 通过调用 calcIndices 函数进行初始化。

获得了一个带索引的表之后,我们便可以在 cache 方法中实现用特性值填充数组。

   class TradeState
   {
      ...
      TradeState(const MonitorInterface *ptr) : owner(ptr)
      {
         cache(); // when creating an object, immediately cache the properties
      }
      
      template<typename T>
      void _get(const int eT &valueconst // overload with record by reference
      {
         value = owner.get(evalue);
      }
      
      void cache()
      {
         ArrayResize(ulongsj);
         ArrayResize(doublesd);
         ArrayResize(stringss);
         for(int i = 0i < limit; ++i)
         {
            switch(indices[i][0])
            {
            case PROP_TYPE_INTEGER_get(iulongs[indices[i][1]]); break;
            case PROP_TYPE_DOUBLE_get(idoubles[indices[i][1]]); break;
            case PROP_TYPE_STRING_get(istrings[indices[i][1]]); break;
            }
         }
      }
   };

我们遍历从 0 到 limit 的整个特性范围,根据 indices[i][0] 中的特性类型,将其值写入 ulongsdoublesstrings 数组中编号为 indices[i][1] 的元素(数组的相应元素通过引用传递给 _get 方法)。

owner.get(e, value) 的调用引用了监视器类的一个标准方法(此处其作为抽象指针 MonitorInterface 可见)。特别是,对于 PositionMonitor 类中的仓位,这将导致 PositionGetIntegerPositionGetDouble,PositionGetString 调用。编译器可选择正确的类型。订单和交易监视器均有它们各自的类似实现,这些实现自动包含在这个基础代码中。

从监视器类继承一个交易对象的快照说明是合乎逻辑的。因为我们必须缓存订单、交易和仓位,所以将新类作为模板并集中适用于所有对象的所有通用算法是一种合理方案。我们姑且称之为 TradeBaseStateTradeState.mqh 文件)。

template<typename M,typename I,typename D,typename S>
class TradeBaseStatepublic M
{
   M::TradeState state;
   bool cached;
   
public:
   TradeBaseState(const ulong t) : M(t), state(&this), cached(ready)
   {
   }
   
   void passthrough(const bool b)   // enable/disable cache as desired
   {
      cached = b;
   }
   ...

前面介绍的特定监视器类中,部分以字母 M 隐藏了具体细节(OrderMonitor.mqhPositionMonitor.mqhDealMonitor.mqh)。其依据是新引入的 M::TradeState 类的 state 缓存对象。根据 M,可在内部形成一个特定的索引表(一个 M 类对应一个),并分配特性数组(每个 M 实例拥有一个,即每个订单、交易、仓位都有一个)。

cached 变量包含一个标志,用于表明 state 中的数组是否填充有特性值以及是否查询对象特性以从缓存中返回值。然后,需要将保存状态与当前状态进行比较。

换言之,当 cached 设置为 false 时,对象会像常规监视器一样从交易环境中读取特性。当 cached 等于 true 时,对象将从内部数组返回以前存储的值。

   virtual long get(const I propertyconst override
   {
      return cached ? state.ulongs[M::TradeState::offset(property)] : M::get(property);
   }
   
   virtual double get(const D propertyconst override
   {
      return cached ? state.doubles[M::TradeState::offset(property)] : M::get(property);
   }
   
   virtual string get(const S propertyconst override
   {
      return cached ? state.strings[M::TradeState::offset(property)] : M::get(property);
   }
   ...

当然,默认是启用缓存的。

我们还必须提供一个直接执行缓存(填充数组)的方法。为此,只需调用 state 对象的 cache 方法。

   bool update()
   {
      if(refresh())
      {
         cached = false// disable reading from the cache
         state.cache();  // read real properties and write to cache
         cached = true;  // enable external cache access back 
         return true;
      }
      return false;
   }

refresh 方法有什么作用呢?

迄今为止,我们一直都是在简单模式下使用监视器对象:创建、读取和删除特性。同时,特性读取假设在交易上下文(在构造函数内部)中选择了相应的订单、交易或仓位。因为我们现在正在改进监视器以支持内部状态,所以必须确保重新分配所需的元素,以便在不确定时间之后读取特性(当然,同时还会检查元素是否仍然存在)。为了实现这一点,我们向模板 MonitorInterface 类添加了 refresh 虚方法。

// TradeBaseMonitor.mqh
template<typename I,typename D,typename S>
class MonitorInterface
{
   ...
   virtual bool refresh() = 0;

成功分配订单、交易或仓位后,该方法必须返回 true。如果结果为 false,则内置 _LastError 变量中应包含以下错误之一:

  • 4753 ERR_TRADE_POSITION_NOT_FOUND;
  • 4754 ERR_TRADE_ORDER_NOT_FOUND;
  • 4755 ERR_TRADE_DEAL_NOT_FOUND;

在这种情况下,在派生类中实现此方法时,表示对象可用性的 ready 元素变量必须重置为 false

例如,在 PositionMonitor 构造函数中,我们曾经并且现在仍然有这样的初始化。这种情况类似于订单和交易监视器。

// PositionMonitor.mqh
   const ulong ticket;
   PositionMonitor(const ulong t): ticket(t)
   {
      if(!PositionSelectByTicket(ticket))
      {
         PrintFormat("Error: PositionSelectByTicket(%lld) failed: %s"ticket,
            E2S(_LastError));
      }
      else
      {
         ready = true;
      }
   }
   ...

现在,我们把 refresh 方法添加到这种类型的所有特定类中(参见示例 PositionMonitor):

// PositionMonitor.mqh
   virtual bool refresh() override
   {
      ready = PositionSelectByTicket(ticket);
      return ready;
   }

但是用特性值填充缓存数组只是成功了一半。另一半是将这些值与订单、交易或仓位的实际状态进行比较。

为了识别差异并将已更改特性的索引写入 changes 数组,生成的 TradeBaseState 类提供了 getChanges 方法。检测到更改时,该方法返回 true

template<typename M,typename I,typename D,typename S>
class TradeBaseStatepublic M
{
   ...
   bool getChanges(int &changes[])
   {
      const bool previous = ready;
      if(refresh())
      {
         // element is selected in the trading environment = properties can be read and compared
         cached = false;    // read directly
         const bool result = M::diff(statechanges);
         cached = true;     // turn cache back on by default
         return result;
      }
      // no longer "ready" = most likely deleted
      return previous != ready// if just deleted, this is also a change 
   }

可以看到,主要工作委托给了 M 类中的某个方法 diff,这是一个新方法,我们需要自己编写。幸运的是,由于有了 OOP,你可以在基础模板 MonitorInterface 中实现一次,该方法将会立即出现在订单、交易和仓位中。

// TradeBaseMonitor.mqh
template<typename I,typename D,typename S>
class MonitorInterface
{
   ...
   bool diff(const TradeState &thatint &changes[])
   {
      ArrayResize(changes0);
      for(int i = 0i < TradeState::limit; ++i)
      {
         switch(TradeState::indices[i][0])
         {
         case PROP_TYPE_INTEGER:
            if(this.get((I)i) != that.ulongs[TradeState::offset(i)])
            {
               PUSH(changesi);
            }
            break;
         case PROP_TYPE_DOUBLE:
            if(!TU::Equal(this.get((D)i), that.doubles[TradeState::offset(i)]))
            {
               PUSH(changesi);
            }
            break;
         case PROP_TYPE_STRING:
            if(this.get((S)i) != that.strings[TradeState::offset(i)])
            {
               PUSH(changesi);
            }
            break;
         }
      }
      return ArraySize(changes) > 0;
   }

所以,一切准备就绪,可以为订单、交易和仓位形成特定的缓存类。例如,仓位将存储在基于 PositionMonitor 的扩展监视器 PositionState 中。

class PositionStatepublic TradeBaseState<PositionMonitor,
   ENUM_POSITION_PROPERTY_INTEGER,
   ENUM_POSITION_PROPERTY_DOUBLE,
   ENUM_POSITION_PROPERTY_STRING>
{
public:
   PositionState(const long t): TradeBaseState(t) { }
};

类似地,在 TradeState.mqh 文件中定义了交易的缓存类。

class DealStatepublic TradeBaseState<DealMonitor,
   ENUM_DEAL_PROPERTY_INTEGER,
   ENUM_DEAL_PROPERTY_DOUBLE,
   ENUM_DEAL_PROPERTY_STRING>
{
public:
   DealState(const long t): TradeBaseState(t) { }
};

对于订单,情况稍微复杂一点,因为它们可以分为活跃订单和历史订单。目前,我们已经有了一个通用的订单监视器类 OrderMonitor。其会试图在活跃订单和历史订单中查找提交的订单号。这种方法不适合缓存,因为 EA 交易需要跟踪订单从一种状态到另一种状态的转换。

出于这个原因,我们向 OrderMonitor.mqh 文件添加了另外两个特定的类: ActiveOrderMonitorHistoryOrderMonitor

// OrderMonitor.mqh
class ActiveOrderMonitorpublic OrderMonitor
{
public:
   ActiveOrderMonitor(const ulong t): OrderMonitor(t)
   {
      if(history// if the order is in history, then it is already inactive
      {
         ready = false;   // reset ready flag
         history = false// this object is only for active orders by definition
      }
   }
   
   virtual bool refresh() override
   {
      ready = OrderSelect(ticket);
      return ready;
   }
};
   
class HistoryOrderMonitorpublic OrderMonitor
{
public:
   HistoryOrderMonitor(const ulong t): OrderMonitor(t) { }
   
   virtual bool refresh() override
   {
      history = true// work only with history
      ready = historyOrderSelectWeak(ticket);
      return ready// readiness is determined by the presence of a ticket in the history
   }
};

二者均仅在自己的区域内搜索订单号。基于这些监视器,即可创建缓存类。

// TradeState.mqh
class OrderStatepublic TradeBaseState<ActiveOrderMonitor,
   ENUM_ORDER_PROPERTY_INTEGER,
   ENUM_ORDER_PROPERTY_DOUBLE,
   ENUM_ORDER_PROPERTY_STRING>
{
public:
   OrderState(const long t): TradeBaseState(t) { }
};
   
class HistoryOrderStatepublic TradeBaseState<HistoryOrderMonitor,
   ENUM_ORDER_PROPERTY_INTEGER,
   ENUM_ORDER_PROPERTY_DOUBLE,
   ENUM_ORDER_PROPERTY_STRING>
{
public:
   HistoryOrderState(const long t): TradeBaseState(t) { }
};

为了方便起见,添加到 TradeBaseState 类的最后一点是将特性值转换为字符串的特殊方法。虽然监视器中有几个版本的 stringify 方法,但它们都是“打印”来自缓存的值(如果元素变量 cached 等于 true)或来自交易环境原始对象的值(如果 cached 等于 false)。为了可视化缓存和更改对象之间的差异(若发现这些差异),我们需要同时从缓存中读取值并绕过缓存。在这方面,我们添加了 stringifyRaw 方法,其始终直接与特性一起进行处理(因为 cached 变量被临时重置并重新安装)。

   // get the string representation of the property 'i' bypassing the cache
   string stringifyRaw(const int i)
   {
      const bool previous = cached;
      cached = false;
      const string s = stringify(i);
      cached = previous;
   }

我们使用一个简单的 EA 交易示例来检查缓存监视器的性能,该监视器可监视活跃订单的状态 (OrderSnapshot.mq5)。后续我们将扩展这个思路,实现对一组订单、交易或仓位进行缓冲,即创建一个功能完整的缓存。

EA 交易将尝试查找活跃订单列表中的最后一个订单,并为其创建 OrderState 对象。如果没有订单,将提示用户创建订单或开仓(后者与在市场上下达并执行订单相关)。只要发现订单,就会检查订单状态是否已经改变。这项检查是在 OnTrade 处理程序中执行的。EA 交易将继续监控该订单,直到订单被卸载为止。

int OnInit()
{
   if(OrdersTotal() == 0)
   {
      Alert("Please, create a pending order or open/close a position");
   }
   else
   {
      OnTrade(); // self-invocation
   }
   return INIT_SUCCEEDED;
}
   
void OnTrade()
{
   static int count = 0;
   // object pointer is stored in static AutoPtr
   static AutoPtr<OrderStateauto;
   // get a "clean" pointer (so as not to dereference auto[] everywhere)
   OrderState *state = auto[];
   
   PrintFormat(">>> OnTrade(%d)"count++);
   
   if(OrdersTotal() > 0 && state == NULL)
   {
      const ulong ticket = OrderGetTicket(OrdersTotal() - 1);
      auto = new OrderState(ticket);
      PrintFormat("Order picked up: %lld %s"ticket,
         auto[].isReady() ? "true" : "false");
      auto[].print(); // initial state at the time of "capturing" the order
   }
   else if(state)
   {
      int changes[];
      if(state.getChanges(changes))
      {
         Print("Order properties changed:");
         ArrayPrint(changes);
         ...
      }
      if(_LastError != 0Print(E2S(_LastError));
   }
}

除了显示已更改特性的数组之外,最好也能显示变更本身。因此,我们将添加这样一个片段,而不是省略号(该片段在未来功能完整的缓存类中非常有用)。

         for(int k = 0k < ArraySize(changes); ++k)
         {
            switch(OrderState::TradeState::type(changes[k]))
            {
            case PROP_TYPE_INTEGER:
               Print(EnumToString((ENUM_ORDER_PROPERTY_INTEGER)changes[k]), ": ",
                  state.stringify(changes[k]), " -> ",
                  state.stringifyRaw(changes[k]));
                  break;
            case PROP_TYPE_DOUBLE:
               Print(EnumToString((ENUM_ORDER_PROPERTY_DOUBLE)changes[k]), ": ",
                  state.stringify(changes[k]), " -> ",
                  state.stringifyRaw(changes[k]));
                  break;
            case PROP_TYPE_STRING:
               Print(EnumToString((ENUM_ORDER_PROPERTY_STRING)changes[k]), ": ",
                  state.stringify(changes[k]), " -> ",
                  state.stringifyRaw(changes[k]));
                  break;
            }
         }

此处我们使用了新的 stringifyRaw 方法。显示变更后,不要忘记更新缓存状态。

         state.update();

如果你在没有活跃订单的账户上运行 EA 交易,并下了一个新订单,你将在日志中看到以下条目(此处创建的 EURUSD buy limit 低于当前市场价格)。

Alert: Please, create a pending order or open/close a position

>>> OnTrade(0)

Order picked up: 1311736135 true

MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>

ENUM_ORDER_PROPERTY_INTEGER Count=14

0 ORDER_TIME_SETUP=2022.04.11 11:42:39

1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00

2 ORDER_TIME_DONE=1970.01.01 00:00:00

3 ORDER_TYPE=ORDER_TYPE_BUY_LIMIT

4 ORDER_TYPE_FILLING=ORDER_FILLING_RETURN

5 ORDER_TYPE_TIME=ORDER_TIME_GTC

6 ORDER_STATE=ORDER_STATE_STARTED

7 ORDER_MAGIC=0

8 ORDER_POSITION_ID=0

9 ORDER_TIME_SETUP_MSC=2022.04.11 11:42:39'729

10 ORDER_TIME_DONE_MSC=1970.01.01 00:00:00'000

11 ORDER_POSITION_BY_ID=0

12 ORDER_TICKET=1311736135

13 ORDER_REASON=ORDER_REASON_CLIENT

ENUM_ORDER_PROPERTY_DOUBLE Count=7

0 ORDER_VOLUME_INITIAL=0.01

1 ORDER_VOLUME_CURRENT=0.01

2 ORDER_PRICE_OPEN=1.087

3 ORDER_PRICE_CURRENT=1.087

4 ORDER_PRICE_STOPLIMIT=0.0

5 ORDER_SL=0.0

6 ORDER_TP=0.0

ENUM_ORDER_PROPERTY_STRING Count=3

0 ORDER_SYMBOL=EURUSD

1 ORDER_COMMENT=

2 ORDER_EXTERNAL_ID=

>>> OnTrade(1)

Order properties changed:

10 14

ORDER_PRICE_CURRENT: 1.087 -> 1.09073

ORDER_STATE: ORDER_STATE_STARTED -> ORDER_STATE_PLACED

>>> OnTrade(2)

>>> OnTrade(3)

>>> OnTrade(4)

此处,你可以看到订单状态是如何从 STARTED(已开始)变为 PLACED(已下单)。如果我们在市场上以少的交易量开盘而不是挂单,我们可能没有时间接收这些变化,因为这样的订单通常设置为非常快的执行速度,并且其观察状态会立即从 STARTED(已开始)变为 FILLED(已成交)。而后者意味着,订单已经被移至历史订单。因此,需要同步监视历史对其进行跟踪。我们将在下一个示例中展示这一点。

请注意,可能会有许多 OnTrade 事件,但并非所有事件都与我们的订单相关。

我们来尝试设置 Take Profit 水平,并查看日志。

>>> OnTrade(5)
Order properties changed:
10 13
ORDER_PRICE_CURRENT: 1.09073 -> 1.09079
ORDER_TP: 0.0 -> 1.097
>>> OnTrade(6)
>>> OnTrade(7)

下一步,改变到期日期:从 GTC 改为一天。

>>> OnTrade(8)
Order properties changed:
10
ORDER_PRICE_CURRENT: 1.09079 -> 1.09082
>>> OnTrade(9)
>>> OnTrade(10)
Order properties changed:
2 6
ORDER_TIME_EXPIRATION: 1970.01.01 00:00:00 -> 2022.04.11 00:00:00
ORDER_TYPE_TIME: ORDER_TIME_GTC -> ORDER_TIME_DAY
>>> OnTrade(11)

此处,在更改订单的过程中,价格有足够的时间进行更改,因此我们在 ORDER_PRICE_CURRENT 中“挂入”了一个关于新值的中间通知。只有在此之后,ORDER_TYPE_TIME 和 ORDER_TIME_EXPIRATION 中的预期更改才会记录到日志中。

接下来,我们移除了该订单。

>>> OnTrade(12)
TRADE_ORDER_NOT_FOUND

现在,对于导致 OnTrade 事件的任何账户操作,我们的 EA 交易将输出 TRADE_ORDER_NOT_FOUND,因为其是为跟踪单个订单而设计的。如果 EA 交易重新启动,它将“缓存”另一个订单(如果有)。但是我们将停止 EA 交易,并开始准备一个更紧急的任务。

通常,缓存和控制更改不是针对单个订单或仓位,而是针对根据特定条件选择的所有订单或仓位或其中一部分。出于此目的,我们将开发一个基础模板类 TradeCache (TradeCache.mqh),并据此为订单、交易和仓位列表创建应用类。

template<typename T,typename F,typename E>
class TradeCache
{
   AutoPtr<Tdata[];
   const E property;
   const int NOT_FOUND_ERROR;
   
public:
   TradeCache(const E idconst int error): property(id), NOT_FOUND_ERROR(error) { }
   
   virtual string rtti() const
   {
      return typename(this); // will be redefined in derived classes for visual output to the log
   }
   ...

在这个模板中,字母 T 表示 TradeState 系列的一个类。可以看出,这种对象的数组以自动指针的形式保留,名称为 data

字母 F 描述了用于选择缓存项的筛选器类的类型(OrderFilter.mqh,包括 HistoryOrderFilterDealFilter.mqhPositionFilter.mqh)。在最简单的情况下,当筛选器不包含 let 条件时,所有元素都将被缓存(相对于来自历史的对象的 采样历史 )。

字母 E 对应于标识对象的 property 所在的枚举。由于这个特性通常为 SOME_TICKET,所以枚举假定为整数型 ENUM_SOMETHING_PROPERTY_INTEGER。

NOT_FOUND_ERROR 变量专用于处理尝试分配不存在的对象进行读取操作时出现的错误代码,例如,针对仓位的 ERR_TRADE_POSITION_NOT_FOUND 错误。

在参数中,主要的类方法 scan 用于扫描对已配置筛选器的引用(应由调用代码配置)。

   void scan(F &f)
   {
      const int existedBefore = ArraySize(data);
      
      ulong tickets[];
      ArrayResize(ticketsexistedBefore);
      for(int i = 0i < existedBefore; ++i)
      {
         tickets[i] = data[i][].get(property);
      }
      ...

在该方法的开头,我们会将已经缓存的对象标识符收集到 tickets 数组中。显然第一次运行时,数组为空。

接下来,我们使用筛选器用相关对象的订单号填充 objects 数组。对于每个新订单号,我们都会创建一个缓存监视器对象 T,并将其添加到 data 数组中。对于旧对象,我们可通过调用 data[j][].getChanges(changes) 来分析是否存在报告,然后通过调用 data[j][].update() 来更新缓存。

      ulong objects[];
      f.select(objects);
      for(int i = 0ji < ArraySize(objects); ++i)
      {
         const ulong ticket = objects[i];
         for(j = 0j < existedBefore; ++j)
         {
            if(tickets[j] == ticket)
            {
               tickets[j] = 0// mark as found
               break;
            }
         }
         
         if(j == existedBefore// this is not in the cache, you need to add
         {
            const T *ptr = new T(ticket);
            PUSH(dataptr);
            onAdded(*ptr);
         }
         else
         {
            ResetLastError();
            int changes[];
            if(data[j][].getChanges(changes))
            {
               onUpdated(data[j][], changes);
               data[j][].update();
            }
            if(_LastErrorPrintFormat("%s: %lld (%s)"rtti(), ticketE2S(_LastError));
         }
      }
      ...

可以看到,在变更的每个阶段,也就是添加对象时或更改对象后,onAddedonUpdated 方法都会被调用。这些都是虚拟存根方法,扫描可以使用它们向程序通知适当的事件。应用程序代码需要使用这些方法的重写版本来实现派生类。我们稍后会谈到这个问题,但是现在我们将继续考虑 scan 方法。

在上面的循环中,tickets 数组中所有找到的订单号均被设置为零,因此剩余的元素对应于交易环境中缺失的对象。接下来,我们调用 getChanges 并将错误代码与 NOT_FOUND_ERROR 进行比较,以此来检查这些对象。如果结果相符,则调用 onRemoved 虚方法。然后,会返回一个布尔标志(由你的应用程序代码提供),表示该项是否应从缓存中移除。

      for(int j = 0j < existedBefore; ++j)
      {
         if(tickets[j] == 0continue// skip processed elements
         
         // this ticket was not found, most likely deleted
         int changes[];
         ResetLastError();
         if(data[j][].getChanges(changes))
         {
            if(_LastError == NOT_FOUND_ERROR// for example, ERR_TRADE_POSITION_NOT_FOUND
            {
               if(onRemoved(data[j][]))
               {
                  data[j] = NULL;             // release the object and array element
               }
               continue;
            }
            
            // NB! Usually we shouldn't fall here
            PrintFormat("Unexpected ticket: %lld (%s) %s"tickets[j],
               E2S(_LastError), rtti());
            onUpdated(data[j][], changestrue);
            data[j][].update();
         }
         else
         {
            PrintFormat("Orphaned element: %lld (%s) %s"tickets[j],
               E2S(_LastError), rtti());
         }
      }
   }

scan 方法的最后,data 数组中的空元素会被清除,但为了简洁起见,此处省略了这个片段。

基础类可提供 onAddedonRemovedonUpdated 方法的标准实现,这些方法用于显示日志中事件的要点。在包含头文件 TradeCache.mqh 之前,在代码中定义 PRINT_DETAILS 宏,这样便可以要求打印每个新对象的所有特性。

   virtual void onAdded(const T &state)
   {
      Print(rtti(), " added: "state.get(property));
      #ifdef PRINT_DETAILS
      state.print();
      #endif
   }
   
   virtual bool onRemoved(const T &state)
   {
      Print(rtti(), " removed: "state.get(property));
      return true// allow the object to be removed from the cache (false to save)
   }
   
   virtual void onUpdated(T &stateconst int &changes[],
      const bool unexpected = false)
   {
      ...
   }

我们不再介绍 onUpdated 方法,因为其实际上与上面显示的从 EA 交易 OrderSnapshot.mq5 输出变更的代码重复。

当然,该基础类具备获取缓存大小和通过编号访问特定对象的功能。

   int size() const
   {
      return ArraySize(data);
   }
   
   T *operator[](int iconst
   {
      return data[i][]; // return pointer (T*) from AutoPtr object
   }

基于基础类 TradeCache,我们可以轻松地创建某些类来缓存仓位、活跃订单和历史订单列表。交易缓存则留作一项独立任务。

class PositionCachepublic TradeCache<PositionState,PositionFilter,
   ENUM_POSITION_PROPERTY_INTEGER>
{
public:
   PositionCache(const ENUM_POSITION_PROPERTY_INTEGER selector = POSITION_TICKET,
      const int error = ERR_TRADE_POSITION_NOT_FOUND): TradeCache(selectorerror) { }
};
   
class OrderCachepublic TradeCache<OrderState,OrderFilter,
   ENUM_ORDER_PROPERTY_INTEGER>
{
public:
   OrderCache(const ENUM_ORDER_PROPERTY_INTEGER selector = ORDER_TICKET,
      const int error = ERR_TRADE_ORDER_NOT_FOUND): TradeCache(selectorerror) { }
};
   
class HistoryOrderCachepublic TradeCache<HistoryOrderState,HistoryOrderFilter,
   ENUM_ORDER_PROPERTY_INTEGER>
{
public:
   HistoryOrderCache(const ENUM_ORDER_PROPERTY_INTEGER selector = ORDER_TICKET,
      const int error = ERR_TRADE_ORDER_NOT_FOUND): TradeCache(selectorerror) { }
};

为了总结所展示功能的开发过程,我们展示了一个主要类的图表。这是 UML 图的简化版本,在 MQL5 中设计复杂程序时会很有用。

交易对象的监视器、筛选器和缓存的类图

交易对象的监视器、筛选器和缓存的类图

模板用黄色标记,抽象类用白色标记,某些实现用彩色显示。实线箭头(尖端填充)表示继承,虚线箭头(尖端空心)表示模板类型。虚线箭头(尖端开放)表示类之间对彼此指定方法的使用。与菱形标志的连接表示一种组合(将一些对象包含到其他对象中)。

为了举例说明缓存的使用,我们来创建一个 EA 交易 TradeSnapshot.mq5,该 EA 交易将会响应 OnTrade 处理程序响应交易环境中的任何变化。对于筛选和缓存,该代码说明了 6 个对象,每种类型的元素有 2 个(筛选和缓存):仓位、活跃订单和历史订单。

PositionFilter filter0;
PositionCache positions;
   
OrderFilter filter1;
OrderCache orders;
   
HistoryOrderFilter filter2;
HistoryOrderCache history;

通过调用 let 方法,没有为筛选器设置任何条件,因此所有发现的在线对象都将进入缓存。历史订单还有一个附加设置。

或者,在启动时,可以将过去的订单加载到给定历史深度的缓存中。这可以通过 HistoryLookup 输入变量来完成。在此变量中,你可以选择最后一天、上周(按持续时间,而不是日历)、月(30天)或年(360天)。默认情况下,不加载过去的历史(更准确地说,仅加载 1 秒的历史)。因为 PRINT_DETAILS 宏是在 EA 交易中定义的,所以对于有大量历史的账户要格外小心:如果没有限制时段,会生成大量日志。

enum ENUM_HISTORY_LOOKUP
{
   LOOKUP_NONE = 1,
   LOOKUP_DAY = 86400,
   LOOKUP_WEEK = 604800,
   LOOKUP_MONTH = 2419200,
   LOOKUP_YEAR = 29030400,
   LOOKUP_ALL = 0,
};
   
input ENUM_HISTORY_LOOKUP HistoryLookup = LOOKUP_NONE;
   
datetime origin;

OnInit 处理程序中,我们可以重置缓存(以防 EA 交易使用新参数重启),计算 origin 变量中历史的开始日期,并第一次调用 OnTrade

int OnInit()
{
   positions.reset();
   orders.reset();
   history.reset();
   origin = HistoryLookup ? TimeCurrent() - HistoryLookup : 0;
   
   OnTrade(); // self start
   return INIT_SUCCEEDED;
}

OnTrade 处理程序非常简单,因为所有的复杂性现在都隐藏在类中。

void OnTrade()
{
   static int count = 0;
   
   PrintFormat(">>> OnTrade(%d)"count++);
   positions.scan(filter0);
   orders.scan(filter1);
   // make a history selection just before using the filter
   // inside the 'scan' method
   HistorySelect(originLONG_MAX);
   history.scan(filter2);
   PrintFormat(">>> positions: %d, orders: %d, history: %d",
      positions.size(), orders.size(), history.size());
}

在空白账户上启动 EA 交易后,我们将会立即看到以下消息:

>>> OnTrade(0)
>>> positions: 0, orders: 0, history: 0

让我们尝试执行最简单的测试案例:在一个空账户(该账户没有未平仓仓位和挂单)上买入或卖出。该日志将记录以下事件(几乎立即发生)。

首先将检测到一个活跃订单。

>>> OnTrade(1)

OrderCache added: 1311792104

MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>

ENUM_ORDER_PROPERTY_INTEGER Count=14

0 ORDER_TIME_SETUP=2022.04.11 12:34:51

1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00

2 ORDER_TIME_DONE=1970.01.01 00:00:00

3 ORDER_TYPE=ORDER_TYPE_BUY

4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK

5 ORDER_TYPE_TIME=ORDER_TIME_GTC

6 ORDER_STATE=ORDER_STATE_STARTED

7 ORDER_MAGIC=0

8 ORDER_POSITION_ID=0

9 ORDER_TIME_SETUP_MSC=2022.04.11 12:34:51'096

10 ORDER_TIME_DONE_MSC=1970.01.01 00:00:00'000

11 ORDER_POSITION_BY_ID=0

12 ORDER_TICKET=1311792104

13 ORDER_REASON=ORDER_REASON_CLIENT

ENUM_ORDER_PROPERTY_DOUBLE Count=7

0 ORDER_VOLUME_INITIAL=0.01

1 ORDER_VOLUME_CURRENT=0.01

2 ORDER_PRICE_OPEN=1.09218

3 ORDER_PRICE_CURRENT=1.09218

4 ORDER_PRICE_STOPLIMIT=0.0

5 ORDER_SL=0.0

6 ORDER_TP=0.0

ENUM_ORDER_PROPERTY_STRING Count=3

0 ORDER_SYMBOL=EURUSD

1 ORDER_COMMENT=

2 ORDER_EXTERNAL_ID=

然后,这个订单会被移到历史中(同时,至少状态、执行时间和仓位 ID 会发生变化)。

HistoryOrderCache added: 1311792104

MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>

ENUM_ORDER_PROPERTY_INTEGER Count=14

0 ORDER_TIME_SETUP=2022.04.11 12:34:51

1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00

2 ORDER_TIME_DONE=2022.04.11 12:34:51

3 ORDER_TYPE=ORDER_TYPE_BUY

4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK

5 ORDER_TYPE_TIME=ORDER_TIME_GTC

6 ORDER_STATE=ORDER_STATE_FILLED

7 ORDER_MAGIC=0

8 ORDER_POSITION_ID=1311792104

9 ORDER_TIME_SETUP_MSC=2022.04.11 12:34:51'096

10 ORDER_TIME_DONE_MSC=2022.04.11 12:34:51'097

11 ORDER_POSITION_BY_ID=0

12 ORDER_TICKET=1311792104

13 ORDER_REASON=ORDER_REASON_CLIENT

ENUM_ORDER_PROPERTY_DOUBLE Count=7

0 ORDER_VOLUME_INITIAL=0.01

1 ORDER_VOLUME_CURRENT=0.0

2 ORDER_PRICE_OPEN=1.09218

3 ORDER_PRICE_CURRENT=1.09218

4 ORDER_PRICE_STOPLIMIT=0.0

5 ORDER_SL=0.0

6 ORDER_TP=0.0

ENUM_ORDER_PROPERTY_STRING Count=3

0 ORDER_SYMBOL=EURUSD

1 ORDER_COMMENT=

2 ORDER_EXTERNAL_ID=

>>> positions: 0, orders: 1, history: 1

注意,这些修改发生在对 OnTrade 的同一次调用中。换言之,当我们的程序正在分析新订单的特性(通过调用 orders.scan)时,订单被终端并行处理,而在检查历史(通过调用 history.scan)时,该订单已经被移入历史。这就是为什么根据这个日志片段的最后一行,两处都列出了该订单的原因。这种行为对于多线程程序而言是正常的,在设计多线程程序时应考虑到这一点。但也并不总是如此。此处,我们只是提请注意这种情况。快速执行 MQL 程序时,这种情况通常不会发生。

如果我们先检查历史,然后再检查在线订单,那么在第一阶段,我们可能会发现订单还不在历史中,而在第二阶段,订单不再在线。也就是说,理论上它可能会丢失一会儿。更现实的情况是,由于历史同步,会跳过处于活跃阶段的订单,即,直接在历史中首次捕获到该订单。

注意,MQL5 不允许你将交易环境作为一个整体进行同步,而只是部分同步:

  • 在活跃订单中,信息与刚刚调用 OrderSelectOrderGetTicket 函数的订单相关
  • 在仓位中,信息与刚刚调用函数 PositionSelectPositionSelectByTicketPositionGetTicket 的仓位相关
  • 对于历史中的订单和交易,信息可在最后一次调用 HistorySelectHistorySelectByPositionHistoryOrderSelect 的上下文中获得 HistoryDealSelect

此外,请注意,交易事件(像任何 MQL5 事件一样)是关于已经发生的变化的消息,这些变更被放入队列中,并会以延迟的方式从队列中检索,并非在发生变更时立即能够检索到。此外,OnTrade 事件发生在相关的 OnTradeTransaction 事件之后。

尝试不同的程序配置,调试,并生成详细日志,可为你的交易系统选择最可靠的算法。

让我们回到我们的日志。在下一次触发 OnTrade 时,情况已经得到修正:活跃订单的缓存检测到订单被删除。在这个过程中,仓位缓存可看到一个未平仓仓位。

>>> OnTrade(2)

PositionCache added: 1311792104

MonitorInterface<ENUM_POSITION_PROPERTY_INTEGER,ENUM_POSITION_PROPERTY_DOUBLE,ENUM_POSITION_PROPERTY_STRING>

ENUM_POSITION_PROPERTY_INTEGER Count=9

0 POSITION_TIME=2022.04.11 12:34:51

1 POSITION_TYPE=POSITION_TYPE_BUY

2 POSITION_MAGIC=0

3 POSITION_IDENTIFIER=1311792104

4 POSITION_TIME_MSC=2022.04.11 12:34:51'097

5 POSITION_TIME_UPDATE=2022.04.11 12:34:51

6 POSITION_TIME_UPDATE_MSC=2022.04.11 12:34:51'097

7 POSITION_TICKET=1311792104

8 POSITION_REASON=POSITION_REASON_CLIENT

ENUM_POSITION_PROPERTY_DOUBLE Count=8

0 POSITION_VOLUME=0.01

1 POSITION_PRICE_OPEN=1.09218

2 POSITION_PRICE_CURRENT=1.09214

3 POSITION_SL=0.00000

4 POSITION_TP=0.00000

5 POSITION_COMMISSION=0.0

6 POSITION_SWAP=0.00

7 POSITION_PROFIT=-0.04

ENUM_POSITION_PROPERTY_STRING Count=3

0 POSITION_SYMBOL=EURUSD

1 POSITION_COMMENT=

2 POSITION_EXTERNAL_ID=

OrderCache removed: 1311792104

>>> positions: 1, orders: 0, history: 1

一段时间后,我们进行平仓。因为在我们的代码中,首先检查仓位缓存 (positions.scan),所以对平仓仓位的更改会包含在日志中。

>>> OnTrade(8)
PositionCache changed: 1311792104
POSITION_PRICE_CURRENT: 1.09214 -> 1.09222
POSITION_PROFIT: -0.04 -> 0.04

此外,在同一次 OnTrade 调用中,我们可检测到一个成交订单的出现及其向历史的瞬时转移(同样,由于终端的快速并行处理)。

OrderCache added: 1311796883

MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>

ENUM_ORDER_PROPERTY_INTEGER Count=14

0 ORDER_TIME_SETUP=2022.04.11 12:39:55

1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00

2 ORDER_TIME_DONE=1970.01.01 00:00:00

3 ORDER_TYPE=ORDER_TYPE_SELL

4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK

5 ORDER_TYPE_TIME=ORDER_TIME_GTC

6 ORDER_STATE=ORDER_STATE_STARTED

7 ORDER_MAGIC=0

8 ORDER_POSITION_ID=1311792104

9 ORDER_TIME_SETUP_MSC=2022.04.11 12:39:55'710

10 ORDER_TIME_DONE_MSC=1970.01.01 00:00:00'000

11 ORDER_POSITION_BY_ID=0

12 ORDER_TICKET=1311796883

13 ORDER_REASON=ORDER_REASON_CLIENT

ENUM_ORDER_PROPERTY_DOUBLE Count=7

0 ORDER_VOLUME_INITIAL=0.01

1 ORDER_VOLUME_CURRENT=0.01

2 ORDER_PRICE_OPEN=1.09222

3 ORDER_PRICE_CURRENT=1.09222

4 ORDER_PRICE_STOPLIMIT=0.0

5 ORDER_SL=0.0

6 ORDER_TP=0.0

ENUM_ORDER_PROPERTY_STRING Count=3

0 ORDER_SYMBOL=EURUSD

1 ORDER_COMMENT=

2 ORDER_EXTERNAL_ID=

HistoryOrderCache added: 1311796883

MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>

ENUM_ORDER_PROPERTY_INTEGER Count=14

0 ORDER_TIME_SETUP=2022.04.11 12:39:55

1 ORDER_TIME_EXPIRATION=1970.01.01 00:00:00

2 ORDER_TIME_DONE=2022.04.11 12:39:55

3 ORDER_TYPE=ORDER_TYPE_SELL

4 ORDER_TYPE_FILLING=ORDER_FILLING_FOK

5 ORDER_TYPE_TIME=ORDER_TIME_GTC

6 ORDER_STATE=ORDER_STATE_FILLED

7 ORDER_MAGIC=0

8 ORDER_POSITION_ID=1311792104

9 ORDER_TIME_SETUP_MSC=2022.04.11 12:39:55'710

10 ORDER_TIME_DONE_MSC=2022.04.11 12:39:55'711

11 ORDER_POSITION_BY_ID=0

12 ORDER_TICKET=1311796883

13 ORDER_REASON=ORDER_REASON_CLIENT

ENUM_ORDER_PROPERTY_DOUBLE Count=7

0 ORDER_VOLUME_INITIAL=0.01

1 ORDER_VOLUME_CURRENT=0.0

2 ORDER_PRICE_OPEN=1.09222

3 ORDER_PRICE_CURRENT=1.09222

4 ORDER_PRICE_STOPLIMIT=0.0

5 ORDER_SL=0.0

6 ORDER_TP=0.0

ENUM_ORDER_PROPERTY_STRING Count=3

0 ORDER_SYMBOL=EURUSD

1 ORDER_COMMENT=

2 ORDER_EXTERNAL_ID=

>>> positions: 1, orders: 1, history: 2

历史缓存中已有 2 个订单,但在历史缓存之前分析的仓位和活跃订单缓存尚未应用这些更改。

但是在下一个 OnTrade 事件中,我们可看到该仓位被平仓,市场订单已经消失。

>>> OnTrade(9)
PositionCache removed: 1311792104
OrderCache removed: 1311796883
>>> positions: 0, orders: 0, history: 2

如果我们在每一个分时报价处监控缓存(或者每秒一次,但不仅限于 OnTrade 事件),我们将随时看到 ORDER_PRICE_CURRENT 和 POSITION_PRICE_CURRENT 特性的变更。POSITION_PROFIT 也会变更。

我们的类没有 persistence,也就是说,它们只存在于 RAM 中,不知道如何在任何长期存储(比如文件)中保存和恢复它们的状态。这意味着程序可能会错过终端会话之间发生的变更。如果需要此类功能,应自己实现。接下来,在本书的第 7 章,我们将研究 MQL5 中内置的 SQLite 数据库支持,其提供了存储交易环境缓存和类似表格数据的最高效、最方便方式。