跨平台的EA交易: 信号

Enrico Lambino | 20 六月, 2017


目录

简介

在前一篇文章, 跨平台EA交易: 订单管理器中, 我们已经展示了 COrderManager 类, 以及它是如何用于自动化建立和关闭交易过程的。在这篇文章中,我们将使用大致相同的原则来自动化生成信号的过程,这可以通过 CSignal 类,以及它的容器, CSignals 类来获得。这篇文章详细介绍了这些类对象的实现。

目标

在本文中介绍的 CSignal 和 CSignal 类有以下目标:

  • 实现应当与 MQL4 和 MQL5 都兼容。
  • 自动化大多数与交易信号评估关联的过程。
  • 实现应当简单。

交易信号

CSignal 和它的容器 CSignals 都是用于评估根据当前市场状态的整体信号的, 交易信号分成两个主要的组: 进场和出场信号。为了进场,EA将需要执行一次交易,信号和其他进场信号都应该统一向一致的方向 (全部都是买入或者全部都是卖出)。另一方面,对于出场信号,每个信号自身是独立的,并且可以根据它自己的输出来影响最终的结果。出场信号也是累积评估的,如果信号1提供的信号是关闭所有卖出的条件,而信号2给出的时关闭所有买入的条件,那么最终的结果是关闭所有交易。

信号类型

EA 有四种信号类型,通过信号对象根据它是如何使用的(用于进场或是出场) 来解释 (而最终是由EA来做解释). 下面的表格显示了信号类型,它们的值,以及它们是如何根据它们的目标用法来做解释的:

 信号类型
 数值  进场 出场
CMD_VOID -1
刷新所有其它信号
退出所有交易i
CMD_NEUTRAL 0
忽略
忽略
CMD_LONG 1
进行买入
退出卖出交易
CMD_SHORT 2
进行卖出
退出买入交易

信号的类型 CMD_LONG 和 CMD_SHORT 是很容易使用自身来做解释的,所以我们更注重另外两种信号类型。

CMD_VOID 是一个数值为 -1 的整数, 代表的是强烈不建议的一个信号。对于进场信号,在输出中给出这样一个信号会取消所有其他入场信号的输出。这表示这个信号的输出是强制的,通过这个信号提出不交易的条件,会使所有其它信号也给出不交易的信号,而不论它们的实际输出以及其他信号是否给出的方向相同。比如,看下面的三个进场信号:

信号 1: CMD_VOID

信号 2: CMD_LONG

信号 3: CMD_SHORT

最终: CMD_VOID

在这个例子中,我们可以看到,信号1会最终使其他两个信号无效,而最终的输出是 CMD_VOID。但是也要注意,信号 2 和 3 给出的方向不一样,这样的话,在这个例子中不论信号1给出什么数值,对于 EA 来说,结果都是不进行交易的条件。

现在让我们考虑一下下面这种稍微修改过的状况:

信号 1: CMD_VOID

信号 2: CMD_LONG

信号 3: CMD_LONG

最终: CMD_VOID

在这个例子中,信号2和3给出的相同的方向(买入), 但是信号1是要求无效,这样,整体的信号还是不进行交易的状况。当给出无效信号时,信号1给出的权重更大,尽管信号2和3是一致的。

当寻找退出信号时,是另一种情况,例子2会给出关闭所有仓位的结果: 所有3个信号都同意关闭买入仓位,而信号1给出的消息是同时关闭买入和卖出仓位。因为所有的出场信号都是累积评估的,最终的输出也就是关闭所有的交易。

CMD_NEUTRAL 是数值为0的整数, 它的用处是给出一个拒绝的信号,它基本相当于选举过程中的"弃权票"。给出中性(neutral)输出的信号放弃了它在最终输出信号中的影响权力,并且把决定权交给了其他的信号。如果只有一个信号,并且信号给出了中性的结果,那结果就是在EA中没有进场也没有出场条件,这与多个信号没有一致给出相同方向的情况是一样的。

让我们使用 CMD_NEUTRAL 来稍微修改一下这个部分的第一个例子:

信号 1: CMD_NEUTRAL

信号 2: CMD_LONG

信号 3: CMD_SHORT

最终: CMD_VOID

在第三个例子中, 信号 1 给出了中性的信号,在这种情况下,只有信号2和3会在最终信号输出上做考虑,而因为信号2和信号3给出的方向不一致,最终的输出结果将是不交易的情况 (CMD_NEUTRAL)。

