跳过初始柱线的绘制

在许多情况下,根据算法的条件,指标值的计算无法从第一根(最左侧可用的)柱线开始,因为需要确保历史中存在指定的最小数量的前置柱线。例如,许多类型的平滑处理意味着,当前值是使用前 N 根柱线的价格数组计算得出的。

在这种情况下,可能无法在最初的几根柱线上计算指标值,或者不打算将这些值显示在图表上,仅作为计算后续值的辅助。

如果要禁止在历史数据的前 N-1 根柱线上显示指标,为相应的图形绘制索引将 PLOT_DRAW_BEGIN 特性设置为 N:PlotIndexSetInteger(index, PLOT_DRAW_BEGIN, N)。默认情况下,该特性为 0,这意味着数据从最开始处开始显示。

此外,我们可以通过将必要的柱线设置为 空值 (默认值为 EMPTY_VALUE),禁止在这些柱线上显示线条。然而,调用PlotIndexSetInteger函数对 PLOT_DRAW_BEGIN 特性执行其他作用。我们通过这种方式向外部程序告知指标缓冲中前导无效值的数量。具体来说,其他可能基于我们指标的时间序列构建的指标,会在其 OnCalculate 处理程序的begin参数中收到 PLOT_DRAW_BEGIN 特性的值。因此,它们将有机会跳过柱线。

IndColorWPR.mq5指标的示例中,我们向 OnInit 函数添加一个类似的设置。

input int WPRPeriod = 14// Period
   
void OnInit()
{
   ...
   PlotIndexSetInteger(0PLOT_DRAW_BEGINWPRPeriod - 1);
   ...
}

现在,在 OnCalculate函数中,可以移除对最初几根柱线的强制清除,因为它们将始终处于隐藏状态。

   if(prev_calculated == 0)
   {
      ArrayFill(WPRBuffer0WPRPeriod - 1EMPTY_VALUE);
   }

但这仅在用户手动将我们的指标选为另一个指标的时间序列来源时,才会正确生效。如果某个程序员决定在其开发中使用我们的指标,则存在另一种获取数据的机制(我们将在下一章讨论这一点),而该机制无法查找 PLOT_DRAW_BEGIN 特性。因此,最好使用显式缓冲区初始化。

为了演示如何在借助我们指标数据计算得出的另一个指标中使用该特性,我们来准备另一个指标。这将是封装在IndTripleEMA.mq5指标中的众所周知的三重指数移动平均线算法。完成后,它将能轻松应用于价格时间序列和任意指标(例如之前的 IndColorWPR.mq5指标)。

此外,我们还将了解描述计算辅助缓冲区 (INDICATOR_CALCULATIONS) 的技术可能性。

三重 EMA 公式由多个计算步骤组成。对初始时间序列 T 进行周期为 P 的简单指数平滑可表示为:

K = 2.0 / (P + 1)
A[i] = T[i] * K + A[i - 1] * (1 - K)

其中,K 是用于考虑原始序列元素的权重因子,在给定周期 P 后计算得出;(1 - K) 是应用于平滑序列 A 元素的惯性系数。为获得序列 A 的第 i 个元素,我们将原始序列 T[i] 的第 K 部分与前一个元素 A[i - 1] 的 (1 - K) 部分相加。

如果我们将上述公式中的平滑处理记为 E 算子,则顾名思义,三重 EMA 包含三次 E 算子的应用,之后将 3 次平滑得到的序列以特殊方式组合。

EMA1 = E(AP), for all i
EMA2 = E(EMA1P), for all i
EMA3 = E(EMA2P), for all i
TEMA = 3 * EMA1 - 3 * EMA2 + EMA3for all i

与周期相同的常规 EMA 相比,三重 EMA 对原始序列的滞后更小。然而,其特点是具有更强的响应性,这可能导致生成的曲线出现不规则,并产生虚假信号。

