请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化策略 帖子:3352800 新帖:0

将指标应用至其他指标

特朗普对头发表于:2 月 10 日 18:00回复(1)

简介

让我们考虑改进指标的任务,即将指标应用至其他指标的值。在本文中,我们将继续使用“真实强弱指数”(TSI) 为例,该指标已在前文中进行了创建和讨论(请参见前文“MQL5:创建自己的指标”)。

基于其他指标的值的自定义指标

在编写使用 OnCalculate() 函数调用的简短形式的指标时,您可能会忽略这样一个事实,即指标的计算不仅可以通过价格数据完成,还可以通过其他指标(无论是内置指标还是自定义指标)的数据实现。

让我们做一个简单的尝试:将内置 RSI 指标及标准设置附于图表,并将 True_Strength_Index_ver2.mq5 自定义指标拖至 RSI 指标窗口中。在显示窗口的 Parameters(参数)选项卡中,指定指标应应用至 Previous Indicator's Data(上一指标数据)(RSI(14))。

结果与我们的预期将大相庭径。TSI 指标的附加线未在 RSI 指标窗口中出现,且在“数据窗口”中您会发现其值也同样不清楚。

尽管事实如此,RSI 的值的定义几乎贯穿整个历史数据,但 TSI 的值(应用至 RSI 数据)要么完全缺失(在开始时)要么始终为 -100:

此现象由 begin 参数的值未在 True_Strength_Index_ver2.mq5 的 OnCalculate() 中到处使用的事实导致。begin 参数指定 price[] 输入参数中空值的数量。这些空值无法用于指标值的计算。让我们回忆一下 OnCalculate() 函数调用第一种形式的定义。
int OnCalculate (const int rates_total,      // price[]数组大小;
                 const int prev_calculated,  // 上次调用计算后的价格柱的数量
                 const int begin,            // price[]数组开始计算的索引
                 const double& price[]       // 指标计算的依据数组
   );

在将指标应用至指定价格常量之一的价格数据时,begin 参数等于 0,因为各个柱均有指定的价格类型。因此,price[] 输入数组始终从其第一个元素 price[0] 起具有正确的数据。但是,如果我们指定其他指标的数据作为计算源,这一点将不再得到保证。

OnCalculate() 的 begin 参数

如果计算是使用其他指标的数据执行,让我们检查一下 price[] 数组包含的值。为此,我们将在 OnCalculate() 函数中添加一些代码,以便输出我们希望检查的值。现在 OnCalculate() 函数的起始部分如下所示:

//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                  |
//+------------------------------------------------------------------+
int OnCalculate (const int rates_total,    // price[]数组大小;
                 const int prev_calculated,// 上次调用计算后的价格柱的数量;
                 const int begin,          // price[]数组开始计算的索引 
                 const double &price[])    // 指标计算的依据数组;
  {
//--- 确定price[]数组数值仅输出一次的标志
   static bool printed=false;
//--- 如果begin变量不为0,有些数值不应该被计算在内

   if(begin>0 && !printed)
     {
      //--- 让我们输出这个数值
      Print("用于计算的数据索引begin等于 ",begin,
            "   price[] 数组长度 =",rates_total);

      //--- 让我们显示不应该计算的数值
      for(int i=0;i<=begin;i++)
        {
         Print("i =",i,"  value =",price[i]);
        }
      //--- 设置标志确定我们已经记录了这些数值
      printed=true;
     }

让我们再次将修改后的指标拖至 RSI(14) 窗口,然后指定将上一指标的数据用于计算。随后我们将会看到未被绘制且不应用于计算的值,此时使用 RSI(14) 指标的值。


指标缓冲区中的空值和 DBL_MAX

price[] 数组的前 14 个元素(索引从 0 至 13,含 0 和 13)具有相同的值 1.797693134862316e+308。该值是内置 EMPTY_VALUE 常量的数字值,出现极为频繁,用于指出指标缓冲区中的空值。

在空值中填入零并不是通用的解决方案,因为该值可能是其他指标的计算结果。由于上述原因,客户端的所有内置指标均返回该数值为空值。之所以选择值 1.797693134862316e+308 是因为它是可能的最大双精度型值,另一个原因是它可以作为 DBL_MAX 常量方便地在 MQL5 中呈现。

要检查某个双精度型数值是否为,可将其与 EMPTY_VALUE 或 DBL_MAX 常量进行比较。两个变量的值相等,但为了使代码一目了然,最好是使用 EMPTY_VALUE 常量。

//+------------------------------------------------------------------+
//| 针对空值返回 true                                                |
//+------------------------------------------------------------------+
bool isEmptyValue(double value_to_check)
  {
//--- 如果数值等于 DBL_MAX, 表明是个空值
   if(value_to_check==EMPTY_VALUE) return(true);
//--- 不等于 DBL_MAX
   return(false);
  }

DBL_MAX 是一个很大的数值,RSI 指标自身无法返回此数值!该数组只有第十五个元素(索引为 14)是值为 50 的合理值。因此,即使我们对作为数据源用于计算的指标一无所知,在此情况下我们也可以使用 begin 参数正确组织数据的处理。更确切地说,我们必须在计算中避免使用这些空值

begin 参数和 PLOT_DRAW_BEGIN 属性的关系

需要注意的是,传递至 OnCalculate() 函数的 begin 参数和定义未绘制的初始柱的数量的 PLOT_DRAW_BEGIN 属性有密切的关系。如果我们从 MetaTrader5 的标准封装中细读 RSI 的源代码,我们将在 OnInit() 函数中发现以下代码:
//--- 设定从哪一个柱开始画指标
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,ExtPeriodRSI);

