从指标获取时间序列数据:CopyBuffer

MQL 程序可以通过指标句柄读取其公共缓冲区中的数据。需要注意,在自定义指标中,此类缓冲区是源代码中通过 SetIndexBuffer 函数调用指定的数组。

MQL5 API 提供了 CopyBuffer函数用于读取缓冲区,该函数有 3 种形式。

int CopyBuffer(int handle, int buffer, int offset, int count, double &array[])

int CopyBuffer(int handle, int buffer, datetime start, int count, double &array[])

int CopyBuffer(int handle, int buffer, datetime start, datetime stop, double &array[])

handle参数用于指定通过 iCustom 或其他函数调用获取的句柄(有关详细信息,请参阅 IndicatorCreate内置指标章节)。buffer参数用于设置要请求数据的指标缓冲区索引。编号从 0 开始。

所请求时间序列的接收元素进入通过引用设置的 array

该函数的三种变体区别在于指定时间戳范围 (start/stop) 或获取数据的柱线的编号 (offset) 和数量 (count) 的方式不同。这些参数的基本用法与我们在 获取报价数组的 Copy 函数概述中学习的内容完全一致。特别是,在 offsetcount 中,复制数据的元素是按从当前向历史方向计数,即起始位置 0 表示当前柱线。接收 array中的元素在物理上按从历史向当前方向的顺序排列(但可以通过 ArraySetAsSeries在逻辑层面反转这种寻址方式)。

CopyBuffer 类似于读取 Copy Open,CopyClose 等类型内置时间序列的函数。其主要区别在于,报价时间序列由终端自身生成,而指标缓冲区中的时间序列由自定义指标或 内置指标计算得出。此外,对于指标而言,我们在处理程序创建函数(如 iCustom)中预先设定了一组特定的交易品种和时间范围,以定义和标识时间序列;而在 CopyBuffer 中,这些信息是通过 handle 间接传递的。

当复制未知数量的数据为目标数组时,建议使用动态数组。在这种情况下,CopyBuffer函数将根据被复制数据的大小分配接收数组的大小。如果需要重复复制已知数量的数据,则最好在静态分配的缓冲区中进行此操作(使用带有 static修饰符的局部变量,或在全局上下文中使用固定大小的变量),以避免重复进行内存分配。

如果接收数组是一个指标缓冲区(即先前通过SetIndexBufer函数在系统中注册的数组),则时间序列和接收缓冲区的索引方式是相同的(前提是请求相同的交易品种/时间范围)。在这种情况下,可轻松实现对接收方的局部填充(特别是用于更新最后几根柱线,详见下文示例)。如果所请求时间序列的交易品种或时间范围与当前图表的交易品种和/或时间范围不匹配,该函数返回的元素数量不会超过源数据和目标数据中最小柱线数量。

如果将普通数组(而非缓冲区)作为array参数传递,则该函数将从第一批元素开始完整填充(动态情况下)或部分填充(静态情况下,大小超限)。因此,如果需要将指标值部分复制到另一个数组的任意位置时,则需要使用中间数组:先将所需数量的元素复制到中间数组,再从中间数组转存至最终目标位置。

该函数将返回复制的元素数量,如果发生错误(包括暂时无可用数据)则返回 -1。

由于指标通常直接或间接依赖于价格时间序列,其计算将在报价同步完成后才开始。因此,需考虑终端中 时间序列组织与存储的技术要点 ,并做好请求数据不会即时出现的准备。具体来说,我们可能会收到 0 数据或少于请求数量的数据。所有此类情况都应根据实际情况处理,例如等待构建或向用户报告问题。

如果请求的时间序列尚未构建,或需要从服务器下载,则该函数的行为会因调用它的 MQL 程序类型不同而有所差异。
 
当从指标请求尚未准备就绪的数据时,该函数将立即返回 - 1,但同时会启动时间序列的加载和构建流程。
 
当从 EA 交易或脚本请求数据时,如果数据可通过本地历史构建,则将启动从服务器的下载及/或开始构建所需的时间序列。该函数将在为函数分配的同步执行超时期限内(45 秒)返回就绪的数据量(调用代码会等待函数执行完成)。