当其余信号给出的方向一致时,情况就不同了。在我们的第四个例子(参见下方)中, 第一个信号是中性的,而其余信号给出的方向一致,在这种情况下,信号1就被忽略,而评估信号2和3,最终结果就是给出一个买入信号。

信号 1: CMD_NEUTRAL

信号 2: CMD_LONG

信号 3: CMD_LONG

最终: CMD_LONG

请注意,信号的顺序没有关系,并且在以下信号集合中:

信号 1: CMD_NEUTRAL

信号 2: CMD_LONG

信号 3: CMD_LONG

信号 4: CMD_NEUTRAL

信号 5: CMD_LONG

信号 6: CMD_NEUTRAL

最终整体信号的结果是 CMD_LONG, 而不是 CMD_NEUTRAL。

当用作退场信号时,输出为 CMD_NEUTRAL 的信号将对最终退场信号没有影响,在这种时候,它就相当于退场信号不存在,而对决定最终信号不做考量。

这里还需要注意的是赋给 CMD_NEUTRAL 的数值是 0, 并且使用的是自定义枚举类型来代替。使用自定义枚举有几个优点,首先,我们能够自定义信号是怎样解释的,另一个优点是我们可以避免交易执行中使用未初始化的变量。例如,ORDER_TYPE_BUY 是数值为0的整数,如果我们有一个 EA 交易,直接把整数值传给一个处理交易请求的方法,而那个变量没有初始化或者没有重新赋值(很可能是不小心的),那么默认值是0,而它可能造成进场建立一个买入订单。另一方面,使用自定义枚举,这样的意外就永远不会发生,因为变量使用0值的结果总是不交易的状况。

与 CExpertSignal 作比较

CExpertSignal 按照以下方式评估整体的方向:

  1. 计算它自己的方向并把它保存到名为 m_direction 的变量中;
  2. 对于它的每个过滤器,
    1. 取得它的方向,
    2.  把方向加到 m_direction (如果某过滤器是反转的,就是减法),
  3. 如果 m_direction 最终的数值超过阈值,就给出交易信号。

使用这种方法,我们可以假设,m_direction 的数值越正,越多的信号就估计价格会上涨(增加超过阈值的机会). 类似地,m_direction 的数值越负,越多的信号就预测价格将会下跌。阈值的数值永远是正的,所以我们在检查用于卖出的信号时,会使用 m_direction 的绝对值。

本文中展示的信号对象可以被认为是 CExpertSignal 的简化版本,但是,它并不是使用数学方法集中估算信号和它的过滤器的,而是按照每个信号单独评估的。这里使用了简单的方法,但是给了交易者或者开发者更多的控制权来扩展每个独立信号可以对最终信号输出所做的影响。

阶段

OnInit 和 OnDeinit

在每个信号的初始化阶段,将会处理它将要使用的指标的创建和初始化,以及类对象中各种方法可能需要的其他类成员(如果有的话)的初始化。在终止化的时候,指标的实例将需要被删除。

OnTick

  1. 准备阶段 (计算) - 在这个阶段,要更新计算所需的数值(信号检查)。
  2. 主要阶段, 或者信号检查(Signal Checking) - 在主要阶段,确定信号的实际输出,这个方法的主体最好只有一行代码,以便于提高代码的可读性(看一下信号实际是怎样做的)。
  3. 最终或者更新阶段 - 在一些信号中,有些已有成员只在真正的信号检查进行过之后才更新,这里的一个例子是追踪前面的卖家报价,也许可以用于比较当前卖家报价或者一些其他数值(也许是来自图表对象或者指标的输出)。在准备阶段更新用来保存之前卖家报价的变量是没有意义的,因为这个值一直等于信号检查阶段的当前卖家报价,

MQL5有订单分时的数组,而MQL4没有这个功能,所以也是没有意义的。尽管这样,MQL4 在此是一个限制因素,所以代码在这里要根据 MQL4 的标准以确保跨平台的兼容性, 把实现部分分开是更加严谨的选择(在这个例子中就是这样)。

实现

CSignal 类

在检查任何交易信号之前,首先要刷新计算所需的数据。这是通过 CSignal 类的 Refresh 方法来完成的, 在那里指标 (以及时间序列数据) 会刷新至最新的数值。以下代码段就显示了 CSignal 的 Refresh 方法:

bool CSignalBase::Refresh(void)
  {
   for(int i=0;i<m_indicators.Total();i++)
     {
      CSeries *indicator=m_indicators.At(i);
      if(indicator!=NULL)
         indicator.Refresh(OBJ_ALL_PERIODS);
     }
   return true;
  }

