以峰谷指标和 ATR 指标为例说明作为类来实施指标

Aleksandr Chugunov | 25 十二月, 2013

我们需要它来做什么?

MetaQuotes Software Corp. 在全新第 5 版的 MetaTrade 客户端中修订了处理自定义指标的概念。现在,它们以快得多的速度执行;每个指标只有一个具有唯一输入参数的例子,因此不管在哪怕一个交易品种的十个图表上使用其副本,它都仅计算一次。

但是一种算法保持不变。在与服务器的连接中断时,或在重大的历史数据同步中断时,prev_calculated 值(对于 MetaTrader 4 而言为 IndicatorCounted())被设置为零,从而导致完全针对整个历史数据重新计算指标(开发人员可能故意为之,以保证指标值在任何情形下都正确无误)。有几个因素能够影响指标的计算速度:

适合您的情形的因素越多,您面临的针对整个历史数据重新计算指标的问题就越有可能出现。此外,用于传输信息的通道较差也会导致情况变糟。

当然,您可以使用额外的输入参数限制指标计算的深度,但是使用 iCustom 指标有一个细微差别。任意图表或任意自定义指标使用的最大柱数是在全局范围内针对整个客户端设置的。内存是针对每个自定义指标缓冲区分配的,并且仅受 TERMINAL_MAXBARS 的限制。

然而,有一个重大的添加 - 如果您在指标的算法中限制所计算的柱的最大数量(例如使用一个输入参数或直接在代码中进行限制),则会在每个新的柱出现时动态分配内存(逐渐增加到指定的 TERMINAL_MAXBARS 限制,或者更多 - 此算法完全取决于开发人员,他们可以在下一版本中进行更改)。


避免针对整个历史数据重新计算指标的方法

目前,我发现解决此问题的方法如下:
  1. 要求 MetaQuotes 在平台级别修订此问题
  2. 为实施 prev_calculated 的对等创建一个单独的类

假定您能够在指标内建立 prev_calculated 的算法,则还有其他情形,但是似乎与 MetaTrader 4 明显不同的 MetaTrader 5 在将 prev_calculated 设为零时“清除”所有指标缓存(即它强行将所有指标数组设为零;您无法控制它,因为此行为是在平台级别实施的)。

让我们单独分析每种情形。


解决该问题的第二种情形的优点和缺点

优点:
缺点:


为实施 prev_calculated 的对等创建 CCustPrevCalculated 类

类的实施本身不包含要说明的任何主题事项。算法除了考虑向两侧扩展历史数据以外,还考虑是否能够从左侧“裁切”数据。此算法还能够处理在计算得出的数据中插入历史数据(对于 MetaTrader 4,这是真的;而在 MetaTrader 5 中,我还没有遇到过)。文件 CustPrevCalculated.mqh 提供了该类的源代码。

让我告诉您关键的地方。


创建对数组元素的环形存取

为了创建此类,我们将使用一种非传统的方法 - 针对数组的一次性分配的内存,对数组元素进行环形存取,避免过多的数组复制程序。让我们通过 5 个元素的例子来进行说明:


对数组元素的环形存取


 
最初,我们处理从 0 开始编号的数组。但是如果我们需要添加下一个值并保持数组大小不变,我们应怎么办(添加新的柱)?有两种方式:

要实施第二种情形,我们需要一个变量 ,让我们将其称为 DataStartInd;它将存储数组零索引的位置。为了便于以后的计算,其编号将对应于数组的通常索引(即从零开始)。在变量 BarsLimit 中,我们将存储数组元素的数量。因此,将使用以下简单公式计算虚拟索引 'I' 的数组元素的实际地址:

变量 DataBarsCount 存储实际使用的内存单元格的数量(例如,我们可以只使用 5 个单元格中的 3 个)。


历史数据同步的算法

对于我自己而言,我已经选择并实施了历史副本(本地历史)与客户端中的历史进行同步的三种算法模式:

同步机制本身取决于程序员设置的另一个参数 - HSMinute(存储为 HistorySynchSecond)。我们假定 Dealer Center(交易中心)只能纠正最后 HSMinute 分钟的历史。如果在该时间范围内的同步期间没有找到差异,则将历史数据视为是相同的并停止比较。如果找到差异,则检查并纠正整个历史数据。

此外,该算法允许在初始化时从指定的结构 MqlRates 仅检查价格/点差/量。例如,要绘制峰谷指标,我们只需要最高价和最低价。


CCustPrevCalculated 类的实际使用

要对 CCustPrevCalculated 类进行初始化,我们需要调用函数 InitData(),该函数在成功时返回 'true':
CCustPrevCalculated CustPrevCalculated;
CustPrevCalculated.InitData(_Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15);
要同步历史数据,我们需要调用函数 PrepareData():
CPCPrepareDataResultCode resData;
resData = CustPrevCalculated.PrepareData();

函数 PrepareData() 能够返回的值的类型:

enum CPCPrepareDataResultCode
  {
   CPCPDRC_NoData,                     // Returned when there is no data for calculation (not prepared by the server)
   CPCPDRC_FullInitialization,         // Full initialization of the array has been performed
   CPCPDRC_Synch,                      // Synchronization with adding new bars has been performed
   CPCPDRC_SynchOnlyLastBar,           // Synchronization of only the last bar has been performed (possible cutting of the history)
   CPCPDRC_NoRecountNotRequired        // Recalculation has not been performed, since the data was not changed
  };


用于数据存取的 CCustPrevCalculated 类的函数

注:为了加快计算,不包括对数组溢出的检查。为了更加精确,如果索引不正确,则会返回错误的值。

名称
用途
 uint GetDataBarsCount()
 返回可用柱的数量
 uint GetDataBarsCalculated()
 返回未改变的柱的数量
 uint GetDataStartInd()
 返回用于环绕存取的索引(对于自定义指标)
 bool GetDataBarsCuttingLeft()
 返回从左侧裁切柱的结果
 double GetDataOpen(int shift, bool AsSeries)
 为偏移柱返回 'Open'(开盘价)
 double GetDataHigh(int shift, bool AsSeries)
 为偏移柱返回 'High'(最高价)
 double GetDataLow(int shift, bool AsSeries)
 为偏移柱返回 "Low"(最低价)
 double GetDataClose(int shift, bool AsSeries)
 为偏移柱返回 'Close'(收盘价)
 datetime GetDataTime(int shift, bool AsSeries)
 为偏移柱返回 'Time'(时间)
 long GetDataTick_volume(int shift, bool AsSeries)
 为偏移柱返回 'Tick_volume'(交易量)
 long GetDataReal_volume(int shift, bool AsSeries)
 为偏移柱返回 'Real_volume'(实际交易量)
 int GetDataSpread(int shift, bool AsSeries)
 为偏移柱返回 'Spread'(点差)


进一步优化 CCustPrevCalculated 类的例子


依据 CCustPrevCalculated 类的数据创建用于计算自定义峰谷指标的 CCustZigZagPPC

此算法依据自定义指标专业峰谷指标。类的源代码位于 ZigZags.mqh 文件中;此外,库 OutsideBar.mqh 用于处理额外的柱。

为了说明我们的指标的一个柱,让我们创建一个单独的结构:

struct ZZBar
  {
   double UP, DN;                      // Buffers of the ZigZag indicator
   OrderFormationBarHighLow OB;       // Buffer for caching of an external bar
  };

也让我们确定类的计算返回的结果:

enum CPCZZResultCode
  {
   CPCZZRC_NotInitialized,             // Class is no initialized
   CPCZZRC_NoData,                     // Faield to receive data (including the external bar)
   CPCZZRC_NotChanged,                 // No changes of ZZ rays
   CPCZZRC_Changed                     // ZZ rays changed
  };

要对 CCustZigZagPPC 类进行初始化,我们需要调用一次 Init() 函数;该函数在成功时返回 'true':