请注意,CopyBuffer 函数可以从缓冲区读取数据,无论其操作模式是 INDICATOR_DATA、INDICATOR_COLOR_INDEX 还是 INDICATOR_CALCULATIONS,其中后两种模式对用户隐藏。

同样需要注意,在调用的指标中,时间序列的偏移量可以通过 PLOT_SHIFT 特性设置,这会影响通过CopyBuffer读取数据的偏移量。例如,如果指标线向未来方向偏移了 N 根柱线,则在 CopyBuffer参数(第一种形式)中必须设置offset 为 (-N),即带有负号,这是因为当前时间序列柱线索引为 0,而随着向未来偏移,未来柱线索引值会逐根递减 1。这种情况尤其常见于 Gator 指标,因为其空值图表会向前偏移 TeethShift参数的值,而第一个图表则会偏移LipsShift 参数的值。应基于这两个参数中的较大值进行修正。具体示例将在 从存在偏移的图表中读取数据章节中详细说明。

MQL5 未提供获取第三方指标的 PLOT_SHIFT 特性的编程工具。因此必要时,需通过输入变量向用户请求该信息。

我们将在 EA 交易章节中详述如何在 EA 交易代码中使用 CopyBuffer函数,但目前我们先聚焦于指标。

接下来我们继续完善辅助指标IndWPR的示例。这次在 UseWPR3.mq5版本中,我们将提供一个指标缓冲区,并通过 CopyBuffer 函数来为其填充来自IndWPR 的数据。为此,我们使用的指令中包含设置缓冲区数量和渲染设置。

#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
   
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrBlue
#property indicator_width1  1
#property indicator_label1  "WPR"

在全局范围内,我们在描述输入参数时使用了 WPR 周期、缓冲区数组以及带描述符的变量。

input int WPRPeriod = 14;
   
double WPRBuffer[];
   
int handle;

OnInit处理程序基本保持不变:仅新增了 SetIndexBuffer 调用。

int OnInit()
{
   SetIndexBuffer(0WPRBuffer);
   handle = iCustom(_Symbol_Period"IndWPR"WPRPeriod);
   return handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}

OnCalculate中,我们将直接复制数据而不做任何转换。

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &data[])
{
   // waiting for the calculation to be ready for all bars
   if(BarsCalculated(Handle) != rates_total)
   {
      return prev_calculated;
   }
   
   // copy the entire timeseries of the subordinate indicator or on new bars to our buffer
   const int n = CopyBuffer(handle00rates_total - prev_calculated + 1WPRBuffer);
   // if there are no errors, our data is ready for all bars rates_total
   return n > -1 ? rates_total : 0;
}

通过编译并运行UseWPR3,我们实际上将获得一个原始 WPR 指标的副本,但水平位调整、数值精度和标题除外。这足以用于测试该机制,但通常基于一个或多个辅助指标的新指标会提供自己的分析思路和数据转换。因此,我们将开发另一个生成买卖交易信号的指标(从交易角度而言,这些信号不应被视为交易模型,因为这仅仅是一个编程任务)。该指标的设计思路如下图所示。

Indicators IndWPR, IndTripleEMA, IndFractals

Indicators IndWPR, IndTripleEMA, IndFractals

我们将 WPR 从超买和超卖区域的退出,分别作为卖出和买入的建议信号。为避免信号对随机波动做出反应,我们对 WPR 应用三重移动平均线,并检查其值是否突破上下区域边界。

作为这些信号的过滤器,我们将检查在此前的最后一个分形形态:顶部分形意味着价格向下反转并确认卖出信号,而底部分形意味着价格向上反转并支持买入信号。分形出现延迟,其延迟的柱线数量等于分形的阶数。

新指标包含在 UseWPRFractals.mq5文件中。

我们需要三个缓冲区:两个信号缓冲区、一个过滤缓冲区。我们本可以将后者设置为 INDICATOR_CALCULATIONS 模式。我们改为使用标准 INDICATOR_DATA,但使用 DRAW_NONE 风格,这样它不会在图表上显示,但数值将在数据窗口中可见。