这表示索引 0 的图形绘制仅从索引为 ExtPeriodRSI(这是指定 RSI 指标时间周期的输入变量)的柱开始,且没有更早的柱的绘图。

在 MQL5 语言中,基于指标 B 的数据的指标 A 的计算始终在指标 B 的零缓冲区值上执行。指标 B 的零缓冲区值作为 price[] 输入参数传递至指标 A 的 OnCalculate() 函数。零缓冲区将通过 SetIndexBuffer() 函数固定分配至零图形绘制。因此:

PLOT_DRAW_BEGIN 属性传递至 begin 参数的规则为对于基于其他(基础)指标 B 的数据的自定义指标 A 的计算,OnCalculate() 函数中 begin 输入参数的值始终等于基础指标 B 的零图形绘制的 PLOT_DRAW_BEGIN 属性的值。

因此,如若我们已使用时间周期 14 创建 RIS 指标(指标 B),然后基于其数据创建自定义指标“真实强弱指数”(指标 A),则:

  • 由于 PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,14),RSI (14) 指标的绘制从第 14 个柱开始;
  • OnCalculate() 函数的 price[] 输入数组包含 RSI 指标的零缓冲区的值;
  • TSI 指标的 OnCalculate() 函数的 begin 输入参数的值从 RSI 指标的零图形绘制的 PLOT_DRAW_BEGIN 属性获得。

请记住,TSI 指标并不是从图表的开始处绘制,因为部分最初的柱的指标值并未确定。将在 TSI 指标中作为线条绘制的第一个柱索引等于 r+s-1,其中:

  • r - MTMBuffer[] 和 AbsMTMBuffer[] 数组第一次指数平滑至相应的 EMA_MTMBuffer[] 和 EMA_AbsMTMBuffer[] 数组的时间周期;
  • s - EMA_MTMBuffer[] 和 EMA_AbsMTMBuffer[] 数组后续平滑的时间周期。

对于索引小于 r+s-1 的柱,没有用于绘制 TSI 指标的值。因此,对于用于计算 TSI 指标的最终 MA2_MTMBuffer[] 和 EMA2_AbsMTMBuffer[] 数组,数据具有额外偏移并起始于索引 r+s-1。您可在“MQL5:创建自己的指标”一文中获得更多信息。

在 OnInit() 函数中,有一则语句用于禁止绘制前 r+s-1 个柱:

//--- 设定从哪一个柱开始画指标
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,r+s-1);

随着输入数据开始向前平移 begin 个柱,我们必须将其纳入考虑并在 OnCalculate() 函数中将数据绘制的起始位置增大 begin 个柱:

   if(prev_calculated==0)
     { 
      //--- 让我们把起始位置增加begin柱数,
      //--- 因为我们引用其他指标进行计算
      if(begin>0)PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,begin+r+s-1);
     }
现在,我们在 TSI 指标的值的计算中将 begin 参数纳入考虑。此外,如果其他指标将使用 TSI 的值进行下述计算,begin 参数将正确传递:beginother_indicator=beginour_indicator+r+s-1。因此,我们可以制定指标利用其他指标的值的规则:

指标利用规则: 如果自定义指标 A 从位置 Na 开始绘制(未绘制前 Na 个值)并基于从位置 Nb 开始绘制的其他指标 B 的数据,则结果指标 A{B} 将从位置 Nab=Na+Nb 开始绘制,其中 A{B} 表示指标 A 在指标 B 的零缓冲区值上计算。

因此,TSI (25,13) {RSI (14)} 表示 TSI (25,13) 指标由 RSI (14) 指标的值组成。作为利用的结果,数据的起始位置现在为 (25+13-1)+14=51。换言之,指标的绘制将会从第 52 个柱开始(柱的索引从 0 开始)。

添加 begin 值以用于指标计算

现在我们知道,price[] 数组有意义的值始终起始于由 begin 参数指定的位置。那么我们来逐步修改代码。首先是计算 MTMBuffer[] 和 AbsMTMBuffer[] 数组值的代码。在缺失 begin 参数的情况下,数组填入起始于索引 1。

//--- 计算 mtm 和 |mtm| 的数值
   int start;
   if(prev_calculated==0) start=1;  // 设置从MTMBuffer[] 和 AbsMTMBuffer[] 的索引1开始计算填写数据 
   else start=prev_calculated-1;    // 设置从上一次计算的最后一个索引开始计算填写数据
   for(int i=start;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

现在我们将要从 (begin+1) 位置开始,并如下修改代码(代码更改以粗体标示):

//--- calculate values for mtm and |mtm|
   int start;
   if(prev_calculated==0) start=begin+1;  // 设置从 MTMBuffer[] 和 AbsMTMBuffer[] 的索引begin+1开始计算填写数据 
   else start=prev_calculated-1;           // 设置从上一次计算的最后一个索引开始计算填写数据

   for(int i=start;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

由于从 price[0] 至 price[begin-1] 的值无法用于计算,我们从 price[begin] 开始。为 MTMBuffer[] 和 AbsMTMBuffer[] 数组计算的最初的值将如下所示:

      MTMBuffer[begin+1]=price[begin+1]-price[begin];
      AbsMTMBuffer[begin+1]=fabs(MTMBuffer[begin+1]);

为此,for 循环中的 start 变量现在具有初始值 start=begin+1 而不是 1

依赖数组的 begin 说明

接下来是 MTMBuffer[] 和 AbsMTMBuffer[] 数组的指数平滑。规则同样简单:如果基础数组的初始位置增大 begin 个柱,则所有依赖数组的初始位置应同样增大 begin 个柱。

//--- 计算数组的首要移动平均
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,               // 索引, 开始从哪个数据开始平滑计算  
                         r,               //指数平均的周期
                          MTMBuffer,       // 计算平均的源缓冲区
                         EMA_MTMBuffer);  // 目标缓冲区
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

此处 MTMBuffer [] 和 AbsMTMBuffer [] 为基础数组,且这些数组中的计算值现在的起始索引将增大 begin。所以,我们就将该偏移添加至 ExponentialMAOnBuffer() 函数。

现在,该部分代码块如下所以:

//--- 计算数组的首要移动平均
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         begin+1,        // 数组中开始元素的索引 
                         r,               // 指数平均的周期
                         MTMBuffer,       // 计算平均的源缓冲区
                         EMA_MTMBuffer);  // 目标缓冲区
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         begin+1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

如您所见,整个修改就是适应由 begin 参数定义的起始数据位置的增大。一点也不复杂。我们将以同样的方式修改第二部分的平滑代码块。

修改前:

//--- 计算数组的次要移动平均
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);

修改后:

//--- 计算数组的次要移动平均
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         begin+r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         begin+r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);

我们将以同样的方式修改最后的计算代码块。

修改前:

//--- 现在计算指标的数值
   if(prev_calculated==0) start=r+s-1; // 设置输入数组的初始索引
   else start=prev_calculated-1;       // 从上一次计算结束的位置开始 
   for(int i=start;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }

修改后:

//--- calculating values of our indicator
   if(prev_calculated==0) start=begin+r+s-1; // 设置输入数组的初始索引
   else start=prev_calculated-1;              // 从上一次计算结束的位置开始
   for(int i=start;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }

指标的调整结束,现在它将在 OnCalculate() 中跳过 price[] 输入数组的前 begin 个空值,并将该省略引起的偏移考虑在内。但我们必须记住,其他某些指标可将 TSI 值用于计算。为此,我们需要将指标的空值设置为 EMPTY_VALUE。

指标缓冲区初始化是否必要?