对 CSignal 的 Refresh 方法的实际调用是在这个类的 Check 方法中进行的,在以下展示的代码中,如果没有能够刷新数据(可能会引起错误信号),该方法将停止进一步处理。

void CSignalBase::Check(void)
  {
   if(!Active())
      return;
   if(!Refresh())
      return;
   if(!Calculate())
      return;
   int res=CMD_NEUTRAL;
   if(LongCondition())
     {
      if (Entry())
         m_signal_open=CMD_LONG;
      if (Exit())
         m_signal_close=CMD_LONG;
     }
   else if(ShortCondition())
     {
      if (Entry())
         m_signal_open=CMD_SHORT;
      if (Exit())
         m_signal_close=CMD_SHORT;
     }
   else
   {
      if (Entry())
         m_signal_open=CMD_NEUTRAL;
      if (Exit())
         m_signal_close=CMD_NEUTRAL;
   }
   if(m_invert)
     {
      SignalInvert(m_signal_open);
      SignalInvert(m_signal_close);
     }
   Update();
  }

在 CSignal 的 Check 方法中,实际的信号是通过调用 LongCondition 和 ShortCondition 方法来决定的,它们基本上和标准库中 CExpertSignal 中的同名方法是一样的。

实际信号的获取是通过调用 CheckOpenLong 和 CheckOpenShort 方法获得的, 它必须从类的外部调用 (从另一个类对象或者在 OnTick 函数中):

bool CSignalBase::CheckOpenLong(void)
  {
   return m_signal_open==CMD_LONG;
  }
bool CSignalBase::CheckOpenShort(void)
  {
   return m_signal_open==CMD_SHORT;
  }

在它自身内部,CSignal 不给出任何买入或者卖出信号,这样,这些方法就是虚方法,而且只会在 CSignal 被扩展的时候才会有真正的实现。

virtual bool      LongCondition(void)=0;
virtual bool      ShortCondition(void)=0;
但是在以上提到的方法实现之前,如果来自时间序列和指标的原始信息不够,需要做进一步的计算,Calculate 方法就必须被第一个实现。和要在 CSignal 中使用的指标类似, 保存数值的变量也是类的成员,所以这些变量可以在 LongCondition 和 ShortCondition 方法中访问。
virtual bool      Calculate(void)=0;
virtual void      Update(void)=0;

请注意,Calculate 方法是 Boolean 类型的,而 Update 方法不返回任何数值。这意味着可以配置 EA 交易,在进行某些计算失败的时候取消信号的检查,而 Update 方法则不是这样,它是 void 类型,也许没有必要给它 Boolean 类型,因为它只在收到真正信号的时候才会调用。

一个特别的 CSignal 类的实例可以为进场信号,退场信号或者两者同时提供输出,这可以通过设置类的 Entry() 和 Exit() 方法来切换,这通常是在 EA 交易的初始化中完成的。

CSignals 类

CSignals 是继承于 CArrayObj 类的,因为父类可以保存 CObject 类型的实例, 而对于这个类,也就可以保存 CSignal 类型的实例。

这个类的初始化包括传入交易品种管理器对象 (CSymbolManager), 它在之前的文章中讨论过。这可以使信号从图表的交易品种中或者其他交易品种中取得它们可能需要的数据。每个信号调用 Init 方法的时候,也会使用它:

bool CSignalsBase::Init(CSymbolManager *symbol_man)
  {
   m_symbol_man= symbol_man;
   m_event_man = aggregator;
   if(!CheckPointer(m_symbol_man))
      return false;
   for(int i=0;i<Total();i++)
     {
      CSignal *signal=At(i);
      if(!signal.Init(symbol_man))
         return false;
     }
   return true;
  }

这个类的 Check 方法可以把信号初始化为中性的信号,然后再迭代每个信号来取得它们的输出,如果方法得到了一个无效(void)信号,或者得到一个虽然有效(买入或者卖出)但是和之前有效信号不同的信号,它就会在最终输出中给出无效信号。 