EMA 平滑处理允许从序列的第二个元素开始即可获得平均值的粗略估计,且这不需要更改算法。这使 EMA 有别于其他平滑方法,后者需要 P 个前置元素,或者在可用元素少于 P 个时,需要对初始样本使用修改后的算法。一些开发者甚至在使用 EMA 时,也倾向于将平滑序列的前 P-1 个元素设为无效值。然而,需要注意的是,EMA 公式中序列过去元素的影响并非局限于 P 个元素,只有当元素数量趋近于无穷大时,这种影响才会变得可以忽略不计(而在众所周知的其他 MA 算法中,恰好是前 P 个元素产生影响)。
 
在本书中,为了研究跳过初始数据的影响,我们不会禁用 EMA 初始值的输出。

要计算三重 EMA 级别,我们需要辅助缓冲区以及一个用于最终序列的缓冲区:后者将以折线图形式显示。

#property indicator_chart_window
#property indicator_buffers 4
#property indicator_plots   1
   
#property indicator_type1   DRAW_LINE
#property indicator_color1  Orange
#property indicator_width1  1
#property indicator_label1  "EMA³"
   
double TemaBuffer[];
double Ema[];
double EmaOfEma[];
double EmaOfEmaOfEma[];
   
void OnInit()
{
   ...
   SetIndexBuffer(0TemaBufferINDICATOR_DATA);
   SetIndexBuffer(1EmaINDICATOR_CALCULATIONS);
   SetIndexBuffer(2EmaOfEmaINDICATOR_CALCULATIONS);
   SetIndexBuffer(3EmaOfEmaOfEmaINDICATOR_CALCULATIONS);
   ...
}

输入变量InpPeriodEMA允许设置平滑周期。第二个变量 InpHandleBegin是一个模式切换开关,通过它,我们可以探究指标在 OnCalculate 处理程序中考虑或忽略begin 参数时的反应。可用模式总结在 BEGIN_POLICY 枚举中,其含义如下(按排列顺序):

  • 根据初始数据的自定义验证据 begin
  • 进行严格位移,不进行处理, begin
  • 即忽略 begin参数并直接计算所有数据

enum BEGIN_POLICY
{
   STRICT// strict
   CUSTOM// custom
   NONE,   // no
};
   
input int InpPeriodEMA = 14;                 // EMA period:
input BEGIN_POLICY InpHandleBegin = STRICT;  // Handle 'begin' parameter:

第二种 CUSTOM 模式基于将每个源元素与 EMPTY_VALUE 进行预先比较,并将其替换为适合算法的值。此模式仅适用于以下指标:那些如实初始化缓冲区未使用开头部分,且不遗留垃圾数据的指标。我们的指标 IndColorWPR按要求填充缓冲区,因此在 STRICT 和 CUSTOM 模式下,你可以预期得到几乎相同的结果。

为基于 InpPeriodEMA计算 EMA 准备 K 常数。

const double K = 2.0 / (InpPeriodEMA + 1);

EMA 函数本身相当简单(此处省略了 CUSTOM 变体下检查 EMPTY_VALUE 的保护代码段)。

void EMA(const double &source[], double &result[], const int posconst int begin = 0)
{
   ...
   if(pos <= begin)
   {
      result[pos] = source[pos];
   }   
   else
   {
      result[pos] = source[pos] * K + result[pos - 1] * (1 - K);
   }
}