在 MQL5 中,默认情况下数组未使用定义的值进行初始化。这同样适用于由 SetIndexBuffer() 函数指定给指标缓冲区的数组。如果数组是指标缓冲区,其大小将取决于 OnCalculate() 函数中的 rates_total 参数的值。

您可能倾向于,例如在 OnCalculate() 开始时,使用 EMPTY_VALUE 值通过 ArrayInitialize() 函数初始化所有指标缓冲区:

//--- 如果是第一次调用 OnCalculate() 
   if(prev_calculated==0)
     {
      ArrayInitialize(TSIBuffer,EMPTY_VALUE);
     }

但我们不推荐这样做,原因如下:在客户端工作时,为交易品种(其数据用于计算指标)接收到新的报价。在一段时间后,柱的数量将增大,因此客户端将为指标缓冲区保留额外的内存。

但新(“附加的”)数组元素的值可能具有任何值,因为在为 any 数组重新分配内存的过程中并未执行初始化。最初的初始化可提供您一个虚假的肯定,所有未明确定义的数组元素将会填入在初始化过程中指定的值。这当然不是事实,永远也不要认为变量的数字值或某些数组元素将使用我们必需的值进行初始化。

您应为指标缓冲区的每个元素设置值。若某些柱的值未通过指标算法进行定义,您应使用空值对其进行明确设置。例如,如果指标缓冲区的某些值通过除法运算计算,在某些情况下除数可能为零。

我们知道,除数为零在 MQL5 中是一个严重运行时错误,并会导致 MQL5 程序的立即终止。因此有必要为该缓冲区元素设置值,而不是通过在代码中处理这一个特殊情况来避免除数为零。也许,使用我们为此图形样式作为空值分配的值会更好。

例如,对于某些图形样式,我们使用 PlotIndexSetDouble() 函数将零定义为空值:

   PlotIndexSetDouble(plotting_style_index,PLOT_EMPTY_VALUE,0);   

则对于此图形中的指标缓冲区的所有空值,明确定义零值是必要的:

   if(divider==0)
      IndicatorBuffer[i]=0;
   else
      IndicatorBuffer[i]=... 

此外,如果已为某些图形指定了 DRAW_BEGIN,指标缓冲区的索引从 0 至 DRAW_BEGIN 的所有元素将自动填入零。

小结

下面,我们作一个简短的总结。有一些必要的条件用于基于其他指标的数据的指标的正确计算(并使其适于在其他 MQL5 程序中使用):

  1. 内置指标中的空值使用 EMPTY_VALUE 常量的值填入,该值与双精度型的最大值 (DBL_MAX) 完全相等。
  2. 有关指标有意义值的起始索引的详细内容,您应分析 begin 输入参数(OnCalculate() 的简短 形式)。
  3. 为禁止针对图形 样式的前 N 个值的绘制,使用以下代码设置 DRAW_BEGIN 参数:
    PlotIndexSetInteger(plotting_style_index,PLOT_DRAW_BEGIN,N);
  4. 如果为某些图形指定 DRAW_BEGIN,所有索引从 0 至 DRAW_BEGIN 的指标缓冲区元素将自动使用空值填入(默认为 EMPTY_VALUE)。
  5. 在 OnCalculate() 函数中加入大小为 begin 个柱的额外偏移,以在您自己的指标中正确使用其他指标数据:
    //--- 如果这是第一次调用 
       if(prev_calculated==0)
         { 
          //--- 把数据起始点增加 begin 个柱数, 
          //--- 因为引用了其他指标进行计算
          if(begin>0)PlotIndexSetInteger(plotting_style_index,PLOT_DRAW_BEGIN,begin+N);
         }
    
  6. 您可以使用以下代码,在 OnInit() 函数中指定不同于 EMPTY_VALUE 的您自己的空值:
    PlotIndexSetDouble(plotting_style_index,PLOT_EMPTY_VALUE,your_empty_value);
  7. 不要依赖于使用以下代码的指标缓冲区的一次性初始化:
    ArrayInitialize(buffer_number,value);
        
    您应针对 OnCalculate() 函数明确且一致地设置指标缓冲区的所有值,包括空值。

当然,在以后有了一定的指标编写经验后,您可能会碰到超出本文讨论范畴的情形,我希望在那个时候,凭借您对 MQL5 的掌握可以自行解决问题。

本文译自 MetaQuotes Software Corp. 撰写的俄文原文

全部回复

0/140

达人推荐

量化课程

    移动端课程