信号将显示在主图表上(默认基于 Close价),因此我们将使用indicator_chart_window 指令。我们仍可调用 WPR 类型的指标(这类指标通常显示在独立窗口中),因为所有从属指标可以无需可视化即可进行计算。如有必要,我们可以绘制指标,但我们在图表章节中将讨论这一点(请参阅 ChartIndicatorAdd)。

#property indicator_chart_window
#property indicator_buffers 3
#property indicator_plots   3
// buffer drawing settings
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrRed
#property indicator_width1  1
#property indicator_label1  "Sell"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrBlue
#property indicator_width2  1
#property indicator_label2  "Buy"
#property indicator_type3   DRAW_NONE
#property indicator_color3  clrGreen
#property indicator_width3  1
#property indicator_label3  "Filter"

在输入变量中,我们将提供指定 WPR 周期、平均(平滑)周期和分形阶数的功能。这些是从属指标的参数。此外,我们引入offset变量,用于指定分析信号的柱线编号。值 0(默认值)表示当前柱线并以分时报价模式分析(注意:最后一根柱线的信号可能会重绘;部分交易者不喜欢这一点)。如果我们将 offset设为 1,我们将分析已形成的柱线,此类信号不会变化。

input int PeriodWPR = 11;
input int PeriodEMA = 5;
input int FractalOrder = 1;
input int Offset = 0;
input double Threshold = 0.2;

Threshold变量用于定义超买和超卖区域阈值,取值范围为 ±1.0(双向)。例如,如果遵循经典 WPR 设置(在 0 到 - 100 的刻度上,水平位分别为 - 20 和 - 80),则 Threshold应设为 0.4。

为指标缓冲区提供以下数组。

double UpBuffer[];   // upper signal means overbought, i.e. selling
double DownBuffer[]; // lower signal means oversold, i.e. buy
double filter[];     // fractal filter direction +1 (up/buy), -1 (down/sell)

指标句柄将保存在全局变量中。

int handleWPRhandleEMA3handleFractals;

我们将像往常一样在OnInit中执行所有设置。由于 CopyBuffer函数使用从现在到过去的索引方式,为保持数据读取统一性,我们将为所有数组设置“序列”标志 (ArraySetAsSeries)。

int OnInit()
{
   // binding buffers
   SetIndexBuffer(0UpBuffer);
   SetIndexBuffer(1DownBuffer);
   SetIndexBuffer(2FilterINDICATOR_DATA); // version: INDICATOR_CALCULATIONS
   ArraySetAsSeries(UpBuffertrue);
   ArraySetAsSeries(DownBuffertrue);
   ArraySetAsSeries(Filtertrue);
   
   // arrow signals
   PlotIndexSetInteger(0PLOT_ARROW234);
   PlotIndexSetInteger(1PLOT_ARROW233);
   
   // subordinate indicators
   handleWPR = iCustom(_Symbol_Period"IndWPR"PeriodWPR);
   handleEMA3 = iCustom(_Symbol_Period"IndTripleEMA"PeriodEMA0handleWPR);
   handleFractals = iCustom(_Symbol_Period"IndFractals"FractalOrder);
   if(handleWPR == INVALID_HANDLE
   || handleEMA3 == INVALID_HANDLE
   || handleFractals == INVALID_HANDLE)
   {
      return INIT_FAILED;
   }
   
   return INIT_SUCCEEDED;
}

iCustom调用中,需特别注意handleEMA3 的创建方式。由于该平均值是基于 WPR 计算的,我们将在调用指标 IndTripleEMA的实际参数后,将 handleWPR(在上一次iCustom 调用中获得)作为最后一个参数传入。在这一过程中,我们必须指定 IndTripleEMA的完整输入参数列表(其中的参数包括 int InpPeriodEMABEGIN_POLICY InpHandleBegin;我们曾使用第二个参数来研究初始柱线的跳过问题,现在虽然不需要该参数,但仍必须传递该参数,因此只需将其设置为 0)。如果我们在调用时省略了第二个参数(因为在当前应用场景中为无关参数),则传递的 handleWPR句柄将在被调用的指标中被解释为InpHandleBegin。因此,IndTripleEMA将被应用于常规 Close 价。

当我们无需传递额外句柄时,iCustom调用语法允许省略末尾任意数量的参数,这些参数将从源代码中获取默认值。