以下是 OnCalculate中完整的三重平滑计算过程。

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
{
   const int _begin = InpHandleBegin == STRICT ? begin : 0;
   // fresh start, or history update
   if(prev_calculated == 0)
   {
      Print("begin="begin" "EnumToString(InpHandleBegin));
      
      // we can change chart settings dynamically
      PlotIndexSetInteger(0PLOT_DRAW_BEGIN_begin);
      
      // preparing arrays
      ArrayInitialize(EmaEMPTY_VALUE);
      ArrayInitialize(EmaOfEmaEMPTY_VALUE);
      ArrayInitialize(EmaOfEmaOfEmaEMPTY_VALUE);
      ArrayInitialize(TemaBufferEMPTY_VALUE);
      Ema[_begin] = EmaOfEma[_begin] = EmaOfEmaOfEma[_begin] = price[_begin];
   }
   
   // main loop, taking into account the start from _begin
   for(int i = fmax(prev_calculated - 1_begin);
      i < rates_total && !IsStopped(); i++)
   {
      EMA(priceEmai_begin);
      EMA(EmaEmaOfEmai_begin);
      EMA(EmaOfEmaEmaOfEmaOfEmai_begin);
      
      if(InpHandleBegin == CUSTOM// protection from empty elements at the beginning
      {
         if(Ema[i] == EMPTY_VALUE
         || EmaOfEma[i] == EMPTY_VALUE
         || EmaOfEmaOfEma[i] == EMPTY_VALUE)
            continue;
      }
      
      TemaBuffer[i] = 3 * Ema[i] - 3 * EmaOfEma[i] + EmaOfEmaOfEma[i];
   }
   return rates_total;
}

第一次启动或更新历史数据时,begin参数的接收值将与用户选择的处理模式一同写入日志。

成功编译后,即可开始进行各项实验。

首先,让我们运行IndColorWPR指标(默认情况下,其周期为 14,这意味着根据源代码,需将 PLOT_DRAW_BEGIN 特性设置为 13,因为索引从 0 开始,第 13 根柱线将是第一个显示计算值的位置)。然后将IndTripleEMA指标拖至显示 WPR 的子窗口中。在弹出的特性设置对话框的 Options选项卡上,在 Apply to下拉列表中选择Previous indicator data。保留 Inputs选项卡的默认值。

下图显示了图表的起始部分。日志将记录以下条目:begin=13 STRICT

考虑数据起点时应用于 WPR 的三重 EMA 指标

考虑数据起点时应用于 WPR 的三重 EMA 指标

请注意,平均线与 WPR 指标一样,均从与起点有一定距离的位置开始。

注意!如果本地历史报价数据较长,用于指标计算的可用柱线数量 rates_total(或iBars(_Symbol, _Period))可能会超过终端设置中允许显示的最大柱线数量。在这种情况下,WPR 线(或任何跳过起始元素的指标,如 MA)起始处的空值元素将变得不可见,它们将被隐藏在图表的左边界之后。如果要复现初始柱线上没有指标线的情况,你需要增加图表上显示的柱线数量,或关闭终端并删除特定交易品种的本地历史数据。

现在,我们来在 IndTripleEMA指标设置中切换至 CUSTOM 模式(日志将显示begin=0 CUSTOM)。指标读数应无明显变化。

最后,我们激活 NONE 模式。日志将输出:begin=0 NONE

此时,图表上的情况看起来有些异常,因为指标线实际上会消失。在数据窗口中,你可以看到元素值变得非常大。

未考虑数据起点时应用于 WPR 指标的三重 EMA 指标

未考虑数据起点时应用于 WPR 指标的三重 EMA 指标

这是因为 EMPTY_VALUE 的值等于最大实数 DBL_MAX。因此,如果不考虑begin参数,使用这些值进行计算时也会生成非常大的数值。根据计算的具体特性,溢出可能导致我们得到特殊值 NaN(非数值,请参阅 检查实数有效性)。其中一个值 -nan(ind)在图中高亮显示(Data Window 已支持输出某些类型的 NaN,例如“inf”和“-inf”,但目前尚不支持“-nan(ind)”)。正如我们所知,此类 NaN 值具有危险性,因为涉及它们的计算将持续生成 NaN。如果未生成 NaN,则当你向右移动浏览柱线时,大数计算中的“瞬态过程”会逐渐减弱(由于 EMA 公式中的衰减因子 (1 - K)),结果将趋于稳定并变得合理。如果将图表滚动到当前时间,你会看到正常的三重 EMA 指标线。

考虑begin参数是一种很好的做法,但这并不能保证数据提供者(如果是第三方指标)正确填写该特性。因此,最好是在你的代码中提供一些保护措施。在该 IndTripleEMA实现中,在初始层面实现了保护措施。

如果我们在价格图表上运行 IndTripleEMA指标,你将始终收到 begin = 0,因为价格时间序列从一开始(即使是最早的柱线)就填写了真实数据。