CCustZigZagPPC ZZ1;
ZZ1.Init(CustPrevCalculated, _Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15, 0, true, 12, 10);

要计算指标,我们需要依据先前计算出来的 CCustPrevCalculated 类的数据开始更新数据:

CPCPrepareDataResultCode resZZ1;
resZZ1 = ZZ1.PrepareData(resData);

然后调用程序 Calculate():

if ( (resZZ1 != CPCPDRC_NoData) && (resZZ1 != CPCPDRC_NoRecountNotRequired) )
   ZZ1.Calculate();

在文件 ScriptSample_CustZigZagPPC.mq5 中提供了一起使用一个 CCustPrevCalculated 类和几个 CCustZigZagPPC 类的完整例子。


CCustZigZagPPC 类的数据存取函数

名称
用途
 uint GetBarsCount()
 返回可用柱的数量
 uint GetBarsCalculated()  返回计算出来的柱的数量
 double GetUP(uint shift, bool AsSeries)
 返回柱的峰谷指标的峰值
 double GetDN(uint shift, bool AsSeries)
 返回柱的峰谷指标的谷值
 OrderFormationBarHighLow GetOB(uint shift, bool AsSeries)  返回柱的 'Outside'(外部)值


图形和程序检查

为了进行图形检查,让我们将原来的指标附加到一个图表,并且在其上附加专门编写的具有相同输入参数的测试指标 Indicator_CustZigZag.mq5(但是您应选择其他颜色以查看两个指标);以下是运行结果:

红色 - 原来的指标,蓝色 - 我们自己的指标,使用最后 100 根柱计算。

我们可以在 EA 交易中以同样的方式比较它们;这样会有区别吗?在测试 EA 交易 Expert_CustZigZagPPC_test.mq5 中,每一次价格变动都会比较从 iCustom("AlexSTAL_ZigZagProf") 获得的结果以及从 CCustZigZagPPC 类获得的结果。计算信息显示在日志中(在第一根柱上可能没有计算,因此缺乏算法需要的历史数据):

(EURUSD,M1)                1.35797; 1.35644; 1.35844; 1.35761; 1.35901; 1.35760; 1.35959; 1.35791; 1.36038; 1.35806; 1.36042; 1.35976; 1.36116; 1.35971; // it is normal
(EURUSD,M1) Tick processed: 1.35797; 1.35644; 1.35844; 1.35761; 1.35901; 1.35760; 1.35959; 1.35791; 1.36038; 1.35806; 1.36042; 1.35976; 1.36116; 
(EURUSD,M1) Divergence on the bar: 7 

让我们更加详细地考虑此 EA 交易。确定工作需要的全局变量

#include <ZigZags.mqh>

CCustPrevCalculated CustPrevCalculated;
CCustZigZagPPC ZZ1;
int HandleZZ;

对变量进行初始化:

int OnInit()
  {
   // Creating new class and initializing it
   CustPrevCalculated.InitData(_Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15);
   
   // Initializing the class ZZ
   ZZ1.Init(GetPointer(CustPrevCalculated), _Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15, 0, true, 12, 10);
   
   // Receiving handle for the custom indicator
   HandleZZ = iCustom(_Symbol, _Period, "AlexSTAL_ZigZagProf", 12, 10, 0 , true);
   Print("ZZ_handle = ", HandleZZ, "  error = ", GetLastError());

   return(0);
  }