CSignalsBase::Check(void)
  {
   if(m_signal_open>0)
      m_signal_open_last=m_signal_open;
   if(m_signal_close>0)
      m_signal_close_last=m_signal_close;
   m_signal_open=CMD_NEUTRAL;
   m_signal_close=CMD_NEUTRAL;
   for(int i=0;i<Total();i++)
     {
      CSignal *signal=At(i);      
      signal.Check();
      if(signal.Entry())
        {
         if(m_signal_open>CMD_VOID)
           {
            ENUM_CMD signal_open=signal.SignalOpen();
            if(m_signal_open==CMD_NEUTRAL)
              {    
               m_signal_open=signal_open;
              }
            else if(m_signal_open!=signal_open)
              {               
               m_signal_open=CMD_VOID;
              }
           }
        }
      if(signal.Exit())
        {
         if(m_signal_close>CMD_VOID)
           {
            ENUM_CMD signal_close=signal.SignalClose();
            if(m_signal_close==CMD_NEUTRAL)
              {
               m_signal_close=signal_close;
              }
            else if(m_signal_close!=signal_close)
              {
               m_signal_close=CMD_VOID;
              }
           }
        }
     }
   if(m_invert)
     {
      CSignal::SignalInvert(m_signal_open);
      CSignal::SignalInvert(m_signal_close);
     }
   if(m_new_signal)
     {
      if(m_signal_open==m_signal_open_last)
         m_signal_open = CMD_NEUTRAL;
      if(m_signal_close==m_signal_close_last)
         m_signal_close= CMD_NEUTRAL;
     }
  }

指标的实例

每个 CSignal 的实例都有它自己的指标实例集合,保存在 m_indicators (一个 CIndicators 类型的实例) 中。在理想状况下,属于特定的 CSignal 实例的每个指标实例都和其他 CSignal 类型的实例是独立分开的。这是和 MQL5 标准库中的方法不同的,它会把 EA 中使用的所有指标保存在 CExpert 的类成员,一个单独的 CIndicators 实例中。尽管这种方法会导致重复的对象(例如,在信号1种有一个移动平均指标对象,而在指标2中有一个一样的对象),这可能导致重复计算,但是它还是有些好处的。首先,它使信号对象变成更加独立的单元,这使每个指标在它们的使用过程中更加自由,特别是在交易品种和时段的选择方面。比如,对于一个多货币对的 EA 交易,单独使用标准库中的 Expert 类很难把一些指标配置成处理多种金融资产的,也许解决这个问题更简单的方法是写一个新的自定义指标(访问和/或处理所需的数据),来被 CExpert 来使用 (而不是修改或者扩展 CExpert)。

局限

  1. 指标的可用性. 并非所有在 MetaTrader 4 中的指标在 MetaTrader 5 中都有 (反之也是如此)。所以,如果我们想要一个跨平台的 EA 交易,使它既能运行于 MetaTrader 4 也能运行于 MetaTrader 5, MetaTrader 4 的指标应该在 MetaTrader 5 中有其对应指标。否则,EA交易将不能在另一个平台上使用。这对于标准指标来说一般不是问题,除了个别的情况(例如,MT4 交易量指标和 MT5版本是不同的)。而对于自定义指标,就是另外一种情况,它们是自定义开发的,必须同时有它们的 MT4 和 MT5 版本,这样跨平台 EA 交易才能使用它们,在两个平台上能够正常工作。
  2. 某些数据的可用性. 有些时间序列数据在 MetaTrader 4 中不可用。所以,有些依赖于只在 MetaTrader 5 上才有的数据(比如,订单分时交易量,tick volume)的策略可能很难甚至无法转换为 MQL4 代码。

例子

例子 #1: 订单管理器示例

在我们前面的文章中,跨平台 EA 交易: 订单管理器中, 我们展示了一个EA交易的例子,来查看订单管理器是如何在一个真正的EA中工作的,这个 EA 的方法是在新柱的开始交易进行买入和卖出交易。以下的代码展示了所述EA交易的 OnTick 函数:

void OnTick()
  {
//---
   static int bars = 0;
   static int direction = 0;
   int current_bars = 0;
   #ifdef __MQL5__
      current_bars = Bars(NULL,PERIOD_CURRENT);
   #else 
      current_bars = Bars;
   #endif
   if (bars<current_bars)
   {   
      symbol_info.RefreshRates();
      COrder *last = order_manager.LatestOrder();
      if (CheckPointer(last) && !last.IsClosed())
         order_manager.CloseOrder(last);
      if (direction<=0)
      {
         Print("进入买入交易..");
         order_manager.TradeOpen(Symbol(),ORDER_TYPE_BUY,symbol_info.Ask());
         direction = 1;
      }
      else
      {
         Print("进入卖出交易..");
         order_manager.TradeOpen(Symbol(),ORDER_TYPE_SELL,symbol_info.Bid());
         direction = -1;
      }   
      bars = current_bars;
   }
  }