OnCalculate处理程序中,我们等待 WPR 指标和分形就绪,然后使用辅助函数 MarkSignals 为整个历史或最后一根柱线计算信号。

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &data[])
{
   if(BarsCalculated(handleEMA3) != rates_total
   || BarsCalculated(handleFractals) != rates_total)
   {
      return prev_calculated;
   }
   
   ArraySetAsSeries(datatrue);
   
   if(prev_calculated == 0// first launch
   {
      ArrayInitialize(UpBufferEMPTY_VALUE);
      ArrayInitialize(DownBufferEMPTY_VALUE);
      ArrayInitialize(Filter0);
      
      // look for signals throughout history
      for(int i = rates_total - FractalOrder - 1i >= 0; --i)
      {
         MarkSignals(iOffsetdata);
      }
   }
   else // online
   {
      for(int i = 0i < rates_total - prev_calculated; ++i)
      {
         UpBuffer[i] = EMPTY_VALUE;
         DownBuffer[i] = EMPTY_VALUE;
         Filter[i] = 0;
      }
      
      // looking for signals on a new bar or each tick (if Offset == 0)
      if(rates_total != prev_calculated
      || Offset == 0)
      {
         MarkSignals(0Offsetdata);
      }
   }
   
   return rates_total;
}

我们重点关注 MarkSignals中隐藏的CopyBuffer 函数的操作。平滑处理后的 WPR 值将被读取到 wpr[2]数组中,分形数据将被读取到 peaks[1]hollows[1] 中。

int MarkSignals(const int barconst int offsetconst double &data[])
{
   double wpr[2];
   double peaks[1], hollows[1];
   ...

然后,我们通过三次CopyBuffer调用填充本地数组。请注意,我们不需要直接读取 IndWPR指标,因为它已被用于IndTripleEMA 的计算中。我们通过 handleEMA3句柄将数据读取到wpr 数组中。需特别注意,分形指标包含 2 个缓冲区,因此调用两次 CopyBuffer函数,分别为 peakshollows 数组使用不同的索引 0 和 1。读取分形时需设置 FractalOrder的偏移量,因为分形只能在左右两侧拥有特定数量柱线的柱线上形成。

   if(CopyBuffer(handleEMA30bar + offset2wpr) == 2
   && CopyBuffer(handleFractals0bar + offset + FractalOrder1peaks) == 1
   && CopyBuffer(handleFractals1bar + offset + FractalOrder1hollows) == 1)
   {
      ...

接下来,我们从 Filter缓冲区的上一根柱线中获取先前的过滤方向(在历史起始处为 0,但当出现向上或向下分形时,会在该位置写入 +1 或 -1,这可以在下面的源代码中看到),并在检测到任何新分形时对它进行相应的更改。

      int filterdirection = (int)Filter[bar + 1];
      
      // the last fractal sets the reversal movement
      if(peaks[0] != EMPTY_VALUE)
      {
         filterdirection = -1// sell
      }
      if(hollows[0] != EMPTY_VALUE)
      {
         filterdirection = +1// buy
      }
   
      Filter[bar] = filterdirection// remember the current direction

最后,我们分析平滑处理后的 WPR 从上部或下部区域向中间区域的过渡情况,同时考虑 Threshold参数中指定的区域宽度。

      // translate 2 WPR values into the range [-1,+1]
      const double old = (wpr[0] + 50) / 50;     // +1.0 -1.0
      const double last = (wpr[1] + 50) / 50;    // +1.0 -1.0
      
      // bounce from the top down
      if(filterdirection == -1
      && old >= 1.0 - Threshold && last <= 1.0 - Threshold)
      {
         UpBuffer[bar] = data[bar];
         return -1// sale
      }
      
      // bounce from the bottom up
      if(filterdirection == +1
      && old <= -1.0 + Threshold && last >= -1.0 + Threshold)
      {
         DownBuffer[bar] = data[bar];
         return +1// purchase
      }
   }
   return 0// no signal
}

下面是该指标在图表上的截屏:

基于 WPR、EMA3 和分形的信号指标 UseWPRFractals

基于 WPR、EMA3 和分形的信号指标 UseWPRFractals