在 EA 交易中处理价格变动:
void OnTick()
  {
   // Calculation of data
   CPCPrepareDataResultCode resData, resZZ1;
   resData = CustPrevCalculated.PrepareData();
   
   // Start recalculation for each indicator! PrepareData obligatory!
   resZZ1 = ZZ1.PrepareData(resData);
   
   // Расчет данных ZZ1
   if ( !((resZZ1 != CPCPDRC_NoData) && (resZZ1 != CPCPDRC_NoRecountNotRequired)) )
      return;

   // Получим результаты расчета
   ZZ1.Calculate();

现在,我们具有用 CCustZigZagPPC 计算出来的 ZZ1.GetBarsCalculated() 根柱。让我们添加用于比较 iCustom("AlexSTAL_ZigZagProf") 和 CCustZigZagPPC 类的数据的代码:

   int tmpBars = (int)ZZ1.GetBarsCalculated();
   double zzUP[], zzDN[];
   CopyBuffer(HandleZZ, 0, 0, tmpBars, zzUP);
   CopyBuffer(HandleZZ, 1, 0, tmpBars, zzDN);
   
   // Perform comparison
   string tmpSt1 = "", tmpSt2 = "";
   for (int i = (tmpBars-1); i >= 0; i--)
     {
      double tmpUP = ZZ1.GetUP(i, false);
      double tmpDN = ZZ1.GetDN(i, false);
      if (tmpUP != zzUP[i])
         Print("Divergence on the bar: ", i);
      if (tmpDN != zzDN[i])
         Print("Divergence on the bar: ", i);
      if (tmpUP != EMPTY_VALUE)
         tmpSt1 = tmpSt1 + DoubleToString(tmpUP, _Digits) + "; ";
      if (tmpDN != EMPTY_VALUE)
         tmpSt1 = tmpSt1 + DoubleToString(tmpDN, _Digits) + "; ";

      if (zzUP[i] != EMPTY_VALUE)
         tmpSt2 = tmpSt2 + DoubleToString(zzUP[i], _Digits) + "; ";
      if (zzDN[i] != EMPTY_VALUE)
         tmpSt2 = tmpSt2 + DoubleToString(zzDN[i], _Digits) + "; ";
     }
  Print("Tick processed: ", tmpSt1);
  Print("                              ", tmpSt2);
  }

这是 CCustZigZagPPC 类在一个 EA 交易或脚本中的简单实践运用。直接存取函数 GetUP()、GetDN()、GetOB() 代替了 CopyBuffer()


将我们的指标移到一个单独的类(以 iATR 为例)

以文件 ZigZags.mqh 为基础,我创建了 MyIndicator.mqh 模板以便依据上述原则快速开发自定义指标。

一般计划:

1. 准备阶段。

2. 选择要从初始(原来的)指标取值到类的外部参数,然后声明它们并对其进行初始化。