我们可以看到,用于处理EA行为的代码行(以及有关信号生成的部分)位于函数的不同部分,对于这样简单的EA交易,代码是很容易理解和管理的。但是,当EA交易变得更加复杂时,维护源代码可能就会变得困难很多,我们的目标就是组织 EA 中信号的生成,使用的就是我们已经在文章中讨论过的信号类。

为此,我们需要在主头文件中扩展 CSignal 类,使用三个保护的(protected)成员: (1) 之前柱的数量, (2) 之前柱之前的数量, 以及 (3) 当前的方向, 代码如下所示:

class SignalOrderManagerExample: public CSignal
  {
protected:
   int               m_bars_prev;
   int               m_bars;
   int               m_direction;
   //类的剩余部分

我们还需要扩展 CSignal 类中的方法,对于 Calculate 方法, 我们还是要使用在之前例子中那样的相同计算:

bool SignalOrderManagerExample::Calculate(void)
  {
   #ifdef __MQL5__
      m_bars=Bars(NULL,PERIOD_CURRENT);
   #else
      m_bars=Bars;
   #endif
   return m_bars>0 && m_bars>m_bars_prev;
  }

取得当前图表上柱的数量的方法在两个平台上是不同的,所以,就像以前的例子中一样,我们必须把实现分开。另外我们要注意,类中的 Calculate 方法的类型是 Boolean 类型,之前已经讨论过,如果 Calculate 方法返回 false, 对给出事件的进一步信号处理就将停止。在此,我们明确定义何时应该进一步处理信号的两条原则: (1) 当前的柱数大于0 以及 (2) 当前的柱数大于之前的柱数。 

然后,我们处理类中的 Update 方法,在我们的自定义类中扩展这个方法,显示在下面的代码行中:

void SignalOrderManagerExample::Update(void)
  {
   m_bars_prev=m_bars;
   m_direction= m_direction<=0?1:-1;
  }

在检查信号之后,我们更新之前的柱数 (m_bars_prev) 为当前柱数 (m_bars)。我们还要更新方向。如果当前的数值小于或者等于0 (之前的方向是卖出,或者是第一次进行交易), 这个变量的新值就设为1。否则,它的值就等于 -1。

最后,我们处理信号自身的生成。基于当前订单分时中估算信号的变量,我们再处理确定输出信号应该是买入信号或者卖出信号的条件。这是通过扩展 CSignal 中的 LongCondition 和 ShortCondition 方法来做到的:

bool SignalOrderManagerExample::LongCondition(void)
  {
   return m_direction<=0;
  }

bool SignalOrderManagerExample::ShortCondition(void)
  {
   return m_direction>0;
  }

本例中的 Init 函数和以前的例子中很类似。除此之外,对于这个例子,我们必须建立我们刚刚定义的 CSignal 类的派生类的实例,以及它的容器 (CSignals):

int OnInit()
  {
//---
   order_manager=new COrderManager();
   symbol_manager=new CSymbolManager();
   symbol_info=new CSymbolInfo();
   if(!symbol_info.Name(Symbol()))
      Print("没有设置交易品种");
   symbol_manager.Add(GetPointer(symbol_info));
   order_manager.Init(symbol_manager,NULL);
   SignalOrderManagerExample *signal_ordermanager=new SignalOrderManagerExample();
   signals=new CSignals();
   signals.Add(GetPointer(signal_ordermanager));
//---
   return(INIT_SUCCEEDED);
  }

在此,我们声明了 signal_ordermanager,它是一个指向 SignalOrderManagerExample 类型的新对象的指针, 我们刚才定义了它。然后我们要通过 signals 指针对 CSignals 做一样的处理,然后通过调用它的 Add 方法来把指向 SignalOrderManagerExample 的指针加上去。

我们的 EA 交易中 CSignal 和 CSignals 的使用在 OnTick 函数中很简单:

void OnTick()
  {
//---
   symbol_info.RefreshRates();   
   signals.Check();
   if(signals.CheckOpenLong())
     {
      close_last();
      Print("进入买入交易..");
      order_manager.TradeOpen(Symbol(),ORDER_TYPE_BUY,symbol_info.Ask());
     }
   else if(signals.CheckOpenShort())
     {
      close_last();
      Print("进入卖出交易..");
      order_manager.TradeOpen(Symbol(),ORDER_TYPE_SELL,symbol_info.Bid());
     }
  }

所有生成当前信号所需的计算都移动到 CSignal 和 CSignals 对象中,这样,我们所要做的就是让 CSignals 进行一次检查,然后通过调用它的 CheckOpenLong 和 CheckOpenShort 方法来取得输出。以下屏幕截图显示了在 MetaTrader 4 和 MetaTrader 5 中 EA 的测试结果。

(MT4)

signal_ordermanager (MT4)

(MT5)

signal_ordermanager (MT5)

例子 #2: MA EA 交易

我们的下一个例子包含了在估算交易信号时对移动平均指标(MA)的使用。MA 是在 MetaTrader 4 和 MetaTrader 5 中都有的标准指标,它也是我们可以用于跨平台 EA 交易中最简单的指标之一。

和前面的例子类似,我们通过扩展 CSignal 来创建一个自定义信号:

class SignalMA: public CSignal
  {
protected:
   CiMA             *m_ma;
   CSymbolInfo      *m_symbol;
   string            m_symbol_name;
   ENUM_TIMEFRAMES   m_timeframe;
   int               m_signal_bar;
   double            m_close;   
   //类的剩余部分

我们可以看到,MQL4 和 MQL5 库中都已经提供了用于移动平均指标的类对象,这使我们把该指标集成到我们自定义信号类中更加容易。尽管可能不是必需,在本例中,我们还将通过 m_symbol 来把目标交易品种保存起来,它是一个 CSybmolInfo 对象的指针。我们还声明了一个叫做 m_close 的对象, 它保存的是信号柱的收盘价。其余的保护的类成员是用于移动平均指标的参数。

前面的例子没有复杂的数据结构要在使用之前做准备,在本例中,则不是这样,有这样的处理(指标), 并且我们必须在类的构造函数中初始化它:

void SignalMA::SignalMA(const string symbol,const ENUM_TIMEFRAMES timeframe,const int period,const int shift,const ENUM_MA_METHOD method,const ENUM_APPLIED_PRICE applied,const int bar)
  {
   m_symbol_name= symbol;
   m_timeframe = timeframe;
   m_signal_bar = bar;
   m_ma=new CiMA();
   m_ma.Create(symbol,timeframe,period,0,method,applied);
   m_indicators.Add(m_ma);   
  }

取得移动平均信号经常包含把它的值与图表上的某个价格来做比较,它可能是图表上的特定价格,例如开盘价或者收盘价,或者当前的卖家报价和买家报价,而后者将需要操作交易品种对象。在本例中,对于那些更想使用卖家报价和买家报价来比较,而不是 OHLC 数据(以及它们的派生数据)的,我们将扩展 Init 方法,并使用交易品种处理器初始化正确的交易品种。

bool SignalMA::Init(CSymbolManager *symbol_man,CEventAggregator *event_man=NULL)
  {
   if(CSignal::Init(symbol_man,event_man))
     {
      if(CheckPointer(m_symbol_man))
        {
         m_symbol=m_symbol_man.Get();
         if(CheckPointer(m_symbol))
            return true;
        }
     }
   return false;
  }

下一个要扩展的是 calculate 方法, 代码显示如下:

bool SignalMA::Calculate(void)
  {
   double close[];
   if(CopyClose(m_symbol_name,m_timeframe,signal_bar,1,close)>0)
     {
      m_close=close[0];
      return true;
     }   
   return false;
  }

不再需要刷新指标的数据了,因为这已经在 CSignal 中的 Refresh 方法中进行过了。或者,我们可以实现 CSignal 类的继承类,这样来取得信号柱的收盘价,我们使用 CCloseBuffer 类。它也是 CSeries 的派生类, 这样我们就能把它加到 m_indicators 中,这样 CCloseBuffer 实例也会和其他指标一起刷新。在这种情况下,就不再需要扩展 CSignal 类的 Refresh 或者 Calcuate 方法了。 

对于这个特定的信号,不需要另外扩展 Update 方法了,所以让我们继续真正生成信号的部分。以下代码段显示了 LongCondition 和 ShortCondition 方法:

bool SignalMA::LongCondition(void)
  {
   return m_close>m_ma.Main(m_signal_bar);
  }

bool SignalMA::ShortCondition(void)
  {
   return m_close<m_ma.Main(m_signal_bar);
  }

条件很简单: 如果信号柱的收盘价高于信号柱的移动平均值,就是买入信号。另一方面,如果收盘价低,我们就有了卖出信号。

和前面的例子类似,我们简单初始化所有所需的指针,并把 CSignal 实例加到它的容器中 (CSignals 实例)。下面展示了在 OnInit 中所需的初始化信号的其他代码:

SignalMA *signal_ma=new SignalMA(Symbol(),(ENUM_TIMEFRAMES) Period(),maperiod,0,mamethod,maapplied,signal_bar);
signals=new CSignals();
signals.Add(GetPointer(signal_ma));
signals.Init(GetPointer(symbol_manager),NULL);

下面的代码显示了 OnTick 函数, 它与前面例子中看到的 OnTick 函数是一样的:

void OnTick()
  {
//---
   symbol_info.RefreshRates();
   signals.Check();
   if(signals.CheckOpenLong())
     {
      close_last();
      Print("进入买入交易..");
      order_manager.TradeOpen(Symbol(),ORDER_TYPE_BUY,symbol_info.Ask());
     }
   else if(signals.CheckOpenShort())
     {
      close_last();
      Print("进入卖出交易..");
      order_manager.TradeOpen(Symbol(),ORDER_TYPE_SELL,symbol_info.Bid());
     }
  }

下面的屏幕截图是在 MT4 和 MT5 上测试 EA 交易的结果,我们可以看到,EA交易执行了相同的逻辑:

(MT4)

signal_ma (MT4)

(MT5)

signal_ma (MT5)

例子 #3: HA EA 交易

在我们下面的例子中,我们将在 EA 交易中处理 Heiken Ashi 指标的使用。和移动平均指标不同,Heiken Ashi 指标是一个自定义指标,所以这个 EA 交易的代码将比前面的例子复杂一些,因为我们还需要通过扩展 CiCustom 类来声明用于 Heiken Ashi 指标的类。开始的时候,让我们展示 CiHA 类的定义,它是我们用于 HA 指标的类对象:

class CiHA: public CiCustom
  {
public:
                     CiHA(void);
                    ~CiHA(void);
   bool              Create(const string symbol,const ENUM_TIMEFRAMES period,
                            const ENUM_INDICATOR type,const int num_params,const MqlParam &params[],const int buffers);
   double            GetData(const int buffer_num,const int index) const;
  };

我们需要扩展两个方法,即 Create 和 GetData 方法。对于 Create 方法, 我们需要重定义类的构造函数,因为需要其他任务来准备指标的实例 (初始化,可以看到标准指标对象是从 CIndicator 扩展的):

bool CiHA::Create(const string symbol,const ENUM_TIMEFRAMES period,const ENUM_INDICATOR type,const int num_params,const MqlParam &params[],const int buffers)
  {
   NumBuffers(buffers);
   if(CIndicator::Create(symbol,period,type,num_params,params))
      return Initialize(symbol,period,num_params,params);
   return false;
  }

在这里,我们声明指标所有的缓冲区的数量,然后把它作为参数来初始化。指标的参数保存在结构中(MqlParam).

对于GetData 方法,两种语言的实现是不同的。在 MQL4 中, 直接调用 iCustom 函数可以得到指标在图表某个柱上的数值,而在 MQL5 中,调用指标的过程是不同的,iCustom 可以给出指标的句柄 (类似于文件操作中的工作). 为了访问 MetaTrader 5 指标在某个柱的数值,应该使用这个句柄,而不是调用 iCustom 函数,在这种情况下,我们把实现分开:

double CiHA::GetData(const int buffer_num,const int index) const
  {
   #ifdef __MQL5__
      return CiCustom::GetData(buffer_num,index);
   #else
      return iCustom(m_symbol,m_period,m_params[0].string_value,buffer_num,index);
   #endif
  }

请注意,在这个方法中,对于 MQL5 版本,我们简单地返回调用父方法 (CiCustom) 的结果,而在 MQL4 中,父方法 (CiCustom) 只会返回0, 所以我们必须扩展它,在其中实际调用 MQL4 的 iCustom 函数。因为 MQL4 函数不使用结构 (MqlParams) 来保存指标参数,调用这个函数几乎对每个自定义指标都是不同的。

在这个EA中 CSignal 的扩展上,和之前例子中比较也没有很大差别。对于构造函数,我们只要简单重新定义方法的参数,来使用所需的指标来估算信号,对于这个特定信号,我们只使用一个指标:

void SignalHA::SignalHA(const string symbol,const ENUM_TIMEFRAMES timeframe,const int numparams,const MqlParam &params[],const int bar)
  {
   m_symbol_name= symbol;
   m_signal_bar = bar;
   m_ha=new CiHA();
   m_ha.Create(symbol,timeframe,IND_CUSTOM,numparams,params,4);
   m_indicators.Add(m_ha);
  }

对于 Calculate 方法, 我们也需要把实现分开, 因为 MetaTrader 4 和 MetaTrader 5 的 Heiken Ashi 指标在它们缓冲区的安排上是不同的。对于前者,它是最低价/最高价,最高价/最低价,开盘价和收盘价占据了第一个(0号缓冲区),第二,第三和第四个缓冲区。而在 MQL5 版本中,排布是开盘价,最高价,最低价和收盘价。所以,我们在从指标中读取数值时,必须根据所使用的平台考虑到特定的缓冲区:

bool SignalHA::Calculate(void)
  {
   #ifdef __MQL5__
      m_open=m_ha.GetData(0,signal_bar);
   #else
      m_open=m_ha.GetData(2,signal_bar);
   #endif
      m_close=m_ha.GetData(3,signal_bar);
   return true;
  }

在 MQL5 版本中, HA 烛形的开盘价格是来自第一个缓冲区(缓冲区 0), 而对于 MQL4 版本, 它可以从第三个缓冲区中找到(缓冲区 2)。HA 烛形的收盘价格在两个版本中都是在第四个缓冲区(缓冲区 3)中读取的,所以我们把语句放到预处理声明之外。

对于信号的估算,我们总是必须根据所评估数值的特定标准来更新 LongCondition 和 ShortCondition 方法,为此我们使用 Heiken Ashi 的典型用法来检查信号柱是多头或是空头:

bool SignalHA::LongCondition(void)
  {
   return m_open<m_close;
  }

bool SignalHA::ShortCondition(void)
  {
   return m_open>m_close;
  }

这个 EA 交易的 OnTick 函数和之前例子中很类似,所以我们继续看 OnInit 函数:

int OnInit()
  {
//---
   order_manager=new COrderManager();
   symbol_manager=new CSymbolManager();
   symbol_info=new CSymbolInfo();
   if(!symbol_info.Name(Symbol()))
      Print("没有设置交易品种");
   symbol_manager.Add(GetPointer(symbol_info));
   order_manager.Init(symbol_manager,NULL);

   MqlParam params[1];
   params[0].type=TYPE_STRING;
   #ifdef __MQL5__
      params[0].string_value="Examples\\Heiken_Ashi";
   #else
      params[0].string_value="Heiken Ashi";
   #endif
      SignalHA *signal_ha=new SignalHA(Symbol(),0,1,params,signal_bar);
   signals=new CSignals();
   signals.Add(GetPointer(signal_ha));
   signals.Init(GetPointer(symbol_manager),NULL);
//---
   return(INIT_SUCCEEDED);
  }

在此我们注意到,Heiken Ashi 指标 ex4 文件的位置在使用的不同交易平台中是不同的。因为 MqlParams 要求保存的第一个参数应该使自定义指标的名称(不包括扩展名), 我们再一次需要把实现分开以指定第一个参数。在 MQL5 中, 指标默认位于 "Indicators\Examples\Heiken Ashi",而在 MQL4 中, 指标可以在 "Indicators\Heiken Ashi" 下找到。

以下的屏幕截图显示了在 MetaTrader 4 和 MetaTrader 5 上EA交易的测试结果。我们可以看到,尽管指标在图表上它们绘制的有些不同,但两个指标都有相同的逻辑,并且两个版本的EA交易都可以基于同样的逻辑来执行:

(MT4)

signal_ha (MT4)

(MT5)

signal_ha (MT5)

例子 #4: 一个基于 HA 和 MA 的 EA 交易

我们的最后一个例子是在EA交易中包含MA和HA指标的组合,在这个例子中没有太多不同,我们只是简单地把第二个和第三个例子中的类定义加上,然后把 CSignalMA 和 CSignalHA 的实例的指针加到 CSignals 的实例中。以下展示了使用这个EA交易的测试结果。

(MT4)

signal_ha_ma (MT4)

(MT5)

signal_ha_ma (MT5)

结论

在本文中,我们已经讨论了 CSignal 和 CSignals 类, 它们是用于跨平台的 EA 交易,在给定的分时中评估和处理整体信号的。这几个类设计用于信号的评估,代码是和EA交易的其他代码分离的。