在我的例子中,ATR 指标有一个外部参数:
input int InpAtrPeriod=14;  // ATR period
class CCustATR
  {
protected:
   ...
   uchar iAtrPeriod;
   ...
public:
   ...
   bool Init(CCustPrevCalculated *CPC, string Instr, ENUM_TIMEFRAMES TF, int Limit, CPCHistorySynchMode HSM, uchar HS, uint HSMinute, uchar AtrPeriod);
bool CCustATR::Init(CCustPrevCalculated *CPC, string Instr, ENUM_TIMEFRAMES TF, int Limit, CPCHistorySynchMode HSM, uchar HS, uint HSMinute, uchar AtrPeriod)
{
      ...
      BarsLimit = Limit;
      iAtrPeriod = AtrPeriod;
      ...

3. 确定初始指标中需要的缓存数量,然后在我们的类中声明它们。同时声明用于返回 INDICATOR_DATA 缓存的函数。

struct ATRBar
  {
   double Val;                          // Indicator buffers
  };

更改成我们的结构:

struct ATRBar
  {
   double ATR;
   double TR;
  };
CPCPrepareDataResultCode CCustATR::PrepareData(CPCPrepareDataResultCode resData)
{
   ...
   for (uint i = (DataBarsCalculated == 0)?0:(DataBarsCalculated+1); i < DataBarsCount; i++)
     {
      Buf[PInd(i, false)].ATR = EMPTY_VALUE;
      Buf[PInd(i, false)].TR = EMPTY_VALUE;
     }
   ...

(如果只有一个缓存,则可以跳过更改)将

class CCustATR
  {
   ...
   double GetVal(uint shift, bool AsSeries);                      // returns the Val value of the buffer for a bar
   ...

改为

class CCustATR
  {
   ...
   double GetATR(uint shift, bool AsSeries);                      // Возвращает значение буфера ATR для бара
   ...

并且更改对应函数的代码:

double CCustATR::GetATR(uint shift, bool AsSeries)
{
   if ( shift > (DataBarsCount-1) )
      return(EMPTY_VALUE);
   return(Buf[PInd(shift, AsSeries)].ATR);
}

注:代替几个返回缓存值的函数,您可以仅使用一个具有额外参数(缓存的编号或名称)的函数。


4. 将初始指标的 OnCalculate() 函数的逻辑复制到类的对应函数

CPCATRResultCode CCustATR::Calculate()
{
   ...
   // Check if there are enough bars for the calculation
   if (DataBarsCount <= iAtrPeriod)
      return(CPCATRRC_NoData);
   ...
   if ( DataBarsCalculated != 0 )
      BarsForRecalculation = DataBarsCount - ATRDataBarsCalculated - 1;
   else
     {
      Buf[PInd(0, false)].TR = 0.0;
      Buf[PInd(0, false)].ATR = 0.0;
      //--- filling out the array of True Range values for each period
      for (uint i = 1; i < DataBarsCount; i++)
         Buf[PInd(i, false)].TR = MathMax(CustPrevCalculated.GetDataHigh(i, false), CustPrevCalculated.GetDataClose(i-1, false)) - 
                                  MathMin(CustPrevCalculated.GetDataLow(i, false), CustPrevCalculated.GetDataClose(i-1, false));
      //--- first AtrPeriod values of the indicator are not calculated
      double firstValue = 0.0;
      for (uint i = 1; i <= iAtrPeriod; i++)
        {
         Buf[PInd(i, false)].ATR = 0;
         firstValue += Buf[PInd(i, false)].TR;
        }
      //--- calculating the first value of the indicator
      firstValue /= iAtrPeriod;
      Buf[PInd(iAtrPeriod, false)].ATR = firstValue;
      
      BarsForRecalculation = DataBarsCount - iAtrPeriod - 2;
     }
   for (uint i = (DataBarsCount - BarsForRecalculation - 1); i < DataBarsCount; i++)
     {
      Buf[PInd(i, false)].TR = MathMax(CustPrevCalculated.GetDataHigh(i, false), CustPrevCalculated.GetDataClose(i-1, false)) - 
                               MathMin(CustPrevCalculated.GetDataLow(i, false), CustPrevCalculated.GetDataClose(i-1, false));
      Buf[PInd(i, false)].ATR = Buf[PInd(i-1, false)].ATR + (Buf[PInd(i, false)].TR-Buf[PInd(i-iAtrPeriod, false)].TR) / iAtrPeriod;
      ...

以上便是全部内容。我们的类已经创建完毕。对于图形检查,您可以创建一个测试指标(在我的例子中为 Indicator_ATRsample.mq5):



在校对本文时我形成了一个想法,如果您使用 CCustPrevCalculated 类以及唯一一个自定义指标,则您可以将这个类的创建、初始化和同步整合到自定义指标中(在我的例子中,它们为 CCustZigZagPPC 和 CCustATR)。当为此目的调用自定义指标的初始化函数时,您需要使用对象的零指针:

   ATR.Init(NULL, _Symbol, _Period, iBars, CPCHSM_Normal, 0, 30, InpAtrPeriod);

此时,一般结构

#include <CustPrevCalculated.mqh>
#include <ATRsample.mqh>
CCustPrevCalculated CustPrevCalculated;
CCustATR ATR;

int OnInit()
  {
   CustPrevCalculated.InitData(_Symbol, _Period, iBars, CPCHSM_Normal, 0, 30);
   ATR.Init(GetPointer(CustPrevCalculated), _Symbol, _Period, iBars, CPCHSM_Normal, 0, 30, InpAtrPeriod);
  }

int OnCalculate(...)
  {
   CPCPrepareDataResultCode resData = CustPrevCalculated.PrepareData();
   CPCPrepareDataResultCode resATR = ATR.PrepareData(resData);
   if ( (resATR != CPCPDRC_NoData) && (resATR != CPCPDRC_NoRecountNotRequired) )
      ATR.Calculate();
  }

将简化为:

#include <ATRsample.mqh>
CCustATR ATR;

int OnInit()
  {
   ATR.Init(NULL, _Symbol, _Period, iBars, CPCHSM_Normal, 0, 30, InpAtrPeriod);
  }

int OnCalculate(...)
  {
   ATR.Calculate();
  }
在文件 Indicator_ATRsample2.mq5 中提供了一个实例。

所介绍的技术在策略测试程序中进行测试时对性能的影响

为了进行检查,我创建了一个测试 EA 交易 (TestSpeed_IndPrevCalculated.mq5),该 EA 依据三种情形之一在每一次价格变动时接收零柱指标的值:

enum eTestVariant
  {
   BuiltIn,    // Built-in indicator iATR
   Custom,     // Custom indicator iCustom("ATR")
   IndClass    // Calculation in the class
  };

此 EA 交易将在 1 个代理上运行 10 次,并采用以下优化参数:

我测量了使用三种指标情形中的每一种时优化的时间。检查结果显示为一个线性直方图。

ATR 指标的三类实施的优化时间

用于测量优化时间的 EA 交易的源代码:

//+------------------------------------------------------------------+
//|                                  TestSpeed_IndPrevCalculated.mq5 |
//|                                         Copyright 2011, AlexSTAL |
//|                                           http://www.alexstal.ru |
//+------------------------------------------------------------------+
#property copyright "Copyright 2011, AlexSTAL"
#property link      "http://www.alexstal.ru"
#property version   "1.00"
//--- connect the include file with the CustATR class
#include <ATRsample.mqh>
//--- set the selection of the parameter as an enumeration
enum eTestVariant
  {
   BuiltIn,    // Built-in indicator iATR
   Custom,     // Custom indicator iCustom("ATR")
   IndClass    // Calculation withing the class
  };
//--- input variables
input eTestVariant TestVariant;
input int          FalseParameter = 0;
//--- period of the ATR indicator
const uchar        InpAtrPeriod = 14;
//--- handle of the built-in or custom indicator
int                Handle;
//--- indicator based on the class 
CCustATR           *ATR;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //---
   switch(TestVariant)
     {
      case BuiltIn:
         Handle = iATR(_Symbol, _Period, InpAtrPeriod);
         break;
      case Custom:
         Handle = iCustom(_Symbol, _Period, "Examples\ATR", InpAtrPeriod);
         break;
      case IndClass:
         ATR = new CCustATR;
         ATR.Init(NULL, _Symbol, _Period, 100, CPCHSM_Normal, 0, 30, InpAtrPeriod);
         break;
     };
   //---
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   switch(TestVariant)
     {
      case IndClass:
         delete ATR;
         break;
     };
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   double tmpValue[1];
   switch(TestVariant)
     {
      case BuiltIn:
         CopyBuffer(Handle, 0, 0, 1, tmpValue);
         break;
      case Custom:
         CopyBuffer(Handle, 0, 0, 1, tmpValue);
         break;
      case IndClass:
         ATR.Calculate();
         tmpValue[0] = ATR.GetATR(0, true);
         break;
     };
  }
//+------------------------------------------------------------------+

如我们所见,与使用普通的自定义指标相比,在策略测试程序中进行测试时,此技术并没有显著降低性能。


有关实际使用此技术的说明


总结

在每一种情形中,程序员都应考虑任务的不同实施方法所具有的优点和缺点。本文所建议的实施也有其自己的优点和缺点。

注:不犯错的人必将一事无成!如果您找到错误,请告诉我。